Skip to content

Commit b8f0415

Browse files
authored
feat(ntx): limit execution cycles for network transactions (#1801)
1 parent 22e2061 commit b8f0415

File tree

6 files changed

+71
-5
lines changed

6 files changed

+71
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
- Fixed `TransactionHeader` serialization for row insertion on database & fixed transaction cursor on retrievals ([#1701](https://github.com/0xMiden/node/issues/1701)).
1717
- Added KMS signing support in validator ([#1677](https://github.com/0xMiden/node/pull/1677)).
1818
- Added per-IP gRPC rate limiting across services as well as global concurrent connection limit ([#1746](https://github.com/0xMiden/node/issues/1746)).
19-
- 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)).
19+
- 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)).
2020

2121
### Changes
2222

@@ -47,6 +47,7 @@
4747
- 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)).
4848
- Fixed incorrectly classifying private notes with the network attachment as network notes ([#1378](https://github.com/0xMiden/node/pull/1738)).
4949
- 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)).
50+
- 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)).
5051

5152
## v0.13.7 (2026-02-25)
5253

bin/node/src/commands/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ const ENV_VALIDATOR_KEY: &str = "MIDEN_NODE_VALIDATOR_KEY";
4848
const ENV_VALIDATOR_KMS_KEY_ID: &str = "MIDEN_NODE_VALIDATOR_KMS_KEY_ID";
4949
const ENV_NTX_DATA_DIRECTORY: &str = "MIDEN_NODE_NTX_DATA_DIRECTORY";
5050
const ENV_NTX_BUILDER_URL: &str = "MIDEN_NODE_NTX_BUILDER_URL";
51+
const ENV_NTX_MAX_CYCLES: &str = "MIDEN_NTX_MAX_CYCLES";
5152

5253
const DEFAULT_NTX_TICKER_INTERVAL: Duration = Duration::from_millis(200);
5354
const DEFAULT_NTX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
5455
const DEFAULT_NTX_SCRIPT_CACHE_SIZE: NonZeroUsize = NonZeroUsize::new(1000).unwrap();
56+
const DEFAULT_NTX_MAX_CYCLES: u32 = 1 << 16;
5557

5658
/// Configuration for the Validator key used to sign blocks.
5759
///
@@ -195,6 +197,17 @@ pub struct NtxBuilderConfig {
195197
)]
196198
pub max_account_crashes: usize,
197199

200+
/// Maximum number of VM execution cycles allowed for a single network transaction.
201+
///
202+
/// Network transactions that exceed this limit will fail. Defaults to 2^16 (65536) cycles.
203+
#[arg(
204+
long = "ntx-builder.max-cycles",
205+
env = ENV_NTX_MAX_CYCLES,
206+
default_value_t = DEFAULT_NTX_MAX_CYCLES,
207+
value_name = "NUM",
208+
)]
209+
pub max_tx_cycles: u32,
210+
198211
/// Directory for the ntx-builder's persistent database.
199212
///
200213
/// If not set, defaults to the node's data directory.
@@ -227,6 +240,7 @@ impl NtxBuilderConfig {
227240
.with_script_cache_size(self.script_cache_size)
228241
.with_idle_timeout(self.idle_timeout)
229242
.with_max_account_crashes(self.max_account_crashes)
243+
.with_max_cycles(self.max_tx_cycles)
230244
}
231245
}
232246

