feat(agglayer): RBAC-based access control for bridge roles#3130
feat(agglayer): RBAC-based access control for bridge roles#3130Fumuran wants to merge 15 commits into
Conversation
Replace the bridge's hard-coded admin / GER-injector / GER-remover account-ID storage slots with the miden-standards access-control stack (Ownable2Step + RoleBasedAccessControl + Authority). The role-gated procedures (register_faucet, store_faucet_metadata_hash, update_ger, remove_ger) now call authority::assert_authorized, which checks the note sender holds the FAUCET_ADMIN / GER_INJECTOR / GER_REMOVER role mapped to that procedure. A governance owner can grant/revoke roles and transfer ownership via Ownable2Step. - miden-standards: add RoleAssignment + RoleBasedAccessControl::with_roles to seed initial role members at construction; AccessControl::Rbac gains a `members` field. - bridge MASM: drop the three ID slots / bespoke assert_sender_is_* procs; gate the four privileged procedures via authority::assert_authorized. - bridge Rust: add BridgeRoleMember, role-symbol + procedure-root accessors, and the fixed procedure->role map; create_bridge_account now takes an owner and a Vec<BridgeRoleMember> instead of three bare account IDs. - build.rs: include the RBAC stack when computing BRIDGE_CODE_COMMITMENT. - update SPEC.md, agglayer integration tests, and bench setups. On-chain role-management notes (grant/revoke role, transfer ownership) are a planned follow-up, so role rotation is not yet exercisable on-chain. Refs #2706. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AggLayerBridge is a zero-field component; use the bare value via its Default/From impl (matching PausableManager and BurnAllowAll) instead of an empty new(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Represent initial role membership as BTreeMap<RoleSymbol, BTreeSet<AccountId>> instead of Vec<RoleAssignment>. Duplicate roles and duplicate members are now impossible by construction, so with_roles drops the two dedup passes (and the DuplicateRole / DuplicateMember error variants), and the RoleAssignment type is removed. The bridge's role grouping collapses to a single entry()-based loop. No behavior change: seeding still equals runtime grant_role (equivalence test passes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bridge: resolve procedure roots via the procedure_root! macro + a component code() getter, matching the PausableManager pattern (removes the ad-hoc bridge_procedure_root helper). - bridge: BridgeRoleMember variants now carry a Vec<AccountId> so all holders of a role are grouped in one entry; rbac_role_members collects via entry()+extend. - docs: link role names to their BridgeRoleMember variants; tighten the bridge component and builder doc comments; bullet-list the with_roles Errors section. - masm: drop the redundant storage-slots comment. - SPEC: switch stored-ID wording to role-membership wording and keep a single authority::assert_authorized explanation (in Administration) instead of repeating it in every note/procedure section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bridge: move the component namespace and the component-code getter into AggLayerBridge (AggLayerBridge::COMPONENT_NAMESPACE and AggLayerBridge::code()), matching the PausableManager shape. - rbac: trim the seeded roles field doc to the essential summary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve import conflicts in lib.rs and the agglayer test files, and adapt to next's AccountId::dummy 4-arg signature (AssetCallbackFlag) in the bridge test helpers. The bridge-account test call sites keep using the create_existing_bridge_account_with_roles helper, so next's direct create_existing_bridge_account import is dropped where unused. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
partylikeits1983
left a comment
There was a problem hiding this comment.
Looks really good, I like the simplification that the RBAC provides.
There are a few minor things I think should be addressed before merging.
| RoleBasedAccessControl::with_roles(members) | ||
| .expect("initial RBAC role assignments should be valid") | ||
| .into(), |
There was a problem hiding this comment.
Nit: This will panic instead of returning an error if the user passes an empty member set (or a set with more than u32::MAX members).
Can we return an error here instead of panicking? And add tests for both error cases?
There was a problem hiding this comment.
Indeed, I overlooked it. I'll update it to return an error and add tests for it
There was a problem hiding this comment.
Hmm, this update will complicate things a bit, since we expect here to have an iterator of components: changing this into iterator of results will require some changes in quite a lot of places, so I'm not sure that it's forth it. For now I'll just move this checks to the Into<AccountComponent> implementation, but in general I agree that we should somehow return an error here instead of panic.
- bridge: replace the BridgeRoleMember enum with a BridgeRoles struct (BTreeSet per role) and a fallible BridgeRoles::new that rejects an empty holder set for any role (new AgglayerBridgeError::EmptyBridgeRole), so a bridge can't be deployed with an unfillable role. create_bridge_account takes BridgeRoles. - standards: make RoleBasedAccessControl::with_roles infallible (drop empty member sets instead of erroring/panicking; remove RbacError and the IntoIterator expect). Add reusable role_config_key / role_membership_key helpers and use them in the From impl instead of building raw words. - testing: move the shared create_existing_bridge_account_with_roles / bridge_test_owner helper into miden_agglayer::testing so the test crate and bench share one copy. - tests: add ERR_SENDER_LACKS_ROLE tests for update_ger (GER_INJECTOR) and register_faucet via CONFIG (FAUCET_ADMIN), and a BridgeRoles::new empty-role test. - SPEC: point the role-management follow-up TODO at #3046; fix a doc link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- changelog: correct the [BREAKING] entry to reflect the BridgeRoles signature (create_bridge_account takes (seed, owner, BridgeRoles)) instead of the removed BridgeRoleMember type. - rbac: note that the u32 member-count conversion in the From impl is purely defensive / infallible. - test: add test_rbac_with_roles_drops_empty_role locking in the empty-role drop semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
create_rbac_account_with_members(owner, BTreeMap::new()) covers the empty-members case, so the separate owner-only helper is unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It was only used by the shared create_existing_bridge_account_with_roles test helper, so fold the build_existing() call into that helper (via the crate-private create_bridge_account_builder) and drop the redundant public wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bridge: drop the verbose BridgeRoles struct doc paragraph (the empty-role rejection is already documented on BridgeRoles::new). - changelog: trim the breaking-change entry per review suggestion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the RBAC access-control refactor with next's new DEREGISTER_AGG_FAUCET feature: - Convert deregister_faucet from assert_sender_is_bridge_admin (removed by this branch) to authority::assert_authorized, mapped to the FAUCET_ADMIN role (procedure_roles + a new procedure_root!/deregister_faucet_root accessor). - Update next's deregister tests to the create_existing_bridge_account_with_roles helper and ERR_SENDER_LACKS_ROLE; update the procedure-roles mapping test to five entries. - SPEC: keep the RBAC role wording with next's shifted section numbers and align the DEREGISTER_AGG_FAUCET section to the FAUCET_ADMIN role terminology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-merge review nits: the config/deregister note-script headers, the register_faucet / store_faucet_metadata_hash panic docs, and the deregister note builder still referred to the 'bridge admin'; update them to the FAUCET_ADMIN role that authority::assert_authorized now enforces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Current state looks fine for me, but I would wait for the #3215 to be merged first, then I'll update this PR and only then re-request the reviews. |
Summary
Refactor phase of #2706. Replaces the AggLayer bridge's hard-coded admin / GER-injector / GER-remover account-ID storage slots with the
miden-standardsaccess-control stack (Ownable2Step+RoleBasedAccessControl+Authority).Per the discussion on the issue, this uses RBAC rather than
Ownable2Stepalone, with a separate governance owner over three subordinate operational roles. Each role-gated bridge procedure now callsauthority::assert_authorized, which checks the note sender holds the role mapped to that procedure.Roles
Ownable2Step): top-level authority; grants/revokes roles, two-step transferable.FAUCET_ADMIN→register_faucet,store_faucet_metadata_hashGER_INJECTOR→update_gerGER_REMOVER→remove_gerChanges
RoleAssignment+RoleBasedAccessControl::with_roles(...)to seed initial role members at construction;AccessControl::Rbacgains amembersfield (yields a seeded RBAC instead ofempty()).assert_sender_is_*procs; gate the four privileged procedures viaauthority::assert_authorized.AggLayerBridgeis now stateless; addBridgeRoleMember, role-symbol + procedure-root accessors, and the fixed procedure→role map.create_bridge_accountnow takes(seed, owner, Vec<BridgeRoleMember>).BRIDGE_CODE_COMMITMENT.SPEC.md, agglayer integration tests, and bench setups. Add awith_roles-equals-runtime-grant_roleequivalence test and a procedure→role map pinning test.Out of scope (follow-up)
On-chain role-management notes (
grant_role/revoke_role/transfer_ownership/accept_ownership) plus their allowlist entries and Rust builders. Roles are seeded and enforced, but rotation is not yet exercisable on-chain — hence this is the refactor phase (Refs #2706, notCloses).Testing
cargo +nightly fmt --all --checkandcargo clippy(affected crates,--all-targets): clean.🤖 Generated with Claude Code