diff --git a/CHANGELOG.md b/CHANGELOG.md index 493570753..d05eb2334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +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)). -- Network transaction actors now share the same gRPC clients, limiting the number of file descriptors being used ([#1806](https://github.com/0xMiden/node/issues/1806)). +- 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 @@ -47,6 +47,7 @@ - Fixed `bundled bootstrap` requiring `--validator.key.hex` or `--validator.key.kms-id` despite a default key being configured ([#1732](https://github.com/0xMiden/node/pull/1732)). - Fixed incorrectly classifying private notes with the network attachment as network notes ([#1378](https://github.com/0xMiden/node/pull/1738)). - Fixed accept header version negotiation rejecting all pre-release versions; pre-release label matching is now lenient, accepting any numeric suffix within the same label (e.g. `alpha.3` accepts `alpha.1`) ([#1755](https://github.com/0xMiden/node/pull/1755)). +- Network transaction actors now share the same gRPC clients, limiting the number of file descriptors being used ([#1806](https://github.com/0xMiden/node/issues/1806)). ## v0.13.7 (2026-02-25) diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index 8f833dbe3..7ccc4e774 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 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", + )] + pub max_tx_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_tx_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 aa488c284..e2bb27347 100644 --- a/crates/ntx-builder/src/actor/execute.rs +++ b/crates/ntx-builder/src/actor/execute.rs @@ -36,6 +36,7 @@ use miden_tx::auth::UnreachableAuth; use miden_tx::{ DataStore, DataStoreError, + ExecutionOptions, FailedNote, LocalTransactionProver, MastForestStore, @@ -108,6 +109,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 { @@ -119,6 +123,7 @@ impl NtxContext { store: StoreClient, script_cache: LruCache, db: Db, + max_cycles: u32, ) -> Self { Self { block_producer, @@ -127,9 +132,24 @@ 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. /// @@ -235,8 +255,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( @@ -279,8 +298,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 e1e2a34d0..e02354f91 100644 --- a/crates/ntx-builder/src/actor/mod.rs +++ b/crates/ntx-builder/src/actor/mod.rs @@ -75,6 +75,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)] @@ -110,6 +112,7 @@ impl AccountActorContext { idle_timeout: Duration::from_secs(60), db: db.clone(), request_tx, + max_cycles: 1 << 16, } } } @@ -217,6 +220,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 { @@ -243,6 +248,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, } } @@ -390,6 +396,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 48930be26..7d96724da 100644 --- a/crates/ntx-builder/src/lib.rs +++ b/crates/ntx-builder/src/lib.rs @@ -67,6 +67,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 // ================================================================================================= @@ -119,6 +125,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, } @@ -143,6 +155,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, } } @@ -224,6 +237,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 @@ -286,6 +306,7 @@ impl NtxBuilderConfig { idle_timeout: self.idle_timeout, db: db.clone(), request_tx, + max_cycles: self.max_cycles, }; Ok(NetworkTransactionBuilder::new(