diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf315e65..0a883da71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Added tracing to the `miden-tx-prover` CLI (#1014). - Added metrics to the `miden-tx-prover` proxy (#1017). - Implemented `to_hex` for `AccountIdPrefix` and `epoch_block_num` for `BlockHeader` (#1039). +- Introduce `AccountIdBuilder` to simplify `AccountId` generation in tests (#1045). ## 0.6.2 (2024-11-20) diff --git a/Cargo.lock b/Cargo.lock index 0ca30ecc1..5551438dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1933,6 +1933,7 @@ dependencies = [ "miden-processor", "miden-verifier", "rand", + "rand_xoshiro", "rstest", "tempfile", "thiserror 2.0.3", @@ -3095,6 +3096,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.10.0" diff --git a/miden-lib/asm/kernels/transaction/api.masm b/miden-lib/asm/kernels/transaction/api.masm index a7b978fe8..48ada8128 100644 --- a/miden-lib/asm/kernels/transaction/api.masm +++ b/miden-lib/asm/kernels/transaction/api.masm @@ -386,7 +386,7 @@ end export.account_vault_get_balance # get the vault root exec.memory::get_acct_vault_root_ptr movdn.2 - # => [faucet_id_hi, faucet_id_lo, acct_vault_root_ptr, pad(13)] + # => [faucet_id_hi, faucet_id_lo, acct_vault_root_ptr, pad(14)] # get the asset balance exec.asset_vault::get_balance @@ -570,7 +570,7 @@ export.get_note_sender # => [sender_hi, sender_lo, pad(16)] # truncate the stack - swapw dropw + movup.2 drop movup.2 drop # => [sender_hi, sender_lo, pad(14)] end @@ -895,7 +895,7 @@ end #! - foreign_account_id_{hi,lo} are the first and second felt of the ID of the foreign account #! whose procedure is going to be executed. #! - FOREIGN_ACCOUNT_ID is the word constructed from the foreign_account_id as follows: -#! [foreign_account_id_hi, foreign_account_lo, 0, 0]. +#! [foreign_account_lo, foreign_account_id_hi, 0, 0]. #! - account_nonce is the nonce of the foreign account. #! - VAULT_ROOT is the commitment of the foreign account's vault. #! - STORAGE_ROOT is the commitment of the foreign account's storage. @@ -926,12 +926,12 @@ export.start_foreign_context # OS => [foreign_account_id_hi, foreign_account_id_lo, pad(14)] # construct the word with account ID to load the core account data from the advice map - swap push.0.0 - # OS => [0, 0, foreign_account_id_lo, foreign_account_id_hi, pad(14)] + push.0.0 + # OS => [0, 0, foreign_account_id_hi, foreign_account_id_lo, pad(14)] # move the core account data to the advice stack adv.push_mapval - # OS => [0, 0, foreign_account_id_lo, foreign_account_id_hi, pad(14)] + # OS => [0, 0, foreign_account_id_hi, foreign_account_id_lo, pad(14)] # AS => [[foreign_account_id_hi, foreign_account_lo, 0, account_nonce], VAULT_ROOT, STORAGE_ROOT, CODE_ROOT] # store the id and nonce of the foreign account to the memory diff --git a/miden-lib/asm/kernels/transaction/lib/account.masm b/miden-lib/asm/kernels/transaction/lib/account.masm index 1dd46e278..ea4c01573 100644 --- a/miden-lib/asm/kernels/transaction/lib/account.masm +++ b/miden-lib/asm/kernels/transaction/lib/account.masm @@ -736,11 +736,11 @@ end #! Validates that the account seed, provided via the advice map, satisfies the seed requirements. #! #! Validation is performed via the following steps: -#! 1. Compute the hash of (SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_HASH). -#! 2. Assert the least significant element of the digest is equal to the account id of the account -#! the transaction is being executed against. -#! 3. Assert the most significant element has sufficient proof of work (trailing zeros) for the -#! account type the transaction is being executed against. +#! 1. Retrieve the anchor block hash by computing the block number of the anchor block and +#! retrieving it from the chain mmr. +#! 2. Compute the hash of (SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_HASH). +#! 3. Assert the two least significant elements of the digest are equal to the account id of the +#! account the transaction is being executed against. #! #! Inputs: [] #! Outputs: [] diff --git a/miden-lib/asm/kernels/transaction/lib/asset_vault.masm b/miden-lib/asm/kernels/transaction/lib/asset_vault.masm index a491d3dad..cbba6e040 100644 --- a/miden-lib/asm/kernels/transaction/lib/asset_vault.masm +++ b/miden-lib/asm/kernels/transaction/lib/asset_vault.masm @@ -432,10 +432,7 @@ end #! - ASSET is the non-fungible asset for which the vault key is built. #! - ASSET_KEY is the vault key of the non-fungible asset. proc.build_non_fungible_asset_vault_key - # Create the asset key from the non-fungible asset. - # --------------------------------------------------------------------------------------------- - - # swap hash0 with faucet id + # create the asset key from the non-fungible asset by swapping hash0 with the faucet id # => [faucet_id_hi, hash2, hash1, hash0] swap.3 # => [hash0, hash2, hash1 faucet_id_hi] diff --git a/miden-lib/src/notes/utils.rs b/miden-lib/src/notes/utils.rs index 31cdf0946..8491a56ca 100644 --- a/miden-lib/src/notes/utils.rs +++ b/miden-lib/src/notes/utils.rs @@ -76,7 +76,7 @@ mod tests { let offered_asset = Asset::Fungible( FungibleAsset::new( - AccountId::new_dummy( + AccountId::dummy( fungible_faucet_id_bytes, AccountType::FungibleFaucet, AccountStorageMode::Public, @@ -89,7 +89,7 @@ mod tests { let requested_asset = Asset::NonFungible( NonFungibleAsset::new( &NonFungibleAssetDetails::new( - AccountId::new_dummy( + AccountId::dummy( non_fungible_faucet_id_bytes, AccountType::NonFungibleFaucet, AccountStorageMode::Public, diff --git a/miden-lib/src/transaction/mod.rs b/miden-lib/src/transaction/mod.rs index 3306270c6..965a26acc 100644 --- a/miden-lib/src/transaction/mod.rs +++ b/miden-lib/src/transaction/mod.rs @@ -179,8 +179,9 @@ impl TransactionKernel { let account_id = account_header.id(); let storage_root = account_header.storage_commitment(); let code_root = account_header.code_commitment(); + // Note: keep in sync with the start_foreign_context kernel procedure let account_key = - Digest::from([account_id.first_felt(), account_id.second_felt(), ZERO, ZERO]); + Digest::from([account_id.second_felt(), account_id.first_felt(), ZERO, ZERO]); // Extend the advice inputs with the new data advice_inputs.extend_map([ diff --git a/miden-lib/src/transaction/procedures/kernel_v0.rs b/miden-lib/src/transaction/procedures/kernel_v0.rs index d7bbf7d4b..de041551c 100644 --- a/miden-lib/src/transaction/procedures/kernel_v0.rs +++ b/miden-lib/src/transaction/procedures/kernel_v0.rs @@ -54,7 +54,7 @@ pub const KERNEL0_PROCEDURES: [Digest; 33] = [ // get_note_inputs_hash digest!("0xe6209e99b726e1ad25b89e712b30bcfa3bad45feb47b81dc51a650b02b0dcbda"), // get_note_sender - digest!("0x9dfb0725ccb6c6f3a5c84bc11cc36e12da9631c1ae5eca40a1348fa5a97df80c"), + digest!("0xf5056720c0e58a9e3e1018d320bc04cd5c4a82ecb4563357f91688e03fe1b6cc"), // get_note_serial_number digest!("0xad91130ec219756213c6cadeaf8a38de8768e50c620cb7c347c531874c6054b6"), // get_script_hash @@ -66,7 +66,7 @@ pub const KERNEL0_PROCEDURES: [Digest; 33] = [ // get_block_number digest!("0x17da2a77b878820854bfff2b5f9eb969a4e2e76a998f97f4967b2b1a7696437c"), // start_foreign_context - digest!("0x8d40e8fc5f0efcc5824221577657811f9e71d7193a27ebdfb528b82204dfb511"), + digest!("0x8c3adb3aff1686205283a59d3908e074c1f4768ac4c7a778ef2b873693ca9101"), // end_foreign_context digest!("0x132b50feca8ecec10937c740640c59733e643e89c1d8304cf4523120e27a0428"), // update_expiration_block_num diff --git a/objects/Cargo.toml b/objects/Cargo.toml index 9681904c8..4696788c2 100644 --- a/objects/Cargo.toml +++ b/objects/Cargo.toml @@ -23,7 +23,7 @@ bench = false concurrent = ["std"] default = ["std"] std = ["assembly/std", "miden-crypto/std", "miden-verifier/std", "vm-core/std", "vm-processor/std"] -testing = ["dep:winter-rand-utils", "dep:rand"] +testing = ["dep:winter-rand-utils", "dep:rand", "dep:rand_xoshiro"] [dependencies] assembly = { workspace = true } @@ -31,6 +31,7 @@ log = { version = "0.4", optional = true } miden-crypto = { workspace = true } miden-verifier = { workspace = true } rand = { workspace = true, optional = true } +rand_xoshiro = { version = "0.6.0", default-features = false, optional = true } thiserror = { workspace = true } vm-core = { workspace = true } vm-processor = { workspace = true } diff --git a/objects/src/accounts/account_id.rs b/objects/src/accounts/account_id.rs index d3fe3de97..f9bff4c04 100644 --- a/objects/src/accounts/account_id.rs +++ b/objects/src/accounts/account_id.rs @@ -46,15 +46,17 @@ impl AccountType { } } -impl From for AccountType { - fn from(id: AccountId) -> Self { - id.account_type() - } -} - -impl From for AccountType { - fn from(id_prefix: AccountIdPrefix) -> Self { - id_prefix.account_type() +#[cfg(any(feature = "testing", test))] +impl rand::distributions::Distribution for rand::distributions::Standard { + /// Samples a uniformly random [`AccountType`] from the given `rng`. + fn sample(&self, rng: &mut R) -> AccountType { + match rng.gen_range(0..4) { + 0 => AccountType::RegularAccountImmutableCode, + 1 => AccountType::RegularAccountUpdatableCode, + 2 => AccountType::FungibleFaucet, + 3 => AccountType::NonFungibleFaucet, + _ => unreachable!("gen_range should not produce higher values"), + } } } @@ -108,15 +110,15 @@ impl FromStr for AccountStorageMode { } } -impl From for AccountStorageMode { - fn from(id: AccountId) -> Self { - id.storage_mode() - } -} - -impl From for AccountStorageMode { - fn from(id_prefix: AccountIdPrefix) -> Self { - id_prefix.storage_mode() +#[cfg(any(feature = "testing", test))] +impl rand::distributions::Distribution for rand::distributions::Standard { + /// Samples a uniformly random [`AccountStorageMode`] from the given `rng`. + fn sample(&self, rng: &mut R) -> AccountStorageMode { + match rng.gen_range(0..2) { + 0 => AccountStorageMode::Public, + 1 => AccountStorageMode::Private, + _ => unreachable!("gen_range should not produce higher values"), + } } } @@ -185,9 +187,9 @@ impl From for AccountIdVersion { /// block as an anchor - which is why it is also referred to as the anchor block - and creating the /// account's initial storage and code. Then a random seed is picked and the hash of (SEED, /// CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_HASH) is computed. If the hash's first element -/// has the desired storage mode, account type, version and the high bit set to zero, the -/// computation part of the ID generation is done. If not, another random seed is picked and the -/// process is repeated. The first felt of the ID is then the first element of the hash. +/// has the desired storage mode, account type and version, the computation part of the ID +/// generation is done. If not, another random seed is picked and the process is repeated. The first +/// felt of the ID is then the first element of the hash. /// /// The second felt of the ID is the second element of the hash. Its upper 16 bits are overwritten /// with the epoch in which the ID is anchored and the lower 8 bits are zeroed. Thus, the first felt @@ -354,7 +356,7 @@ impl AccountId { /// significant end of the ID. /// - In the second felt the anchor epoch is set to 0 and the lower 8 bits are cleared. #[cfg(any(feature = "testing", test))] - pub fn new_dummy( + pub fn dummy( mut bytes: [u8; 15], account_type: AccountType, storage_mode: AccountStorageMode, @@ -855,7 +857,7 @@ mod tests { AccountType::RegularAccountUpdatableCode, ] { for storage_mode in [AccountStorageMode::Private, AccountStorageMode::Public] { - let id = AccountId::new_dummy(input, account_type, storage_mode); + let id = AccountId::dummy(input, account_type, storage_mode); assert_eq!(id.account_type(), account_type); assert_eq!(id.storage_mode(), storage_mode); assert_eq!(id.version(), AccountIdVersion::VERSION_0); diff --git a/objects/src/accounts/account_id_anchor.rs b/objects/src/accounts/account_id_anchor.rs index 542d797de..ef5d3fcd5 100644 --- a/objects/src/accounts/account_id_anchor.rs +++ b/objects/src/accounts/account_id_anchor.rs @@ -27,6 +27,12 @@ impl AccountIdAnchor { /// A "pre-genesis" [`AccountIdAnchor`] which can be used to anchor accounts created in the /// genesis block. + /// + /// This anchor should only be used for accounts included in the genesis state, but should not + /// be used as actual anchors in a running network. The reason is that this anchor has the same + /// `epoch` as the genesis block will have (epoch `0`). However, the genesis block will have a + /// different block_hash than this anchor ([`EMPTY_WORD`]) and so any account ID that would use + /// this anchor would be rejected as invalid by the transaction kernel. pub const PRE_GENESIS: Self = Self { epoch: 0, block_hash: Digest::new(EMPTY_WORD), diff --git a/objects/src/accounts/account_id_prefix.rs b/objects/src/accounts/account_id_prefix.rs index 5124e2873..0a955a4df 100644 --- a/objects/src/accounts/account_id_prefix.rs +++ b/objects/src/accounts/account_id_prefix.rs @@ -251,7 +251,7 @@ mod tests { AccountType::RegularAccountUpdatableCode, ] { for storage_mode in [AccountStorageMode::Private, AccountStorageMode::Public] { - let id = AccountId::new_dummy(input, account_type, storage_mode); + let id = AccountId::dummy(input, account_type, storage_mode); let prefix = id.prefix(); assert_eq!(prefix.account_type(), account_type); assert_eq!(prefix.storage_mode(), storage_mode); diff --git a/objects/src/accounts/builder/mod.rs b/objects/src/accounts/builder/mod.rs index f419b5d44..c6683be54 100644 --- a/objects/src/accounts/builder/mod.rs +++ b/objects/src/accounts/builder/mod.rs @@ -235,7 +235,7 @@ impl AccountBuilder { let account_id = { let bytes = <[u8; 15]>::try_from(&init_seed[0..15]) .expect("we should have sliced exactly 15 bytes off"); - AccountId::new_dummy(bytes, self.account_type, self.storage_mode) + AccountId::dummy(bytes, self.account_type, self.storage_mode) }; Ok(Account::from_parts(account_id, vault, storage, code, Felt::ONE)) diff --git a/objects/src/accounts/delta/mod.rs b/objects/src/accounts/delta/mod.rs index a09720171..63421c522 100644 --- a/objects/src/accounts/delta/mod.rs +++ b/objects/src/accounts/delta/mod.rs @@ -287,7 +287,9 @@ mod tests { AccountStorageMode, AccountType, StorageMapDelta, }, assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, - testing::account_id::ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + testing::account_id::{ + AccountIdBuilder, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + }, ONE, ZERO, }; @@ -334,12 +336,11 @@ mod tests { let non_fungible: Asset = NonFungibleAsset::new( &NonFungibleAssetDetails::new( - AccountId::new_dummy( - [10; 15], - AccountType::NonFungibleFaucet, - AccountStorageMode::Public, - ) - .prefix(), + AccountIdBuilder::new() + .account_type(AccountType::NonFungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .build_with_rng(&mut rand::thread_rng()) + .prefix(), vec![6], ) .unwrap(), @@ -347,7 +348,10 @@ mod tests { .unwrap() .into(); let fungible_2: Asset = FungibleAsset::new( - AccountId::new_dummy([10; 15], AccountType::FungibleFaucet, AccountStorageMode::Public), + AccountIdBuilder::new() + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .build_with_rng(&mut rand::thread_rng()), 10, ) .unwrap() diff --git a/objects/src/assets/nonfungible.rs b/objects/src/assets/nonfungible.rs index 5dc978386..feaee7067 100644 --- a/objects/src/assets/nonfungible.rs +++ b/objects/src/assets/nonfungible.rs @@ -113,7 +113,7 @@ impl NonFungibleAsset { let mut vault_key = self.0; // Swap first felt of faucet ID with hash0. - vault_key.swap(0, 3); + vault_key.swap(0, FAUCET_ID_POS); // Set the fungible bit to zero by taking the bitwise `and` of the felt with the inverted // is_faucet mask. diff --git a/objects/src/notes/metadata.rs b/objects/src/notes/metadata.rs index f84fee277..f2c9d20cf 100644 --- a/objects/src/notes/metadata.rs +++ b/objects/src/notes/metadata.rs @@ -12,9 +12,8 @@ use super::{ /// /// Note type and tag must be internally consistent according to the following rules: /// -/// - For off-chain notes, the most significant bit of the tag must be 0. -/// - For public notes, the second most significant bit of the tag must be 0. -/// - For encrypted notes, two most significant bits of the tag must be 00. +/// - For private and encrypted notes, the two most significant bits of the tag must be `0b11`. +/// - For public notes, the two most significant bits of the tag can be set to any value. /// /// # Word layout & validity /// @@ -41,7 +40,7 @@ pub struct NoteMetadata { /// The ID of the account which created the note. sender: AccountId, - /// Defines how the note is to be stored (e.g., on-chain or off-chain). + /// Defines how the note is to be stored (e.g. public or private). note_type: NoteType, /// A value which can be used by the recipient(s) to identify notes intended for them. @@ -161,8 +160,6 @@ impl TryFrom for NoteMetadata { impl Serializable for NoteMetadata { fn write_into(&self, target: &mut W) { - // TODO: Do we need a serialization format that is different from the Word encoding? It was - // previously different. Word::from(self).write_into(target); } } diff --git a/objects/src/notes/note_tag.rs b/objects/src/notes/note_tag.rs index f1bed70d6..eaf8f6a0f 100644 --- a/objects/src/notes/note_tag.rs +++ b/objects/src/notes/note_tag.rs @@ -41,7 +41,7 @@ pub enum NoteExecutionMode { /// Tags are light-weight values used to speed up queries. The 2 most significant bits of the tags /// have the following interpretation: /// -/// | Prefix | Execution hint | Target | Allowed [NoteType] | +/// | Prefix | Execution mode | Target | Allowed [NoteType] | /// | ------ | :------------: | :------: | :----------------: | /// | `0b00` | Network | Specific | [NoteType::Public] | /// | `0b01` | Network | Use case | [NoteType::Public] | diff --git a/objects/src/testing/account_id.rs b/objects/src/testing/account_id.rs index 8784cb5d1..fcd065c52 100644 --- a/objects/src/testing/account_id.rs +++ b/objects/src/testing/account_id.rs @@ -1,3 +1,5 @@ +use rand::SeedableRng; + use crate::accounts::{AccountId, AccountStorageMode, AccountType}; // CONSTANTS @@ -120,3 +122,97 @@ pub const fn account_id( id } + +/// A builder for creating [`AccountId`]s for testing purposes. +/// +/// This is essentially a wrapper around [`AccountId::dummy`] generating random values as its input. +/// Refer to its documentation for details. +/// +/// # Example +/// +/// ``` +/// # use miden_objects::accounts::{AccountType, AccountStorageMode, AccountId}; +/// # use miden_objects::testing::account_id::{AccountIdBuilder}; +/// +/// let mut rng = rand::thread_rng(); +/// +/// // A random AccountId with random AccountType and AccountStorageMode. +/// let random_id1: AccountId = AccountIdBuilder::new().build_with_rng(&mut rng); +/// +/// // A random AccountId with the given AccountType and AccountStorageMode. +/// let random_id2: AccountId = AccountIdBuilder::new() +/// .account_type(AccountType::FungibleFaucet) +/// .storage_mode(AccountStorageMode::Public) +/// .build_with_rng(&mut rng); +/// assert_eq!(random_id2.account_type(), AccountType::FungibleFaucet); +/// assert_eq!(random_id2.storage_mode(), AccountStorageMode::Public); +/// ``` +pub struct AccountIdBuilder { + account_type: Option, + storage_mode: Option, +} + +impl AccountIdBuilder { + /// Creates a new [`AccountIdBuilder`]. + pub fn new() -> Self { + Self { account_type: None, storage_mode: None } + } + + /// Sets the [`AccountType`] of the generated [`AccountId`] to the provided value. + pub fn account_type(mut self, account_type: AccountType) -> Self { + self.account_type = Some(account_type); + self + } + + /// Sets the [`AccountStorageMode`] of the generated [`AccountId`] to the provided value. + pub fn storage_mode(mut self, storage_mode: AccountStorageMode) -> Self { + self.storage_mode = Some(storage_mode); + self + } + + /// Builds an [`AccountId`] using the provided [`rand::Rng`]. + /// + /// If no [`AccountType`] or [`AccountStorageMode`] were previously set, random ones are + /// generated. + pub fn build_with_rng(self, rng: &mut R) -> AccountId { + let account_type = match self.account_type { + Some(account_type) => account_type, + None => rng.gen(), + }; + + let storage_mode = match self.storage_mode { + Some(storage_mode) => storage_mode, + None => rng.gen(), + }; + + AccountId::dummy(rng.gen(), account_type, storage_mode) + } + + /// Builds an [`AccountId`] using the provided seed as input for an RNG implemented in + /// [`rand_xoshiro`]. + /// + /// If no [`AccountType`] or [`AccountStorageMode`] were previously set, random ones are + /// generated. + pub fn build_with_seed(self, rng_seed: [u8; 32]) -> AccountId { + // Match the implementation of rand::rngs::SmallRng and use different RNGs depending on the + // platform. + #[cfg(not(target_pointer_width = "64"))] + let mut rng = { + let rng_seed: [u8; 16] = rng_seed.as_slice()[0..16] + .try_into() + .expect("we should have sliced off 16 elements"); + rand_xoshiro::Xoshiro128PlusPlus::from_seed(rng_seed) + }; + + #[cfg(target_pointer_width = "64")] + let mut rng = rand_xoshiro::Xoshiro256PlusPlus::from_seed(rng_seed); + + self.build_with_rng(&mut rng) + } +} + +impl Default for AccountIdBuilder { + fn default() -> Self { + Self::new() + } +}