-
Notifications
You must be signed in to change notification settings - Fork 129
feat(standards): add NetworkAccount auth component
#2817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: ajl-kernel-tx-script-root
Are you sure you want to change the base?
Changes from all commits
9d9748f
61845a0
02f4937
6e98a69
ec695a8
80527d5
7fb2124
5fa0894
f6ac48f
0f07d27
4bee55f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||||||||||||
| # The MASM code of the NetworkAccount authentication component. | ||||||||||||||||||||||
| # | ||||||||||||||||||||||
| # See the `NetworkAccount` Rust type's documentation for more details. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| use miden::protocol::active_account | ||||||||||||||||||||||
| use miden::protocol::native_account | ||||||||||||||||||||||
| use miden::protocol::tx | ||||||||||||||||||||||
| use miden::protocol::input_note | ||||||||||||||||||||||
| use miden::core::word | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # CONSTANTS | ||||||||||||||||||||||
| # ================================================================================================ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # The slot holding the map of whitelisted input-note script roots. Keys are the 4-felt note script | ||||||||||||||||||||||
| # roots; any non-empty value marks a root as whitelisted. | ||||||||||||||||||||||
| const WHITELIST_SLOT = word("miden::standards::auth::network_account::whitelist") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # ERRORS | ||||||||||||||||||||||
| # ================================================================================================ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const ERR_NETWORK_ACCOUNT_TX_SCRIPT_NOT_ALLOWED="transactions with a tx script cannot be executed against a network account" | ||||||||||||||||||||||
| const ERR_NETWORK_ACCOUNT_NOTE_NOT_WHITELISTED="input note script root is not in the network account whitelist" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # AUTH PROCEDURE | ||||||||||||||||||||||
| # ================================================================================================ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| #! Authenticates a transaction against a NetworkAccount. | ||||||||||||||||||||||
| #! | ||||||||||||||||||||||
| #! Enforces two invariants: | ||||||||||||||||||||||
| #! 1. No transaction script may have been executed in this transaction. | ||||||||||||||||||||||
| #! 2. Every consumed input note must have a script root present in the whitelist stored at | ||||||||||||||||||||||
| #! `WHITELIST_SLOT`. | ||||||||||||||||||||||
| #! | ||||||||||||||||||||||
| #! If both checks pass, the nonce is incremented when the account state changed or the account is | ||||||||||||||||||||||
| #! new, matching the behavior of the NoAuth and SingleSig components. | ||||||||||||||||||||||
| #! | ||||||||||||||||||||||
| #! Inputs: [pad(16)] | ||||||||||||||||||||||
| #! Outputs: [pad(16)] | ||||||||||||||||||||||
| #! | ||||||||||||||||||||||
| #! Invocation: call | ||||||||||||||||||||||
| @auth_script | ||||||||||||||||||||||
| pub proc auth_tx_network_account | ||||||||||||||||||||||
| # ---- Reject transactions that executed a tx script ---- | ||||||||||||||||||||||
| exec.tx::get_tx_script_root | ||||||||||||||||||||||
| # => [TX_SCRIPT_ROOT, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| padw exec.word::eq | ||||||||||||||||||||||
| # => [no_tx_script, pad(16)] | ||||||||||||||||||||||
|
Comment on lines
+44
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert.err=ERR_NETWORK_ACCOUNT_TX_SCRIPT_NOT_ALLOWED | ||||||||||||||||||||||
| # => [pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # ---- Reject any input note whose script root is not whitelisted ---- | ||||||||||||||||||||||
| exec.tx::get_num_input_notes | ||||||||||||||||||||||
| # => [num_input_notes, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| dup neq.0 | ||||||||||||||||||||||
| while.true | ||||||||||||||||||||||
| # => [i, pad(16)] | ||||||||||||||||||||||
| sub.1 | ||||||||||||||||||||||
| # => [i-1, pad(16)] | ||||||||||||||||||||||
|
Comment on lines
+59
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Nit: We could rename |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| dup exec.input_note::get_script_root | ||||||||||||||||||||||
| # => [NOTE_SCRIPT_ROOT, i-1, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| push.WHITELIST_SLOT[0..2] | ||||||||||||||||||||||
| # => [slot_prefix, slot_suffix, NOTE_SCRIPT_ROOT, i-1, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| exec.active_account::get_map_item | ||||||||||||||||||||||
| # => [VALUE, i-1, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| padw exec.word::eq not | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Nit |
||||||||||||||||||||||
| # => [is_whitelisted, i-1, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert.err=ERR_NETWORK_ACCOUNT_NOTE_NOT_WHITELISTED | ||||||||||||||||||||||
| # => [i-1, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| dup neq.0 | ||||||||||||||||||||||
| # => [should_continue, i-1, pad(16)] | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| # => [0, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| drop | ||||||||||||||||||||||
| # => [pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # ---- Increment nonce iff the account state changed or the account is new ---- | ||||||||||||||||||||||
| exec.active_account::get_initial_commitment | ||||||||||||||||||||||
| # => [INITIAL_COMMITMENT, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| exec.active_account::compute_commitment | ||||||||||||||||||||||
| # => [CURRENT_COMMITMENT, INITIAL_COMMITMENT, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| exec.word::eq not | ||||||||||||||||||||||
| # => [has_account_state_changed, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| exec.active_account::get_nonce eq.0 | ||||||||||||||||||||||
| # => [is_new_account, has_account_state_changed, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| or | ||||||||||||||||||||||
| # => [should_increment_nonce, pad(16)] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if.true | ||||||||||||||||||||||
| exec.native_account::incr_nonce drop | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| # => [pad(16)] | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,181 @@ | ||||||
| use alloc::vec::Vec; | ||||||
|
|
||||||
| use miden_protocol::account::component::{ | ||||||
| AccountComponentMetadata, | ||||||
| SchemaType, | ||||||
| StorageSchema, | ||||||
| StorageSlotSchema, | ||||||
| }; | ||||||
| use miden_protocol::account::{ | ||||||
| AccountComponent, | ||||||
| AccountType, | ||||||
| StorageMap, | ||||||
| StorageMapKey, | ||||||
| StorageSlot, | ||||||
| StorageSlotName, | ||||||
| }; | ||||||
| use miden_protocol::utils::sync::LazyLock; | ||||||
| use miden_protocol::{Felt, Word}; | ||||||
|
|
||||||
| use crate::account::components::network_account_library; | ||||||
|
|
||||||
| // CONSTANTS | ||||||
| // ================================================================================================ | ||||||
|
|
||||||
| static WHITELIST_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| { | ||||||
| StorageSlotName::new("miden::standards::auth::network_account::whitelist") | ||||||
| .expect("storage slot name should be valid") | ||||||
| }); | ||||||
|
Comment on lines
+25
to
+28
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I'd call this "allow list", which is a bit more consistent with "block list" (used in other contexts #2819). |
||||||
|
|
||||||
| // A "sentinel value" is a placeholder value whose only job is to be distinguishable from a known | ||||||
| // default, letting readers of the data detect a condition (here: "this key is present"). We call | ||||||
| // this constant a sentinel because we only ever check whether the stored value differs from the | ||||||
| // empty word; its actual contents carry no information. | ||||||
| // | ||||||
| // Storage maps treat an empty word (`[0, 0, 0, 0]`) as "key absent", so the MASM presence check | ||||||
| // compares the looked-up value against the empty word. Any non-empty word would serve as the | ||||||
| // sentinel; we pick `[1, 0, 0, 0]` for readability when inspecting storage. | ||||||
| const WHITELIST_SENTINEL: Word = | ||||||
| Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]); | ||||||
|
|
||||||
| // NETWORK ACCOUNT | ||||||
| // ================================================================================================ | ||||||
|
|
||||||
| /// An [`AccountComponent`] implementing the authentication scheme used by network-owned accounts | ||||||
| /// such as network faucets and the AggLayer bridge. | ||||||
| /// | ||||||
| /// The component exports a single auth procedure, `auth_tx_network_account`, that rejects the | ||||||
| /// transaction unless: | ||||||
| /// - no transaction script was executed, and | ||||||
| /// - every consumed input note has a script root present in the component's whitelist. | ||||||
| /// | ||||||
| /// The whitelist is stored in a storage map at a well-known slot (see [`Self::whitelist_slot`]) | ||||||
| /// so off-chain services can identify a network account by inspecting its storage. | ||||||
| /// | ||||||
| /// The whitelist is fixed at account creation; there is intentionally no procedure to mutate it | ||||||
| /// after deployment. | ||||||
| pub struct NetworkAccount { | ||||||
| allowed_script_roots: Vec<Word>, | ||||||
| } | ||||||
|
|
||||||
| impl NetworkAccount { | ||||||
| /// The name of the component. | ||||||
| pub const NAME: &'static str = "miden::standards::components::auth::network_account"; | ||||||
|
|
||||||
| /// Creates a new [`NetworkAccount`] component with the provided list of allowed input-note | ||||||
| /// script roots. | ||||||
| pub fn new(allowed_script_roots: Vec<Word>) -> Self { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes me wish we had a |
||||||
| Self { allowed_script_roots } | ||||||
| } | ||||||
|
|
||||||
| /// Returns the storage slot holding the whitelist of allowed input-note script roots. | ||||||
| pub fn whitelist_slot() -> &'static StorageSlotName { | ||||||
| &WHITELIST_SLOT_NAME | ||||||
| } | ||||||
|
|
||||||
| /// Returns the storage slot schema for the whitelist slot. | ||||||
| pub fn whitelist_slot_schema() -> (StorageSlotName, StorageSlotSchema) { | ||||||
| ( | ||||||
| Self::whitelist_slot().clone(), | ||||||
| StorageSlotSchema::map( | ||||||
| "Allowed input-note script roots", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| SchemaType::native_word(), | ||||||
| SchemaType::native_word(), | ||||||
| ), | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| /// Returns the [`AccountComponentMetadata`] for this component. | ||||||
| pub fn component_metadata() -> AccountComponentMetadata { | ||||||
| let storage_schema = StorageSchema::new(vec![Self::whitelist_slot_schema()]) | ||||||
| .expect("storage schema should be valid"); | ||||||
|
|
||||||
| AccountComponentMetadata::new(Self::NAME, AccountType::all()) | ||||||
| .with_description( | ||||||
| "Authentication component for network accounts that restricts input notes to a \ | ||||||
| fixed whitelist and forbids tx scripts", | ||||||
| ) | ||||||
| .with_storage_schema(storage_schema) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl From<NetworkAccount> for AccountComponent { | ||||||
| fn from(network_account: NetworkAccount) -> Self { | ||||||
| let map_entries = network_account | ||||||
| .allowed_script_roots | ||||||
| .into_iter() | ||||||
| .map(|root| (StorageMapKey::new(root), WHITELIST_SENTINEL)); | ||||||
|
|
||||||
| let storage_slots = vec![StorageSlot::with_map( | ||||||
| NetworkAccount::whitelist_slot().clone(), | ||||||
| StorageMap::with_entries(map_entries) | ||||||
| .expect("whitelist entries should produce a valid storage map"), | ||||||
| )]; | ||||||
|
|
||||||
| let metadata = NetworkAccount::component_metadata(); | ||||||
|
|
||||||
| AccountComponent::new(network_account_library(), storage_slots, metadata).expect( | ||||||
| "NetworkAccount component should satisfy the requirements of a valid account component", | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // TESTS | ||||||
| // ================================================================================================ | ||||||
|
|
||||||
| #[cfg(test)] | ||||||
| mod tests { | ||||||
| use miden_protocol::account::{AccountBuilder, StorageMapKey}; | ||||||
|
|
||||||
| use super::*; | ||||||
| use crate::account::wallets::BasicWallet; | ||||||
|
|
||||||
| #[test] | ||||||
| fn network_account_component_builds() { | ||||||
| let root_a = Word::from([1u32, 2, 3, 4]); | ||||||
| let root_b = Word::from([5u32, 6, 7, 8]); | ||||||
|
|
||||||
| let _account = AccountBuilder::new([0; 32]) | ||||||
| .with_auth_component(NetworkAccount::new(vec![root_a, root_b])) | ||||||
| .with_component(BasicWallet) | ||||||
| .build() | ||||||
| .expect("account building with NetworkAccount failed"); | ||||||
| } | ||||||
|
|
||||||
| #[test] | ||||||
| fn network_account_with_empty_whitelist_builds() { | ||||||
| let _account = AccountBuilder::new([0; 32]) | ||||||
| .with_auth_component(NetworkAccount::new(Vec::new())) | ||||||
| .with_component(BasicWallet) | ||||||
| .build() | ||||||
| .expect("account building with empty NetworkAccount whitelist failed"); | ||||||
| } | ||||||
|
|
||||||
| #[test] | ||||||
| fn whitelist_storage_contains_expected_entries() { | ||||||
| use miden_protocol::account::StorageSlotContent; | ||||||
|
|
||||||
| let root_a = Word::from([1u32, 2, 3, 4]); | ||||||
| let root_b = Word::from([5u32, 6, 7, 8]); | ||||||
|
|
||||||
| let component: AccountComponent = NetworkAccount::new(vec![root_a, root_b]).into(); | ||||||
|
|
||||||
| let storage_slots = component.storage_slots(); | ||||||
| assert_eq!(storage_slots.len(), 1); | ||||||
|
|
||||||
| let StorageSlotContent::Map(map) = storage_slots[0].content() else { | ||||||
| panic!("whitelist slot must be a map"); | ||||||
| }; | ||||||
|
|
||||||
| assert_eq!( | ||||||
| map.get(&StorageMapKey::new(root_a)), | ||||||
| WHITELIST_SENTINEL, | ||||||
| "root_a should resolve to the sentinel value" | ||||||
| ); | ||||||
| assert_eq!( | ||||||
| map.get(&StorageMapKey::new(root_b)), | ||||||
| WHITELIST_SENTINEL, | ||||||
| "root_b should resolve to the sentinel value" | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This takes auth args as inputs, right? If so, I'd explicitly drop them here for safety.