bin/node/src/tests.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ fn bundled_bootstrap_parses() {
3535
fn bundled_start_parses() {
3636
let _ = parse(&["bundled", "start"]);
3737
}
38+
39+
#[test]
40+
fn bundled_start_with_max_cycles_parses() {
41+
let _ = parse(&["bundled", "start", "--ntx-builder.max-cycles", "131072"]);
42+
}

crates/ntx-builder/src/actor/execute.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use miden_tx::auth::UnreachableAuth;
3636
use miden_tx::{
3737
DataStore,
3838
DataStoreError,
39+
ExecutionOptions,
3940
FailedNote,
4041
LocalTransactionProver,
4142
MastForestStore,
@@ -108,6 +109,9 @@ pub struct NtxContext {
108109

109110
/// Local database for persistent note script caching.
110111
db: Db,
112+
113+
/// Maximum number of VM execution cycles for network transactions.
114+
max_cycles: u32,
111115
}
112116

113117
impl NtxContext {
@@ -119,6 +123,7 @@ impl NtxContext {
119123
store: StoreClient,
120124
script_cache: LruCache<Word, NoteScript>,
121125
db: Db,
126+
max_cycles: u32,
122127
) -> Self {
123128
Self {
124129
block_producer,
@@ -127,9 +132,24 @@ impl NtxContext {
127132
store,
128133
script_cache,
129134
db,
135+
max_cycles,
130136
}
131137
}
132138

139+
/// Creates a [`TransactionExecutor`] configured with the network transaction cycle limit.
140+
fn create_executor<'a, 'b>(
141+
&self,
142+
data_store: &'a NtxDataStore,
143+
) -> TransactionExecutor<'a, 'b, NtxDataStore, UnreachableAuth> {
144+
let exec_options =
145+
ExecutionOptions::new(Some(self.max_cycles), self.max_cycles, false, false)
146+
.expect("max_cycles should be within valid range");
147+
148+
TransactionExecutor::new(data_store)
149+
.with_options(exec_options)
150+
.expect("execution options should be valid for transaction executor")
151+
}
152+
133153
/// Executes a transaction end-to-end: filtering, executing, proving, and submitted to the block
134154
/// producer.
135155
///
@@ -235,8 +255,7 @@ impl NtxContext {
235255
data_store: &NtxDataStore,
236256
notes: Vec<Note>,
237257
) -> NtxResult<(InputNotes<InputNote>, Vec<FailedNote>)> {
238-
let executor: TransactionExecutor<'_, '_, _, UnreachableAuth> =
239-
TransactionExecutor::new(data_store);
258+
let executor = self.create_executor(data_store);
240259
let checker = NoteConsumptionChecker::new(&executor);
241260

242261
match Box::pin(checker.check_notes_consumability(
@@ -279,8 +298,7 @@ impl NtxContext {
279298
data_store: &NtxDataStore,
280299
notes: InputNotes<InputNote>,
281300
) -> NtxResult<ExecutedTransaction> {
282-
let executor: TransactionExecutor<'_, '_, _, UnreachableAuth> =
283-
TransactionExecutor::new(data_store);
301+
let executor = self.create_executor(data_store);
284302

285303
Box::pin(executor.execute_transaction(
286304
data_store.account.id(),

crates/ntx-builder/src/actor/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ pub struct AccountActorContext {
7575
pub db: Db,
7676
/// Channel for sending requests to the coordinator (via the builder event loop).
7777
pub request_tx: mpsc::Sender<ActorRequest>,
78+
/// Maximum number of VM execution cycles for network transactions.
79+
pub max_cycles: u32,
7880
}
7981

8082
#[cfg(test)]
@@ -110,6 +112,7 @@ impl AccountActorContext {
110112
idle_timeout: Duration::from_secs(60),
111113
db: db.clone(),
112114
request_tx,
115+
max_cycles: 1 << 16,
113116
}
114117
}
115118
}
@@ -217,6 +220,8 @@ pub struct AccountActor {
217220
idle_timeout: Duration,
218221
/// Channel for sending requests to the coordinator.
219222
request_tx: mpsc::Sender<ActorRequest>,
223+
/// Maximum number of VM execution cycles for network transactions.
224+
max_cycles: u32,
220225
}
221226

222227
impl AccountActor {
@@ -243,6 +248,7 @@ impl AccountActor {
243248
max_note_attempts: actor_context.max_note_attempts,
244249
idle_timeout: actor_context.idle_timeout,
245250
request_tx: actor_context.request_tx.clone(),
251+
max_cycles: actor_context.max_cycles,
246252
}
247253
}
248254

@@ -390,6 +396,7 @@ impl AccountActor {
390396
self.store.clone(),
391397
self.script_cache.clone(),
392398
self.db.clone(),
399+
self.max_cycles,
393400
);
394401

395402
let notes = tx_candidate.notes.clone();

crates/ntx-builder/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
6767
/// Default maximum number of crashes an account actor is allowed before being deactivated.
6868
const DEFAULT_MAX_ACCOUNT_CRASHES: usize = 10;
6969

70+
/// Default maximum number of VM execution cycles allowed for a network transaction.
71+
///
72+
/// This limits the computational cost of network transactions. The protocol maximum is
73+
/// `1 << 29` but network transactions should be much cheaper.
74+
const DEFAULT_MAX_TX_CYCLES: u32 = 1 << 16;
75+
7076
// CONFIGURATION
7177
// =================================================================================================
7278

@@ -119,6 +125,12 @@ pub struct NtxBuilderConfig {
119125
/// Once this limit is reached, no new transactions will be created for this account.
120126
pub max_account_crashes: usize,
121127

128+
/// Maximum number of VM execution cycles allowed for a single network transaction.
129+
///
130+
/// Network transactions that exceed this limit will fail with an execution error.
131+
/// Defaults to 64k cycles.
132+
pub max_cycles: u32,
133+
122134
/// Path to the SQLite database file used for persistent state.
123135
pub database_filepath: PathBuf,
124136
}
@@ -143,6 +155,7 @@ impl NtxBuilderConfig {
143155
account_channel_capacity: DEFAULT_ACCOUNT_CHANNEL_CAPACITY,
144156
idle_timeout: DEFAULT_IDLE_TIMEOUT,
145157
max_account_crashes: DEFAULT_MAX_ACCOUNT_CRASHES,
158+
max_cycles: DEFAULT_MAX_TX_CYCLES,
146159
database_filepath,
147160
}
148161
}
@@ -224,6 +237,13 @@ impl NtxBuilderConfig {
224237
self
225238
}
226239

240+
/// Sets the maximum number of VM execution cycles for network transactions.
241+
#[must_use]
242+
pub fn with_max_cycles(mut self, max: u32) -> Self {
243+
self.max_cycles = max;
244+
self
245+
}
246+
227247
/// Builds and initializes the network transaction builder.
228248
///
229249
/// This method connects to the store and block producer services, fetches the current
@@ -286,6 +306,7 @@ impl NtxBuilderConfig {
286306
idle_timeout: self.idle_timeout,
287307
db: db.clone(),
288308
request_tx,
309+
max_cycles: self.max_cycles,
289310
};
290311

291312
Ok(NetworkTransactionBuilder::new(

0 commit comments

Comments
 (0)