From 0d76887a1e6dc331d00749d82d629fae64dc7c49 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Fri, 31 Jul 2026 12:22:17 -0300 Subject: [PATCH] chore: update protocol --- Cargo.lock | 283 +++++++++++- Cargo.toml | 12 +- bin/benchmark/src/create_proofs.rs | 4 +- bin/network-monitor/Cargo.toml | 4 + bin/network-monitor/src/counter.rs | 379 +++++++++++++---- bin/network-monitor/src/deploy/counter.rs | 151 ++++++- bin/network-monitor/src/deploy/mod.rs | 401 ++++++++++++++++-- bin/network-monitor/src/deploy/wallet.rs | 2 +- bin/network-monitor/src/monitor/tasks.rs | 3 +- bin/ntx-builder/src/actor/allowlist.rs | 23 +- bin/ntx-builder/src/test_utils.rs | 19 +- bin/remote-prover/src/server/tests.rs | 8 +- bin/stress-test/src/seeding/mod.rs | 4 +- .../src/server/validator_service/tests.rs | 8 +- crates/rpc/src/tests.rs | 4 +- .../store/src/account_state_forest/tests.rs | 2 +- .../store/src/db/models/queries/accounts.rs | 26 +- .../db/models/queries/accounts/delta/tests.rs | 14 +- .../src/db/models/queries/accounts/tests.rs | 14 +- crates/store/src/db/tests.rs | 18 +- crates/store/src/genesis/config/mod.rs | 2 +- crates/store/src/genesis/config/tests.rs | 2 +- 22 files changed, 1166 insertions(+), 217 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ea97c773..2bff3b4ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,80 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alloy-primitives" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f007e257069855bdf21d27762fd3f3705a613f805c9a08309bf353503f081d71" +dependencies = [ + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "itoa", + "paste", + "ruint", + "sha3 0.11.0", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5655c38d5f84955bf727b2eeb62fddd91ebb98fd1d7ae6eb77f73ea88f9b9cf" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6277c780e07b76951e09a59788dde230d1582612324177d11a43a61e21a6bb83" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap", + "proc-macro-error3", + "proc-macro2", + "quote", + "sha3 0.11.0", + "syn 2.0.119", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9762b2ad3e5a0c09886de54fe549ab0056681df843cb082e2df7e1c0eb270d30" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.119", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-types" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96e74d6213180f78dbdccddce8af02a639c160c94b0a543fa35c77c58b8a7fc" +dependencies = [ + "alloy-primitives", + "alloy-sol-macro", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1096,6 +1170,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.10.2" @@ -1108,6 +1194,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1489,10 +1584,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", "rustc_version 0.4.1", "syn 2.0.119", + "unicode-xid", ] [[package]] @@ -1782,6 +1879,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -3044,6 +3150,17 @@ dependencies = [ "libc", ] +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "matchers" version = "0.2.0" @@ -3118,6 +3235,28 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "miden-agglayer" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" +dependencies = [ + "alloy-sol-types", + "fs-err", + "miden-assembly", + "miden-core", + "miden-core-lib", + "miden-crypto", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "miden-protocol", + "miden-standards", + "miden-utils-sync", + "regex", + "thiserror 2.0.19", + "walkdir", +] + [[package]] name = "miden-air" version = "0.25.7" @@ -3208,9 +3347,8 @@ dependencies = [ [[package]] name = "miden-block-prover" -version = "0.16.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616b8fa0652e25c5711bcd01f6f1f50e803a01db962e5af6b4a2bd7612dfde78" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" dependencies = [ "miden-protocol", "thiserror 2.0.19", @@ -3457,6 +3595,7 @@ dependencies = [ "miden-node-utils", "miden-protocol", "miden-standards", + "miden-testing", "miden-tx", "rand 0.10.2", "rand_chacha 0.10.0", @@ -3818,9 +3957,8 @@ dependencies = [ [[package]] name = "miden-protocol" -version = "0.16.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5fd2fec04e0651a9ad7886541afc1bd6d8ee0de91a2850e9f4ead829c0fe3f" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" dependencies = [ "bech32", "fs-err", @@ -3911,9 +4049,8 @@ dependencies = [ [[package]] name = "miden-standards" -version = "0.16.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f431c2bcd8b63ee45d12155565a23a9abc78240663d275e0e95eb475c9ac6438" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" dependencies = [ "bon", "fs-err", @@ -3923,6 +4060,7 @@ dependencies = [ "miden-package-registry", "miden-project", "miden-protocol", + "primitive-types", "rand 0.10.2", "regex", "thiserror 2.0.19", @@ -3953,12 +4091,11 @@ dependencies = [ [[package]] name = "miden-testing" -version = "0.16.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610ba28ab9d73da0fcde6aa2389b2029e096df0c7248fde558d7d44be67ec8f7" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.15.0", "miden-block-prover", "miden-core-lib", "miden-crypto", @@ -3974,10 +4111,11 @@ dependencies = [ [[package]] name = "miden-tx" -version = "0.16.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eff35d682e8e4266bbe314e3b95f98ce5fea515bdc80573b907daaba937e8c1" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" dependencies = [ + "bon", + "miden-agglayer", "miden-processor", "miden-protocol", "miden-prover", @@ -3987,9 +4125,8 @@ dependencies = [ [[package]] name = "miden-tx-batch" -version = "0.16.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bec9ae6f93ad985a86999e81cb90a7e11090a7ea25eed661f85e384bee2bb9" +version = "0.16.0-beta.1" +source = "git+https://github.com/0xMiden/protocol.git?branch=next#d951e7a9fa746f4177e8b1207076266e811830f7" dependencies = [ "miden-processor", "miden-protocol", @@ -4843,6 +4980,16 @@ dependencies = [ "wnaf", ] +[[package]] +name = "primitive-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721a1da530b5a2633218dc9f75713394c983c352be88d2d7c9ee85e2c4c21794" +dependencies = [ + "fixed-hash", + "uint", +] + [[package]] name = "priority-queue" version = "2.7.0" @@ -4863,6 +5010,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr3" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82366fd7d8b7a440d66d13418820c69df9b3908bcb1a0476d7f5ce5d12f5a04d" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error3" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b511283ea8a74b4b39447b128c5d00f03a356b7424554b13e298a5550100d9ac" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -5139,6 +5308,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.5" @@ -5434,6 +5612,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ruint" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45caf26f647c19115bf9c453c70ffe4a4a3a6390dceebd942610584f99b8ddce" +dependencies = [ + "proptest", + "rand 0.8.7", + "rand 0.9.5", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rusqlite" version = "0.37.0" @@ -5898,6 +6097,16 @@ dependencies = [ "keccak 0.1.6", ] +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + [[package]] name = "sha3" version = "0.12.0" @@ -6134,6 +6343,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "083be3061e64d362cbe6ef12cfe1307ba3884326d8856448fe8a120fa2c44ebf" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6814,6 +7035,18 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" @@ -6838,6 +7071,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-width" version = "0.1.14" @@ -6850,6 +7089,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index ca34e8077..f587edc5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,12 +54,12 @@ miden-node-tracing-macro = { path = "crates/tracing-macro", version = "0.16.0 miden-node-utils = { path = "crates/utils", version = "0.16.0-alpha.3" } # miden-protocol dependencies. These should be updated in sync. -miden-block-prover = { version = "=0.16.0-alpha.2" } -miden-protocol = { default-features = false, version = "=0.16.0-alpha.2" } -miden-standards = { version = "=0.16.0-alpha.2" } -miden-testing = { version = "=0.16.0-alpha.2" } -miden-tx = { default-features = false, version = "=0.16.0-alpha.2" } -miden-tx-batch = { version = "=0.16.0-alpha.2" } +miden-block-prover = { branch = "next", git = "https://github.com/0xMiden/protocol.git" } +miden-protocol = { branch = "next", default-features = false, git = "https://github.com/0xMiden/protocol.git" } +miden-standards = { branch = "next", git = "https://github.com/0xMiden/protocol.git" } +miden-testing = { branch = "next", git = "https://github.com/0xMiden/protocol.git" } +miden-tx = { branch = "next", default-features = false, git = "https://github.com/0xMiden/protocol.git" } +miden-tx-batch = { branch = "next", git = "https://github.com/0xMiden/protocol.git" } # Other miden dependencies. These should align with those expected by miden-protocol. miden-crypto = { version = "0.28" } diff --git a/bin/benchmark/src/create_proofs.rs b/bin/benchmark/src/create_proofs.rs index 5848977ea..edc5cea5c 100644 --- a/bin/benchmark/src/create_proofs.rs +++ b/bin/benchmark/src/create_proofs.rs @@ -429,7 +429,7 @@ fn create_faucet() -> (Account, SecretKey) { .active_burn_policy(BurnPolicy::allow_all()) .build(), ) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( key_pair.public_key().into(), AuthScheme::Falcon512Poseidon2, ))) @@ -447,7 +447,7 @@ fn create_wallet( let init_seed: Vec<_> = index.to_be_bytes().into_iter().chain([0u8; 24]).collect(); AccountBuilder::new(init_seed.try_into().unwrap()) .account_type(AccountType::Private) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( public_key.clone().into(), AuthScheme::Falcon512Poseidon2, ))) diff --git a/bin/network-monitor/Cargo.toml b/bin/network-monitor/Cargo.toml index 659a2fea3..88a41507f 100644 --- a/bin/network-monitor/Cargo.toml +++ b/bin/network-monitor/Cargo.toml @@ -41,3 +41,7 @@ tonic = { features = ["codegen", "tls-native-roots", "transport"], tonic-health = { workspace = true } tracing = { workspace = true } url = { features = ["serde"], workspace = true } + +[dev-dependencies] +miden-protocol = { features = ["std", "testing"], workspace = true } +miden-testing = { workspace = true } diff --git a/bin/network-monitor/src/counter.rs b/bin/network-monitor/src/counter.rs index 39773c36c..37c59f59b 100644 --- a/bin/network-monitor/src/counter.rs +++ b/bin/network-monitor/src/counter.rs @@ -9,13 +9,12 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result}; use miden_node_proto::clients::RpcClient; -use miden_node_proto::generated::rpc::BlockHeaderByNumberRequest; use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_node_utils::tracing::miden_instrument; use miden_protocol::account::auth::AuthSecretKey; use miden_protocol::account::{Account, AccountCode, AccountId, AccountPatch}; use miden_protocol::asset::AssetVault; -use miden_protocol::block::{BlockHeader, BlockNumber}; +use miden_protocol::block::BlockNumber; use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey; use miden_protocol::note::{ Note, @@ -29,14 +28,10 @@ use miden_protocol::note::{ PartialNote, PartialNoteMetadata, }; -use miden_protocol::transaction::{ - InputNotes, - PartialBlockchain, - TransactionArgs, - TransactionScript, -}; +use miden_protocol::transaction::{InputNotes, TransactionArgs, TransactionScript}; use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_protocol::{Felt, Word}; +use miden_standards::account::auth::{FeeConversionInfo, commit_fee_conversion_info}; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint}; use miden_tx::auth::BasicAuthenticator; @@ -50,6 +45,7 @@ use crate::config::MonitorConfig; use crate::deploy::counter::COUNTER_SLOT_NAME; use crate::deploy::wallet::WALLET_COUNTER_SLOT_NAME; use crate::deploy::{ + CounterAnchor, DeployedMonitorAccounts, MonitorDataStore, TransactionSubmissionClient, @@ -72,9 +68,12 @@ const RESYNC_FAILURE_THRESHOLD: usize = 3; /// Number of consecutive increment failures before regenerating accounts from scratch. const REGENERATE_FAILURE_THRESHOLD: usize = 10; -/// Minimum time between account regeneration attempts. +/// Minimum time between account regeneration attempts once one has succeeded. const REGENERATE_COOLDOWN: Duration = Duration::from_hours(1); +/// Minimum time before retrying a regeneration attempt that *failed*. +const REGENERATE_RETRY_COOLDOWN: Duration = Duration::from_mins(1); + /// Number of consecutive polls observing the pending-increments gap above /// [`MonitorConfig::counter_pending_unhealthy_threshold`] before flipping the Network Transactions /// card to unhealthy. Buffers against a single in-flight batch of notes flapping the card. @@ -109,10 +108,10 @@ pub struct TrackedAccounts { /// Produced by [`setup_increment_task`]. struct TxBuilder { wallet_account: Account, - counter_account: Account, + counter_id: AccountId, secret_key: SecretKey, increment_script: NoteScript, - block_header: BlockHeader, + counter_anchor: Arc, rng: ChaCha20Rng, } @@ -120,10 +119,22 @@ struct TxBuilder { // ================================================================================================ /// Tracks consecutive increment failures and gates re-sync / regeneration actions. -#[derive(Default)] struct FailureTracker { consecutive_failures: usize, - last_regeneration: Option, + resynced_this_streak: bool, + last_regeneration_attempt: Option, + regeneration_cooldown: Duration, +} + +impl Default for FailureTracker { + fn default() -> Self { + Self { + consecutive_failures: 0, + resynced_this_streak: false, + last_regeneration_attempt: None, + regeneration_cooldown: REGENERATE_COOLDOWN, + } + } } impl FailureTracker { @@ -133,19 +144,35 @@ impl FailureTracker { fn reset(&mut self) { self.consecutive_failures = 0; + self.resynced_this_streak = false; } fn should_resync(&self) -> bool { - self.consecutive_failures >= RESYNC_FAILURE_THRESHOLD + self.consecutive_failures >= RESYNC_FAILURE_THRESHOLD && !self.resynced_this_streak + } + + fn mark_resynced(&mut self) { + self.resynced_this_streak = true; } fn should_regenerate(&self) -> bool { self.consecutive_failures >= REGENERATE_FAILURE_THRESHOLD - && self.last_regeneration.is_none_or(|t| t.elapsed() >= REGENERATE_COOLDOWN) + && self + .last_regeneration_attempt + .is_none_or(|t| t.elapsed() >= self.regeneration_cooldown) } - fn mark_regenerated(&mut self) { - self.last_regeneration = Some(Instant::now()); + /// Records that a regeneration attempt is starting. + fn mark_regeneration_attempt(&mut self) { + self.last_regeneration_attempt = Some(Instant::now()); + self.regeneration_cooldown = REGENERATE_COOLDOWN; + } + + /// Records that the regeneration attempt failed: shorten its cooldown and re-arm the re-sync + /// path, so the streak is not left with both recovery paths disabled. + fn mark_regeneration_failed(&mut self) { + self.regeneration_cooldown = REGENERATE_RETRY_COOLDOWN; + self.resynced_this_streak = false; } } @@ -174,7 +201,7 @@ impl IncrementService { /// [`crate::monitor::tasks`]. pub const NAME: &'static str = "Local Transactions"; - pub async fn new( + pub fn new( config: MonitorConfig, accounts: DeployedMonitorAccounts, prover: LocalTransactionProver, @@ -182,14 +209,13 @@ impl IncrementService { accounts_sender: watch::Sender, latency_state: Arc>, ) -> Result { - let mut rpc_client = submission_client.rpc_client(); + let rpc_client = submission_client.rpc_client(); let (tx, details) = setup_increment_task( accounts.wallet, accounts.secret_key, - accounts.counter, - &mut rpc_client, - ) - .await?; + accounts.counter.id(), + accounts.counter_anchor, + )?; Ok(Self { config, rpc_client, @@ -301,10 +327,9 @@ impl IncrementService { let (tx, details) = setup_increment_task( accounts.wallet, accounts.secret_key, - accounts.counter, - &mut self.rpc_client, - ) - .await?; + accounts.counter.id(), + accounts.counter_anchor, + )?; self.tx = tx; self.details = details; @@ -329,7 +354,7 @@ impl IncrementService { async fn submit_increment(&mut self) -> Result<(String, AccountPatch, BlockNumber)> { let (network_note, note_recipient) = create_network_note( &self.tx.wallet_account, - &self.tx.counter_account, + self.tx.counter_id, self.tx.increment_script.clone(), &mut self.tx.rng, )?; @@ -340,19 +365,25 @@ impl IncrementService { let script = create_increment_tx_script(&network_note)?; let mut tx_args = TransactionArgs::default().with_tx_script(script); + let (auth_args, conversion_info_preimage) = fee_conversion_auth_args( + self.tx.counter_anchor.block_header.fee_parameters().fee_faucet_id(), + &mut self.tx.rng, + ); + tx_args = tx_args.with_auth_args(auth_args); + tx_args.extend_advice_map([(auth_args, conversion_info_preimage)]); tx_args.add_output_note_recipient(Box::new(note_recipient)); let wallet_account = self.tx.wallet_account.clone(); - let counter_account = self.tx.counter_account.clone(); - let block_header = self.tx.block_header.clone(); + let anchor = self.tx.counter_anchor.clone(); let secret_key = self.tx.secret_key.clone(); let prover = self.prover.clone(); let (proven_tx, tx_inputs, account_patch) = spawn_blocking_in_current_span(move || { let account_id = wallet_account.id(); - let block_num = block_header.block_num(); - let mut data_store = MonitorDataStore::new(block_header, PartialBlockchain::default()); + let block_num = anchor.block_header.block_num(); + let mut data_store = + MonitorDataStore::new(anchor.block_header.clone(), anchor.blockchain.clone()); data_store.add_account(wallet_account); - data_store.add_account(counter_account); + data_store.add_foreign_account(anchor.counter_account.clone(), anchor.witness.clone()); let authenticator = BasicAuthenticator::new(&[AuthSecretKey::Falcon512Poseidon2(secret_key)]); @@ -423,20 +454,23 @@ impl Service for IncrementService { self.failures.record_failure(); last_error = Some(format!("create/submit note failed: {e}")); - if self.failures.should_resync() && self.try_resync_wallet_account().await.is_ok() { - self.failures.reset(); + let resynced_now = + self.failures.should_resync() && self.try_resync_wallet_account().await.is_ok(); + if resynced_now { + self.failures.mark_resynced(); } - if self.failures.should_regenerate() { + if !resynced_now && self.failures.should_regenerate() { warn!( target: LOG_TARGET, consecutive_failures = self.failures.consecutive_failures, "re-sync ineffective, regenerating accounts from scratch" ); - self.failures.mark_regenerated(); + self.failures.mark_regeneration_attempt(); match self.try_regenerate_accounts().await { Ok(()) => self.failures.reset(), Err(regen_err) => { + self.failures.mark_regeneration_failed(); error!(target: LOG_TARGET, error = ?regen_err, "Account regeneration failed"); }, } @@ -702,25 +736,23 @@ impl CounterTrackingService { // SETUP // ================================================================================================ -/// Fetch the genesis block header and build the data store + increment script needed to produce -/// network notes from a freshly-created wallet/counter pair. The accounts are passed in already -/// constructed by [`create_and_deploy_accounts`]; there is no file I/O. -async fn setup_increment_task( +/// Build the increment script and transaction state needed to produce network notes from a +/// freshly-created wallet/counter pair. The accounts and the counter's FPI anchor are passed in +/// already constructed by [`create_and_deploy_accounts`]; there is no file I/O. +fn setup_increment_task( wallet_account: Account, secret_key: SecretKey, - counter_account: Account, - rpc_client: &mut RpcClient, + counter_id: AccountId, + counter_anchor: CounterAnchor, ) -> Result<(TxBuilder, IncrementDetails)> { - let block_header = get_genesis_block_header(rpc_client).await?; - let increment_script = create_increment_script()?; let tx = TxBuilder { wallet_account, - counter_account, + counter_id, secret_key, increment_script, - block_header, + counter_anchor: Arc::new(counter_anchor), rng: ChaCha20Rng::from_rng(&mut rand::rng()), }; @@ -841,29 +873,6 @@ fn update_expected_and_pending( // RPC HELPERS // ================================================================================================ -/// Get the genesis block header. -async fn get_genesis_block_header(rpc_client: &mut RpcClient) -> Result { - let block_header_request = BlockHeaderByNumberRequest { - block_num: Some(BlockNumber::GENESIS.as_u32()), - include_mmr_proof: None, - }; - - let response = rpc_client - .get_block_header_by_number(block_header_request) - .await - .context("Failed to get genesis block header from RPC")? - .into_inner(); - - let genesis_block_header = response - .block_header - .ok_or_else(|| anyhow::anyhow!("No block header in response"))?; - - let block_header: BlockHeader = - genesis_block_header.try_into().context("Failed to convert block header")?; - - Ok(block_header) -} - /// Fetch the storage header of the given account from RPC. /// /// Returns `None` if the account does not exist or has no details available. @@ -1180,7 +1189,7 @@ fn create_increment_tx_script(network_note: &Note) -> Result ); let mut code_builder = CodeBuilder::new() - .with_dynamically_linked_library(&wallet_component) + .with_dynamically_linked_package(&wallet_component) .context("Failed to dynamically link wallet counter component")?; // The note's attachments (e.g. the network-account target) are resolved at runtime from the @@ -1196,26 +1205,40 @@ fn create_increment_tx_script(network_note: &Note) -> Result Ok(tx_script) } +/// Build the auth args committing to paying the transaction fee in the chain's native fee asset at +/// rate 1/1, together with the advice-map preimage `miden::standards::fee::load_conversion_info` +/// verifies against them in-VM. +fn fee_conversion_auth_args(fee_faucet_id: AccountId, rng: &mut ChaCha20Rng) -> (Word, Vec) { + // The salt keeps the auth args usable as a per-transaction unique value for replay protection. + let salt = random_word(rng); + commit_fee_conversion_info(FeeConversionInfo::one_to_one(fee_faucet_id), salt) +} + +/// Draw a random [`Word`] from `rng`. +fn random_word(rng: &mut ChaCha20Rng) -> Word { + Word::new([ + Felt::new_unchecked(rng.random()), + Felt::new_unchecked(rng.random()), + Felt::new_unchecked(rng.random()), + Felt::new_unchecked(rng.random()), + ]) +} + /// Create a network note that targets the counter account. fn create_network_note( wallet_account: &Account, - counter_account: &Account, + counter_account_id: AccountId, script: NoteScript, rng: &mut ChaCha20Rng, ) -> Result<(Note, NoteRecipient)> { - let target = NetworkAccountTarget::new(counter_account.id(), NoteExecutionHint::Always) + let target = NetworkAccountTarget::new(counter_account_id, NoteExecutionHint::Always) .context("Failed to create NetworkAccountTarget for counter account")?; let attachment: NoteAttachment = target.into(); let attachments = NoteAttachments::from(attachment); let partial_metadata = PartialNoteMetadata::new(wallet_account.id(), NoteType::Public); - let serial_num = Word::new([ - Felt::new_unchecked(rng.random()), - Felt::new_unchecked(rng.random()), - Felt::new_unchecked(rng.random()), - Felt::new_unchecked(rng.random()), - ]); + let serial_num = random_word(rng); let recipient = NoteRecipient::new(serial_num, script, NoteStorage::new(vec![])?); @@ -1244,22 +1267,213 @@ async fn fetch_chain_tip(rpc_client: &mut RpcClient) -> Result { #[cfg(test)] mod tests { + use miden_protocol::account::Account; + use miden_protocol::account::auth::AuthSecretKey; + use miden_protocol::asset::FungibleAsset; + use miden_protocol::transaction::{InputNotes, TransactionArgs}; + use miden_testing::MockChain; + use miden_tx::TransactionExecutor; + use miden_tx::auth::BasicAuthenticator; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use crate::counter::{ + FailureTracker, PENDING_UNHEALTHY_CONFIRMATION_POLLS, + REGENERATE_FAILURE_THRESHOLD, + RESYNC_FAILURE_THRESHOLD, build_tracking_status, create_increment_script, create_increment_tx_script, create_network_note, + fee_conversion_auth_args, }; use crate::deploy::counter::create_counter_account; - use crate::deploy::wallet::create_wallet_account; + use crate::deploy::wallet::{WALLET_COUNTER_SLOT_NAME, create_wallet_account}; + use crate::deploy::{MonitorDataStore, execute_counter_genesis_tx}; use crate::status::{CounterTrackingDetails, Status}; const THRESHOLD: u64 = 5; + /// Executes one increment transaction end to end against a chain that holds the deployed + /// counter account. + #[tokio::test] + async fn increment_transaction_executes_against_the_committed_counter() -> anyhow::Result<()> { + let fee_faucet_id = FungibleAsset::mock_issuer(); + let (wallet, secret_key) = create_wallet_account()?; + let counter = create_counter_account(wallet.id(), fee_faucet_id)?; + + // The counter reaches the chain through its own creation transaction, so the chain must + // hold the committed (post-creation) state, exactly as `resolve_counter_anchor` requires + // on-chain. + let bootstrap_chain = MockChain::builder().fee_faucet_id(fee_faucet_id).build()?; + let creation_tx = + execute_counter_genesis_tx(&counter, &bootstrap_chain.genesis_block_header()).await?; + let committed_counter = Account::try_from(creation_tx.account_patch())?; + + let mut builder = MockChain::builder().fee_faucet_id(fee_faucet_id); + builder.add_account(committed_counter.clone())?; + let chain = builder.build()?; + + let block_header = chain.latest_block_header(); + let witness = chain + .account_witnesses([committed_counter.id()]) + .remove(&committed_counter.id()) + .expect("a witness was requested for the counter"); + assert_eq!( + witness.state_commitment(), + committed_counter.to_commitment(), + "the chain must hold the counter in the state fed to the executor" + ); + + let increment_script = create_increment_script()?; + let mut rng = ChaCha20Rng::from_seed([11u8; 32]); + let (network_note, note_recipient) = + create_network_note(&wallet, committed_counter.id(), increment_script, &mut rng)?; + let script = create_increment_tx_script(&network_note)?; + + let mut tx_args = TransactionArgs::default().with_tx_script(script); + let (auth_args, preimage) = + fee_conversion_auth_args(block_header.fee_parameters().fee_faucet_id(), &mut rng); + tx_args = tx_args.with_auth_args(auth_args); + tx_args.extend_advice_map([(auth_args, preimage)]); + tx_args.add_output_note_recipient(Box::new(note_recipient)); + + let mut data_store = + MonitorDataStore::new(block_header.clone(), chain.latest_partial_blockchain()); + data_store.add_account(wallet.clone()); + data_store.add_foreign_account(committed_counter, witness); + + let authenticator = + BasicAuthenticator::new(&[AuthSecretKey::Falcon512Poseidon2(secret_key)]); + let executor = TransactionExecutor::new(&data_store).with_authenticator(&authenticator); + + let executed_tx = executor + .execute_transaction( + wallet.id(), + block_header.block_num(), + InputNotes::default(), + tx_args, + ) + .await?; + + // The wallet's own procedure emits the increment note and bumps its counter slot + // atomically, so a successful execution must show both. + assert_eq!( + executed_tx.output_notes().num_notes(), + 1, + "the increment transaction must emit exactly the network note" + ); + let updated_wallet = Account::try_from(executed_tx.account_patch())?; + let counter_slot = updated_wallet.storage().get_item(&WALLET_COUNTER_SLOT_NAME)?; + assert_eq!( + counter_slot.as_elements()[0].as_canonical_u64(), + 1, + "the wallet's expected-value slot must be bumped by the same transaction" + ); + + Ok(()) + } + + /// A failed regeneration must not park the service with both recovery paths disabled: the full + /// cooldown only applies once an attempt has succeeded, and the re-sync path is re-armed so the + /// streak keeps trying to recover. + #[test] + fn failed_regeneration_leaves_a_recovery_path_open() { + let mut failures = FailureTracker::default(); + for _ in 0..REGENERATE_FAILURE_THRESHOLD { + failures.record_failure(); + } + failures.mark_resynced(); + assert!(failures.should_regenerate()); + + failures.mark_regeneration_attempt(); + failures.mark_regeneration_failed(); + + assert!( + failures.should_resync(), + "a failed regeneration must re-arm the cheaper re-sync path" + ); + assert_eq!( + failures.regeneration_cooldown, + super::REGENERATE_RETRY_COOLDOWN, + "a failed regeneration must be retried on the short cooldown, not the hourly one" + ); + } + + /// A successful regeneration keeps the hourly cooldown: it deploys a new counter account and + /// restarts the on-chain count, so it must not be repeated while its accounts are settling. + #[test] + fn successful_regeneration_keeps_the_full_cooldown() { + let mut failures = FailureTracker::default(); + for _ in 0..REGENERATE_FAILURE_THRESHOLD { + failures.record_failure(); + } + + failures.mark_regeneration_attempt(); + failures.reset(); + + assert_eq!(failures.regeneration_cooldown, super::REGENERATE_COOLDOWN); + assert!(!failures.should_regenerate(), "a cleared streak must not regenerate"); + + for _ in 0..REGENERATE_FAILURE_THRESHOLD { + failures.record_failure(); + } + assert!( + !failures.should_regenerate(), + "a fresh failure streak must still wait out the hourly cooldown" + ); + } + + /// Recovery must escalate: re-sync the wallet, then regenerate if failures keep coming. A + /// re-sync that reset the counter would cap it below the regeneration threshold forever, + /// leaving no recovery path for a counter account or FPI anchor invalidated by a chain reset. + #[test] + fn recovery_escalates_from_resync_to_regeneration() { + let mut failures = FailureTracker::default(); + + for _ in 0..RESYNC_FAILURE_THRESHOLD { + failures.record_failure(); + } + assert!(failures.should_resync(), "a re-sync should be attempted at the threshold"); + assert!(!failures.should_regenerate()); + + // A re-sync that succeeded must not repeat, so the counter can climb. + failures.mark_resynced(); + failures.record_failure(); + assert!(!failures.should_resync(), "a successful re-sync must not repeat"); + + while failures.consecutive_failures < REGENERATE_FAILURE_THRESHOLD { + failures.record_failure(); + } + assert!(failures.should_regenerate(), "regeneration must become reachable"); + + // A successful increment clears the escalation. + failures.reset(); + assert!(!failures.should_resync()); + assert!(!failures.should_regenerate()); + } + + /// A re-sync that *fails* must be retried, since the transient RPC errors that make a re-sync + /// fail are the same ones that make increments fail. Gating on the attempt rather than on its + /// success would forfeit re-sync for the whole streak after one blip. + #[test] + fn failed_resync_is_retried_on_the_next_failure() { + let mut failures = FailureTracker::default(); + + for _ in 0..RESYNC_FAILURE_THRESHOLD { + failures.record_failure(); + } + assert!(failures.should_resync()); + + // The re-sync attempt errored, so nothing is marked. + failures.record_failure(); + assert!(failures.should_resync(), "a failed re-sync must be retried"); + + failures.mark_resynced(); + assert!(!failures.should_resync()); + } + /// The wallet's self-counter component, the increment note script, and the combined increment /// transaction script must all assemble, and the tx script must link against the wallet's /// `increment` procedure. This guards the hand-authored MASM (which mirrors `miden-standards` @@ -1267,12 +1481,13 @@ mod tests { #[test] fn increment_masm_assembles_and_links() { let (wallet, _secret_key) = create_wallet_account().expect("wallet account should build"); - let counter = create_counter_account(wallet.id()).expect("counter account should build"); + let counter = create_counter_account(wallet.id(), FungibleAsset::mock_issuer()) + .expect("counter account should build"); let note_script = create_increment_script().expect("note script should compile"); let mut rng = ChaCha20Rng::from_seed([7u8; 32]); let (network_note, _recipient) = - create_network_note(&wallet, &counter, note_script, &mut rng) + create_network_note(&wallet, counter.id(), note_script, &mut rng) .expect("network note should build"); create_increment_tx_script(&network_note) diff --git a/bin/network-monitor/src/deploy/counter.rs b/bin/network-monitor/src/deploy/counter.rs index c6b9cbf2b..fa09ac4f1 100644 --- a/bin/network-monitor/src/deploy/counter.rs +++ b/bin/network-monitor/src/deploy/counter.rs @@ -14,9 +14,11 @@ use miden_protocol::account::{ StorageSlot, StorageSlotName, }; +use miden_protocol::asset::AssetAmount; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; use miden_standards::account::auth::AuthNetworkAccount; +use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager}; use miden_standards::code_builder::CodeBuilder; use miden_standards::tx_script::ExpirationTransactionScript; @@ -40,7 +42,10 @@ pub static COUNTER_SLOT_NAME: LazyLock = LazyLock::new(|| { skip_all, ret(level = "debug"), )] -pub fn create_counter_account(owner_account_id: AccountId) -> Result { +pub fn create_counter_account( + owner_account_id: AccountId, + fee_faucet_id: AccountId, +) -> Result { // Load and customize the MASM script let script = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/assets/counter_program.masm")); @@ -69,24 +74,142 @@ pub fn create_counter_account(owner_account_id: AccountId) -> Result { allowed_scripts.insert(increment_script.root()); - // Allowlist the canonical expiration tx script the ntx-builder attaches to every network - // transaction; without this the account's network transactions are rejected by the tx-script - // allowlist. The root is delta-independent, so the single canonical root covers any delta the - // ntx-builder is configured with. - let allowed_tx_scripts = BTreeSet::from_iter([ExpirationTransactionScript::script_root()]); - let network_account_auth: AccountComponent = - AuthNetworkAccount::with_allowed_notes(allowed_scripts) - .expect("list is not empty") - .with_allowed_tx_scripts(allowed_tx_scripts) - .into(); - - // Create the counter program account + // The account's auth procedure prices every note it consumes, and any transaction creating a + // note targeted at it prices that note through the same policy via FPI. Every allowlisted note + // script must have a schedule entry, since a script without one aborts fee estimation. + let fee_policy = BasicConstantFeePolicy::new() + .with_fees([(increment_script.root(), AssetAmount::ZERO)]) + .into(); + let fee_policy_manager = FeePolicyManager::builder() + .fee_faucet_id(fee_faucet_id) + .active_fee_policy(fee_policy) + .build(); + + let auth_component = AuthNetworkAccount::custom(allowed_scripts, fee_policy_manager)? + .with_allowed_tx_scripts([ExpirationTransactionScript::script_root()]); + let init_seed: [u8; 32] = rand::random(); let counter_account = AccountBuilder::new(init_seed) .account_type(AccountType::Public) + .with_components(auth_component) .with_component(account_code) - .with_auth_component(network_account_auth) .build()?; Ok(counter_account) } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::account::StorageMapKey; + use miden_protocol::asset::{AssetId, FungibleAsset}; + use miden_standards::account::auth::NetworkAccount; + use miden_standards::note::{FeeSponsorshipNote, NetworkAccountConfigNote}; + + use super::*; + use crate::deploy::wallet::create_wallet_account; + + /// Every note script the account allowlists must also be priced. `NetworkAccount::new` does not + /// look at fee-policy storage at all, so without this the fee wiring could be deleted whole and + /// every other test would stay green while the live FPI aborted fee estimation. + #[test] + fn every_allowlisted_note_script_is_priced_at_zero() { + let (wallet, _secret_key) = create_wallet_account().expect("wallet account should build"); + let fee_faucet_id = FungibleAsset::mock_issuer(); + let counter = create_counter_account(wallet.id(), fee_faucet_id) + .expect("counter account should build"); + + let allowlisted = NetworkAccount::new(counter.clone()) + .expect("counter should be a valid network account") + .allowed_notes() + .allowed_script_roots() + .clone(); + assert_eq!( + allowlisted.len(), + 1, + "only the increment note may be allowlisted, got {allowlisted:?}" + ); + + // A scheduled entry is `[fee_amount, 0, 0, 1]`: the trailing set-marker is what + // distinguishes an explicit zero fee from an absent key, since storage maps prune zero + // words and return the zero word for anything unset. + let expected_entry = Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ONE]); + for root in &allowlisted { + let entry = counter + .storage() + .get_map_item( + BasicConstantFeePolicy::fee_schedule_slot_name(), + StorageMapKey::new(root.as_word()), + ) + .expect("the fee schedule slot should be a map"); + assert_eq!( + entry, expected_entry, + "note script root {root} is allowlisted but has no zero-fee schedule entry" + ); + } + } + + /// The counter carries no `Authority` component, so the two notes `AuthNetworkAccount::new` + /// would allowlist by default must stay out of the allowlist: a `NETWORK_ACCOUNT_CONFIG` note + /// anyone could send would abort in `assert_authorized`, and an unpaired `FEE_SPONSORSHIP` note + /// would abort fee collection. Both aborts are network transactions failing against a public + /// account, which is exactly what the tracking card reports as unhealthy. + #[test] + fn counter_does_not_allowlist_notes_it_cannot_service() { + let (wallet, _secret_key) = create_wallet_account().expect("wallet account should build"); + let counter = create_counter_account(wallet.id(), FungibleAsset::mock_issuer()) + .expect("counter account should build"); + + let network_account = + NetworkAccount::new(counter).expect("counter should be a valid network account"); + let allowlisted = network_account.allowed_notes().allowed_script_roots(); + + assert!( + !allowlisted.contains(&NetworkAccountConfigNote::script_root()), + "the config note needs an Authority component the counter does not have" + ); + assert!( + !allowlisted.contains(&FeeSponsorshipNote::script_root()), + "the counter prices its notes at zero, so it never collects sponsored fees" + ); + // Dropping the defaults must not cost the account its serviceability: the ntx builder + // attaches the expiration script to every network transaction, and the store classifies an + // account whose tx-script allowlist lacks that root as non-network. + assert!( + network_account.allows_tx_script(&ExpirationTransactionScript::script_root()), + "the canonical expiration tx script must stay allowlisted" + ); + } + + /// The fee policy must be the *active* one and denominated in the faucet passed in, otherwise + /// fee estimation dispatches nowhere or charges the wrong asset. + #[test] + fn fee_policy_is_active_and_uses_the_given_faucet() { + let (wallet, _secret_key) = create_wallet_account().expect("wallet account should build"); + let fee_faucet_id = FungibleAsset::mock_issuer(); + let counter = create_counter_account(wallet.id(), fee_faucet_id) + .expect("counter account should build"); + + let active = counter + .storage() + .get_item(FeePolicyManager::active_fee_policy_slot()) + .expect("the active fee policy slot should exist"); + assert_eq!( + active, + BasicConstantFeePolicy::root().as_word(), + "the basic constant fee policy must be the active policy" + ); + + let fee_asset = counter + .storage() + .get_item(FeePolicyManager::fee_asset_id_slot()) + .expect("the fee asset slot should exist"); + assert_eq!( + fee_asset, + AssetId::new_fungible(fee_faucet_id).to_word(), + "fees must be charged in the fee faucet's asset" + ); + } +} diff --git a/bin/network-monitor/src/deploy/mod.rs b/bin/network-monitor/src/deploy/mod.rs index 7c477f105..7f7241500 100644 --- a/bin/network-monitor/src/deploy/mod.rs +++ b/bin/network-monitor/src/deploy/mod.rs @@ -10,23 +10,37 @@ use std::time::Duration; use anyhow::{Context, Result}; use backon::{ExponentialBuilder, Retryable}; use miden_node_proto::clients::{Builder, RpcClient}; +use miden_node_proto::domain::account::AccountResponse; use miden_node_proto::domain::encryption::{ TransactionInputsSealer, TrustedTransactionEncryptionState, verify_transaction_encryption_key, }; -use miden_node_proto::generated::rpc::BlockHeaderByNumberRequest; +use miden_node_proto::generated::rpc::{ + AccountRequest as ProtoAccountRequest, + BlockHeaderByNumberRequest, + FinalityLevel, + SyncChainMmrRequest, +}; use miden_node_proto::generated::transaction::ProvenTransaction as ProtoProvenTransaction; use miden_node_utils::retry; use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_node_utils::tracing::miden_instrument; use miden_protocol::Word; -use miden_protocol::account::{Account, AccountId, PartialAccount, StorageMapKey}; +use miden_protocol::account::{ + Account, + AccountId, + PartialAccount, + StorageMapKey, + StorageMapWitness, + StorageSlotContent, +}; use miden_protocol::asset::{AssetId, AssetWitness}; +use miden_protocol::block::account_tree::AccountWitness; use miden_protocol::block::{BlockHeader, BlockNumber}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey as ValidatorPublicKey; use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey; -use miden_protocol::crypto::merkle::mmr::{MmrPeaks, PartialMmr}; +use miden_protocol::crypto::merkle::mmr::{Forest, MmrDelta, MmrPeaks, PartialMmr}; use miden_protocol::note::{NoteScript, NoteScriptRoot}; use miden_protocol::transaction::{ AccountInputs, @@ -63,6 +77,7 @@ pub struct DeployedMonitorAccounts { pub wallet: Account, pub secret_key: SecretKey, pub counter: Account, + pub counter_anchor: CounterAnchor, } /// RPC client and verified transaction-input sealer shared by monitor submission workflows. @@ -283,27 +298,258 @@ pub async fn create_and_deploy_accounts( ) -> Result { tracing::info!(target: LOG_TARGET, "Creating fresh monitor accounts"); + let mut rpc_client = submission_client.rpc_client(); + + // The genesis header is immutable, so it is fetched once and reused by every step below. + let genesis_header = fetch_genesis_block_header(&mut rpc_client).await?; + ensure_monitor_supported_fee_parameters(&genesis_header)?; + let (wallet_account, secret_key) = create_wallet_account()?; - let counter_account = create_counter_account(wallet_account.id())?; + let fee_faucet_id = genesis_header.fee_parameters().fee_faucet_id(); + let counter_account = create_counter_account(wallet_account.id(), fee_faucet_id)?; + + let committed_counter = + deploy_counter_account(&counter_account, &genesis_header, submission_client, prover) + .await?; + let counter_anchor = + resolve_counter_anchor(&mut rpc_client, &genesis_header, &committed_counter).await?; - deploy_counter_account(&counter_account, submission_client, prover).await?; tracing::info!(target: LOG_TARGET, "Successfully created and deployed accounts"); Ok(DeployedMonitorAccounts { wallet: wallet_account, secret_key, counter: counter_account, + counter_anchor, }) } -/// Execute the counter account's genesis (creation) transaction in-memory. +/// Rejects chains the monitor's accounts cannot pay fees on. /// -/// Fetches the genesis block header from RPC, builds a [`MonitorDataStore`] over it, and executes -/// the creation transaction. Does not prove or submit. -async fn execute_counter_genesis_tx( - counter_account: &Account, +/// Both monitor accounts are created in memory with empty vaults and are never funded: there is no +/// faucet the monitor could claim the fee asset from. So on a chain with a non-zero verification base +/// fee the wallet's `AuthSingleSig` fee payment would find no asset to withdraw and the counter's +/// network transaction would abort the same way, on every single increment. The counter's fee +/// schedule prices its own note at zero for the same reason. +pub fn ensure_monitor_supported_fee_parameters(genesis_header: &BlockHeader) -> Result<()> { + let verification_base_fee = genesis_header.fee_parameters().verification_base_fee(); + anyhow::ensure!( + verification_base_fee == 0, + "the network monitor requires a chain with a zero verification base fee, but this chain's \ + genesis sets it to {verification_base_fee}: the monitor's accounts hold no assets and \ + cannot pay transaction fees", + ); + Ok(()) +} + +/// The immutable chain state that counter-increment transactions are anchored at. +/// +/// Every increment transaction emits a note targeted at the counter account, which makes the +/// paying account's auth procedure invoke the counter account's `estimate_note_fee` through FPI. +/// The kernel authenticates the foreign account against the reference block's account root, so the +/// transaction must reference a block that already contains the counter account, and the counter +/// state fed to the executor must be the state committed in that block. +/// +/// The anchor is resolved once, right after deployment and before any increment note exists, and +/// then reused: the referenced block is historical and immutable, so later increments (which do +/// change the counter's live state) never invalidate it. +pub struct CounterAnchor { + /// Header of the block the increment transactions reference. + pub block_header: BlockHeader, + /// Chain MMR whose peaks hash to `block_header.chain_commitment()`. + pub blockchain: PartialBlockchain, + /// The counter account exactly as committed in `block_header`. + pub counter_account: Account, + /// Witness proving `counter_account`'s inclusion in `block_header`'s account tree. + pub witness: AccountWitness, +} + +/// Number of attempts to resolve the counter anchor before giving up. +const ANCHOR_RESOLUTION_ATTEMPTS: usize = 30; + +/// Delay between counter anchor resolution attempts. +const ANCHOR_RESOLUTION_DELAY: Duration = Duration::from_secs(1); + +/// Resolve the [`CounterAnchor`] for a freshly deployed counter account. +/// +/// Retries until the chain tip contains the counter account in the state `committed_counter` +/// describes, since the deployment transaction needs a block to land in first. +async fn resolve_counter_anchor( rpc_client: &mut RpcClient, -) -> Result { + genesis_header: &BlockHeader, + committed_counter: &Account, +) -> Result { + let genesis_commitment = genesis_header.commitment(); + let expected_state = committed_counter.to_commitment(); + let mut last_error = None; + + for attempt in 1..=ANCHOR_RESOLUTION_ATTEMPTS { + if attempt > 1 { + tokio::time::sleep(ANCHOR_RESOLUTION_DELAY).await; + } + + match try_resolve_counter_anchor( + rpc_client, + committed_counter, + expected_state, + genesis_commitment, + ) + .await + { + Ok(Some(anchor)) => { + tracing::info!( + target: LOG_TARGET, + { + account.id = %committed_counter.id(), + block.number = %anchor.block_header.block_num(), + }, + "Resolved counter FPI anchor" + ); + return Ok(anchor); + }, + Ok(None) => tracing::debug!( + target: LOG_TARGET, + { account.id = %committed_counter.id(), attempt }, + "Counter account not yet committed in the expected state; retrying" + ), + Err(err) => { + tracing::debug!( + target: LOG_TARGET, + { account.id = %committed_counter.id(), attempt, error = ?err }, + "Counter anchor resolution attempt failed; retrying" + ); + last_error = Some(err); + }, + } + } + + let reason = last_error.map_or_else( + || "the account was never committed in the expected state".to_string(), + |err| format!("{err:#}"), + ); + anyhow::bail!( + "could not resolve the FPI anchor for counter account {} within {} attempts: {reason}", + committed_counter.id(), + ANCHOR_RESOLUTION_ATTEMPTS + ) +} + +/// One [`resolve_counter_anchor`] attempt. +/// +/// Returns `Ok(None)` when the chain is reachable but does not yet hold the counter account in the +/// expected state, which is the normal case while the deployment transaction is still in flight. +async fn try_resolve_counter_anchor( + rpc_client: &mut RpcClient, + committed_counter: &Account, + expected_state: Word, + genesis_commitment: Word, +) -> Result> { + let (block_header, blockchain) = fetch_tip_chain_state(rpc_client, genesis_commitment).await?; + let block_num = block_header.block_num(); + + let witness = fetch_account_witness(rpc_client, committed_counter.id(), block_num).await?; + + // An account absent from the tree yields a witness for the requested ID whose commitment is the + // empty word, so a plain state comparison covers "not committed yet" as well. Pin the ID too: + // an account-ID prefix collision makes the tree return a witness for the *other* account, and + // `MonitorDataStore` keys witnesses by the account they prove. + if witness.id() != committed_counter.id() { + anyhow::bail!( + "account tree returned a witness for {} when {} was requested", + witness.id(), + committed_counter.id() + ); + } + + if witness.state_commitment() != expected_state { + return Ok(None); + } + + Ok(Some(CounterAnchor { + block_header, + blockchain, + counter_account: committed_counter.clone(), + witness, + })) +} + +/// Fetch the chain tip header together with a [`PartialBlockchain`] whose peaks hash to that +/// header's chain commitment, making the pair usable as a transaction reference block. +async fn fetch_tip_chain_state( + rpc_client: &mut RpcClient, + genesis_commitment: Word, +) -> Result<(BlockHeader, PartialBlockchain)> { + let response = rpc_client + .sync_chain_mmr(SyncChainMmrRequest { + // The MMR is seeded with the genesis block below, so the delta starts at block 1. + current_client_block_height: BlockNumber::GENESIS.as_u32(), + finality_level: FinalityLevel::Committed.into(), + }) + .await + .context("failed to sync the chain MMR")? + .into_inner(); + + let tip_header: BlockHeader = response + .block_header + .context("sync_chain_mmr response did not include a block header")? + .try_into() + .context("failed to convert the sync target block header")?; + + let delta: MmrDelta = response + .mmr_delta + .context("sync_chain_mmr response did not include an MMR delta")? + .try_into() + .context("failed to convert the MMR delta")?; + + let mut mmr = PartialMmr::from_peaks( + MmrPeaks::new(Forest::new(0).context("empty forest should be valid")?, Vec::new()) + .context("empty MMR peaks should be valid")?, + ); + + if tip_header.block_num() != BlockNumber::GENESIS { + mmr.add(genesis_commitment, false) + .context("failed to seed the MMR with the genesis block")?; + mmr.apply(delta).context("failed to apply the MMR delta")?; + } + + anyhow::ensure!( + mmr.peaks().hash_peaks() == tip_header.chain_commitment(), + "synced MMR peaks do not match the chain commitment of block {}", + tip_header.block_num() + ); + + let blockchain = PartialBlockchain::new(mmr, Vec::new()) + .context("failed to build the partial blockchain")?; + + Ok((tip_header, blockchain)) +} + +/// Fetch the account-tree witness proving an account's state in the given block. +async fn fetch_account_witness( + rpc_client: &mut RpcClient, + account_id: AccountId, + block_num: BlockNumber, +) -> Result { + let request = ProtoAccountRequest { + account_id: Some(account_id.into()), + block_num: Some(block_num.into()), + details: None, + }; + + let response = rpc_client + .get_account(request) + .await + .context("failed to fetch the account witness")? + .into_inner(); + + let response = + AccountResponse::try_from(response).context("failed to convert the account response")?; + + Ok(response.witness) +} + +/// Fetch the genesis block header from RPC. +async fn fetch_genesis_block_header(rpc_client: &mut RpcClient) -> Result { let block_header_request = BlockHeaderByNumberRequest { block_num: Some(BlockNumber::GENESIS.as_u32()), include_mmr_proof: None, @@ -312,15 +558,25 @@ async fn execute_counter_genesis_tx( let response = rpc_client .get_block_header_by_number(block_header_request) .await - .context("Failed to get block header from RPC")?; + .context("Failed to get the genesis block header from RPC")?; let root_block_header = response .into_inner() .block_header .ok_or_else(|| anyhow::anyhow!("No block header in response"))?; - let genesis_header: BlockHeader = - root_block_header.try_into().context("Failed to convert block header")?; + root_block_header.try_into().context("Failed to convert block header") +} + +/// Execute the counter account's genesis (creation) transaction in-memory. +/// +/// Builds a [`MonitorDataStore`] over the genesis block header and executes the creation +/// transaction. Does not prove or submit. +pub(crate) async fn execute_counter_genesis_tx( + counter_account: &Account, + genesis_header: &BlockHeader, +) -> Result { + let genesis_header = genesis_header.clone(); let genesis_chain_mmr = PartialBlockchain::new(PartialMmr::from_peaks(MmrPeaks::default()), Vec::new()) @@ -351,37 +607,39 @@ async fn execute_counter_genesis_tx( /// /// Used as the static payload for the remote-prover probe: it produces a real, self-consistent /// transaction the remote prover can re-execute and prove, without depending on the network -/// transaction service or any pre-existing on-chain account. The only network access is a single -/// RPC read for the genesis block header; nothing is proven or submitted here. +/// transaction service or any pre-existing on-chain account. The only network access is the RPC +/// handshake plus a single read of the genesis block header, which supplies both the reference block +/// and the fee faucet the counter's fee policy is denominated in. Nothing is proven or submitted +/// here. pub async fn build_probe_transaction_inputs(rpc_url: &Url) -> Result { let (wallet_account, _secret_key) = create_wallet_account()?; - let counter_account = create_counter_account(wallet_account.id())?; let (mut rpc_client, _) = create_genesis_aware_rpc_client(rpc_url, Duration::from_secs(10)).await?; - let executed_tx = execute_counter_genesis_tx(&counter_account, &mut rpc_client).await?; + let genesis_header = fetch_genesis_block_header(&mut rpc_client).await?; + ensure_monitor_supported_fee_parameters(&genesis_header)?; + let fee_faucet_id = genesis_header.fee_parameters().fee_faucet_id(); + let counter_account = create_counter_account(wallet_account.id(), fee_faucet_id)?; + let executed_tx = execute_counter_genesis_tx(&counter_account, &genesis_header).await?; Ok(executed_tx.tx_inputs().clone()) } /// Deploy a counter account to the network by submitting its genesis transaction via RPC. -#[miden_instrument( - target = COMPONENT, - name = "deploy-counter-account", - skip_all, - ret(level = "debug"), -)] +#[miden_instrument(target = COMPONENT, name = "deploy-counter-account", skip_all)] pub async fn deploy_counter_account( counter_account: &Account, + genesis_header: &BlockHeader, submission_client: &TransactionSubmissionClient, prover: &LocalTransactionProver, -) -> Result<()> { - let mut rpc_client = submission_client.rpc_client(); - - let executed_tx = execute_counter_genesis_tx(counter_account, &mut rpc_client).await?; +) -> Result { + let executed_tx = execute_counter_genesis_tx(counter_account, genesis_header).await?; let transaction_inputs = executed_tx.tx_inputs().to_bytes(); + let committed_counter = Account::try_from(executed_tx.account_patch()) + .context("counter creation patch should convert to an account")?; + let prover = prover.clone(); let proven_tx = spawn_blocking_in_current_span(move || prover.prove(executed_tx)) .await @@ -390,7 +648,7 @@ pub async fn deploy_counter_account( submission_client.submit(&proven_tx, &transaction_inputs).await?; - Ok(()) + Ok(committed_counter) } // MONITOR DATA STORE @@ -399,6 +657,7 @@ pub async fn deploy_counter_account( /// A [`DataStore`] implementation for the network monitor. pub struct MonitorDataStore { accounts: HashMap, + account_witnesses: HashMap, block_header: BlockHeader, partial_block_chain: PartialBlockchain, mast_store: TransactionMastStore, @@ -408,6 +667,7 @@ impl MonitorDataStore { pub fn new(block_header: BlockHeader, partial_block_chain: PartialBlockchain) -> Self { Self { accounts: HashMap::new(), + account_witnesses: HashMap::new(), block_header, partial_block_chain, mast_store: TransactionMastStore::new(), @@ -420,6 +680,13 @@ impl MonitorDataStore { self.accounts.insert(account.id(), account); } + /// Register an account the transaction reaches through a foreign procedure invocation, together + /// with the account-tree witness proving its state in the store's reference block. + pub fn add_foreign_account(&mut self, account: Account, witness: AccountWitness) { + self.add_account(account); + self.account_witnesses.insert(witness.id(), witness); + } + /// Returns a reference to the account or a standardized "unknown account" error. fn get_account(&self, account_id: AccountId) -> Result<&Account, DataStoreError> { self.accounts.get(&account_id).ok_or_else(|| DataStoreError::Other { @@ -443,19 +710,49 @@ impl DataStore for MonitorDataStore { async fn get_storage_map_witness( &self, - _account_id: AccountId, - _map_root: Word, - _map_key: StorageMapKey, - ) -> Result { - unimplemented!("Not needed") + account_id: AccountId, + map_root: Word, + map_key: StorageMapKey, + ) -> Result { + let account = self.get_account(account_id)?; + + account + .storage() + .slots() + .iter() + .filter_map(|slot| match slot.content() { + StorageSlotContent::Map(map) => Some(map), + StorageSlotContent::Value(_) => None, + }) + .find(|map| map.root() == map_root) + .map(|map| map.open(&map_key)) + .ok_or_else(|| DataStoreError::Other { + error_msg: format!( + "no storage map with the requested root in account {account_id}" + ) + .into(), + source: None, + }) } async fn get_foreign_account_inputs( &self, - _foreign_account_id: AccountId, + foreign_account_id: AccountId, _ref_block: BlockNumber, ) -> Result { - unimplemented!("Not needed") + let account = self.get_account(foreign_account_id)?; + let witness = + self.account_witnesses.get(&foreign_account_id).cloned().ok_or_else(|| { + DataStoreError::Other { + error_msg: format!( + "no account witness for foreign account {foreign_account_id}" + ) + .into(), + source: None, + } + })?; + + Ok(AccountInputs::new(PartialAccount::from(account), witness)) } async fn get_vault_asset_witnesses( @@ -496,3 +793,35 @@ impl MastForestStore for MonitorDataStore { self.mast_store.get(procedure_hash) } } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_testing::MockChain; + + use super::ensure_monitor_supported_fee_parameters; + + /// A zero verification base fee is the only configuration the monitor's asset-less accounts can + /// operate under, and the guard must say so at startup instead of letting every increment abort + /// inside fee payment. + #[test] + fn fee_parameter_guard_accepts_only_a_zero_base_fee() { + let zero_fee_chain = MockChain::builder().build().expect("chain should build"); + ensure_monitor_supported_fee_parameters(&zero_fee_chain.genesis_block_header()) + .expect("a zero base fee is supported"); + + let fee_charging_chain = MockChain::builder() + .verification_base_fee(500) + .build() + .expect("chain should build"); + let err = + ensure_monitor_supported_fee_parameters(&fee_charging_chain.genesis_block_header()) + .expect_err("a non-zero base fee must be rejected"); + assert!( + format!("{err:#}").contains("500"), + "the error should name the offending base fee, got: {err:#}" + ); + } +} diff --git a/bin/network-monitor/src/deploy/wallet.rs b/bin/network-monitor/src/deploy/wallet.rs index bdc4cd658..64aa7ff8c 100644 --- a/bin/network-monitor/src/deploy/wallet.rs +++ b/bin/network-monitor/src/deploy/wallet.rs @@ -87,7 +87,7 @@ pub fn create_wallet_account() -> Result<(Account, SecretKey)> { let account = AccountBuilder::new(init_seed) .account_type(AccountType::Public) - .with_auth_component(auth_component) + .with_component(auth_component) .with_component(counter_component) .build() .context("failed to build wallet account")?; diff --git a/bin/network-monitor/src/monitor/tasks.rs b/bin/network-monitor/src/monitor/tasks.rs index 91717be39..9e8a3b692 100644 --- a/bin/network-monitor/src/monitor/tasks.rs +++ b/bin/network-monitor/src/monitor/tasks.rs @@ -276,8 +276,7 @@ async fn bootstrap_ntx( submission_client, accounts_tx, latency_state.clone(), - ) - .await?; + )?; let tracking_svc = CounterTrackingService::new(config.clone(), accounts_rx, latency_state).await?; diff --git a/bin/ntx-builder/src/actor/allowlist.rs b/bin/ntx-builder/src/actor/allowlist.rs index 3b68cfc56..5df53babd 100644 --- a/bin/ntx-builder/src/actor/allowlist.rs +++ b/bin/ntx-builder/src/actor/allowlist.rs @@ -67,7 +67,9 @@ pub fn partition_by_allowlist( mod tests { use std::collections::BTreeSet; + use miden_protocol::asset::FungibleAsset; use miden_standards::account::auth::{AuthNetworkAccount, NetworkAccountNoteAllowlistError}; + use miden_standards::account::fees::FeePolicyManager; use super::*; use crate::test_utils::{ @@ -90,8 +92,11 @@ end"; let note = mock_single_target_note(account_id, 10); let root = note.as_note().script().root(); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([root])) - .expect("non-empty allowlist should construct"), + AuthNetworkAccount::new( + BTreeSet::from_iter([root]), + FeePolicyManager::mock(FungibleAsset::mock_issuer()), + ) + .expect("non-empty allowlist should construct"), ); let partitioned_notes = @@ -113,8 +118,11 @@ end"; assert_ne!(allowed_root, rejected_root); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([allowed_root])) - .expect("non-empty allowlist should construct"), + AuthNetworkAccount::new( + BTreeSet::from_iter([allowed_root]), + FeePolicyManager::mock(FungibleAsset::mock_issuer()), + ) + .expect("non-empty allowlist should construct"), ); let partitioned_notes = partition_by_allowlist(&account, vec![rejected_note.clone()]) @@ -138,8 +146,11 @@ end"; assert_ne!(allowed_root, rejected_root); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([allowed_root])) - .expect("non-empty allowlist should construct"), + AuthNetworkAccount::new( + BTreeSet::from_iter([allowed_root]), + FeePolicyManager::mock(FungibleAsset::mock_issuer()), + ) + .expect("non-empty allowlist should construct"), ); let partitioned_notes = diff --git a/bin/ntx-builder/src/test_utils.rs b/bin/ntx-builder/src/test_utils.rs index 882c07fe9..2d7c12398 100644 --- a/bin/ntx-builder/src/test_utils.rs +++ b/bin/ntx-builder/src/test_utils.rs @@ -73,20 +73,22 @@ pub fn mock_account(_account_id: AccountId) -> miden_protocol::account::Account AccountBuilder::new([0u8; 32]) .account_type(AccountType::Public) .with_component(MockAccountComponent::with_slots(vec![])) - .with_auth_component(NoopAuthComponent) + .with_component(NoopAuthComponent) .build_existing() .unwrap() } -/// Creates a mock network [`Account`] with the provided auth component. -pub fn mock_account_with_auth_component(auth_component: impl Into) -> Account { +/// Creates a mock network [`Account`] with the provided auth components. +pub fn mock_account_with_auth_component( + auth_components: impl IntoIterator>, +) -> Account { use miden_protocol::account::AccountBuilder; use miden_standards::testing::account_component::MockAccountComponent; AccountBuilder::new([0u8; 32]) .account_type(AccountType::Public) .with_component(MockAccountComponent::with_slots(vec![])) - .with_auth_component(auth_component) + .with_components(auth_components) .build_existing() .unwrap() } @@ -120,13 +122,18 @@ pub fn mock_network_account_update() -> (Account, miden_protocol::account::Accou use std::collections::BTreeSet; use miden_protocol::account::{AccountPatch, AccountUpdateDetails}; + use miden_protocol::asset::FungibleAsset; use miden_standards::account::auth::AuthNetworkAccount; + use miden_standards::account::fees::FeePolicyManager; // The allowlist content is irrelevant here; any non-empty set yields a valid network account. let root = mock_single_target_note(mock_network_account_id(), 1).as_note().script().root(); let account = mock_account_with_auth_component( - AuthNetworkAccount::with_allowed_notes(BTreeSet::from_iter([root])) - .expect("non-empty allowlist should construct"), + AuthNetworkAccount::new( + BTreeSet::from_iter([root]), + FeePolicyManager::mock(FungibleAsset::mock_issuer()), + ) + .expect("non-empty allowlist should construct"), ); let details = AccountUpdateDetails::Public( AccountPatch::try_from(account.clone()).expect("full-state patch should build"), diff --git a/bin/remote-prover/src/server/tests.rs b/bin/remote-prover/src/server/tests.rs index 646240954..7b154ee19 100644 --- a/bin/remote-prover/src/server/tests.rs +++ b/bin/remote-prover/src/server/tests.rs @@ -93,8 +93,8 @@ impl ProofRequestExt for ProofRequest { let mock_chain = mock_chain_builder.build().unwrap(); let tx_context = mock_chain - .build_tx_context(account.id(), &[note_1.id()], &[]) - .unwrap() + .build_transaction(account.id()) + .authenticated_input_note(note_1.id()) .build() .unwrap(); @@ -126,8 +126,8 @@ impl ProofRequestExt for ProofRequest { let mock_chain = mock_chain_builder.build().unwrap(); let tx = mock_chain - .build_tx_context(account.id(), &[note_1.id()], &[]) - .unwrap() + .build_transaction(account.id()) + .authenticated_input_note(note_1.id()) .build() .unwrap(); diff --git a/bin/stress-test/src/seeding/mod.rs b/bin/stress-test/src/seeding/mod.rs index eeb21fae5..e8a6d571a 100644 --- a/bin/stress-test/src/seeding/mod.rs +++ b/bin/stress-test/src/seeding/mod.rs @@ -763,7 +763,7 @@ fn create_account( let init_seed: Vec<_> = index.to_be_bytes().into_iter().chain([0u8; 24]).collect(); let mut builder = AccountBuilder::new(init_seed.try_into().unwrap()) .account_type(account_type) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( public_key.into(), AuthScheme::Falcon512Poseidon2, ))) @@ -862,7 +862,7 @@ fn create_faucet_with_seed(index: u64) -> Account { .active_burn_policy(BurnPolicy::allow_all()) .build(), ) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( key_pair.public_key().into(), AuthScheme::Falcon512Poseidon2, ))) diff --git a/bin/validator/src/server/validator_service/tests.rs b/bin/validator/src/server/validator_service/tests.rs index 1df0240be..f518c70a7 100644 --- a/bin/validator/src/server/validator_service/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -373,8 +373,8 @@ async fn proven_transaction_fixture() -> &'static ProvenTransactionFixture { let chain = chain_builder.build().unwrap(); let context_a = chain - .build_tx_context(account_a.id(), &[note_a.id()], &[]) - .unwrap() + .build_transaction(account_a.id()) + .authenticated_input_note(note_a.id()) .build() .unwrap(); let executed_a = Box::pin(context_a.execute()).await.unwrap(); @@ -382,8 +382,8 @@ async fn proven_transaction_fixture() -> &'static ProvenTransactionFixture { let transaction = LocalTransactionProver::default().prove(inputs.clone()).unwrap(); let context_b = chain - .build_tx_context(account_b.id(), &[note_b.id()], &[]) - .unwrap() + .build_transaction(account_b.id()) + .authenticated_input_note(note_b.id()) .build() .unwrap(); let mismatch_inputs = Box::pin(context_b.execute()).await.unwrap().tx_inputs().clone(); diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index fa5156596..e984464cd 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -131,7 +131,7 @@ fn build_test_account(seed: [u8; 32]) -> (Account, AccountPatch) { .account_type(AccountType::Public) .with_assets(vec![]) .with_component(BasicWallet) - .with_auth_component(NoopAuthComponent) + .with_component(NoopAuthComponent) .build_existing() .unwrap(); @@ -448,7 +448,7 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_commitment() { // Assert that the error is due to the invalid account delta commitment. let err = response.as_ref().unwrap_err().message(); assert!( - err.contains("failed to validate account patch in transaction account update"), + err.contains("expected account patch commitment"), "expected error message to contain patch commitment error but got: {err}" ); } diff --git a/crates/store/src/account_state_forest/tests.rs b/crates/store/src/account_state_forest/tests.rs index b066b8ab8..37bcd2aec 100644 --- a/crates/store/src/account_state_forest/tests.rs +++ b/crates/store/src/account_state_forest/tests.rs @@ -858,7 +858,7 @@ fn storage_map_empty_entries_query() { let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::Public) .with_component(account_component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index fa658f367..df3c62527 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -46,7 +46,12 @@ use miden_protocol::account::{ use miden_protocol::asset::{Asset, AssetId, AssetVault}; use miden_protocol::block::{BlockAccountUpdate, BlockNumber}; use miden_protocol::utils::serde::{Deserializable, Serializable}; -use miden_standards::account::auth::{NetworkAccount, NetworkAccountNoteAllowlist}; +use miden_standards::account::auth::{ + NetworkAccount, + NetworkAccountNoteAllowlist, + NetworkAccountTxScriptAllowlist, +}; +use miden_standards::tx_script::ExpirationTransactionScript; use crate::COMPONENT; use crate::db::models::conv::{SqlTypeConvert, nonce_to_raw_sql, raw_sql_to_nonce}; @@ -1178,13 +1183,24 @@ fn prepare_precomputed_full_account_update( }) .collect::, _>>()?; - let is_network_account = account_id.is_public() - && patch + let allowlist_entries = |slot_name: &StorageSlotName| { + patch .storage() .maps() - .find(|(slot_name, _)| *slot_name == NetworkAccountNoteAllowlist::slot_name()) + .find(|(candidate, _)| *candidate == slot_name) .and_then(|(_slot_name, map_patch)| map_patch.entries()) - .is_some_and(|entries| !entries.is_empty()); + }; + let has_allowed_notes = allowlist_entries(NetworkAccountNoteAllowlist::slot_name()) + .is_some_and(|entries| entries.as_map().values().any(|value| *value != Word::empty())); + let allows_expiration_tx_script = + allowlist_entries(NetworkAccountTxScriptAllowlist::slot_name()).is_some_and(|entries| { + entries + .as_map() + .get(&StorageMapKey::new(ExpirationTransactionScript::script_root().as_word())) + .is_some_and(|value| *value != Word::empty()) + }); + let is_network_account = + account_id.is_public() && has_allowed_notes && allows_expiration_tx_script; let state = PrecomputedFullAccountState { nonce, code, diff --git a/crates/store/src/db/models/queries/accounts/delta/tests.rs b/crates/store/src/db/models/queries/accounts/delta/tests.rs index 54b92b199..5b753f00b 100644 --- a/crates/store/src/db/models/queries/accounts/delta/tests.rs +++ b/crates/store/src/db/models/queries/accounts/delta/tests.rs @@ -135,7 +135,7 @@ fn callback_delta_test_account(seed: [u8; 32], slot_index: usize) -> Account { AccountBuilder::new(seed) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -269,7 +269,7 @@ fn optimized_delta_matches_full_account_method() { let account = AccountBuilder::new(ACCOUNT_SEED) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -476,7 +476,7 @@ fn optimized_delta_updates_non_empty_vault() { let account = AccountBuilder::new(ACCOUNT_SEED) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -712,7 +712,7 @@ fn optimized_delta_updates_storage_map_header() { let account = AccountBuilder::new(ACCOUNT_SEED) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -850,7 +850,7 @@ fn partial_public_upsert_requires_precomputed_state() { let account = AccountBuilder::new(ACCOUNT_SEED) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -921,7 +921,7 @@ fn partial_public_upsert_rejects_bad_precomputed_root() { let account = AccountBuilder::new(ACCOUNT_SEED) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -1081,7 +1081,7 @@ fn upsert_full_state_delta() { let account = AccountBuilder::new(ACCOUNT_SEED) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index 44a41e9b6..ad8377bf7 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -154,7 +154,7 @@ fn create_test_account_with_storage() -> (Account, AccountId) { let account = AccountBuilder::new([1u8; 32]) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -238,7 +238,7 @@ fn create_account_with_map_storage( AccountBuilder::new([9u8; 32]) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -524,7 +524,7 @@ fn test_upsert_accounts_updates_is_latest_flag() { let account_2 = AccountBuilder::new([1u8; 32]) .account_type(AccountType::Public) .with_component(component_2) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -624,7 +624,7 @@ fn test_upsert_accounts_with_multiple_storage_slots() { let account = AccountBuilder::new([2u8; 32]) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -700,7 +700,7 @@ fn test_upsert_accounts_with_empty_storage() { let account = AccountBuilder::new([3u8; 32]) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -852,7 +852,7 @@ fn test_select_latest_account_storage_multiple_slots() { let account = AccountBuilder::new([9u8; 32]) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -1305,7 +1305,7 @@ fn build_account_with_code(push_value: u32) -> Account { AccountBuilder::new([2u8; 32]) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 614867462..4e953a111 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -1718,7 +1718,7 @@ fn create_account_with_code(code_str: &str, seed: [u8; 32]) -> Account { AccountBuilder::new(seed) .account_type(AccountType::Public) .with_component(component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -1867,7 +1867,7 @@ fn mock_account_code_and_storage( .account_type(account_type) .with_assets(assets) .with_component(account_component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2033,7 +2033,7 @@ async fn genesis_with_account_assets() { .account_type(AccountType::Public) .with_component(account_component) .with_assets([fungible_asset.into()]) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2105,7 +2105,7 @@ async fn genesis_with_account_storage_map() { let account = AccountBuilder::new([2u8; 32]) .account_type(AccountType::Public) .with_component(account_component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2170,7 +2170,7 @@ async fn genesis_with_account_assets_and_storage() { .account_type(AccountType::Public) .with_component(account_component) .with_assets([fungible_asset.into()]) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2214,7 +2214,7 @@ async fn genesis_with_multiple_accounts() { let account1 = AccountBuilder::new([1u8; 32]) .account_type(AccountType::Public) .with_component(account_component1) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2238,7 +2238,7 @@ async fn genesis_with_multiple_accounts() { .account_type(AccountType::Public) .with_component(account_component2) .with_assets([fungible_asset.into()]) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2271,7 +2271,7 @@ async fn genesis_with_multiple_accounts() { let account3 = AccountBuilder::new([3u8; 32]) .account_type(AccountType::Public) .with_component(account_component3) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) @@ -2771,7 +2771,7 @@ fn db_roundtrip_account_storage_with_maps() { let account = AccountBuilder::new([50u8; 32]) .account_type(AccountType::Public) .with_component(account_component) - .with_auth_component(AuthSingleSig::new(Approver::new( + .with_component(AuthSingleSig::new(Approver::new( PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::Falcon512Poseidon2, ))) diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index 96c929dbb..f1aa88943 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -458,7 +458,7 @@ impl FungibleFaucetConfig { // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases. let faucet_account = AccountBuilder::new(init_seed) .account_type(account_type.into()) - .with_auth_component(auth) + .with_component(auth) .with_component(faucet) .with_components( TokenPolicyManager::builder() diff --git a/crates/store/src/genesis/config/tests.rs b/crates/store/src/genesis/config/tests.rs index 284b62b0d..a68042756 100644 --- a/crates/store/src/genesis/config/tests.rs +++ b/crates/store/src/genesis/config/tests.rs @@ -231,7 +231,7 @@ fn parsing_native_faucet_from_file() -> TestResult { let faucet_account = AccountBuilder::new(init_seed) .account_type(AccountType::Public) - .with_auth_component(auth) + .with_component(auth) .with_component(faucet) .with_components( TokenPolicyManager::builder()