From 71be4d355935eb8990a5b8feb53577b3972409bb Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Tue, 17 Mar 2026 16:24:40 -0300 Subject: [PATCH 1/4] feat(ntx-builder): limit execution cycles for network transactions Default to 64k (1 << 16) cycles instead of the protocol maximum (1 << 29). This is configurable via --ntx-builder.max-cycles CLI flag or MIDEN_NTX_MAX_CYCLES env var. Closes #1800 --- bin/node/src/commands/mod.rs | 14 ++++++++++++ bin/node/src/tests.rs | 5 +++++ crates/ntx-builder/src/actor/execute.rs | 30 +++++++++++++++++++++---- crates/ntx-builder/src/actor/mod.rs | 7 ++++++ crates/ntx-builder/src/lib.rs | 21 +++++++++++++++++ 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index 8f833dbe3..c7dd105a0 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -48,10 +48,12 @@ const ENV_VALIDATOR_KEY: &str = "MIDEN_NODE_VALIDATOR_KEY"; const ENV_VALIDATOR_KMS_KEY_ID: &str = "MIDEN_NODE_VALIDATOR_KMS_KEY_ID"; const ENV_NTX_DATA_DIRECTORY: &str = "MIDEN_NODE_NTX_DATA_DIRECTORY"; const ENV_NTX_BUILDER_URL: &str = "MIDEN_NODE_NTX_BUILDER_URL"; +const ENV_NTX_MAX_CYCLES: &str = "MIDEN_NTX_MAX_CYCLES"; const DEFAULT_NTX_TICKER_INTERVAL: Duration = Duration::from_millis(200); const DEFAULT_NTX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); const DEFAULT_NTX_SCRIPT_CACHE_SIZE: NonZeroUsize = NonZeroUsize::new(1000).unwrap(); +const DEFAULT_NTX_MAX_CYCLES: u32 = 1 << 16; /// Configuration for the Validator key used to sign blocks. /// @@ -195,6 +197,17 @@ pub struct NtxBuilderConfig { )] pub max_account_crashes: usize, + /// Maximum number of VM execution cycles allowed for a single network transaction. + /// + /// Network transactions that exceed this limit will fail. Defaults to 64k (65536) cycles. + #[arg( + long = "ntx-builder.max-cycles", + env = ENV_NTX_MAX_CYCLES, + default_value_t = DEFAULT_NTX_MAX_CYCLES, + value_name = "NUM" + )] + pub max_cycles: u32, + /// Directory for the ntx-builder's persistent database. /// /// If not set, defaults to the node's data directory. @@ -227,6 +240,7 @@ impl NtxBuilderConfig { .with_script_cache_size(self.script_cache_size) .with_idle_timeout(self.idle_timeout) .with_max_account_crashes(self.max_account_crashes) + .with_max_cycles(self.max_cycles) } } diff --git a/bin/node/src/tests.rs b/bin/node/src/tests.rs index f4968f79f..a25d95823 100644 --- a/bin/node/src/tests.rs +++ b/bin/node/src/tests.rs @@ -35,3 +35,8 @@ fn bundled_bootstrap_parses() { fn bundled_start_parses() { let _ = parse(&["bundled", "start"]); } + +#[test] +fn bundled_start_with_max_cycles_parses() { + let _ = parse(&["bundled", "start", "--ntx-builder.max-cycles", "131072"]); +} diff --git a/crates/ntx-builder/src/actor/execute.rs b/crates/ntx-builder/src/actor/execute.rs index d06c283c1..c1993e603 100644 --- a/crates/ntx-builder/src/actor/execute.rs +++ b/crates/ntx-builder/src/actor/execute.rs @@ -39,6 +39,7 @@ use miden_tx::utils::Serializable; use miden_tx::{ DataStore, DataStoreError, + ExecutionOptions, FailedNote, LocalTransactionProver, MastForestStore, @@ -111,6 +112,9 @@ pub struct NtxContext { /// Local database for persistent note script caching. db: Db, + + /// Maximum number of VM execution cycles for network transactions. + max_cycles: u32, } impl NtxContext { @@ -122,6 +126,7 @@ impl NtxContext { store: StoreClient, script_cache: LruCache, db: Db, + max_cycles: u32, ) -> Self { Self { block_producer, @@ -130,9 +135,28 @@ impl NtxContext { store, script_cache, db, + max_cycles, } } + /// Creates a [`TransactionExecutor`] configured with the network transaction cycle limit. + fn create_executor<'a, 'b>( + &self, + data_store: &'a NtxDataStore, + ) -> TransactionExecutor<'a, 'b, NtxDataStore, UnreachableAuth> { + let exec_options = ExecutionOptions::new( + Some(self.max_cycles), + self.max_cycles, + false, + false, + ) + .expect("max_cycles should be within valid range"); + + TransactionExecutor::new(data_store) + .with_options(exec_options) + .expect("execution options should be valid for transaction executor") + } + /// Executes a transaction end-to-end: filtering, executing, proving, and submitted to the block /// producer. /// @@ -238,8 +262,7 @@ impl NtxContext { data_store: &NtxDataStore, notes: Vec, ) -> NtxResult<(InputNotes, Vec)> { - let executor: TransactionExecutor<'_, '_, _, UnreachableAuth> = - TransactionExecutor::new(data_store); + let executor = self.create_executor(data_store); let checker = NoteConsumptionChecker::new(&executor); match Box::pin(checker.check_notes_consumability( @@ -282,8 +305,7 @@ impl NtxContext { data_store: &NtxDataStore, notes: InputNotes, ) -> NtxResult { - let executor: TransactionExecutor<'_, '_, _, UnreachableAuth> = - TransactionExecutor::new(data_store); + let executor = self.create_executor(data_store); Box::pin(executor.execute_transaction( data_store.account.id(), diff --git a/crates/ntx-builder/src/actor/mod.rs b/crates/ntx-builder/src/actor/mod.rs index eadbea43d..399448cf7 100644 --- a/crates/ntx-builder/src/actor/mod.rs +++ b/crates/ntx-builder/src/actor/mod.rs @@ -77,6 +77,8 @@ pub struct AccountActorContext { pub db: Db, /// Channel for sending requests to the coordinator (via the builder event loop). pub request_tx: mpsc::Sender, + /// Maximum number of VM execution cycles for network transactions. + pub max_cycles: u32, } #[cfg(test)] @@ -112,6 +114,7 @@ impl AccountActorContext { idle_timeout: Duration::from_secs(60), db: db.clone(), request_tx, + max_cycles: 1 << 16, } } } @@ -219,6 +222,8 @@ pub struct AccountActor { idle_timeout: Duration, /// Channel for sending requests to the coordinator. request_tx: mpsc::Sender, + /// Maximum number of VM execution cycles for network transactions. + max_cycles: u32, } impl AccountActor { @@ -254,6 +259,7 @@ impl AccountActor { max_note_attempts: actor_context.max_note_attempts, idle_timeout: actor_context.idle_timeout, request_tx: actor_context.request_tx.clone(), + max_cycles: actor_context.max_cycles, } } @@ -401,6 +407,7 @@ impl AccountActor { self.store.clone(), self.script_cache.clone(), self.db.clone(), + self.max_cycles, ); let notes = tx_candidate.notes.clone(); diff --git a/crates/ntx-builder/src/lib.rs b/crates/ntx-builder/src/lib.rs index d6d8c5a01..a583f787c 100644 --- a/crates/ntx-builder/src/lib.rs +++ b/crates/ntx-builder/src/lib.rs @@ -66,6 +66,12 @@ const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); /// Default maximum number of crashes an account actor is allowed before being deactivated. const DEFAULT_MAX_ACCOUNT_CRASHES: usize = 10; +/// Default maximum number of VM execution cycles allowed for a network transaction. +/// +/// This limits the computational cost of network transactions. The protocol maximum is +/// `1 << 29` but network transactions should be much cheaper. +const DEFAULT_MAX_TX_CYCLES: u32 = 1 << 16; + // CONFIGURATION // ================================================================================================= @@ -118,6 +124,12 @@ pub struct NtxBuilderConfig { /// Once this limit is reached, no new transactions will be created for this account. pub max_account_crashes: usize, + /// Maximum number of VM execution cycles allowed for a single network transaction. + /// + /// Network transactions that exceed this limit will fail with an execution error. + /// Defaults to 64k cycles. + pub max_cycles: u32, + /// Path to the SQLite database file used for persistent state. pub database_filepath: PathBuf, } @@ -142,6 +154,7 @@ impl NtxBuilderConfig { account_channel_capacity: DEFAULT_ACCOUNT_CHANNEL_CAPACITY, idle_timeout: DEFAULT_IDLE_TIMEOUT, max_account_crashes: DEFAULT_MAX_ACCOUNT_CRASHES, + max_cycles: DEFAULT_MAX_TX_CYCLES, database_filepath, } } @@ -223,6 +236,13 @@ impl NtxBuilderConfig { self } + /// Sets the maximum number of VM execution cycles for network transactions. + #[must_use] + pub fn with_max_cycles(mut self, max: u32) -> Self { + self.max_cycles = max; + self + } + /// Builds and initializes the network transaction builder. /// /// This method connects to the store and block producer services, fetches the current @@ -283,6 +303,7 @@ impl NtxBuilderConfig { idle_timeout: self.idle_timeout, db: db.clone(), request_tx, + max_cycles: self.max_cycles, }; Ok(NetworkTransactionBuilder::new( From 10dc9d881d7d7f7a1b7d8b05ac43ab9941e131c9 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Tue, 17 Mar 2026 17:03:26 -0300 Subject: [PATCH 2/4] chore: linting --- crates/ntx-builder/src/actor/execute.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/ntx-builder/src/actor/execute.rs b/crates/ntx-builder/src/actor/execute.rs index c1993e603..b3fb9dc03 100644 --- a/crates/ntx-builder/src/actor/execute.rs +++ b/crates/ntx-builder/src/actor/execute.rs @@ -144,13 +144,9 @@ impl NtxContext { &self, data_store: &'a NtxDataStore, ) -> TransactionExecutor<'a, 'b, NtxDataStore, UnreachableAuth> { - let exec_options = ExecutionOptions::new( - Some(self.max_cycles), - self.max_cycles, - false, - false, - ) - .expect("max_cycles should be within valid range"); + let exec_options = + ExecutionOptions::new(Some(self.max_cycles), self.max_cycles, false, false) + .expect("max_cycles should be within valid range"); TransactionExecutor::new(data_store) .with_options(exec_options) From 96481f26b0cc45a32f4bb01a3e81a472cf860da0 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Wed, 18 Mar 2026 11:12:57 -0300 Subject: [PATCH 3/4] chore: address comments --- bin/node/src/commands/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index c7dd105a0..7ccc4e774 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -199,14 +199,14 @@ pub struct NtxBuilderConfig { /// Maximum number of VM execution cycles allowed for a single network transaction. /// - /// Network transactions that exceed this limit will fail. Defaults to 64k (65536) cycles. + /// Network transactions that exceed this limit will fail. Defaults to 2^16 (65536) cycles. #[arg( long = "ntx-builder.max-cycles", env = ENV_NTX_MAX_CYCLES, default_value_t = DEFAULT_NTX_MAX_CYCLES, - value_name = "NUM" + value_name = "NUM", )] - pub max_cycles: u32, + pub max_tx_cycles: u32, /// Directory for the ntx-builder's persistent database. /// @@ -240,7 +240,7 @@ impl NtxBuilderConfig { .with_script_cache_size(self.script_cache_size) .with_idle_timeout(self.idle_timeout) .with_max_account_crashes(self.max_account_crashes) - .with_max_cycles(self.max_cycles) + .with_max_cycles(self.max_tx_cycles) } } From d4b5b8cd89cd950974e75aed36af3485c754df31 Mon Sep 17 00:00:00 2001 From: Juan Munoz Date: Wed, 18 Mar 2026 11:31:47 -0300 Subject: [PATCH 4/4] docs: add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1d2fd66..eec4b7a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fixed `TransactionHeader` serialization for row insertion on database & fixed transaction cursor on retrievals ([#1701](https://github.com/0xMiden/node/issues/1701)). - Added KMS signing support in validator ([#1677](https://github.com/0xMiden/node/pull/1677)). - Added per-IP gRPC rate limiting across services as well as global concurrent connection limit ([#1746](https://github.com/0xMiden/node/issues/1746)). +- Added limit to execution cycles for a transaction network, configurable through CLI args (`--ntx-builder.max-tx-cycles`) ([#1801](https://github.com/0xMiden/node/issues/1801)). ### Changes