Skip to content

feat(bench): price consumption of each note against a network account#3354

Open
mmagician wants to merge 9 commits into
nextfrom
mmagician-claude/note-consumption-costs
Open

feat(bench): price consumption of each note against a network account#3354
mmagician wants to merge 9 commits into
nextfrom
mmagician-claude/note-consumption-costs

Conversation

@mmagician

@mmagician mmagician commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Part of #3344

Benchmarks the cost of consuming every note & its execution path in miden-standards and miden-agglayer and checks the results in as generated cost tables.

Most of the diff is for benching context setup - which I frankly have skimmed over, as it's not an important part - and we can improve on later. The generated values in terms of cycle counts look correct.

The actual modifications to other crates are pretty small and should be reviewed carefully (mostly the structure of where things live - and potentially it's worth taking a brief look at #3356 which will be the user of those tables)

claude added 5 commits July 17, 2026 15:11
The committed cycle counts predate recent kernel and standards changes on
next. Regenerating moves every scenario, and the two-P2ID-Falcon scenario's
padded chiplets bracket crosses from 65536 to 131072 rows; the scenario
expectations mirror is updated accordingly (the miden-vm consumer's copy
must be refreshed to match).

This drift going unnoticed is the motivating example for the cost-table
snapshot check added later in this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidates the bench-transaction modules into the lib target (the bin
previously re-declared and re-compiled them), adds ExecutionBenchmark::all()
as the single scenario list, and routes context construction through one
build_benchmark_context dispatch used by main. The duplicated bridge role
wallet + bridge account setup shared by the CLAIM and B2AGG builders moves
into a BridgeFixture helper.

No behavior change: the regenerated bench-tx.json is byte-identical before
and after this refactor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and agglayer notes

Adds one benchmark scenario per standard note script and execution path,
each measuring the canonical network-account transaction as it exists
today: an AuthNetworkAccount-authenticated account (with the components
the note requires) consumes the note on a chain charging a verification
base fee, paying the fee via a TX_FEE note from its own vault.

Covered: P2ID; P2IDE claim/reclaim; SWAP public/private payback; PSWAP
full/partial fill; MINT on fungible/non-fungible network faucets; BURN;
FAUCET_POLICY_ACTION, PAUSE_ACTION, OWNER_ACTION, RBAC_ACTION (one
representative selector each); FEE_SPONSORSHIP with-feature/reclaim; and
fee-paying variants of CLAIM L1/L2, B2AGG (empty and populated frontier),
plus CONFIG_AGG_BRIDGE, DEREGISTER_AGG_FAUCET, UPDATE_GER and REMOVE_GER
consumed by the bridge account. The pre-existing zero-fee CLAIM/B2AGG
scenarios are unchanged.

All fixtures draw randomness exclusively from the chain builder's seeded
RNG; two consecutive runs produce a byte-identical bench-tx.json.

The MINT scenarios inject an explicit vault asset witness for the minted
asset: the executor only pre-fetches witnesses for input-note assets, so
minting from a faucet whose vault is non-empty (here: holding the fee
asset) otherwise fails during asset-preservation checks. Worth an
upstream fix in miden-tx eventually, since any fee-paying network faucet
has a non-empty vault.

part of #3344

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds checked-in, generated cost tables exposing each standard and
agglayer note's consumption cost in VM cycles: the total cycle count of
the canonical network-account transaction consuming the note, maximum
across its benchmarked execution paths. Standards costs live in
miden_standards::note::costs, agglayer costs in miden_agglayer::costs;
both modules are plain constants and no_std-clean.

The tables are written by the bench-transaction binary via the new
update-note-costs argument (make update-note-costs), which runs the full
benchmark suite and then renders the per-note maxima, so bench-tx.json
and the tables are always regenerated together. Costs are denominated in
cycles, not fee units, because the fee
(verification_base_fee * (ilog2(cycles) + 1)) depends on a block-header
parameter that can change independently of these measurements.

Freshness is enforced by snapshot tests running in the regular CI test
suite: one rstest case per priced note re-executes its scenarios and
compares the measured maximum against the compiled-in constant, failing
with a pointer to make update-note-costs on any drift. A companion test
guards the scenario-to-note mapping so no consumption scenario silently
goes unpriced.

part of #3344

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread crates/miden-agglayer/src/costs/mod.rs Outdated
Comment thread crates/miden-agglayer/src/costs/mod.rs
Comment thread crates/miden-standards/src/note/costs/mod.rs Outdated
Comment thread crates/miden-standards/src/note/costs/table.rs Outdated
Comment thread crates/miden-agglayer/src/costs/table.rs Outdated
Comment thread bin/bench-transaction/src/context_setups/network_wallet.rs Outdated
claude and others added 2 commits July 18, 2026 20:14
- Tighten the costs module docs in miden-standards and apply the
  suggested simplification in miden-agglayer (which defers to the
  standards module for the canonical-transaction definition).
- Generated table docs are now succinct per-note summaries listing each
  benchmarked path with its cycle count and marking the maximum, e.g.
  "Cycles of consuming a P2IDE note: claim 13310, reclaim 13465
  (maximum)."; single-path notes say so. Constant values unchanged.
- Fix terminology: the consumers are network accounts with the required
  components (BasicWallet / faucet / management), not
  "network-authenticated wallets".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mmagician
mmagician marked this pull request as ready for review July 18, 2026 20:27
@mmagician
mmagician requested a review from bobbinth July 18, 2026 20:28
@mmagician mmagician changed the title feat(bench): price note consumption for network accounts (scenarios, cost tables, CI check) feat(bench): price consumption of each note against a network account Jul 18, 2026
claude added 2 commits July 18, 2026 21:06
The exact-match check turned every cycle-shifting merge into the base
branch into a CI failure for all open PRs (as happened when a fee change
on next moved every scenario by 0.2-1.5%). The snapshot tests now fail
only when a measured cost diverges from its checked-in constant by more
than 5% in either direction.

Fee-wise the tolerance is safe: the fee is logarithmic in cycles and the
pricing safety margin (one verification cycle by default, roughly a 2x
cycle headroom) dwarfs the tolerated drift. Meaningful shifts still fail
with a pointer to make update-note-costs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the canonical network-account consumption scenario for the
NETWORK_ACCOUNT_CONFIG note introduced by #3330: an owner-controlled
network account (AuthNetworkAccount, which auto-allowlists the config
note root, plus Ownable2Step authority) consumes a config note carrying
the representative AddAllowedNoteScript action and pays the fee.

Regenerating the tables also refreshes all other constants to the
current base (the drift from the recent next merge is reset to zero),
and bench-tx.json gains the new scenario.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mmagician pushed a commit that referenced this pull request Jul 18, 2026
The rebases had fuzzily re-applied older changelog hunks, leaving a
stale #3356 entry (describing the since-removed standard_note_prices
API) and a second #3354 bullet. Exactly one concise entry per PR
remains.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mmagician mmagician added the fees Related to fees. label Jul 18, 2026

