Use dynamic dispatch in `Authority`
- Dominant language
- Rust
- Stars
- 132
- Forks
- 167
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 111
Description
## Summary
Components that must invoke a procedure of a different component currently hard-code the target with a static `exec`, and select it with an `if-else` chain over a configuration value. Replace this with a storage slot that holds the procedure root of the target, and invoke that root with `dyncall`. The slot schema marks the slot as a dependency procedure root, so tooling can find every such root from the storage schema alone.
## Background
The idea comes from [this review discussion](https://github.com/0xMiden/protocol/pull/3527#discussion_r3798650393). It separates two different kinds of dependency:
- Static dependency: the code that a component needs to compile. `Authority` needs the `Rbac` and `Ownable2Step` modules to link, because it names their procedures directly.
- Runtime dependency: the components that must be installed on an account for another component to operate. Which one applies depends on a configuration value that is only known at instantiation time, so the condition cannot be expressed statically.
Both of these are affected by this refactor:
- The refactoring removes the static dependency: when `Authority` dispatches through a stored root, it no longer names `ownable2step` or `rbac`, and it assembles without them.
- The runtime dependency (e.g. rbac) can be read from the stored proc root, and any component not used (e.g. ownable2step) does not appear as a runtime dependency.
## The model already exists in the codebase
Two components already hold procedure roots in storage and dispatch with `dyncall` and serve as the reference for the refactoring:
- [`TokenPolicyManager`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/faucets/policies/policy_manager.masm#L19).
- [`FeeManager`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/fees/fee_manager.masm#L33).
What needs to be done here is declaring their proc root slots in the schema as a "stored procedure root" to prepare them for dependency resolution.
## The component to refactor: Authority
[`Authority`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/access/authority.masm) is the only component that selects a dependency with a branch over a stored discriminator. It stores `authority` and dispatches on it in two places:
- [`assert_authorized`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/access/authority.masm#L120) does nothing under `AuthControlled`, calls `ownable2step::assert_sender_is_owner` under `OwnerControlled`, and calls [`assert_authorized_rbac`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/access/authority.masm#L250) under `RbacControlled`.
- [`assert_sender_is_emergency_authority`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/access/authority.masm#L181) makes the same selection for `freeze` and `unfreeze`, and bypasses the frozen flag.
After the refactoring the component holds two slots:
- `ASSERT_IS_AUTHORIZED_PROC_ROOT` which holds the root of the new `assert_is_authorized` procedure.
- `FROZEN_FLAG` which holds `[is_frozen, 0, 0, 0]`.
The signature of `assert_is_authorized` is:
```
#! Inputs: [CALLER_PROC_ROOT, pad(12)]
#! Outputs: [pad(16)]
```
It receives the procedure root obtained via `caller` as a parameter. This is necessary so `assert_authorized_rbac` keeps working. If it used `caller` itself, it would get the wrong root as it is now `dyncalled` instead of `exec`-ed.
`Authority` must become agnostic of the authorization schemes and must no longer name `ownable2step` or `rbac`. Each scheme must expose one procedure with the agreed signature. `Authority` keeps only the stored root and the frozen flag in its storage.
- `AuthControlled` sets the root to the empty word (which will skip the invocation).
- The owner scheme needs no new component. [`assert_sender_is_owner`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/access/ownable2step.masm#L245) is an `exec` helper today, so `Ownable2Step` only gets a `call` entry point for it.
- The RBAC scheme needs a new `RbacAuthority` component, because the per-procedure role map that `Authority` holds today needs a dedicated owner, and `Authority` should be agnostic to the underlying scheme. The logic of its `assert_is_authorized` is essentially today's `assert_authorized_rbac`.
The frozen flag stays in `Authority` and the dispatch becomes uniform for all three entry points:
- `assert_authorized` first asserts that the account is not frozen, then reads the stored root. An empty word means that no check applies and the procedure returns. Otherwise it `dyncall`s the root with the procedure root from `caller`.
- `freeze` and `unfreeze` skip the frozen check, read the same slot, and panic when it holds the empty word, because an account without an authorization scheme has no emergency authority. Otherwise they `dyncall` the `assert_is_authorized` with their own procedure root.
This removes `assert_sender_is_emergency_authority` and needs no second slot.
One consequence to keep in mind: the dispatch target is an account procedure that the kernel records as called. Auth components that gate on the set of called procedures, such as the per-procedure thresholds of `multisig_smart`, now see it.
Notably, the collection and verification step in the account builder is the general dependency resolution problem. It is tracked in [#2621](https://github.com/0xMiden/protocol/issues/2621) and is not part of this issue. This issue covers only the component-side refactoring, which is the precondition for it.
## Components that are not affected
[`MintOwnerOnly`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/faucets/policies/mint/owner_controlled/owner_only.masm#L32) and [`BurnOwnerOnly`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/faucets/policies/burn/owner_controlled/owner_only.masm#L27) call `ownable2step::assert_sender_is_owner` directly, but this is a single static target. They declare `Ownable2Step` as a dependency under #2621.
[`pausable::assert_not_paused`](https://github.com/0xMiden/protocol/blob/3320f0a1b7c31b458cad0563d2a1e47cb13a2eff/crates/miden-standards/asm/standards/access/pausable/mod.masm#L127) tests whether the pause slot is installed and skips the check when it is not. A stored root that holds the empty word to say "skip the check" gives the same result as the `has_storage_slot` test, so the check stays as it is.
Contributor guide
Research direction
Start with crates/miden-standards/asm/standards/access/authority.masm, then compare the stored-root and dyncall patterns in policy_manager.masm and fee_manager.masm. Review ownable2step.masm and the Authority entry points, keeping #2621 out of scope; done means Authority no longer names the authorization schemes, its schema exposes the dependency root, and the affected dispatch paths use the agreed procedure signature.
Written by the indexing model from the issue text.
Assessment
- Domain
- blockchain
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100