1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! One network transaction built for one network account.
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context;
use miden_node_tracing::{error, info, miden_instrument};
use miden_node_utils::lru_cache::LruCache;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::note::{Note, NoteScript, Nullifier};
use miden_protocol::transaction::{TransactionArgs, TransactionId};
use crate::candidate::TransactionCandidate;
use crate::chain_state::ChainState;
use crate::clients::{RemoteTransactionProver, RpcClient};
use crate::db::NtxDbReader;
use crate::execute::{NtxError, NtxExecutionResult};
use crate::selection::{
Selection,
attribute_failed_notes,
log_deferred_notes,
log_oversized_notes,
select_candidate,
};
use crate::{LOG_TARGET, NoteError, execute};
// NETWORK TRANSACTION CONTEXT
// ================================================================================================
/// gRPC clients used by a build to interact with the node's services.
#[derive(Clone)]
pub struct GrpcClients {
/// Client for interacting with the RPC service in order to load account state.
pub rpc: RpcClient,
/// Client for remote transaction proving.
pub prover: RemoteTransactionProver,
}
/// Configuration knobs for one network transaction.
#[derive(Debug, Clone, Copy)]
pub struct NetworkTransactionConfig {
/// Maximum number of notes per transaction. Sponsorship notes count against this budget.
pub max_notes_per_tx: NonZeroUsize,
/// Maximum number of note execution attempts before dropping a note.
pub max_note_attempts: usize,
/// Maximum number of VM execution cycles for network transactions.
pub max_cycles: u32,
/// Initial sleep applied between per-request retries on transient infrastructure failures
/// (prover unreachable, RPC transport error, RPC gRPC hiccup). Doubles each retry up to
/// [`Self::request_backoff_max`].
pub request_backoff_initial: Duration,
/// Upper bound on the per-request retry backoff sleep.
pub request_backoff_max: Duration,
}
/// Resources every build shares.
#[derive(Clone)]
pub struct NetworkTransactionContext {
/// gRPC clients used by the build.
pub clients: GrpcClients,
/// Read-only database handle. A build performs no writes.
pub db: NtxDbReader,
/// Shared LRU cache for note scripts retrieved over RPC.
pub script_cache: LruCache<Word, NoteScript>,
/// [`TransactionArgs`] used by every network transaction. These are constant and are therefore
/// prebuilt once and cloned per transaction.
pub tx_args: TransactionArgs,
/// Configuration knobs for one network transaction.
pub config: NetworkTransactionConfig,
}
#[cfg(test)]
impl NetworkTransactionContext {
/// Creates a minimal [`NetworkTransactionContext`] for tests.
pub fn test(db: &NtxDbReader) -> Self {
use url::Url;
let url = Url::parse("http://127.0.0.1:1").unwrap();
let block_header = crate::test_utils::mock_block_header(0_u32.into());
let trusted_validator_signing_keys = block_header.validator_config().keys().to_vec();
Self {
clients: GrpcClients {
rpc: RpcClient::new(
url.clone(),
Word::default(),
trusted_validator_signing_keys,
Duration::from_secs(10),
Duration::from_millis(1),
Duration::from_millis(10),
)
.expect("rpc client should be constructed"),
prover: RemoteTransactionProver::new(url, Duration::from_secs(10))
.expect("prover client should be constructed"),
},
db: db.clone(),
script_cache: LruCache::new(NonZeroUsize::new(1).unwrap()),
tx_args: crate::selection::build_tx_args(
std::num::NonZeroU16::new(30).expect("literal is non-zero"),
),
config: NetworkTransactionConfig {
max_notes_per_tx: NonZeroUsize::new(20).expect("literal is non-zero"),
max_note_attempts: 30,
max_cycles: 1 << 18,
request_backoff_initial: Duration::from_millis(1),
request_backoff_max: Duration::from_millis(10),
},
}
}
}
// NETWORK TRANSACTION OUTCOME
// ================================================================================================
/// Note bookkeeping a build produced. The scheduler persists all of it, whatever the
/// [`NetworkTransactionResult`].
#[derive(Default)]
pub struct NoteUpdates {
/// Notes whose attempt counter must be incremented, keyed by the nullifier the failure is
/// recorded under.
pub failed: Vec<(Nullifier, NoteError)>,
/// Notes that can never be consumed and must be marked permanently unconsumable.
pub discarded: Vec<Nullifier>,
/// Corrected eligibility blocks for notes whose stored block is earlier than the exact rule
/// allows. Persisting these is what stops the account being selected again for a note it cannot
/// consume. See [`crate::db::eligibility`].
pub eligibility: Vec<(Nullifier, BlockNumber)>,
/// Note scripts fetched over RPC that should be persisted to the local cache.
pub scripts: Vec<(Word, NoteScript)>,
}
/// How a build ended.
pub enum NetworkTransactionResult {
/// A transaction was proven and submitted. The account is now in flight and is not retried
/// until the transaction commits or expires.
Submitted { tx_id: TransactionId },
/// Selection produced no candidate. Any note whose stored eligibility was wrong is corrected
/// through [`NoteUpdates::eligibility`], so the account is not selected again for it.
NoWork,
/// The candidate failed for a reason attributed to its notes, which are named in
/// [`NoteUpdates::failed`].
Failed,
/// The build could not be attributed to any note: the account or its notes could not be read.
/// No note is penalized, because the failure is not the notes' fault.
Aborted(anyhow::Error),
}
/// What a build did, and what the scheduler must persist as a result.
pub struct NetworkTransactionOutcome {
/// The account the build ran against.
pub account_id: AccountId,
/// Reference block of the build. Note bookkeeping is recorded against it.
pub block_num: BlockNumber,
/// Note bookkeeping to persist, populated on every path.
pub notes: NoteUpdates,
/// How the build ended.
pub result: NetworkTransactionResult,
}
// NETWORK TRANSACTION BUILD
// ================================================================================================
impl NetworkTransactionContext {
/// Builds, proves and submits one network transaction for `account_id`.
#[miden_instrument(
name = "ntx.build",
fields(
account.id = account_id,
reference_block.number = chain.chain_tip_header.block_num(),
),
)]
pub async fn build(
self,
account_id: AccountId,
chain: ChainState,
) -> NetworkTransactionOutcome {
let block_num = chain.chain_tip_header.block_num();
match self.try_build(account_id, chain).await {
Ok(outcome) => outcome,
Err(err) => NetworkTransactionOutcome {
account_id,
block_num,
notes: NoteUpdates::default(),
result: NetworkTransactionResult::Aborted(err),
},
}
}
/// The build body. Errors here are infrastructure failures that cannot be charged to a note.
async fn try_build(
&self,
account_id: AccountId,
chain: ChainState,
) -> anyhow::Result<NetworkTransactionOutcome> {
let block_num = chain.chain_tip_header.block_num();
let Some(account) = self
.db
.get_account(account_id)
.await
.context("failed to load committed account")?
else {
return Ok(NetworkTransactionOutcome {
account_id,
block_num,
notes: NoteUpdates::default(),
result: NetworkTransactionResult::NoWork,
});
};
let account = Arc::new(account);
let Selection { candidate, rejected, stale_eligibility } = select_candidate(
&self.db,
&account,
chain,
self.config.max_notes_per_tx,
self.config.max_note_attempts,
)
.await?;
let mut notes = NoteUpdates {
failed: rejected,
eligibility: stale_eligibility,
..NoteUpdates::default()
};
let Some(candidate) = candidate else {
return Ok(NetworkTransactionOutcome {
account_id,
block_num,
notes,
result: NetworkTransactionResult::NoWork,
});
};
let result = self.execute_candidate(account_id, candidate, &mut notes).await;
Ok(NetworkTransactionOutcome { account_id, block_num, notes, result })
}
/// Executes, proves and submits `candidate`, recording the note bookkeeping into `notes`.
async fn execute_candidate(
&self,
account_id: AccountId,
candidate: TransactionCandidate,
notes: &mut NoteUpdates,
) -> NetworkTransactionResult {
let sponsor_to_feature = candidate.sponsor_to_feature_nullifier();
let note_ids: Vec<_> = candidate
.notes
.iter()
.flat_map(|sponsored| {
std::iter::once(sponsored.feature.as_note().id())
.chain(sponsored.sponsorships.iter().map(Note::id))
})
.collect();
let feature_nullifiers: Vec<_> = candidate
.notes
.iter()
.map(|sponsored| sponsored.feature.as_note().nullifier())
.collect();
info!(
target: LOG_TARGET,
"executing network transaction",
account.id = account_id,
note.ids = note_ids.as_slice(),
note.count = note_ids.len()
);
let context = execute::NtxContext::new(
self.clients.prover.clone(),
self.clients.rpc.clone(),
self.script_cache.clone(),
self.db.clone(),
self.config.max_cycles,
self.tx_args.clone(),
self.config.request_backoff_initial,
self.config.request_backoff_max,
);
match context.execute_transaction(candidate).await {
Ok(NtxExecutionResult {
tx_id,
failed_notes,
deferred_notes,
oversized_notes,
fetched_scripts,
}) => {
info!(
target: LOG_TARGET,
"network transaction executed",
account.id = account_id,
transaction.id = tx_id,
note.failed.count = failed_notes.len(),
note.deferred.count = deferred_notes.len(),
note.oversized.count = oversized_notes.len()
);
notes.scripts = fetched_scripts;
log_deferred_notes(deferred_notes);
let (oversized_sponsorships, oversized_features): (Vec<_>, Vec<_>) =
oversized_notes
.into_iter()
.partition(|f| sponsor_to_feature.contains_key(&f.note().id()));
let mut to_penalize = failed_notes;
to_penalize.extend(oversized_sponsorships);
notes.failed.extend(attribute_failed_notes(to_penalize, &sponsor_to_feature));
notes.discarded = log_oversized_notes(oversized_features);
NetworkTransactionResult::Submitted { tx_id }
},
Err(err) => {
error!(
&err,
target: LOG_TARGET,
"network transaction failed",
account.id = account_id,
note.ids = note_ids.as_slice()
);
let failed = match err {
NtxError::AllNotesFailed(per_note) => {
attribute_failed_notes(per_note, &sponsor_to_feature)
},
other => {
let error: NoteError = Arc::new(other);
feature_nullifiers
.into_iter()
.map(|nullifier| {
info!(
error.as_ref(),
target: LOG_TARGET,
"note failed: transaction execution error",
note.nullifier = nullifier
);
(nullifier, error.clone())
})
.collect()
},
};
notes.failed.extend(failed);
NetworkTransactionResult::Failed
},
}
}
}