@PhilippGackstatter PhilippGackstatter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I think this doesn't cover all cases such as user-defined mint policies for the MintNote, but it's a good start.

Comment on lines +160 to +165
/// Returns the transaction context for a network ownable account consuming an OWNER_ACTION note.
///
/// The account carries the `Ownable2Step` component owned by the owner wallet (mirrors
/// `create_ownable_account` in the `ownable2step` test suite). The benchmarked action is
/// `TransferOwnership`, nominating a new owner.
pub fn tx_consume_owner_action_note_network() -> Result<TransactionContext> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the notes that support multiple actions, we should run the most expensive operation for estimation. Not necessarily for this PR, but seems important for actually using the resulting values for the fee policy.

Comment on lines +81 to +102
let token_policy_manager = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::owner_only())
.active_burn_policy(BurnPolicy::allow_all())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();

let account_builder = AccountBuilder::new(builder.rng_mut().random())
.account_type(AccountType::Public)
.with_asset_callbacks(AssetCallbackFlag::from(token_policy_manager.has_transfer_policy()))
.with_component(faucet)
.with_components(AccessControl::Ownable2Step { owner: owner_account_id })
.with_components(token_policy_manager)
.with_component(Pausable::unpaused())
.with_component(PausableManager)
.with_assets([super::fee_funding_asset()?]);

