From 0825c6c65c022071e94e30a6c5bc0a131cbb4620 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 07:55:45 +0000 Subject: [PATCH 01/10] test(bench): drive the cost drift guard from `PricedNote::all` `checked_in_cost_matches_benched_cycles` listed its notes as 22 hand-written rstest cases, mirroring the 23 entries of `PricedNote::all`. The CONSTANT_FEE_POLICY_CONFIG case was missing, leaving `CONSTANT_FEE_POLICY_CONFIG_CONSUMPTION_CYCLES` with no drift guard: the other tests over `PricedNote::all` cover the note, but neither checks the constant's value. Replace the case list with a loop over `PricedNote::all`, so a note added to the tables cannot go unchecked - the generator renders the tables from that same list. Drift is collected across all the notes before it is asserted on, so one run still reports every stale constant. `rstest` was this crate's only use of the dev-dependency and is dropped with it. Wall clock is unchanged: `created_notes_cover_executed_output_notes` already executes every scenario serially in a single test and dominates the module's runtime; the two now run concurrently at ~47s each. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- Cargo.lock | 1 - bin/bench-transaction/Cargo.toml | 1 - bin/bench-transaction/README.md | 2 +- bin/bench-transaction/src/note_costs.rs | 72 ++++++++++++------------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6994b08f3f..5d0bf88d10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,7 +577,6 @@ dependencies = [ "miden-testing", "miden-tx", "rand 0.10.2", - "rstest", "serde", "serde_json", "tokio", diff --git a/bin/bench-transaction/Cargo.toml b/bin/bench-transaction/Cargo.toml index 3e32af16ec..62a4ad664e 100644 --- a/bin/bench-transaction/Cargo.toml +++ b/bin/bench-transaction/Cargo.toml @@ -64,4 +64,3 @@ tokio = { optional = true, workspace = true } [dev-dependencies] criterion = { features = ["async_tokio", "html_reports"], workspace = true } -rstest = { workspace = true } diff --git a/bin/bench-transaction/README.md b/bin/bench-transaction/README.md index 8befbaf0a8..daeeebe1d6 100644 --- a/bin/bench-transaction/README.md +++ b/bin/bench-transaction/README.md @@ -40,7 +40,7 @@ Regenerate the tables (and `bench-tx.json`) with: make update-note-costs ``` -Freshness is enforced in CI: the `checked_in_cost_matches_benched_cycles` snapshot tests in `src/note_costs.rs` re-execute every priced scenario during the regular test run and fail when a measured cost drifts more than 5% from its checked-in constant. Drift within the tolerance (from unrelated changes landing on the base branch) is absorbed without regeneration - fee-wise this is safe, since the fee is logarithmic in cycles and the pricing safety margin dwarfs it. A PR that meaningfully changes cycle counts must run `make update-note-costs` and commit the updated tables - which doubles as review signal, since cost regressions show up as table diffs. +Freshness is enforced in CI: the `checked_in_cost_matches_benched_cycles` snapshot test in `src/note_costs.rs` re-executes every priced scenario during the regular test run and fails when a measured cost drifts more than 5% from its checked-in constant. It walks `PricedNote::all` - the same list the generator renders the tables from - so a newly priced note is covered without touching the test. Drift within the tolerance (from unrelated changes landing on the base branch) is absorbed without regeneration - fee-wise this is safe, since the fee is logarithmic in cycles and the pricing safety margin dwarfs it. A PR that meaningfully changes cycle counts must run `make update-note-costs` and commit the updated tables - which doubles as review signal, since cost regressions show up as table diffs. ### Benchmark Groups diff --git a/bin/bench-transaction/src/note_costs.rs b/bin/bench-transaction/src/note_costs.rs index be840d4243..42f72df0ec 100644 --- a/bin/bench-transaction/src/note_costs.rs +++ b/bin/bench-transaction/src/note_costs.rs @@ -357,7 +357,6 @@ mod tests { use miden_protocol::transaction::RawOutputNote; use miden_standards::note::TxFeeNote; use miden_tx::NetworkNotePricer; - use rstest::rstest; use super::*; @@ -440,47 +439,42 @@ mod tests { Ok(()) } - /// Snapshot check enforcing freshness of the checked-in cost tables: re-executes each priced - /// note's benchmark scenarios and compares the measured maximum against the compiled-in - /// constant, failing when they diverge by more than [`DRIFT_TOLERANCE_PERCENT`]. Catches - /// kernel, standards, or agglayer changes that meaningfully shift a note's consumption cost - /// without the tables having been regenerated. - #[rstest] - #[case::p2id(PricedNote::Standard(StandardNote::P2ID))] - #[case::p2ide(PricedNote::Standard(StandardNote::P2IDE))] - #[case::swap(PricedNote::Standard(StandardNote::SWAP))] - #[case::pswap(PricedNote::Standard(StandardNote::PSWAP))] - #[case::mint(PricedNote::Standard(StandardNote::MINT))] - #[case::burn(PricedNote::Standard(StandardNote::BURN))] - #[case::faucet_policy_config(PricedNote::Standard(StandardNote::FAUCET_POLICY_CONFIG))] - #[case::faucet_metadata_config(PricedNote::Standard(StandardNote::FAUCET_METADATA_CONFIG))] - #[case::min_burn_amount_config(PricedNote::Standard(StandardNote::MIN_BURN_AMOUNT_CONFIG))] - #[case::allowlist_config(PricedNote::Standard(StandardNote::ALLOWLIST_CONFIG))] - #[case::blocklist_config(PricedNote::Standard(StandardNote::BLOCKLIST_CONFIG))] - #[case::pause_config(PricedNote::Standard(StandardNote::PAUSE_CONFIG))] - #[case::owner_config(PricedNote::Standard(StandardNote::OWNER_CONFIG))] - #[case::rbac_config(PricedNote::Standard(StandardNote::RBAC_CONFIG))] - #[case::network_account_config(PricedNote::Standard(StandardNote::NETWORK_ACCOUNT_CONFIG))] - #[case::fee_sponsorship(PricedNote::Standard(StandardNote::FEE_SPONSORSHIP))] - #[case::claim(PricedNote::Agglayer(AgglayerNote::CLAIM))] - #[case::b2agg(PricedNote::Agglayer(AgglayerNote::B2AGG))] - #[case::config_agg_bridge(PricedNote::Agglayer(AgglayerNote::CONFIG_AGG_BRIDGE))] - #[case::deregister_agg_faucet(PricedNote::Agglayer(AgglayerNote::DEREGISTER_AGG_FAUCET))] - #[case::update_ger(PricedNote::Agglayer(AgglayerNote::UPDATE_GER))] - #[case::remove_ger(PricedNote::Agglayer(AgglayerNote::REMOVE_GER))] + /// Snapshot check enforcing freshness of the checked-in cost tables: re-executes the + /// benchmark scenarios of every note in [`PricedNote::all`] and compares each measured + /// maximum against the compiled-in constant, failing when they diverge by more than + /// [`DRIFT_TOLERANCE_PERCENT`]. Catches kernel, standards, or agglayer changes that + /// meaningfully shift a note's consumption cost without the tables having been + /// regenerated. + /// + /// Driven by [`PricedNote::all`] rather than a per-note case list, so a note cannot be + /// added to the tables without its constant gaining a drift guard. Drift is collected + /// across all the notes before it is asserted on, so one run reports every stale constant + /// (a scenario that fails to execute still aborts the run immediately). #[tokio::test] - async fn checked_in_cost_matches_benched_cycles(#[case] note: PricedNote) -> Result<()> { - let measured = benched_cycles(note).await?; - let committed = note.committed_cycles(); + async fn checked_in_cost_matches_benched_cycles() -> Result<()> { + let mut stale = Vec::new(); + for ¬e in PricedNote::all() { + let measured = benched_cycles(note).await?; + let committed = note.committed_cycles(); + + let (measured_scaled, committed) = (u64::from(measured) * 100, u64::from(committed)); + let within_tolerance = measured_scaled <= committed * (100 + DRIFT_TOLERANCE_PERCENT) + && measured_scaled >= committed * (100 - DRIFT_TOLERANCE_PERCENT); + if !within_tolerance { + stale.push(format!( + "{}: measured {measured} cycles vs checked-in {committed}", + note.name(), + )); + } + } - let (measured_scaled, committed) = (u64::from(measured) * 100, u64::from(committed)); - let within_tolerance = measured_scaled <= committed * (100 + DRIFT_TOLERANCE_PERCENT) - && measured_scaled >= committed * (100 - DRIFT_TOLERANCE_PERCENT); assert!( - within_tolerance, - "cost table stale for {note:?}: measured {measured} cycles vs checked-in \ - {committed} (more than {DRIFT_TOLERANCE_PERCENT}% apart): run `make \ - update-note-costs` and commit the updated tables", + stale.is_empty(), + "cost table stale for {} note(s), each more than {DRIFT_TOLERANCE_PERCENT}% from \ + its checked-in constant: {}. Run `make update-note-costs` and commit the updated \ + tables", + stale.len(), + stale.join("; "), ); Ok(()) } From 7158781c90e366c97318543e242a12898561a0cb Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 25 Aug 2026 11:16:42 +0200 Subject: [PATCH 02/10] Apply suggestions from code review Co-authored-by: Marti --- bin/bench-transaction/README.md | 2 +- bin/bench-transaction/src/note_costs.rs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/bin/bench-transaction/README.md b/bin/bench-transaction/README.md index daeeebe1d6..a08b685fc2 100644 --- a/bin/bench-transaction/README.md +++ b/bin/bench-transaction/README.md @@ -40,7 +40,7 @@ Regenerate the tables (and `bench-tx.json`) with: make update-note-costs ``` -Freshness is enforced in CI: the `checked_in_cost_matches_benched_cycles` snapshot test in `src/note_costs.rs` re-executes every priced scenario during the regular test run and fails when a measured cost drifts more than 5% from its checked-in constant. It walks `PricedNote::all` - the same list the generator renders the tables from - so a newly priced note is covered without touching the test. Drift within the tolerance (from unrelated changes landing on the base branch) is absorbed without regeneration - fee-wise this is safe, since the fee is logarithmic in cycles and the pricing safety margin dwarfs it. A PR that meaningfully changes cycle counts must run `make update-note-costs` and commit the updated tables - which doubles as review signal, since cost regressions show up as table diffs. +Freshness is enforced in CI: the `checked_in_cost_matches_benched_cycles` snapshot test in `src/note_costs.rs` re-executes every priced scenario during the regular test run and fails when a measured cost drifts more than 5% from its checked-in constant. It walks `PricedNote::all` - so all priced notes are covered. Drift within the tolerance (from unrelated changes landing on the base branch) is absorbed without regeneration - fee-wise this is safe, since the fee is logarithmic in cycles and the pricing safety margin dwarfs it. A PR that meaningfully changes cycle counts must run `make update-note-costs` and commit the updated tables - which doubles as review signal, since cost regressions show up as table diffs. ### Benchmark Groups diff --git a/bin/bench-transaction/src/note_costs.rs b/bin/bench-transaction/src/note_costs.rs index 42f72df0ec..f27211ed53 100644 --- a/bin/bench-transaction/src/note_costs.rs +++ b/bin/bench-transaction/src/note_costs.rs @@ -445,11 +445,6 @@ mod tests { /// [`DRIFT_TOLERANCE_PERCENT`]. Catches kernel, standards, or agglayer changes that /// meaningfully shift a note's consumption cost without the tables having been /// regenerated. - /// - /// Driven by [`PricedNote::all`] rather than a per-note case list, so a note cannot be - /// added to the tables without its constant gaining a drift guard. Drift is collected - /// across all the notes before it is asserted on, so one run reports every stale constant - /// (a scenario that fails to execute still aborts the run immediately). #[tokio::test] async fn checked_in_cost_matches_benched_cycles() -> Result<()> { let mut stale = Vec::new(); From 68bd41f98ad2a8a7be9c0cf5968a33e5b2b36f6d Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 09:35:28 +0000 Subject: [PATCH 03/10] refactor(bench): add stable script-root-derived labels for input notes A note ID commits to the note's recipient, so any edit to an inlined note script renames it. That makes note IDs unusable as keys in a checked-in artifact. Add `NoteLabels`, which names each input note by its kind - resolved from its script root - suffixed with an occurrence index when a scenario consumes several notes of that kind. Not wired into the snapshot writer yet; the next commit switches `bench-tx.json` over to these labels. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- bin/bench-transaction/src/lib.rs | 2 + bin/bench-transaction/src/note_labels.rs | 166 +++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 bin/bench-transaction/src/note_labels.rs diff --git a/bin/bench-transaction/src/lib.rs b/bin/bench-transaction/src/lib.rs index 844f7a1b13..fc1d07a874 100644 --- a/bin/bench-transaction/src/lib.rs +++ b/bin/bench-transaction/src/lib.rs @@ -6,3 +6,5 @@ pub mod context_setups; pub mod cycle_counting_benchmarks; #[cfg(feature = "concurrent")] pub mod note_costs; +#[cfg(feature = "concurrent")] +pub mod note_labels; diff --git a/bin/bench-transaction/src/note_labels.rs b/bin/bench-transaction/src/note_labels.rs new file mode 100644 index 0000000000..7f86766ae0 --- /dev/null +++ b/bin/bench-transaction/src/note_labels.rs @@ -0,0 +1,166 @@ +//! Stable labels for the input notes of a benchmark scenario. +//! +//! A note ID commits to the note's recipient - script root, inputs and serial number - which makes +//! it unusable as a key in a checked-in artifact: any edit to an inlined note script renames every +//! per-note key. A label instead names the note's kind, resolved from its script root, and changes +//! only when that kind, its multiplicity within the transaction, or the note's position among the +//! same-kind notes changes. + +use std::collections::BTreeMap; + +use miden_agglayer::AgglayerNote; +use miden_protocol::note::{NoteId, NoteScriptRoot}; +use miden_protocol::transaction::TransactionInputs; +use miden_standards::note::StandardNote; + +/// Label of an input note whose script root belongs to neither the standard nor the agglayer set. +/// +/// Reachable in the ordinary course: the fee-sponsorship scenarios consume a sponsored feature +/// note whose script is assembled inline by the test fixtures. +const UNKNOWN_NOTE_LABEL: &str = "UNKNOWN"; + +/// Labels of a transaction's input notes, in consumption order. +/// +/// Held as a flat slice rather than a map: a transaction consumes a handful of notes, so the +/// linear lookup is cheaper than the ordering machinery a map would require. +#[derive(Debug)] +pub struct NoteLabels(Vec<(NoteId, String)>); + +impl NoteLabels { + /// Resolves a label for each input note of the transaction. + /// + /// A note that is the only one of its kind in the transaction gets the bare kind name + /// (`P2ID`). When a transaction consumes several notes of one kind, each is suffixed with its + /// index among that kind's occurrences, in input-note order (`P2ID#0`, `P2ID#1`). + pub fn from_inputs(inputs: &TransactionInputs) -> Self { + Self::from_script_roots( + inputs + .input_notes() + .iter() + .map(|input_note| (input_note.note().id(), input_note.note().script().root())), + ) + } + + /// Returns the label of the given note, or `None` if it is not one of the labelled input + /// notes. + /// + /// A miss is distinct from [`UNKNOWN_NOTE_LABEL`], which is itself a legitimate label: callers + /// join measurements keyed by the kernel-reported note ID against these labels, and an ID that + /// fails to resolve is a defect in that join rather than an unrecognised note kind. + pub fn label(&self, id: NoteId) -> Option<&str> { + self.0 + .iter() + .find(|(labelled, _)| *labelled == id) + .map(|(_, label)| label.as_str()) + } + + /// Labels the notes identified by the given `(ID, script root)` pairs, in consumption order. + /// + /// Two passes: whether a kind needs an index suffix is only known once every note has been + /// seen. + fn from_script_roots(notes: impl Iterator) -> Self { + let kinds: Vec<(NoteId, &'static str)> = + notes.map(|(id, root)| (id, note_kind(root))).collect(); + + let mut occurrences: BTreeMap<&'static str, usize> = BTreeMap::new(); + for (_, kind) in &kinds { + *occurrences.entry(kind).or_default() += 1; + } + + let mut next_index: BTreeMap<&'static str, usize> = BTreeMap::new(); + let labels = kinds + .into_iter() + .map(|(id, kind)| { + let index = next_index.entry(kind).or_default(); + let label = if occurrences[kind] > 1 { + format!("{kind}#{index}") + } else { + kind.to_string() + }; + *index += 1; + (id, label) + }) + .collect(); + + Self(labels) + } +} + +/// Returns the name of the note kind the script root identifies, or [`UNKNOWN_NOTE_LABEL`] if it +/// matches no known note. +fn note_kind(root: NoteScriptRoot) -> &'static str { + StandardNote::from_script_root(root) + .map(|note| note.name()) + .or_else(|| AgglayerNote::from_script_root(root).map(|note| note.name())) + .unwrap_or(UNKNOWN_NOTE_LABEL) +} + +#[cfg(test)] +mod tests { + use miden_protocol::Word; + + use super::*; + + /// Builds a note ID that is distinct per `seed` and unrelated to any script root, so the tests + /// exercise labelling independently of how real IDs are derived. + fn note_id(seed: u32) -> NoteId { + NoteId::from_raw(Word::from([seed, seed, seed, seed])) + } + + fn labels(notes: &[(NoteId, NoteScriptRoot)]) -> NoteLabels { + NoteLabels::from_script_roots(notes.iter().copied()) + } + + #[test] + fn sole_note_of_a_kind_keeps_the_bare_name() { + let (p2id, claim) = (note_id(1), note_id(2)); + let labels = labels(&[ + (p2id, StandardNote::P2ID.script_root()), + (claim, AgglayerNote::CLAIM.script_root()), + ]); + + assert_eq!(labels.label(p2id), Some("P2ID")); + assert_eq!(labels.label(claim), Some("CLAIM")); + } + + #[test] + fn repeated_kind_is_indexed_in_input_order() { + let (first, second, third) = (note_id(1), note_id(2), note_id(3)); + let labels = labels(&[ + (first, StandardNote::P2ID.script_root()), + (second, StandardNote::P2ID.script_root()), + (third, StandardNote::P2ID.script_root()), + ]); + + assert_eq!(labels.label(first), Some("P2ID#0")); + assert_eq!(labels.label(second), Some("P2ID#1")); + assert_eq!(labels.label(third), Some("P2ID#2")); + } + + #[test] + fn kinds_are_indexed_independently_of_each_other() { + let (p2id, p2ide, other_p2id) = (note_id(1), note_id(2), note_id(3)); + let labels = labels(&[ + (p2id, StandardNote::P2ID.script_root()), + (p2ide, StandardNote::P2IDE.script_root()), + (other_p2id, StandardNote::P2ID.script_root()), + ]); + + assert_eq!(labels.label(p2id), Some("P2ID#0")); + assert_eq!(labels.label(other_p2id), Some("P2ID#1")); + assert_eq!(labels.label(p2ide), Some("P2IDE"), "a lone P2IDE must not be indexed"); + } + + /// The placeholder is a label like any other - it is indexed when repeated, and it stays + /// distinguishable from a lookup miss, which returns `None`. + #[test] + fn unrecognised_script_roots_share_the_indexed_placeholder() { + let (first, second, absent) = (note_id(1), note_id(2), note_id(3)); + let unrecognised = NoteScriptRoot::from_raw(Word::from([9u32; 4])); + let labels = labels(&[(first, unrecognised), (second, unrecognised)]); + + assert_eq!(labels.label(first), Some("UNKNOWN#0")); + assert_eq!(labels.label(second), Some("UNKNOWN#1")); + assert_eq!(labels.label(absent), None, "an unlabelled note must not read as UNKNOWN"); + } +} From 9e0b98e1524f1efbe9de87ac11d485931c8a946f Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 09:50:48 +0000 Subject: [PATCH 04/10] refactor(bench): key bench-tx.json note costs by label, add total cycles `note_execution` was keyed by the hex of a note commitment, so an inlined MASM change renamed every entry and re-sorted the map - a diff that reads as large cycle swings when the underlying counts barely moved. Emit an array of `{ note, cycles }` in consumption order instead, labelled via `NoteLabels`. A measured note that does not join against the transaction's input notes aborts the run rather than being emitted unlabelled. The join is on the note's details commitment, because that is what a measurement entry carries despite being typed `NoteId`; see the TODO on `measured_note_key` and #3724, which fixes the host to report the real ID. Also record each scenario's `total_cycles`, the figure the checked-in cost tables are derived from, so the snapshot carries it alongside the stages. Regenerating `bench-tx.json` follows in the next commit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- bin/bench-transaction/README.md | 6 +- .../trace_capture.rs | 11 +- .../src/cycle_counting_benchmarks/utils.rs | 130 ++++++++++++++-- bin/bench-transaction/src/main.rs | 6 +- bin/bench-transaction/src/note_labels.rs | 141 +++++++++++++----- 5 files changed, 238 insertions(+), 56 deletions(-) diff --git a/bin/bench-transaction/README.md b/bin/bench-transaction/README.md index a08b685fc2..70eb8b56be 100644 --- a/bin/bench-transaction/README.md +++ b/bin/bench-transaction/README.md @@ -49,8 +49,12 @@ Each of the above transactions is measured in two groups: For each transaction, data is collected on the number of cycles required to complete: - Prologue + - `total_cycles`: the sum of the prologue, notes-processing, tx-script and epilogue intervals, + and the figure the cost tables above are derived from - All notes processing - - Each note execution + - Each note execution, in consumption order and labelled by note kind (`P2ID`, or `P2ID#0` / + `P2ID#1` when a scenario consumes several notes of one kind). Notes whose script is neither a + standard nor an agglayer one are labelled `UNKNOWN`. - Transaction script processing - Epilogue: - Total number of cycles diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/trace_capture.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/trace_capture.rs index eac07f652f..de873183d6 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/trace_capture.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/trace_capture.rs @@ -15,8 +15,11 @@ use miden_tx::{ TransactionProverHost, }; +use crate::note_labels::NoteLabels; + /// Executes the transaction, then replays its inputs through the trace-build path to capture a -/// `TraceLenSummary`. +/// `TraceLenSummary`. Also resolves the stable labels of the transaction's input notes, which the +/// measurements are reported under. /// /// Two passes: `TransactionExecutor` first so the authenticator resolves any required signatures /// into the `ExecutedTransaction`'s inputs, then `LocalTransactionProver::prove`'s trace-build @@ -24,16 +27,18 @@ use miden_tx::{ /// per-iteration. pub async fn capture_measurements_and_trace_summary( mock_tx: MockTransaction, -) -> Result<(TransactionMeasurements, TraceLenSummary)> { +) -> Result<(TransactionMeasurements, TraceLenSummary, NoteLabels)> { let executed = mock_tx .execute() .await .context("pre-execution (to resolve signatures) failed")?; let (tx_inputs, _tx_outputs, _account_patch, measurements) = executed.into_parts(); + // resolved before `tx_inputs` is consumed by the trace-build pass + let note_labels = NoteLabels::from_inputs(&tx_inputs)?; let trace_summary = build_trace_summary(tx_inputs).await?; - Ok((measurements, trace_summary)) + Ok((measurements, trace_summary, note_labels)) } // TODO(#2841): integrate `TraceLenSummary` into `TransactionMeasurements` so we can drop this diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 62e1c37c29..e92493c4e5 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -1,6 +1,3 @@ -extern crate alloc; -pub use alloc::collections::BTreeMap; -pub use alloc::string::String; use std::fs::{read_to_string, write}; use std::path::Path; @@ -11,6 +8,7 @@ use serde::Serialize; use serde_json::{Value, from_str, to_string_pretty}; use super::ExecutionBenchmark; +use crate::note_labels::{NoteLabels, measured_note_key}; // MEASUREMENTS PRINTER // ================================================================================================ @@ -20,35 +18,60 @@ use super::ExecutionBenchmark; #[derive(Debug, Clone, Serialize)] pub struct MeasurementsPrinter { prologue: usize, + total_cycles: usize, notes_processing: usize, - note_execution: BTreeMap, + /// A sequence rather than a map keyed by note: the entries stay in the order the kernel + /// measured the notes, so a note whose cycle count moves shows up as a one-line diff instead + /// of re-sorting the whole section. + note_execution: Vec, tx_script_processing: usize, epilogue: EpilogueMeasurements, trace: TraceMeasurements, } impl MeasurementsPrinter { - pub fn from_parts(measurements: TransactionMeasurements, trace: TraceLenSummary) -> Self { - let note_execution_map = measurements + pub fn from_parts( + measurements: TransactionMeasurements, + trace: TraceLenSummary, + note_labels: &NoteLabels, + ) -> anyhow::Result { + let note_execution = measurements .note_execution .iter() - .map(|(id, len)| (id.to_hex(), *len)) - .collect(); - - MeasurementsPrinter { + .map(|(measured, cycles)| { + let note = note_labels.label(measured_note_key(*measured)).with_context(|| { + format!("measured note key {measured} matches no input note of the transaction") + })?; + Ok(NoteExecution { note: note.to_string(), cycles: *cycles }) + }) + .collect::>>()?; + + Ok(MeasurementsPrinter { prologue: measurements.prologue, + total_cycles: measurements.total_cycles(), notes_processing: measurements.notes_processing, - note_execution: note_execution_map, + note_execution, tx_script_processing: measurements.tx_script_processing, epilogue: EpilogueMeasurements::from_parts( measurements.epilogue, measurements.auth_procedure, ), trace: TraceMeasurements::from(trace), - } + }) } } +/// Cycles spent executing one input note. +/// +/// `note` is the note's label; its `#N` suffix, when present, is the note's ordinal among the +/// same-kind notes in *input-note* order, which need not match this entry's position in the +/// measurement sequence. +#[derive(Debug, Clone, Serialize)] +struct NoteExecution { + note: String, + cycles: usize, +} + /// Helper structure holding the cycle count for different intervals in the epilogue, namely: /// - `total` interval holds the total number of cycles required to execute the epilogue /// - `auth_procedure` interval holds the number of cycles required to execute the authentication @@ -136,9 +159,14 @@ pub fn write_bench_results_to_json( #[cfg(test)] mod tests { use miden_processor::trace::{ChipletsLengths, TraceLenSummary}; + use miden_protocol::Word; + use miden_protocol::note::{NoteDetailsCommitment, NoteId}; + use miden_protocol::transaction::TransactionMeasurements; + use miden_standards::note::StandardNote; use serde::Deserialize; - use super::{ExecutionBenchmark, TraceMeasurements}; + use super::{ExecutionBenchmark, MeasurementsPrinter, TraceMeasurements}; + use crate::note_labels::NoteLabels; /// Minimal mirror of the bench-tx.json `trace` section used to validate the committed file /// against the producer's contract. @@ -343,6 +371,82 @@ mod tests { assert_eq!(measurements.range_rows, summary.range_trace_len()); } + // MEASUREMENTS PRINTER + // -------------------------------------------------------------------------------------------- + + /// A note key, in both the form the input notes are labelled by and the form a measurement + /// entry reports it as. + fn note_key(seed: u32) -> (NoteDetailsCommitment, NoteId) { + let word = Word::from([seed, seed, seed, seed]); + (NoteDetailsCommitment::from_raw(word), NoteId::from_raw(word)) + } + + fn trace_summary() -> TraceLenSummary { + TraceLenSummary::new(10, 20, ChipletsLengths::from_parts(30, 40, 50, 60, 70)) + } + + /// Measurements whose stages sum to a `total_cycles` of 100. + fn measurements(note_execution: Vec<(NoteId, usize)>) -> TransactionMeasurements { + TransactionMeasurements { + prologue: 10, + notes_processing: 20, + note_execution, + tx_script_processing: 30, + epilogue: 40, + auth_procedure: 5, + } + } + + /// The emitted entries keep the order the kernel measured the notes in - not the order the + /// labels were resolved in, and not a sort by label - which is what makes a cycle-count change + /// a one-line diff. + #[test] + fn note_execution_keeps_measurement_order_and_uses_labels() { + let ((first, first_measured), (second, second_measured)) = (note_key(1), note_key(2)); + let labels = NoteLabels::from_script_roots( + [ + (first, StandardNote::P2ID.script_root()), + (second, StandardNote::P2ID.script_root()), + ] + .into_iter(), + ) + .expect("note keys are distinct"); + + let printer = MeasurementsPrinter::from_parts( + measurements(vec![(second_measured, 200), (first_measured, 100)]), + trace_summary(), + &labels, + ) + .expect("every measured note is labelled"); + + let entries: Vec<(&str, usize)> = printer + .note_execution + .iter() + .map(|entry| (entry.note.as_str(), entry.cycles)) + .collect(); + assert_eq!(entries, vec![("P2ID#1", 200), ("P2ID#0", 100)]); + assert_eq!(printer.total_cycles, 100); + } + + /// A measured note that is not among the transaction's input notes is a defect in the join, + /// not a note of an unrecognised kind, so it aborts the run rather than being labelled. + #[test] + fn measured_note_without_a_label_is_an_error() { + let labels = NoteLabels::from_script_roots(core::iter::empty()).expect("no notes to label"); + + let err = MeasurementsPrinter::from_parts( + measurements(vec![(note_key(1).1, 100)]), + trace_summary(), + &labels, + ) + .expect_err("an unlabelled note must not be silently emitted"); + + assert!( + err.to_string().contains("matches no input note of the transaction"), + "unexpected error: {err}", + ); + } + #[test] fn trace_measurements_preserve_poseidon2_permutation_rows() { let summary = TraceLenSummary::new_with_padded( diff --git a/bin/bench-transaction/src/main.rs b/bin/bench-transaction/src/main.rs index b5d538e75d..abc3eae897 100644 --- a/bin/bench-transaction/src/main.rs +++ b/bin/bench-transaction/src/main.rs @@ -18,12 +18,14 @@ async fn run_scenario( let mock_tx = build_benchmark_context(bench) .await .with_context(|| format!("failed to build mock transaction for `{bench}`"))?; - let (measurements, trace) = capture_measurements_and_trace_summary(mock_tx) + let (measurements, trace, note_labels) = capture_measurements_and_trace_summary(mock_tx) .await .with_context(|| format!("failed to capture measurements for `{bench}`"))?; let total_cycles = u32::try_from(measurements.total_cycles()) .context("total cycle count does not fit into u32")?; - Ok((bench, MeasurementsPrinter::from_parts(measurements, trace), total_cycles)) + let printer = MeasurementsPrinter::from_parts(measurements, trace, ¬e_labels) + .with_context(|| format!("failed to render measurements for `{bench}`"))?; + Ok((bench, printer, total_cycles)) } #[tokio::main(flavor = "current_thread")] diff --git a/bin/bench-transaction/src/note_labels.rs b/bin/bench-transaction/src/note_labels.rs index 7f86766ae0..9ae76e745a 100644 --- a/bin/bench-transaction/src/note_labels.rs +++ b/bin/bench-transaction/src/note_labels.rs @@ -1,22 +1,28 @@ //! Stable labels for the input notes of a benchmark scenario. //! -//! A note ID commits to the note's recipient - script root, inputs and serial number - which makes -//! it unusable as a key in a checked-in artifact: any edit to an inlined note script renames every -//! per-note key. A label instead names the note's kind, resolved from its script root, and changes -//! only when that kind, its multiplicity within the transaction, or the note's position among the -//! same-kind notes changes. +//! A note's identity commits to its recipient - script root, inputs and serial number - which +//! makes it unusable as a key in a checked-in artifact: any edit to an inlined note script renames +//! every per-note key. A label instead names the note's kind, resolved from its script root, and +//! changes only when that kind, its multiplicity within the transaction, or the note's position +//! among the same-kind notes changes. +//! +//! Labels are keyed by the note's details commitment, which is what a measurement entry carries +//! today; see [`measured_note_key`]. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; +use anyhow::{Result, ensure}; use miden_agglayer::AgglayerNote; -use miden_protocol::note::{NoteId, NoteScriptRoot}; +use miden_protocol::note::{NoteDetailsCommitment, NoteId, NoteScriptRoot}; use miden_protocol::transaction::TransactionInputs; use miden_standards::note::StandardNote; /// Label of an input note whose script root belongs to neither the standard nor the agglayer set. /// /// Reachable in the ordinary course: the fee-sponsorship scenarios consume a sponsored feature -/// note whose script is assembled inline by the test fixtures. +/// note whose script is assembled inline by the test fixtures. Distinct inline-script notes all +/// collapse onto this one kind, so several of them in a single transaction are told apart only by +/// their occurrence index - that is, by consumption order. const UNKNOWN_NOTE_LABEL: &str = "UNKNOWN"; /// Labels of a transaction's input notes, in consumption order. @@ -24,7 +30,7 @@ const UNKNOWN_NOTE_LABEL: &str = "UNKNOWN"; /// Held as a flat slice rather than a map: a transaction consumes a handful of notes, so the /// linear lookup is cheaper than the ordering machinery a map would require. #[derive(Debug)] -pub struct NoteLabels(Vec<(NoteId, String)>); +pub struct NoteLabels(Vec<(NoteDetailsCommitment, String)>); impl NoteLabels { /// Resolves a label for each input note of the transaction. @@ -32,35 +38,53 @@ impl NoteLabels { /// A note that is the only one of its kind in the transaction gets the bare kind name /// (`P2ID`). When a transaction consumes several notes of one kind, each is suffixed with its /// index among that kind's occurrences, in input-note order (`P2ID#0`, `P2ID#1`). - pub fn from_inputs(inputs: &TransactionInputs) -> Self { - Self::from_script_roots( - inputs - .input_notes() - .iter() - .map(|input_note| (input_note.note().id(), input_note.note().script().root())), - ) + /// + /// # Errors + /// Returns an error if two input notes share a details commitment, which would make the join + /// in [`NoteLabels::label`] ambiguous. See [`measured_note_key`]. + pub fn from_inputs(inputs: &TransactionInputs) -> Result { + Self::from_script_roots(inputs.input_notes().iter().map(|input_note| { + (input_note.note().details_commitment(), input_note.note().script().root()) + })) } /// Returns the label of the given note, or `None` if it is not one of the labelled input /// notes. /// /// A miss is distinct from [`UNKNOWN_NOTE_LABEL`], which is itself a legitimate label: callers - /// join measurements keyed by the kernel-reported note ID against these labels, and an ID that - /// fails to resolve is a defect in that join rather than an unrecognised note kind. - pub fn label(&self, id: NoteId) -> Option<&str> { + /// join the kernel-reported measurement key against these labels, and a key that fails to + /// resolve is a defect in that join rather than an unrecognised note kind. + pub fn label(&self, note: NoteDetailsCommitment) -> Option<&str> { self.0 .iter() - .find(|(labelled, _)| *labelled == id) + .find(|(labelled, _)| *labelled == note) .map(|(_, label)| label.as_str()) } - /// Labels the notes identified by the given `(ID, script root)` pairs, in consumption order. + /// Labels the notes identified by the given `(details commitment, script root)` pairs, in + /// consumption order. /// /// Two passes: whether a kind needs an index suffix is only known once every note has been /// seen. - fn from_script_roots(notes: impl Iterator) -> Self { - let kinds: Vec<(NoteId, &'static str)> = - notes.map(|(id, root)| (id, note_kind(root))).collect(); + pub(crate) fn from_script_roots( + notes: impl Iterator, + ) -> Result { + let kinds: Vec<(NoteDetailsCommitment, &'static str)> = + notes.map(|(note, root)| (note, note_kind(root))).collect(); + + // The details commitment excludes the metadata and attachments that the nullifier binds, + // so unlike a note ID it is not unique among a transaction's input notes. Two notes + // sharing one would both resolve to the first one's label, publishing one note's cycle + // count twice and dropping the other's. No benchmark scenario does this today; fail + // loudly rather than silently misattribute if one ever does. + let mut seen = BTreeSet::new(); + for (note, _) in &kinds { + ensure!( + seen.insert(*note), + "input notes share the details commitment {}, so their labels would be ambiguous", + note.to_hex(), + ); + } let mut occurrences: BTreeMap<&'static str, usize> = BTreeMap::new(); for (_, kind) in &kinds { @@ -70,7 +94,7 @@ impl NoteLabels { let mut next_index: BTreeMap<&'static str, usize> = BTreeMap::new(); let labels = kinds .into_iter() - .map(|(id, kind)| { + .map(|(note, kind)| { let index = next_index.entry(kind).or_default(); let label = if occurrences[kind] > 1 { format!("{kind}#{index}") @@ -78,14 +102,30 @@ impl NoteLabels { kind.to_string() }; *index += 1; - (id, label) + (note, label) }) .collect(); - Self(labels) + Ok(Self(labels)) } } +/// Reinterprets the note key of a `TransactionMeasurements::note_execution` entry as the details +/// commitment it actually holds, so it can be looked up with [`NoteLabels::label`]. +/// +/// TODO(#3724): the entry is typed `(NoteId, usize)`, but the host reads the input note segment's +/// base word - the details commitment - rather than the ID cached at `INPUT_NOTE_ID_OFFSET`, so +/// the key is a details commitment wearing a `NoteId`. Once +/// lands the key becomes a real note ID: key +/// [`NoteLabels`] by `NoteId`, resolve it from `input_note.note().id()`, and delete this. +/// +/// That switch is not graceful - a real note ID never equals a details commitment, so *every* +/// lookup misses at once and the generator aborts on its first scenario. Whoever lands #3724 has +/// to make this change in the same PR. +pub(crate) fn measured_note_key(measured: NoteId) -> NoteDetailsCommitment { + NoteDetailsCommitment::from_raw(measured.as_word()) +} + /// Returns the name of the note kind the script root identifies, or [`UNKNOWN_NOTE_LABEL`] if it /// matches no known note. fn note_kind(root: NoteScriptRoot) -> &'static str { @@ -101,19 +141,19 @@ mod tests { use super::*; - /// Builds a note ID that is distinct per `seed` and unrelated to any script root, so the tests - /// exercise labelling independently of how real IDs are derived. - fn note_id(seed: u32) -> NoteId { - NoteId::from_raw(Word::from([seed, seed, seed, seed])) + /// Builds a note key that is distinct per `seed` and unrelated to any script root, so the + /// tests exercise labelling independently of how real notes are committed to. + fn note(seed: u32) -> NoteDetailsCommitment { + NoteDetailsCommitment::from_raw(Word::from([seed, seed, seed, seed])) } - fn labels(notes: &[(NoteId, NoteScriptRoot)]) -> NoteLabels { - NoteLabels::from_script_roots(notes.iter().copied()) + fn labels(notes: &[(NoteDetailsCommitment, NoteScriptRoot)]) -> NoteLabels { + NoteLabels::from_script_roots(notes.iter().copied()).expect("note keys are distinct") } #[test] fn sole_note_of_a_kind_keeps_the_bare_name() { - let (p2id, claim) = (note_id(1), note_id(2)); + let (p2id, claim) = (note(1), note(2)); let labels = labels(&[ (p2id, StandardNote::P2ID.script_root()), (claim, AgglayerNote::CLAIM.script_root()), @@ -125,7 +165,7 @@ mod tests { #[test] fn repeated_kind_is_indexed_in_input_order() { - let (first, second, third) = (note_id(1), note_id(2), note_id(3)); + let (first, second, third) = (note(1), note(2), note(3)); let labels = labels(&[ (first, StandardNote::P2ID.script_root()), (second, StandardNote::P2ID.script_root()), @@ -139,7 +179,7 @@ mod tests { #[test] fn kinds_are_indexed_independently_of_each_other() { - let (p2id, p2ide, other_p2id) = (note_id(1), note_id(2), note_id(3)); + let (p2id, p2ide, other_p2id) = (note(1), note(2), note(3)); let labels = labels(&[ (p2id, StandardNote::P2ID.script_root()), (p2ide, StandardNote::P2IDE.script_root()), @@ -155,7 +195,7 @@ mod tests { /// distinguishable from a lookup miss, which returns `None`. #[test] fn unrecognised_script_roots_share_the_indexed_placeholder() { - let (first, second, absent) = (note_id(1), note_id(2), note_id(3)); + let (first, second, absent) = (note(1), note(2), note(3)); let unrecognised = NoteScriptRoot::from_raw(Word::from([9u32; 4])); let labels = labels(&[(first, unrecognised), (second, unrecognised)]); @@ -163,4 +203,31 @@ mod tests { assert_eq!(labels.label(second), Some("UNKNOWN#1")); assert_eq!(labels.label(absent), None, "an unlabelled note must not read as UNKNOWN"); } + + /// The measurement key round-trips into the details commitment the input notes are keyed by, + /// so the join in `MeasurementsPrinter::from_parts` resolves. + #[test] + fn measured_note_key_preserves_the_underlying_word() { + let commitment = note(7); + + assert_eq!(measured_note_key(NoteId::from_raw(commitment.as_word())), commitment); + } + + /// Unlike a note ID, a details commitment is not unique among input notes: it excludes the + /// metadata the nullifier binds. Labelling must refuse rather than misattribute. + #[test] + fn duplicate_details_commitments_are_rejected() { + let shared = note(1); + + let err = NoteLabels::from_script_roots( + [ + (shared, StandardNote::P2ID.script_root()), + (shared, StandardNote::P2ID.script_root()), + ] + .into_iter(), + ) + .expect_err("a shared details commitment must not be labelled"); + + assert!(err.to_string().contains("share the details commitment"), "unexpected: {err}"); + } } From f461ac0af67965f84d10a9f3ff2d9f1f8b786618 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 11:07:57 +0000 Subject: [PATCH 05/10] chore(bench): regenerate bench-tx.json in the label-keyed shape One-time rewrite of the whole artifact: every scenario gains `total_cycles` and its `note_execution` becomes an ordered array keyed by note label. The `trace` section - the part miden-vm's synthetic bench consumes - is unchanged in shape. The cost tables are deliberately left alone. Regenerating them here would move every constant, but every move is sub-threshold (worst is BURN at -3.1% against the 5% drift tolerance), so the guard never asked for it and this refactor has no business shifting fee-affecting values. Also extend `committed_bench_tx_matches_trace_contract` to cover the two new sections, so a stale or hand-edited artifact fails instead of passing on the `trace` section alone. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- bin/bench-transaction/bench-tx.json | 1209 ++++++++++------- .../src/cycle_counting_benchmarks/utils.rs | 59 +- 2 files changed, 745 insertions(+), 523 deletions(-) diff --git a/bin/bench-transaction/bench-tx.json b/bin/bench-transaction/bench-tx.json index 894c10c53b..fa49731841 100644 --- a/bin/bench-transaction/bench-tx.json +++ b/bin/bench-transaction/bench-tx.json @@ -1,23 +1,27 @@ { "consume single P2ID note with Falcon signing": { - "prologue": 4492, - "notes_processing": 2151, - "note_execution": { - "0xc4e2427e43c7965071b694bff219dd2b4f668978639085cf4df6e54972aa2426": 2109 - }, + "prologue": 4513, + "total_cycles": 80329, + "notes_processing": 2173, + "note_execution": [ + { + "note": "P2ID", + "cycles": 2131 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 73556, - "auth_procedure": 72437 + "total": 73602, + "auth_procedure": 72465 }, "trace": { - "core_rows": 80284, - "chiplets_rows": 11351, - "poseidon2_permutation_rows": 54160, - "range_rows": 20521, + "core_rows": 80373, + "chiplets_rows": 11391, + "poseidon2_permutation_rows": 54352, + "range_rows": 20579, "chiplets_shape": { - "hasher_rows": 8360, - "bitwise_rows": 592, + "hasher_rows": 8384, + "bitwise_rows": 608, "memory_rows": 2397, "kernel_rom_rows": 1, "ace_rows": 0 @@ -25,24 +29,28 @@ } }, "consume single P2ID note with ECDSA signing": { - "prologue": 4492, - "notes_processing": 2151, - "note_execution": { - "0x7f66e8f045f7dd99060ad8623e15bb2f18e333af0ef1c51156ebe43e1a08bd77": 2109 - }, + "prologue": 4513, + "total_cycles": 12691, + "notes_processing": 2173, + "note_execution": [ + { + "note": "P2ID", + "cycles": 2131 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 5918, - "auth_procedure": 4799 + "total": 5964, + "auth_procedure": 4827 }, "trace": { - "core_rows": 12646, - "chiplets_rows": 5608, - "poseidon2_permutation_rows": 19056, - "range_rows": 1859, + "core_rows": 12735, + "chiplets_rows": 5656, + "poseidon2_permutation_rows": 19248, + "range_rows": 1901, "chiplets_shape": { - "hasher_rows": 4008, - "bitwise_rows": 848, + "hasher_rows": 4040, + "bitwise_rows": 864, "memory_rows": 750, "kernel_rom_rows": 1, "ace_rows": 0 @@ -50,25 +58,32 @@ } }, "consume two P2ID notes with Falcon signing": { - "prologue": 5328, - "notes_processing": 4484, - "note_execution": { - "0x3dda3c3c3a56ee37e27a6cb453913fec8663269d75aa7f4cf66fc31771401939": 2109, - "0x811b72abe68d792c3bd5c57464a8dc3c803703e741cb8630ca25bb8a2cea31a7": 2324 - }, + "prologue": 5372, + "total_cycles": 83471, + "notes_processing": 4528, + "note_execution": [ + { + "note": "P2ID#0", + "cycles": 2131 + }, + { + "note": "P2ID#1", + "cycles": 2346 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 73484, - "auth_procedure": 72401 + "total": 73530, + "auth_procedure": 72429 }, "trace": { - "core_rows": 83381, - "chiplets_rows": 13360, - "poseidon2_permutation_rows": 54784, - "range_rows": 20457, + "core_rows": 83515, + "chiplets_rows": 13424, + "poseidon2_permutation_rows": 54960, + "range_rows": 20549, "chiplets_shape": { - "hasher_rows": 9880, - "bitwise_rows": 944, + "hasher_rows": 9912, + "bitwise_rows": 976, "memory_rows": 2534, "kernel_rom_rows": 1, "ace_rows": 0 @@ -76,25 +91,32 @@ } }, "consume two P2ID notes with ECDSA signing": { - "prologue": 5328, - "notes_processing": 4484, - "note_execution": { - "0x3dda3c3c3a56ee37e27a6cb453913fec8663269d75aa7f4cf66fc31771401939": 2109, - "0x811b72abe68d792c3bd5c57464a8dc3c803703e741cb8630ca25bb8a2cea31a7": 2324 - }, + "prologue": 5372, + "total_cycles": 15833, + "notes_processing": 4528, + "note_execution": [ + { + "note": "P2ID#0", + "cycles": 2131 + }, + { + "note": "P2ID#1", + "cycles": 2346 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 5846, - "auth_procedure": 4763 + "total": 5892, + "auth_procedure": 4791 }, "trace": { - "core_rows": 15743, - "chiplets_rows": 7617, - "poseidon2_permutation_rows": 19696, - "range_rows": 1441, + "core_rows": 15877, + "chiplets_rows": 7689, + "poseidon2_permutation_rows": 19872, + "range_rows": 1457, "chiplets_shape": { - "hasher_rows": 5528, - "bitwise_rows": 1200, + "hasher_rows": 5568, + "bitwise_rows": 1232, "memory_rows": 887, "kernel_rom_rows": 1, "ace_rows": 0 @@ -103,21 +125,22 @@ }, "create single P2ID note with Falcon signing": { "prologue": 1876, + "total_cycles": 78929, "notes_processing": 35, - "note_execution": {}, - "tx_script_processing": 1854, + "note_execution": [], + "tx_script_processing": 1883, "epilogue": { - "total": 75067, - "auth_procedure": 73021 + "total": 75135, + "auth_procedure": 73049 }, "trace": { - "core_rows": 78876, - "chiplets_rows": 10845, - "poseidon2_permutation_rows": 52464, - "range_rows": 20463, + "core_rows": 78973, + "chiplets_rows": 10893, + "poseidon2_permutation_rows": 52640, + "range_rows": 20351, "chiplets_shape": { - "hasher_rows": 8008, - "bitwise_rows": 544, + "hasher_rows": 8040, + "bitwise_rows": 560, "memory_rows": 2291, "kernel_rom_rows": 1, "ace_rows": 0 @@ -126,21 +149,22 @@ }, "create single P2ID note with ECDSA signing": { "prologue": 1876, + "total_cycles": 11291, "notes_processing": 35, - "note_execution": {}, - "tx_script_processing": 1854, + "note_execution": [], + "tx_script_processing": 1883, "epilogue": { - "total": 7429, - "auth_procedure": 5383 + "total": 7497, + "auth_procedure": 5411 }, "trace": { - "core_rows": 11238, - "chiplets_rows": 5102, - "poseidon2_permutation_rows": 17360, - "range_rows": 1271, + "core_rows": 11335, + "chiplets_rows": 5150, + "poseidon2_permutation_rows": 17536, + "range_rows": 1275, "chiplets_shape": { - "hasher_rows": 3656, - "bitwise_rows": 800, + "hasher_rows": 3688, + "bitwise_rows": 816, "memory_rows": 644, "kernel_rom_rows": 1, "ace_rows": 0 @@ -149,22 +173,26 @@ }, "consume CLAIM note (L1 to Miden)": { "prologue": 4821, - "notes_processing": 28305, - "note_execution": { - "0xb3af39ecb1ce4d350adbc2c16f9fd10f3ccd7e30f1fa482d53114a213829fa4b": 28263 - }, + "total_cycles": 49745, + "notes_processing": 28303, + "note_execution": [ + { + "note": "CLAIM", + "cycles": 28261 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 16564, - "auth_procedure": 11312 + "total": 16580, + "auth_procedure": 11311 }, "trace": { - "core_rows": 49775, - "chiplets_rows": 20164, - "poseidon2_permutation_rows": 41072, - "range_rows": 3501, + "core_rows": 49789, + "chiplets_rows": 20172, + "poseidon2_permutation_rows": 41136, + "range_rows": 3489, "chiplets_shape": { - "hasher_rows": 13184, + "hasher_rows": 13192, "bitwise_rows": 2760, "memory_rows": 4218, "kernel_rom_rows": 1, @@ -174,22 +202,26 @@ }, "consume CLAIM note (L2 to Miden)": { "prologue": 4821, - "notes_processing": 38467, - "note_execution": { - "0x10fe6fd6fef122257048c92009b43e6df141e09cf1c75f99cc8d2f92017bde43": 38425 - }, + "total_cycles": 59907, + "notes_processing": 38465, + "note_execution": [ + { + "note": "CLAIM", + "cycles": 38423 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 16564, - "auth_procedure": 11312 + "total": 16580, + "auth_procedure": 11311 }, "trace": { - "core_rows": 59937, - "chiplets_rows": 22914, - "poseidon2_permutation_rows": 43216, - "range_rows": 3699, + "core_rows": 59951, + "chiplets_rows": 22922, + "poseidon2_permutation_rows": 43280, + "range_rows": 3685, "chiplets_shape": { - "hasher_rows": 14736, + "hasher_rows": 14744, "bitwise_rows": 3016, "memory_rows": 5160, "kernel_rom_rows": 1, @@ -198,24 +230,28 @@ } }, "consume B2AGG note (bridge-out)": { - "prologue": 5106, - "notes_processing": 119258, - "note_execution": { - "0xe81d88c72f6fbfe4453b450f29d2c107eb04907da068a61beb1bc90da0a56ec2": 119216 - }, + "prologue": 5128, + "total_cycles": 149572, + "notes_processing": 119280, + "note_execution": [ + { + "note": "B2AGG", + "cycles": 119238 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 25085, - "auth_procedure": 10151 + "total": 25123, + "auth_procedure": 10150 }, "trace": { - "core_rows": 149534, - "chiplets_rows": 71108, - "poseidon2_permutation_rows": 113680, - "range_rows": 4887, + "core_rows": 149616, + "chiplets_rows": 71148, + "poseidon2_permutation_rows": 113744, + "range_rows": 4905, "chiplets_shape": { - "hasher_rows": 57160, - "bitwise_rows": 3552, + "hasher_rows": 57176, + "bitwise_rows": 3576, "memory_rows": 10394, "kernel_rom_rows": 1, "ace_rows": 0 @@ -223,24 +259,28 @@ } }, "consume B2AGG note (bridge-out, 2^31 leaves)": { - "prologue": 5106, - "notes_processing": 117567, - "note_execution": { - "0xe81d88c72f6fbfe4453b450f29d2c107eb04907da068a61beb1bc90da0a56ec2": 117525 - }, + "prologue": 5128, + "total_cycles": 147593, + "notes_processing": 117589, + "note_execution": [ + { + "note": "B2AGG", + "cycles": 117547 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 24797, - "auth_procedure": 10151 + "total": 24835, + "auth_procedure": 10150 }, "trace": { - "core_rows": 147555, - "chiplets_rows": 70129, - "poseidon2_permutation_rows": 115072, - "range_rows": 4941, + "core_rows": 147637, + "chiplets_rows": 70177, + "poseidon2_permutation_rows": 115136, + "range_rows": 4939, "chiplets_shape": { - "hasher_rows": 56304, - "bitwise_rows": 3552, + "hasher_rows": 56328, + "bitwise_rows": 3576, "memory_rows": 10271, "kernel_rom_rows": 1, "ace_rows": 0 @@ -248,24 +288,28 @@ } }, "consume B2AGG note (bridge-out, 2^31-1 leaves)": { - "prologue": 5106, - "notes_processing": 63398, - "note_execution": { - "0xe81d88c72f6fbfe4453b450f29d2c107eb04907da068a61beb1bc90da0a56ec2": 63356 - }, + "prologue": 5128, + "total_cycles": 84784, + "notes_processing": 63420, + "note_execution": [ + { + "note": "B2AGG", + "cycles": 63378 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 16157, - "auth_procedure": 10151 + "total": 16195, + "auth_procedure": 10150 }, "trace": { - "core_rows": 84746, - "chiplets_rows": 40271, - "poseidon2_permutation_rows": 51504, - "range_rows": 3781, + "core_rows": 84828, + "chiplets_rows": 40319, + "poseidon2_permutation_rows": 51568, + "range_rows": 3855, "chiplets_shape": { - "hasher_rows": 30136, - "bitwise_rows": 3552, + "hasher_rows": 30160, + "bitwise_rows": 3576, "memory_rows": 6581, "kernel_rom_rows": 1, "ace_rows": 0 @@ -273,24 +317,28 @@ } }, "consume P2ID note (network account)": { - "prologue": 3868, - "notes_processing": 2151, - "note_execution": { - "0x78438b9aa97434c7c975348963bd663562965e1f1db79254ecaaa9d41fc8c96e": 2109 - }, + "prologue": 3890, + "total_cycles": 18397, + "notes_processing": 2173, + "note_execution": [ + { + "note": "P2ID", + "cycles": 2131 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12227, - "auth_procedure": 9004 + "total": 12293, + "auth_procedure": 9030 }, "trace": { - "core_rows": 18331, - "chiplets_rows": 8329, - "poseidon2_permutation_rows": 25680, - "range_rows": 1475, + "core_rows": 18441, + "chiplets_rows": 8393, + "poseidon2_permutation_rows": 25824, + "range_rows": 1493, "chiplets_shape": { - "hasher_rows": 6400, - "bitwise_rows": 832, + "hasher_rows": 6432, + "bitwise_rows": 864, "memory_rows": 1095, "kernel_rom_rows": 1, "ace_rows": 0 @@ -298,24 +346,28 @@ } }, "consume P2ID note (16 assets, network account)": { - "prologue": 12043, - "notes_processing": 29525, - "note_execution": { - "0x3dc96c9a47952e4eb15af508916f3fef731cf2cd58e25f717975463d20aef230": 29483 - }, + "prologue": 12335, + "total_cycles": 56965, + "notes_processing": 29817, + "note_execution": [ + { + "note": "P2ID", + "cycles": 29775 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 14706, - "auth_procedure": 9323 + "total": 14772, + "auth_procedure": 9349 }, "trace": { - "core_rows": 56359, - "chiplets_rows": 32489, - "poseidon2_permutation_rows": 42240, - "range_rows": 3635, + "core_rows": 57009, + "chiplets_rows": 32849, + "poseidon2_permutation_rows": 42320, + "range_rows": 3561, "chiplets_shape": { - "hasher_rows": 24800, - "bitwise_rows": 4792, + "hasher_rows": 24888, + "bitwise_rows": 5064, "memory_rows": 2895, "kernel_rom_rows": 1, "ace_rows": 0 @@ -323,24 +375,28 @@ } }, "consume P2IDE note (claim, network account)": { - "prologue": 3868, - "notes_processing": 2259, - "note_execution": { - "0x63eabc40d31379b86e0ded89177dbabf362e0fcfd67e02182e5db0f9df5c5f7d": 2217 - }, + "prologue": 3890, + "total_cycles": 18505, + "notes_processing": 2281, + "note_execution": [ + { + "note": "P2IDE", + "cycles": 2239 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12227, - "auth_procedure": 9004 + "total": 12293, + "auth_procedure": 9030 }, "trace": { - "core_rows": 18439, - "chiplets_rows": 8365, - "poseidon2_permutation_rows": 25776, - "range_rows": 1467, + "core_rows": 18549, + "chiplets_rows": 8421, + "poseidon2_permutation_rows": 25920, + "range_rows": 1489, "chiplets_shape": { - "hasher_rows": 6432, - "bitwise_rows": 832, + "hasher_rows": 6456, + "bitwise_rows": 864, "memory_rows": 1099, "kernel_rom_rows": 1, "ace_rows": 0 @@ -348,24 +404,28 @@ } }, "consume P2IDE note (claim, 16 assets, network account)": { - "prologue": 12043, - "notes_processing": 29633, - "note_execution": { - "0x7629580d7445907dd8d5674f42d8a03706bb1d5e925fc8bc9f5814eaf1817f02": 29591 - }, + "prologue": 12335, + "total_cycles": 57073, + "notes_processing": 29925, + "note_execution": [ + { + "note": "P2IDE", + "cycles": 29883 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 14706, - "auth_procedure": 9323 + "total": 14772, + "auth_procedure": 9349 }, "trace": { - "core_rows": 56467, - "chiplets_rows": 32525, - "poseidon2_permutation_rows": 42336, - "range_rows": 3647, + "core_rows": 57117, + "chiplets_rows": 32877, + "poseidon2_permutation_rows": 42416, + "range_rows": 3575, "chiplets_shape": { - "hasher_rows": 24832, - "bitwise_rows": 4792, + "hasher_rows": 24912, + "bitwise_rows": 5064, "memory_rows": 2899, "kernel_rom_rows": 1, "ace_rows": 0 @@ -373,24 +433,28 @@ } }, "consume P2IDE note (reclaim, network account)": { - "prologue": 3971, - "notes_processing": 2311, - "note_execution": { - "0x0cb8c3d38910a1dacb14737ce17290bb10bd2102cc312aab73aeefa6c707430f": 2269 - }, + "prologue": 3993, + "total_cycles": 18660, + "notes_processing": 2333, + "note_execution": [ + { + "note": "P2IDE", + "cycles": 2291 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12227, - "auth_procedure": 9004 + "total": 12293, + "auth_procedure": 9030 }, "trace": { - "core_rows": 18594, - "chiplets_rows": 8396, - "poseidon2_permutation_rows": 25968, - "range_rows": 1511, + "core_rows": 18704, + "chiplets_rows": 8452, + "poseidon2_permutation_rows": 26112, + "range_rows": 1507, "chiplets_shape": { - "hasher_rows": 6456, - "bitwise_rows": 832, + "hasher_rows": 6480, + "bitwise_rows": 864, "memory_rows": 1106, "kernel_rom_rows": 1, "ace_rows": 0 @@ -398,24 +462,28 @@ } }, "consume SWAP note (public payback, network account)": { - "prologue": 3877, - "notes_processing": 4490, - "note_execution": { - "0x82d7f7ae0d13f63dae7e20e1cd20c33291c91edb9cdac8b26c249356d50326f1": 4448 - }, + "prologue": 3899, + "total_cycles": 22432, + "notes_processing": 4537, + "note_execution": [ + { + "note": "SWAP", + "cycles": 4495 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 13871, - "auth_procedure": 9525 + "total": 13955, + "auth_procedure": 9551 }, "trace": { - "core_rows": 22323, - "chiplets_rows": 10379, - "poseidon2_permutation_rows": 27872, - "range_rows": 1779, + "core_rows": 22476, + "chiplets_rows": 10459, + "poseidon2_permutation_rows": 27952, + "range_rows": 1757, "chiplets_shape": { - "hasher_rows": 7976, - "bitwise_rows": 1152, + "hasher_rows": 8008, + "bitwise_rows": 1200, "memory_rows": 1249, "kernel_rom_rows": 1, "ace_rows": 0 @@ -423,24 +491,28 @@ } }, "consume SWAP note (private payback, network account)": { - "prologue": 3877, - "notes_processing": 3987, - "note_execution": { - "0x380f3b2672cde5e59ac03e7d616b374526eeb55b36c26058c579b1adb291e447": 3945 - }, + "prologue": 3899, + "total_cycles": 21929, + "notes_processing": 4034, + "note_execution": [ + { + "note": "SWAP", + "cycles": 3992 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 13871, - "auth_procedure": 9525 + "total": 13955, + "auth_procedure": 9551 }, "trace": { - "core_rows": 21820, - "chiplets_rows": 10258, - "poseidon2_permutation_rows": 27552, - "range_rows": 1757, + "core_rows": 21973, + "chiplets_rows": 10338, + "poseidon2_permutation_rows": 27632, + "range_rows": 1753, "chiplets_shape": { - "hasher_rows": 7872, - "bitwise_rows": 1152, + "hasher_rows": 7904, + "bitwise_rows": 1200, "memory_rows": 1232, "kernel_rom_rows": 1, "ace_rows": 0 @@ -448,24 +520,28 @@ } }, "consume PSWAP note (full fill, network account)": { - "prologue": 3868, - "notes_processing": 7286, - "note_execution": { - "0x72d4eff497e5ed31de0b876e71a833c0d417d697f884f6adc770989aa0297182": 7244 - }, + "prologue": 3890, + "total_cycles": 25262, + "notes_processing": 7336, + "note_execution": [ + { + "note": "PSWAP", + "cycles": 7294 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 13907, - "auth_procedure": 9525 + "total": 13995, + "auth_procedure": 9551 }, "trace": { - "core_rows": 25146, - "chiplets_rows": 11216, + "core_rows": 25306, + "chiplets_rows": 11304, "poseidon2_permutation_rows": 30496, - "range_rows": 1833, + "range_rows": 1829, "chiplets_shape": { - "hasher_rows": 8496, - "bitwise_rows": 1280, + "hasher_rows": 8536, + "bitwise_rows": 1328, "memory_rows": 1438, "kernel_rom_rows": 1, "ace_rows": 0 @@ -473,24 +549,28 @@ } }, "consume PSWAP note (partial fill, network account)": { - "prologue": 3868, - "notes_processing": 9764, - "note_execution": { - "0x72d4eff497e5ed31de0b876e71a833c0d417d697f884f6adc770989aa0297182": 9722 - }, + "prologue": 3890, + "total_cycles": 29179, + "notes_processing": 9836, + "note_execution": [ + { + "note": "PSWAP", + "cycles": 9794 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 15302, - "auth_procedure": 9729 + "total": 15412, + "auth_procedure": 9755 }, "trace": { - "core_rows": 29019, - "chiplets_rows": 12964, + "core_rows": 29223, + "chiplets_rows": 13076, "poseidon2_permutation_rows": 32288, - "range_rows": 1937, + "range_rows": 1973, "chiplets_shape": { - "hasher_rows": 9704, - "bitwise_rows": 1656, + "hasher_rows": 9752, + "bitwise_rows": 1720, "memory_rows": 1602, "kernel_rom_rows": 1, "ace_rows": 0 @@ -499,24 +579,28 @@ }, "consume MINT note (fungible faucet, network account)": { "prologue": 5614, - "notes_processing": 7929, - "note_execution": { - "0x4a83ca97c7bf0131e229dafb19dc5098f001bcde3ade01f64fa6ad366211623f": 7887 - }, + "total_cycles": 33864, + "notes_processing": 9183, + "note_execution": [ + { + "note": "MINT", + "cycles": 9141 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 18938, - "auth_procedure": 9280 + "total": 19026, + "auth_procedure": 9306 }, "trace": { - "core_rows": 32566, - "chiplets_rows": 13031, - "poseidon2_permutation_rows": 28368, - "range_rows": 2853, + "core_rows": 33908, + "chiplets_rows": 13366, + "poseidon2_permutation_rows": 28448, + "range_rows": 2923, "chiplets_shape": { - "hasher_rows": 9792, - "bitwise_rows": 1152, - "memory_rows": 2085, + "hasher_rows": 10056, + "bitwise_rows": 1200, + "memory_rows": 2108, "kernel_rom_rows": 1, "ace_rows": 0 } @@ -524,48 +608,56 @@ }, "consume MINT note (non-fungible faucet, network account)": { "prologue": 5663, - "notes_processing": 10838, - "note_execution": { - "0xc95ff03a78dc62970c83d47146a48dc73162836fbaedf444f64f7adbc1c29181": 10796 - }, + "total_cycles": 37388, + "notes_processing": 12388, + "note_execution": [ + { + "note": "MINT", + "cycles": 12346 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 19212, - "auth_procedure": 9289 + "total": 19296, + "auth_procedure": 9315 }, "trace": { - "core_rows": 35798, - "chiplets_rows": 14146, - "poseidon2_permutation_rows": 30240, - "range_rows": 3049, + "core_rows": 37432, + "chiplets_rows": 14543, + "poseidon2_permutation_rows": 30288, + "range_rows": 3041, "chiplets_shape": { - "hasher_rows": 10872, - "bitwise_rows": 1056, - "memory_rows": 2216, + "hasher_rows": 11192, + "bitwise_rows": 1104, + "memory_rows": 2245, "kernel_rom_rows": 1, "ace_rows": 0 } } }, "consume BURN note (network account)": { - "prologue": 6195, - "notes_processing": 4495, - "note_execution": { - "0x3fc3626c806a146f13e1ac9e59c0eac978fb59766d51c45911444bcad9b1f20f": 4453 - }, + "prologue": 6217, + "total_cycles": 28550, + "notes_processing": 4527, + "note_execution": [ + { + "note": "BURN", + "cycles": 4485 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 17699, - "auth_procedure": 9078 + "total": 17765, + "auth_procedure": 9104 }, "trace": { - "core_rows": 28474, - "chiplets_rows": 11610, - "poseidon2_permutation_rows": 27552, - "range_rows": 2619, + "core_rows": 28594, + "chiplets_rows": 11666, + "poseidon2_permutation_rows": 27600, + "range_rows": 2599, "chiplets_shape": { - "hasher_rows": 8840, - "bitwise_rows": 888, + "hasher_rows": 8864, + "bitwise_rows": 920, "memory_rows": 1880, "kernel_rom_rows": 1, "ace_rows": 0 @@ -574,23 +666,27 @@ }, "consume FAUCET_POLICY_CONFIG note (network account)": { "prologue": 5549, + "total_cycles": 26794, "notes_processing": 3592, - "note_execution": { - "0x1f2ce020e34fbcc888d18a7dd727829d1f1795fbb5c90927db0c51cf26bcab0b": 3550 - }, + "note_execution": [ + { + "note": "FAUCET_POLICY_CONFIG", + "cycles": 3550 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 17546, - "auth_procedure": 9069 + "total": 17612, + "auth_procedure": 9095 }, "trace": { - "core_rows": 26772, - "chiplets_rows": 10220, - "poseidon2_permutation_rows": 26144, - "range_rows": 2439, + "core_rows": 26838, + "chiplets_rows": 10252, + "poseidon2_permutation_rows": 26224, + "range_rows": 2445, "chiplets_shape": { - "hasher_rows": 7896, - "bitwise_rows": 568, + "hasher_rows": 7912, + "bitwise_rows": 584, "memory_rows": 1754, "kernel_rom_rows": 1, "ace_rows": 0 @@ -599,23 +695,27 @@ }, "consume FAUCET_METADATA_CONFIG note (network account)": { "prologue": 5000, + "total_cycles": 27265, "notes_processing": 6023, - "note_execution": { - "0xc5d07af771df01abcd0fb395b32d26f7ba9205f09d4360ecca7876101017ecf4": 5981 - }, + "note_execution": [ + { + "note": "FAUCET_METADATA_CONFIG", + "cycles": 5981 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 16135, - "auth_procedure": 8988 + "total": 16201, + "auth_procedure": 9014 }, "trace": { - "core_rows": 27243, - "chiplets_rows": 10321, - "poseidon2_permutation_rows": 25440, + "core_rows": 27309, + "chiplets_rows": 10353, + "poseidon2_permutation_rows": 25520, "range_rows": 2403, "chiplets_shape": { - "hasher_rows": 7920, - "bitwise_rows": 576, + "hasher_rows": 7936, + "bitwise_rows": 592, "memory_rows": 1823, "kernel_rom_rows": 1, "ace_rows": 0 @@ -624,23 +724,27 @@ }, "consume MIN_BURN_AMOUNT_CONFIG note (network account)": { "prologue": 5606, + "total_cycles": 25830, "notes_processing": 2418, - "note_execution": { - "0x7aeb862ad5ae354ca222f42cb76f60de52b8cfc78a8c540bce55b8b6a2dc6e81": 2376 - }, + "note_execution": [ + { + "note": "MIN_BURN_AMOUNT_CONFIG", + "cycles": 2376 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 17699, - "auth_procedure": 9078 + "total": 17765, + "auth_procedure": 9104 }, "trace": { - "core_rows": 25808, - "chiplets_rows": 9830, - "poseidon2_permutation_rows": 24736, - "range_rows": 2463, + "core_rows": 25874, + "chiplets_rows": 9870, + "poseidon2_permutation_rows": 24816, + "range_rows": 2459, "chiplets_shape": { - "hasher_rows": 7552, - "bitwise_rows": 568, + "hasher_rows": 7576, + "bitwise_rows": 584, "memory_rows": 1708, "kernel_rom_rows": 1, "ace_rows": 0 @@ -649,23 +753,27 @@ }, "consume ALLOWLIST_CONFIG note (network account)": { "prologue": 5606, + "total_cycles": 26576, "notes_processing": 2985, - "note_execution": { - "0xc27f85d59a55783f8405b4aaa258e2bbc0789f9b5d600a9fc6b0b648fbb08854": 2943 - }, + "note_execution": [ + { + "note": "ALLOWLIST_CONFIG", + "cycles": 2943 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 17878, - "auth_procedure": 9078 + "total": 17944, + "auth_procedure": 9104 }, "trace": { - "core_rows": 26554, - "chiplets_rows": 10464, - "poseidon2_permutation_rows": 27280, - "range_rows": 2497, + "core_rows": 26620, + "chiplets_rows": 10496, + "poseidon2_permutation_rows": 27360, + "range_rows": 2475, "chiplets_shape": { - "hasher_rows": 8112, - "bitwise_rows": 568, + "hasher_rows": 8128, + "bitwise_rows": 584, "memory_rows": 1782, "kernel_rom_rows": 1, "ace_rows": 0 @@ -674,23 +782,27 @@ }, "consume BLOCKLIST_CONFIG note (network account)": { "prologue": 5606, + "total_cycles": 26576, "notes_processing": 2985, - "note_execution": { - "0x57e359b416c6054eaaee8ec6d101dfa7ccd1e8897014e6004cd8b87e1b82ba77": 2943 - }, + "note_execution": [ + { + "note": "BLOCKLIST_CONFIG", + "cycles": 2943 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 17878, - "auth_procedure": 9078 + "total": 17944, + "auth_procedure": 9104 }, "trace": { - "core_rows": 26554, - "chiplets_rows": 10464, - "poseidon2_permutation_rows": 27104, - "range_rows": 2513, + "core_rows": 26620, + "chiplets_rows": 10496, + "poseidon2_permutation_rows": 27184, + "range_rows": 2495, "chiplets_shape": { - "hasher_rows": 8112, - "bitwise_rows": 568, + "hasher_rows": 8128, + "bitwise_rows": 584, "memory_rows": 1782, "kernel_rom_rows": 1, "ace_rows": 0 @@ -699,23 +811,27 @@ }, "consume PAUSE_CONFIG note (network account)": { "prologue": 3476, + "total_cycles": 18308, "notes_processing": 2433, - "note_execution": { - "0xebe32aa38938bfc16f277b8542e58ed4b6b7a4ea9be764972291b965299f59a6": 2391 - }, + "note_execution": [ + { + "note": "PAUSE_CONFIG", + "cycles": 2391 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12292, - "auth_procedure": 8763 + "total": 12358, + "auth_procedure": 8789 }, "trace": { - "core_rows": 18286, - "chiplets_rows": 7506, - "poseidon2_permutation_rows": 23536, - "range_rows": 1569, + "core_rows": 18352, + "chiplets_rows": 7538, + "poseidon2_permutation_rows": 23616, + "range_rows": 1567, "chiplets_shape": { - "hasher_rows": 5792, - "bitwise_rows": 568, + "hasher_rows": 5808, + "bitwise_rows": 584, "memory_rows": 1144, "kernel_rom_rows": 1, "ace_rows": 0 @@ -724,23 +840,27 @@ }, "consume OWNER_CONFIG note (network account)": { "prologue": 3335, + "total_cycles": 17889, "notes_processing": 2461, - "note_execution": { - "0x413694d4ebf2f4f4ad41eb749201f02c2d3e4c08da7f36eb957ccab987124fb7": 2419 - }, + "note_execution": [ + { + "note": "OWNER_CONFIG", + "cycles": 2419 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 11986, - "auth_procedure": 8745 + "total": 12052, + "auth_procedure": 8771 }, "trace": { - "core_rows": 17867, - "chiplets_rows": 7386, - "poseidon2_permutation_rows": 23392, - "range_rows": 1503, + "core_rows": 17933, + "chiplets_rows": 7426, + "poseidon2_permutation_rows": 23472, + "range_rows": 1505, "chiplets_shape": { - "hasher_rows": 5688, - "bitwise_rows": 584, + "hasher_rows": 5712, + "bitwise_rows": 600, "memory_rows": 1112, "kernel_rom_rows": 1, "ace_rows": 0 @@ -749,23 +869,27 @@ }, "consume RBAC_CONFIG note (network account)": { "prologue": 3514, + "total_cycles": 21712, "notes_processing": 5257, - "note_execution": { - "0xe8b49ff8fb19d28da5c7e5266fe1a243b31cbb6e350636449d856e180797e81d": 5215 - }, + "note_execution": [ + { + "note": "RBAC_CONFIG", + "cycles": 5215 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12834, - "auth_procedure": 8772 + "total": 12900, + "auth_procedure": 8798 }, "trace": { - "core_rows": 21690, - "chiplets_rows": 9844, - "poseidon2_permutation_rows": 29824, - "range_rows": 1665, + "core_rows": 21756, + "chiplets_rows": 9884, + "poseidon2_permutation_rows": 29904, + "range_rows": 1687, "chiplets_shape": { - "hasher_rows": 7832, - "bitwise_rows": 584, + "hasher_rows": 7856, + "bitwise_rows": 600, "memory_rows": 1426, "kernel_rom_rows": 1, "ace_rows": 0 @@ -774,23 +898,27 @@ }, "consume NETWORK_ACCOUNT_CONFIG note (network account)": { "prologue": 3419, + "total_cycles": 18789, "notes_processing": 2958, - "note_execution": { - "0x89ace2ea72ffd23f3252bf317ed8155bf1e79fc769d166f538487c5b74d646c6": 2916 - }, + "note_execution": [ + { + "note": "NETWORK_ACCOUNT_CONFIG", + "cycles": 2916 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12305, - "auth_procedure": 8754 + "total": 12371, + "auth_procedure": 8780 }, "trace": { - "core_rows": 18767, - "chiplets_rows": 8042, - "poseidon2_permutation_rows": 26080, - "range_rows": 1557, + "core_rows": 18833, + "chiplets_rows": 8082, + "poseidon2_permutation_rows": 26160, + "range_rows": 1581, "chiplets_shape": { - "hasher_rows": 6280, - "bitwise_rows": 568, + "hasher_rows": 6304, + "bitwise_rows": 584, "memory_rows": 1192, "kernel_rom_rows": 1, "ace_rows": 0 @@ -799,23 +927,27 @@ }, "consume CONSTANT_FEE_POLICY_CONFIG note (network account)": { "prologue": 3475, + "total_cycles": 19683, "notes_processing": 3643, - "note_execution": { - "0xa7d88a5ea6ffa94733184dccae85c121eaa85ec4b00e6231fe0305de18a2782d": 3601 - }, + "note_execution": [ + { + "note": "CONSTANT_FEE_POLICY_CONFIG", + "cycles": 3601 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 12458, - "auth_procedure": 8763 + "total": 12524, + "auth_procedure": 8789 }, "trace": { - "core_rows": 19661, - "chiplets_rows": 8336, - "poseidon2_permutation_rows": 26192, - "range_rows": 1601, + "core_rows": 19727, + "chiplets_rows": 8368, + "poseidon2_permutation_rows": 26272, + "range_rows": 1605, "chiplets_shape": { - "hasher_rows": 6496, - "bitwise_rows": 568, + "hasher_rows": 6512, + "bitwise_rows": 584, "memory_rows": 1270, "kernel_rom_rows": 1, "ace_rows": 0 @@ -823,25 +955,32 @@ } }, "consume FEE_SPONSORSHIP note with feature note (network account)": { - "prologue": 5071, + "prologue": 5093, + "total_cycles": 21249, "notes_processing": 1155, - "note_execution": { - "0x6199ee3ed355b92a02eb0d8f15891778431c64c1de383fb8d12a79c8cd7820b6": 648, - "0xaa511953232bfdd8bde965cf9b0cca4b57e4a9d9a83ba375ea8002caec40eff1": 456 - }, + "note_execution": [ + { + "note": "UNKNOWN", + "cycles": 456 + }, + { + "note": "FEE_SPONSORSHIP", + "cycles": 648 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 14872, - "auth_procedure": 11793 + "total": 14960, + "auth_procedure": 11841 }, "trace": { - "core_rows": 21183, - "chiplets_rows": 9444, - "poseidon2_permutation_rows": 25984, + "core_rows": 21293, + "chiplets_rows": 9500, + "poseidon2_permutation_rows": 26064, "range_rows": 1565, "chiplets_shape": { - "hasher_rows": 7320, - "bitwise_rows": 904, + "hasher_rows": 7344, + "bitwise_rows": 936, "memory_rows": 1218, "kernel_rom_rows": 1, "ace_rows": 0 @@ -849,24 +988,28 @@ } }, "consume FEE_SPONSORSHIP note (reclaim)": { - "prologue": 3754, - "notes_processing": 2843, - "note_execution": { - "0x69c307c81334697abfed0ba239cb2a5f3357658acb54bb3d3792c16a774d081b": 2801 - }, + "prologue": 3776, + "total_cycles": 16945, + "notes_processing": 2865, + "note_execution": [ + { + "note": "FEE_SPONSORSHIP", + "cycles": 2823 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 10168, - "auth_procedure": 7946 + "total": 10263, + "auth_procedure": 8001 }, "trace": { - "core_rows": 16850, - "chiplets_rows": 7730, - "poseidon2_permutation_rows": 23392, - "range_rows": 1553, + "core_rows": 16989, + "chiplets_rows": 7802, + "poseidon2_permutation_rows": 23568, + "range_rows": 1571, "chiplets_shape": { - "hasher_rows": 5712, - "bitwise_rows": 1152, + "hasher_rows": 5752, + "bitwise_rows": 1184, "memory_rows": 864, "kernel_rom_rows": 1, "ace_rows": 0 @@ -875,23 +1018,27 @@ }, "consume CLAIM note (L1 to Miden, with fee payment)": { "prologue": 4821, - "notes_processing": 28305, - "note_execution": { - "0xb3af39ecb1ce4d350adbc2c16f9fd10f3ccd7e30f1fa482d53114a213829fa4b": 28263 - }, + "total_cycles": 53839, + "notes_processing": 28303, + "note_execution": [ + { + "note": "CLAIM", + "cycles": 28261 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 20608, - "auth_procedure": 13974 + "total": 20674, + "auth_procedure": 14000 }, "trace": { - "core_rows": 53819, - "chiplets_rows": 22282, - "poseidon2_permutation_rows": 46336, - "range_rows": 3633, + "core_rows": 53883, + "chiplets_rows": 22314, + "poseidon2_permutation_rows": 46416, + "range_rows": 3557, "chiplets_shape": { - "hasher_rows": 14816, - "bitwise_rows": 3096, + "hasher_rows": 14832, + "bitwise_rows": 3112, "memory_rows": 4368, "kernel_rom_rows": 1, "ace_rows": 0 @@ -900,23 +1047,27 @@ }, "consume CLAIM note (L2 to Miden, with fee payment)": { "prologue": 4821, - "notes_processing": 38467, - "note_execution": { - "0x10fe6fd6fef122257048c92009b43e6df141e09cf1c75f99cc8d2f92017bde43": 38425 - }, + "total_cycles": 64001, + "notes_processing": 38465, + "note_execution": [ + { + "note": "CLAIM", + "cycles": 38423 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 20608, - "auth_procedure": 13974 + "total": 20674, + "auth_procedure": 14000 }, "trace": { - "core_rows": 63981, - "chiplets_rows": 25032, - "poseidon2_permutation_rows": 48480, - "range_rows": 3871, + "core_rows": 64045, + "chiplets_rows": 25064, + "poseidon2_permutation_rows": 48560, + "range_rows": 3821, "chiplets_shape": { - "hasher_rows": 16368, - "bitwise_rows": 3352, + "hasher_rows": 16384, + "bitwise_rows": 3368, "memory_rows": 5310, "kernel_rom_rows": 1, "ace_rows": 0 @@ -924,24 +1075,28 @@ } }, "consume B2AGG note (bridge-out, with fee payment)": { - "prologue": 5106, - "notes_processing": 119258, - "note_execution": { - "0xe81d88c72f6fbfe4453b450f29d2c107eb04907da068a61beb1bc90da0a56ec2": 119216 - }, + "prologue": 5128, + "total_cycles": 153666, + "notes_processing": 119280, + "note_execution": [ + { + "note": "B2AGG", + "cycles": 119238 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 29129, - "auth_procedure": 12813 + "total": 29217, + "auth_procedure": 12839 }, "trace": { - "core_rows": 153578, - "chiplets_rows": 73218, - "poseidon2_permutation_rows": 117808, - "range_rows": 5063, + "core_rows": 153710, + "chiplets_rows": 73290, + "poseidon2_permutation_rows": 117856, + "range_rows": 5061, "chiplets_shape": { - "hasher_rows": 58784, - "bitwise_rows": 3888, + "hasher_rows": 58816, + "bitwise_rows": 3928, "memory_rows": 10544, "kernel_rom_rows": 1, "ace_rows": 0 @@ -949,24 +1104,28 @@ } }, "consume B2AGG note (bridge-out, 2^31-1 leaves, with fee payment)": { - "prologue": 5106, - "notes_processing": 63398, - "note_execution": { - "0xe81d88c72f6fbfe4453b450f29d2c107eb04907da068a61beb1bc90da0a56ec2": 63356 - }, + "prologue": 5128, + "total_cycles": 88878, + "notes_processing": 63420, + "note_execution": [ + { + "note": "B2AGG", + "cycles": 63378 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 20201, - "auth_procedure": 12813 + "total": 20289, + "auth_procedure": 12839 }, "trace": { - "core_rows": 88790, - "chiplets_rows": 42389, - "poseidon2_permutation_rows": 55600, - "range_rows": 3957, + "core_rows": 88922, + "chiplets_rows": 42461, + "poseidon2_permutation_rows": 55664, + "range_rows": 3993, "chiplets_shape": { - "hasher_rows": 31768, - "bitwise_rows": 3888, + "hasher_rows": 31800, + "bitwise_rows": 3928, "memory_rows": 6731, "kernel_rom_rows": 1, "ace_rows": 0 @@ -975,23 +1134,27 @@ }, "consume CONFIG_AGG_BRIDGE note (with fee payment)": { "prologue": 4508, + "total_cycles": 33909, "notes_processing": 13318, - "note_execution": { - "0x4a95146177b6458432d5a1f60f8ade60686c30743fcc89271463bc1bf577ff97": 13276 - }, + "note_execution": [ + { + "note": "CONFIG_AGG_BRIDGE", + "cycles": 13276 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 15976, - "auth_procedure": 8916 + "total": 16042, + "auth_procedure": 8942 }, "trace": { - "core_rows": 33887, - "chiplets_rows": 15156, - "poseidon2_permutation_rows": 37200, - "range_rows": 2449, + "core_rows": 33953, + "chiplets_rows": 15188, + "poseidon2_permutation_rows": 37280, + "range_rows": 2481, "chiplets_shape": { - "hasher_rows": 12168, - "bitwise_rows": 624, + "hasher_rows": 12184, + "bitwise_rows": 640, "memory_rows": 2362, "kernel_rom_rows": 1, "ace_rows": 0 @@ -1000,23 +1163,27 @@ }, "consume DEREGISTER_AGG_FAUCET note (with fee payment)": { "prologue": 4549, + "total_cycles": 33260, "notes_processing": 12628, - "note_execution": { - "0xe4cef1d192ed7353b3cdb747f5b6beec5419c36c49d96a6d954e3c694ecc9653": 12586 - }, + "note_execution": [ + { + "note": "DEREGISTER_AGG_FAUCET", + "cycles": 12586 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 15976, - "auth_procedure": 8916 + "total": 16042, + "auth_procedure": 8942 }, "trace": { - "core_rows": 33238, - "chiplets_rows": 14786, - "poseidon2_permutation_rows": 35984, - "range_rows": 2361, + "core_rows": 33304, + "chiplets_rows": 14818, + "poseidon2_permutation_rows": 36064, + "range_rows": 2369, "chiplets_shape": { - "hasher_rows": 11936, - "bitwise_rows": 616, + "hasher_rows": 11952, + "bitwise_rows": 632, "memory_rows": 2232, "kernel_rom_rows": 1, "ace_rows": 0 @@ -1025,23 +1192,27 @@ }, "consume UPDATE_GER note (with fee payment)": { "prologue": 4490, + "total_cycles": 24120, "notes_processing": 4347, - "note_execution": { - "0x0f17499940516b9743f89ef52d6cb1d3bb002d1c196e033af53a792302987c9a": 4305 - }, + "note_execution": [ + { + "note": "UPDATE_GER", + "cycles": 4305 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 15176, - "auth_procedure": 8916 + "total": 15242, + "auth_procedure": 8942 }, "trace": { - "core_rows": 24098, - "chiplets_rows": 10019, - "poseidon2_permutation_rows": 29424, - "range_rows": 2099, + "core_rows": 24164, + "chiplets_rows": 10051, + "poseidon2_permutation_rows": 29504, + "range_rows": 2095, "chiplets_shape": { - "hasher_rows": 7808, - "bitwise_rows": 568, + "hasher_rows": 7824, + "bitwise_rows": 584, "memory_rows": 1641, "kernel_rom_rows": 1, "ace_rows": 0 @@ -1050,23 +1221,27 @@ }, "consume REMOVE_GER note (with fee payment)": { "prologue": 4549, + "total_cycles": 25239, "notes_processing": 5371, - "note_execution": { - "0x7305f2b4fc451fa2cdb9c1799357f8b43637ea09330da85535339a02fc427395": 5329 - }, + "note_execution": [ + { + "note": "REMOVE_GER", + "cycles": 5329 + } + ], "tx_script_processing": 41, "epilogue": { - "total": 15212, - "auth_procedure": 8916 + "total": 15278, + "auth_procedure": 8942 }, "trace": { - "core_rows": 25217, - "chiplets_rows": 10335, - "poseidon2_permutation_rows": 29920, - "range_rows": 2135, + "core_rows": 25283, + "chiplets_rows": 10367, + "poseidon2_permutation_rows": 30000, + "range_rows": 2177, "chiplets_shape": { - "hasher_rows": 8040, - "bitwise_rows": 568, + "hasher_rows": 8056, + "bitwise_rows": 584, "memory_rows": 1725, "kernel_rom_rows": 1, "ace_rows": 0 diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index e92493c4e5..19deac53d8 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -168,13 +168,30 @@ mod tests { use super::{ExecutionBenchmark, MeasurementsPrinter, TraceMeasurements}; use crate::note_labels::NoteLabels; - /// Minimal mirror of the bench-tx.json `trace` section used to validate the committed file - /// against the producer's contract. + /// Minimal mirror of a bench-tx.json scenario, used to validate the committed file against + /// the producer's contract. #[derive(Deserialize)] struct ScenarioForTest { + prologue: u64, + total_cycles: u64, + notes_processing: u64, + note_execution: Vec, + tx_script_processing: u64, + epilogue: EpilogueForTest, trace: TraceForTest, } + #[derive(Deserialize)] + struct NoteExecutionForTest { + note: String, + cycles: u64, + } + + #[derive(Deserialize)] + struct EpilogueForTest { + total: u64, + } + #[derive(Deserialize)] struct TraceForTest { core_rows: u64, @@ -275,12 +292,42 @@ mod tests { t.poseidon2_permutation_rows.next_power_of_two().max(MIN_TRACE_LEN) } - fn parse_and_assert_trace_contract(name: &str, raw: &serde_json::Value) -> TraceForTest { + fn parse_and_assert_scenario_contract(name: &str, raw: &serde_json::Value) -> TraceForTest { let scenario: ScenarioForTest = serde_json::from_value(raw.clone()) .unwrap_or_else(|err| panic!("scenario `{name}` does not match the schema: {err}")); let trace = &scenario.trace; let chiplets_shape = &trace.chiplets_shape; + assert_eq!( + scenario.total_cycles, + scenario.prologue + + scenario.notes_processing + + scenario.tx_script_processing + + scenario.epilogue.total, + "{name}: total_cycles must be the sum of the measured stages", + ); + + // only the note-creating scenarios consume nothing + assert_eq!( + scenario.note_execution.is_empty(), + name.starts_with("create"), + "{name}: a consuming scenario must measure at least one note, a creating one none", + ); + for entry in &scenario.note_execution { + assert!( + !entry.note.is_empty() && !entry.note.starts_with("0x"), + "{name}: note_execution should be keyed by a label, found `{}`", + entry.note, + ); + assert!(entry.cycles > 0, "{name}: note `{}` should cost > 0 cycles", entry.note); + } + let note_cycles: u64 = scenario.note_execution.iter().map(|entry| entry.cycles).sum(); + assert!( + note_cycles <= scenario.notes_processing, + "{name}: per-note cycles ({note_cycles}) exceed notes_processing ({})", + scenario.notes_processing, + ); + assert!(trace.core_rows > 0, "{name}: core_rows should be > 0"); assert!(trace.chiplets_rows > 0, "{name}: chiplets_rows should be > 0"); assert!( @@ -312,7 +359,7 @@ mod tests { let raw = scenarios .get(name) .unwrap_or_else(|| panic!("scenario `{name}` is missing from bench-tx.json")); - let trace = parse_and_assert_trace_contract(name, raw); + let trace = parse_and_assert_scenario_contract(name, raw); let core_side = padded_core_side(&trace); let chiplets = padded_chiplets(&trace); @@ -334,7 +381,7 @@ mod tests { } #[test] - fn committed_bench_tx_matches_trace_contract() { + fn committed_bench_tx_matches_producer_contract() { let parsed: serde_json::Value = serde_json::from_str(COMMITTED_BENCH_TX_JSON) .expect("bench-tx.json should be valid JSON"); let scenarios = parsed.as_object().expect("bench-tx.json should contain an object"); @@ -344,7 +391,7 @@ mod tests { "bench-tx.json should contain every ExecutionBenchmark scenario", ); for (name, raw) in scenarios { - parse_and_assert_trace_contract(name, raw); + parse_and_assert_scenario_contract(name, raw); } for expected in COMMITTED_SCENARIO_EXPECTATIONS { assert_scenario(&parsed, expected); From bca10d3bdfc3f1dd04b64262ec6f2a4b3ad47f3a Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 11:21:04 +0000 Subject: [PATCH 06/10] test(bench): assert both note-cost properties off one execution pass `checked_in_cost_matches_benched_cycles` and `created_notes_cover_executed_output_notes` each walked `PricedNote::all` and executed all ~31 priced scenarios independently, so a test run paid for ~62 transaction executions to assert two properties of the same `NoteCost`. Merge them: execute each scenario once and assert the cycle count and the `created_notes` declaration off that result. Failures still accumulate across all notes before being asserted on, and the two kinds are reported separately - a stale cost table and a wrong `created_notes` list need different fixes. The shared execution pass also validates the label join `bench-tx.json`'s generator depends on, which nothing else exercises against a real transaction. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- bin/bench-transaction/README.md | 2 +- bin/bench-transaction/src/note_costs.rs | 193 ++++++++++++++---------- 2 files changed, 113 insertions(+), 82 deletions(-) diff --git a/bin/bench-transaction/README.md b/bin/bench-transaction/README.md index 70eb8b56be..6401573e08 100644 --- a/bin/bench-transaction/README.md +++ b/bin/bench-transaction/README.md @@ -40,7 +40,7 @@ Regenerate the tables (and `bench-tx.json`) with: make update-note-costs ``` -Freshness is enforced in CI: the `checked_in_cost_matches_benched_cycles` snapshot test in `src/note_costs.rs` re-executes every priced scenario during the regular test run and fails when a measured cost drifts more than 5% from its checked-in constant. It walks `PricedNote::all` - so all priced notes are covered. Drift within the tolerance (from unrelated changes landing on the base branch) is absorbed without regeneration - fee-wise this is safe, since the fee is logarithmic in cycles and the pricing safety margin dwarfs it. A PR that meaningfully changes cycle counts must run `make update-note-costs` and commit the updated tables - which doubles as review signal, since cost regressions show up as table diffs. +Freshness is enforced in CI: the `checked_in_note_costs_match_executed_scenarios` snapshot test in `src/note_costs.rs` re-executes every priced scenario during the regular test run and fails when a measured cost drifts more than 5% from its checked-in constant, or when a note's declared `created_notes` do not match what its scenarios actually create. It walks `PricedNote::all` - so all priced notes are covered. Drift within the tolerance (from unrelated changes landing on the base branch) is absorbed without regeneration - fee-wise this is safe, since the fee is logarithmic in cycles and the pricing safety margin dwarfs it. A PR that meaningfully changes cycle counts must run `make update-note-costs` and commit the updated tables - which doubles as review signal, since cost regressions show up as table diffs. ### Benchmark Groups diff --git a/bin/bench-transaction/src/note_costs.rs b/bin/bench-transaction/src/note_costs.rs index f27211ed53..65f6e37823 100644 --- a/bin/bench-transaction/src/note_costs.rs +++ b/bin/bench-transaction/src/note_costs.rs @@ -14,8 +14,6 @@ use miden_protocol::note::NoteScriptRoot; use miden_standards::note::StandardNote; use miden_standards::note::costs::NoteCost; -#[cfg(test)] -use crate::context_setups::build_benchmark_context; use crate::cycle_counting_benchmarks::ExecutionBenchmark; /// Header line of the generated table files. @@ -238,37 +236,6 @@ fn path_label(bench: ExecutionBenchmark) -> &'static str { } } -/// Executes the note's benchmark scenarios and returns each scenario's total cycle count. -#[cfg(test)] -async fn benched_path_cycles(note: PricedNote) -> Result> { - let mut path_cycles = Vec::new(); - for &bench in note.scenarios() { - let mock_tx = build_benchmark_context(bench) - .await - .with_context(|| format!("failed to build mock transaction for `{bench}`"))?; - let executed = mock_tx - .execute() - .await - .with_context(|| format!("failed to execute transaction for `{bench}`"))?; - let cycles = u32::try_from(executed.measurements().total_cycles()) - .context("total cycle count does not fit into u32")?; - path_cycles.push((bench, cycles)); - } - Ok(path_cycles) -} - -/// Executes the note's benchmark scenarios and returns the maximum total cycle count. -#[cfg(test)] -async fn benched_cycles(note: PricedNote) -> Result { - let max_cycles = benched_path_cycles(note) - .await? - .into_iter() - .map(|(_, cycles)| cycles) - .max() - .expect("every priced note has at least one scenario"); - Ok(max_cycles) -} - /// Rewrites the two generated cost table files from the given per-scenario cycle counts /// (typically the measurements the benchmark run just collected, so nothing is executed /// twice). @@ -359,6 +326,8 @@ mod tests { use miden_tx::NetworkNotePricer; use super::*; + use crate::context_setups::build_benchmark_context; + use crate::note_labels::{NoteLabels, measured_note_key}; /// Relative drift (in percent) tolerated between a measured cost and its checked-in /// constant before the snapshot check fails. @@ -405,72 +374,134 @@ mod tests { assert_eq!(subjects["P2ID"], "a P2ID"); } - /// Ties the hand-maintained created-notes metadata backing recursive pricing to actual - /// execution: every priced scenario is executed, and the script roots of the full output - /// notes other than the TX_FEE note must be exactly the consumed note's declared - /// `created_notes`, so both under- and over-declaration are caught. - #[tokio::test] - async fn created_notes_cover_executed_output_notes() -> Result<()> { - for ¬e in PricedNote::all() { - let declared: BTreeSet = - note.note_cost().created_notes().iter().copied().collect(); + /// What executing one priced note's benchmark scenarios established. + struct ExecutedScenarios { + /// Maximum total cycle count across the note's benchmarked paths - the figure its cost + /// table constant holds. + max_cycles: u32, + /// Script roots of the full output notes the scenarios created, other than the TX_FEE + /// note, tagged with the scenario that created each. + created: Vec<(ExecutionBenchmark, NoteScriptRoot)>, + } - let mut observed = BTreeSet::new(); - for &bench in note.scenarios() { - let executed = build_benchmark_context(bench).await?.execute().await?; - for output_note in executed.output_notes().iter() { - let RawOutputNote::Full(created) = output_note else { - continue; - }; - let root = created.script().root(); - if root == TxFeeNote::script_root() { - continue; - } - assert!( - declared.contains(&root), - "scenario `{bench}` created a note with undeclared script root {root}", - ); - observed.insert(root); - } + /// Executes each of the note's benchmark scenarios exactly once, collecting everything the + /// checked-in [`NoteCost`] is asserted against. + /// + /// Also validates the label join that `bench-tx.json`'s generator depends on. Only a real + /// execution exercises it - see the TODO on [`measured_note_key`] - and these are the only + /// real executions in the test suite, so the check rides along here instead of paying for its + /// own. A broken join is an infrastructure failure, so it aborts rather than accumulating. + async fn execute_scenarios(note: PricedNote) -> Result { + let mut max_cycles = 0; + let mut created = Vec::new(); + + for &bench in note.scenarios() { + let executed = build_benchmark_context(bench) + .await + .with_context(|| format!("failed to build mock transaction for `{bench}`"))? + .execute() + .await + .with_context(|| format!("failed to execute transaction for `{bench}`"))?; + + let labels = NoteLabels::from_inputs(executed.tx_inputs()) + .with_context(|| format!("failed to label the input notes of `{bench}`"))?; + for (measured, _) in &executed.measurements().note_execution { + anyhow::ensure!( + labels.label(measured_note_key(*measured)).is_some(), + "scenario `{bench}` measured note key {measured}, which matches none of its \ + input notes", + ); } - assert_eq!(observed, declared, "declared created notes were not observed for {note:?}"); + let cycles = u32::try_from(executed.measurements().total_cycles()) + .context("total cycle count does not fit into u32")?; + max_cycles = max_cycles.max(cycles); + + for output_note in executed.output_notes().iter() { + let RawOutputNote::Full(output_note) = output_note else { + continue; + }; + let root = output_note.script().root(); + if root != TxFeeNote::script_root() { + created.push((bench, root)); + } + } } - Ok(()) + + Ok(ExecutedScenarios { max_cycles, created }) } - /// Snapshot check enforcing freshness of the checked-in cost tables: re-executes the - /// benchmark scenarios of every note in [`PricedNote::all`] and compares each measured - /// maximum against the compiled-in constant, failing when they diverge by more than - /// [`DRIFT_TOLERANCE_PERCENT`]. Catches kernel, standards, or agglayer changes that - /// meaningfully shift a note's consumption cost without the tables having been - /// regenerated. + /// Snapshot check tying the checked-in [`NoteCost`] of every note in [`PricedNote::all`] to a + /// real execution of its benchmark scenarios. Two properties are asserted off the same run: + /// + /// 1. the measured maximum cycle count is within [`DRIFT_TOLERANCE_PERCENT`] of the compiled-in + /// constant, catching kernel, standards, or agglayer changes that meaningfully shift a + /// note's consumption cost without the tables having been regenerated; + /// 2. the script roots of the full output notes other than the TX_FEE note are exactly the + /// note's declared `created_notes`, the hand-maintained metadata backing recursive pricing, + /// so both under- and over-declaration are caught. + /// + /// The two share one execution pass because executing a scenario is by far the expensive part + /// and both properties read the same result. + /// + /// Driven by [`PricedNote::all`] rather than a per-note case list, so a note cannot be added + /// to the tables without gaining both guards. Failures are collected across all the notes + /// before they are asserted on, so one run reports every stale constant and every misdeclared + /// `created_notes` list (a scenario that fails to execute still aborts the run immediately). #[tokio::test] - async fn checked_in_cost_matches_benched_cycles() -> Result<()> { - let mut stale = Vec::new(); + async fn checked_in_note_costs_match_executed_scenarios() -> Result<()> { + let mut stale_costs = Vec::new(); + let mut misdeclared_created_notes = Vec::new(); + for ¬e in PricedNote::all() { - let measured = benched_cycles(note).await?; - let committed = note.committed_cycles(); + let executed = execute_scenarios(note).await?; - let (measured_scaled, committed) = (u64::from(measured) * 100, u64::from(committed)); + let measured = executed.max_cycles; + let (measured_scaled, committed) = + (u64::from(measured) * 100, u64::from(note.committed_cycles())); let within_tolerance = measured_scaled <= committed * (100 + DRIFT_TOLERANCE_PERCENT) && measured_scaled >= committed * (100 - DRIFT_TOLERANCE_PERCENT); if !within_tolerance { - stale.push(format!( + stale_costs.push(format!( "{}: measured {measured} cycles vs checked-in {committed}", note.name(), )); } + + let declared: BTreeSet = + note.note_cost().created_notes().iter().copied().collect(); + let observed: BTreeSet = + executed.created.iter().map(|(_, root)| *root).collect(); + for (bench, root) in executed.created.iter().filter(|(_, r)| !declared.contains(r)) { + misdeclared_created_notes + .push(format!("{}: `{bench}` created undeclared {root}", note.name())); + } + for root in declared.difference(&observed) { + misdeclared_created_notes + .push(format!("{}: declared {root} is never created", note.name())); + } + } + + let mut failures = Vec::new(); + if !stale_costs.is_empty() { + failures.push(format!( + "cost table stale for {} note(s), each more than {DRIFT_TOLERANCE_PERCENT}% from \ + its checked-in constant: {}. Run `make update-note-costs` and commit the updated \ + tables", + stale_costs.len(), + stale_costs.join("; "), + )); + } + if !misdeclared_created_notes.is_empty() { + failures.push(format!( + "`created_notes` misdeclared for {} note(s): {}. Fix the declaration in the \ + note's `NoteCost`; regenerating the tables does not touch it", + misdeclared_created_notes.len(), + misdeclared_created_notes.join("; "), + )); } + assert!(failures.is_empty(), "{}", failures.join("\n")); - assert!( - stale.is_empty(), - "cost table stale for {} note(s), each more than {DRIFT_TOLERANCE_PERCENT}% from \ - its checked-in constant: {}. Run `make update-note-costs` and commit the updated \ - tables", - stale.len(), - stale.join("; "), - ); Ok(()) } From 3ac204e6fec6c06d4d68f2b7079ca5767820744e Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 11:26:06 +0000 Subject: [PATCH 07/10] chore: mark the CI-guarded generated artifacts as linguist-generated Collapses them in GitHub's PR diff and drops them from the repo's language stats. Limited to files something automated is already checking, since collapsing a diff takes a reviewer's eyes off it: `bench-tx.json` is parsed by `committed_bench_tx_matches_producer_contract`, and `Cargo.lock` is gated by `cargo deny`. The note cost tables stay uncollapsed - their constants reach `NetworkNotePricer` - as do the agglayer Solidity test vectors, which are the expected-value side of the bridge conformance tests and have no CI job regenerating them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- .gitattributes | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 4157440bf2..cfab4b4026 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,13 @@ -CHANGELOG.md merge=union \ No newline at end of file +CHANGELOG.md merge=union + +# Generated files, collapsed in GitHub diffs and excluded from language stats. Only files whose +# contents are guarded by CI belong here - collapsing a diff removes a reviewer's eyes from it, so +# something else has to be watching: +# - bench-tx.json: `committed_bench_tx_matches_producer_contract` parses the committed file. +# - Cargo.lock: `cargo deny` runs on every PR with `unknown-registry`/`unknown-git` denied. +# Deliberately absent: the two note cost tables (`miden-standards`/`miden-agglayer` costs/table.rs), +# whose constants reach `NetworkNotePricer`, and the agglayer Solidity test vectors, which are the +# expected-value side of the bridge conformance tests and are regenerated only by a manual +# `make generate-solidity-test-vectors`. Both should be read, not collapsed. +bin/bench-transaction/bench-tx.json linguist-generated=true +Cargo.lock linguist-generated=true From 93d3cf1cf34b25b95946722237addd37dcc6746e Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 11:38:40 +0000 Subject: [PATCH 08/10] docs: add changelog entry for the bench-tx.json reshape Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0301ff4379..3a32ec07d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Changes +- Keyed `bench-tx.json`'s per-note cycle counts by note label instead of note commitment, and added `total_cycles` to each scenario ([#3726](https://github.com/0xMiden/protocol/pull/3726)). - [BREAKING] Refactored `AccountVaultDelta` to track generic assets. `FungibleAssetDelta`, `NonFungibleAssetDelta` and `NonFungibleDeltaAction` were removed ([3485](https://github.com/0xMiden/protocol/pull/3485)). - [BREAKING] Moved the internal shared helpers of `miden::protocol::input_note`, `miden::protocol::active_note`, and the note memory-write helpers into private `input_note_internal` and `note_internal` modules ([#3501](https://github.com/0xMiden/protocol/pull/3501)). - [BREAKING] Changed asset callbacks into validation-only interfaces that return no asset value; the transaction kernel retains and uses the original value, preventing callbacks from modifying it. The kernel commitment changes ([#3505](https://github.com/0xMiden/protocol/issues/3505), [#3513](https://github.com/0xMiden/protocol/pull/3513)). From bc70758c9848597b6f28b16c70c0a77d9141e3e9 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 25 Aug 2026 14:02:41 +0200 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: Marti --- .gitattributes | 10 ---------- .../src/cycle_counting_benchmarks/utils.rs | 3 --- bin/bench-transaction/src/note_costs.rs | 8 -------- bin/bench-transaction/src/note_labels.rs | 3 --- 4 files changed, 24 deletions(-) diff --git a/.gitattributes b/.gitattributes index cfab4b4026..d557c00bf8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,3 @@ CHANGELOG.md merge=union - -# Generated files, collapsed in GitHub diffs and excluded from language stats. Only files whose -# contents are guarded by CI belong here - collapsing a diff removes a reviewer's eyes from it, so -# something else has to be watching: -# - bench-tx.json: `committed_bench_tx_matches_producer_contract` parses the committed file. -# - Cargo.lock: `cargo deny` runs on every PR with `unknown-registry`/`unknown-git` denied. -# Deliberately absent: the two note cost tables (`miden-standards`/`miden-agglayer` costs/table.rs), -# whose constants reach `NetworkNotePricer`, and the agglayer Solidity test vectors, which are the -# expected-value side of the bridge conformance tests and are regenerated only by a manual -# `make generate-solidity-test-vectors`. Both should be read, not collapsed. bin/bench-transaction/bench-tx.json linguist-generated=true Cargo.lock linguist-generated=true diff --git a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs index 19deac53d8..5f276d05bc 100644 --- a/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs +++ b/bin/bench-transaction/src/cycle_counting_benchmarks/utils.rs @@ -20,9 +20,6 @@ pub struct MeasurementsPrinter { prologue: usize, total_cycles: usize, notes_processing: usize, - /// A sequence rather than a map keyed by note: the entries stay in the order the kernel - /// measured the notes, so a note whose cycle count moves shows up as a one-line diff instead - /// of re-sorting the whole section. note_execution: Vec, tx_script_processing: usize, epilogue: EpilogueMeasurements, diff --git a/bin/bench-transaction/src/note_costs.rs b/bin/bench-transaction/src/note_costs.rs index 65f6e37823..95c8ccdfcb 100644 --- a/bin/bench-transaction/src/note_costs.rs +++ b/bin/bench-transaction/src/note_costs.rs @@ -440,14 +440,6 @@ mod tests { /// 2. the script roots of the full output notes other than the TX_FEE note are exactly the /// note's declared `created_notes`, the hand-maintained metadata backing recursive pricing, /// so both under- and over-declaration are caught. - /// - /// The two share one execution pass because executing a scenario is by far the expensive part - /// and both properties read the same result. - /// - /// Driven by [`PricedNote::all`] rather than a per-note case list, so a note cannot be added - /// to the tables without gaining both guards. Failures are collected across all the notes - /// before they are asserted on, so one run reports every stale constant and every misdeclared - /// `created_notes` list (a scenario that fails to execute still aborts the run immediately). #[tokio::test] async fn checked_in_note_costs_match_executed_scenarios() -> Result<()> { let mut stale_costs = Vec::new(); diff --git a/bin/bench-transaction/src/note_labels.rs b/bin/bench-transaction/src/note_labels.rs index 9ae76e745a..418af888f9 100644 --- a/bin/bench-transaction/src/note_labels.rs +++ b/bin/bench-transaction/src/note_labels.rs @@ -26,9 +26,6 @@ use miden_standards::note::StandardNote; const UNKNOWN_NOTE_LABEL: &str = "UNKNOWN"; /// Labels of a transaction's input notes, in consumption order. -/// -/// Held as a flat slice rather than a map: a transaction consumes a handful of notes, so the -/// linear lookup is cheaper than the ordering machinery a map would require. #[derive(Debug)] pub struct NoteLabels(Vec<(NoteDetailsCommitment, String)>); From 632b757bde108f85bb89d7d182eda81fd9e69aef Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 25 Aug 2026 13:39:34 +0000 Subject: [PATCH 10/10] fix(bench): stop public docs linking to crate-private items `cargo doc` denies `rustdoc::private_intra_doc_links`, so the module doc and two public methods failed to document while pointing at `measured_note_key` and `UNKNOWN_NOTE_LABEL`. Both stay crate-private, so drop the links and name them in plain code spans. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01LXcU9HrrmWTCTZTJZnuLSU --- bin/bench-transaction/src/note_labels.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/bench-transaction/src/note_labels.rs b/bin/bench-transaction/src/note_labels.rs index 418af888f9..79bf4ef061 100644 --- a/bin/bench-transaction/src/note_labels.rs +++ b/bin/bench-transaction/src/note_labels.rs @@ -7,7 +7,7 @@ //! among the same-kind notes changes. //! //! Labels are keyed by the note's details commitment, which is what a measurement entry carries -//! today; see [`measured_note_key`]. +//! today; see `measured_note_key`. use std::collections::{BTreeMap, BTreeSet}; @@ -38,7 +38,7 @@ impl NoteLabels { /// /// # Errors /// Returns an error if two input notes share a details commitment, which would make the join - /// in [`NoteLabels::label`] ambiguous. See [`measured_note_key`]. + /// in [`NoteLabels::label`] ambiguous. See `measured_note_key`. pub fn from_inputs(inputs: &TransactionInputs) -> Result { Self::from_script_roots(inputs.input_notes().iter().map(|input_note| { (input_note.note().details_commitment(), input_note.note().script().root()) @@ -48,7 +48,7 @@ impl NoteLabels { /// Returns the label of the given note, or `None` if it is not one of the labelled input /// notes. /// - /// A miss is distinct from [`UNKNOWN_NOTE_LABEL`], which is itself a legitimate label: callers + /// A miss is distinct from `UNKNOWN_NOTE_LABEL`, which is itself a legitimate label: callers /// join the kernel-reported measurement key against these labels, and a key that fails to /// resolve is a defect in that join rather than an unrecognised note kind. pub fn label(&self, note: NoteDetailsCommitment) -> Option<&str> {