builder.add_account_from_builder(
super::network_auth([MintNote::script_root(), BurnNote::script_root()]),
account_builder,
AccountState::Exists,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Might be worth deduplicating with add_existing_network_faucet and adding a add_existing_network_faucet_with_assets to the mock chain builder.

Comment on lines +208 to +211
mock_chain
.build_tx_context(faucet.id(), &[mint_note.id()], &[])?
.extend_advice_inputs(minted_asset_witness(&faucet, mint_asset.id()))
.build()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add this custom witness here?

Comment on lines +303 to +318
// Sponsorship path: the network account consumes the fee-unaware feature note together
// with the FEE_SPONSORSHIP note paying for it, collecting the sponsored fee via an
// allowlisted transaction script.
let collect_script = collect_fee_tx_script(sponsored_asset)?;

let sponsor = builder.add_existing_wallet(Auth::basic_ecdsa())?;
// The feature note is completely fee-unaware; P2ANY stands in for a real network note.
let feature_note = builder.add_p2any_note(sponsor.id(), NoteType::Public, [])?;

let network_account = builder.add_existing_wallet_with_assets(
Auth::NetworkAccount {
allowed_script_roots: BTreeSet::from([
feature_note.script().root(),
FeeSponsorshipNote::script_root(),
]),
allowed_tx_script_roots: BTreeSet::from([collect_script.root()]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we still need the fee collection tx script now that fee collection was integrated into AuthNetworkAccount?

@mmagician
mmagician requested a review from Fumuran July 21, 2026 14:13

@partylikeits1983 partylikeits1983 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Approving granted the minor issues I pointed out are fixed.

This PR forced me to re-understand the number of cycles it takes to consume a B2AGG note which was fun :)

Comment on lines +3 to +15
//! Each constant is the number of VM cycles of the canonical network-account transaction
//! consuming the note, measured by the `bench-transaction` binary: an account authenticated
//! with [`AuthNetworkAccount`](crate::account::auth::AuthNetworkAccount) (carrying the
//! components the note requires) consumes the note on a fee-charging chain, so the measured
//! cycles include the allowlist checks and TX_FEE note creation.
//!
//! The values are denominated in cycles rather than fee units, since the fee
//! (`verification_base_fee * (ilog2(cycles) + 1)`) depends on a block-header parameter.
//!
//! The table is regenerated with `make update-note-costs`; a snapshot test in
//! `bench-transaction` fails CI when a checked-in value drifts more than 5% from the measured
//! one (small drift from unrelated changes is tolerated - the pricing safety margin dwarfs
//! it).

@partylikeits1983 partylikeits1983 Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would mention here that the values are an estimate, and that its actually not possible to correctly price consumption of a note that has branches. I'd be very explicit about this in the docs.

For example, even with the P2ID note, it can contain up to 64 assets, and on consumption it will loop through N number of assets. Although the percentage of P2ID and P2IDE notes which will have 64 assets is low, maybe we should price the worst case anyways.

Edit: I actually think its pretty important to price a P2ID & P2IDE note with consumption of 64 assets, otherwise this opens a griefing vector (dependent on the fee policy).

Comment on lines +49 to +53
pub const fn all() -> &'static [ExecutionBenchmark] {
&[
ExecutionBenchmark::ConsumeSingleP2IDFalcon,
ExecutionBenchmark::ConsumeSingleP2IDEcdsa,
ExecutionBenchmark::ConsumeTwoP2IDFalcon,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone adds a new variant but forgets to list it here nothing will fail. A simple guard would be to assert all().len() against the number of ExecutionBenchmark variants, that way forgetting to add a variant becomes a compile error instead of silently not pricing a note.

write_bench_results_to_json(path, benchmark_results)?;

if update_costs {
bench_transaction::note_costs::update_cost_tables().await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update-note-costs executes every priced scenario twice. Once in the loop above (line 41), then again inside update_cost_tables (note_costs.rs:275 re-runs benched_path_cycles per note). Reusing the measurements from the first pass would halve regeneration time.

/// Cycles of consuming a CLAIM note: L1 origin 44275, L2 origin 57256 (maximum).
pub const CLAIM_CONSUMPTION_CYCLES: u64 = 57256;

/// Cycles of consuming a B2AGG note: empty frontier 141492 (maximum), 2^31-1 leaves 76890.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent some time double checking this.

Just as a note, the cycle count for B2AGG notes will oscillate dependent on the leaf count. The number of writes equals the number of 0-bits in its count. So essentially the # of cycles of a B2AGG note follows this:

- note #1: count 0 (binary all zeros) → 32 writes → the maximum, never repeated
- note #2: count 1 (1) → 31 writes → slightly cheaper
- note #3: count 2 (10) → 31 writes → same as #2
- note #4: count 3 (11) → 30 writes → cheaper
- note #5: count 4 (100) → 31 writes → more expensive again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fees Related to fees.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants