Skip to main content
Version: Next (unstable)

MASM Changes

Several protocol and standards procedures kept their names but changed their stack effect, and the assembler does not check call sites against procedure signatures. 0.16 code that calls them still assembles and then misbehaves: tx::get_block_commitment now consumes a block number from the stack, and a custom guardian component that still passes the old note count to guardian::verify_signature silently skips the guardian signature check. Around those, the reference-block and fee-asset accessors were renamed, several standards modules moved, the core library's precompile wrappers moved under miden::core, and many procedure roots changed.

Quick Fix​

# Before (0.16)
use miden::standards::note_tag
use miden::standards::note::execution_hint
use miden::precompiles::hashes::keccak256

exec.tx::get_block_number
exec.tx::get_block_commitment
exec.tx::get_fee_faucet_id exec.fungible_asset::create_id
exec.active_account::compute_commitment
# After (0.17)
use miden::standards::note::note_tag
use miden::standards::note::note_execution_hint
use miden::core::precompiles::hashes::keccak256

exec.tx::get_reference_block_number
exec.tx::get_reference_block_commitment # get_block_commitment now takes a block number
exec.tx::get_fee_asset_id
exec.native_account::compute_commitment # panics in a foreign (FPI) context
miden-project.toml
# Before (0.16)
miden-core = { linkage = "dynamic", version = "0.29" }
miden-protocol = { linkage = "dynamic", version = "0.16.0" }
miden-standards = { linkage = "static", version = "0.16.0" }

# After (0.17)
miden-core = { linkage = "dynamic", version = "0.35" }
miden-protocol = { linkage = "dynamic", version = "0.17.0" }
miden-standards = { linkage = "dynamic", version = "0.17.0" }

The standard components made the same switch to linkage = "dynamic" for miden-standards: their packages call into the standards library instead of embedding it. The auth procedure roots of the standard single-sig, multisig and network-account auth components change, and with them the code commitment of every account that uses them. A dynamically linked package needs the standards library at run time: TransactionMastStore::new() already loads it, but a custom MAST store must serve it too, or execution fails with procedure with root digest <root> could not be found (see Transaction Changes).

Renamed procedures fail loudly at assembly. The procedures in the stack-effect table do not: after the renames, audit every call site listed there by hand.

If you encounter errors, continue reading for detailed migration steps.


Summary​

This page holds four groups of change. The first is the dangerous one: a dozen procedures in miden::protocol and miden::standards changed their stack effect under an unchanged name. Nothing fails to compile; call sites either trip an assertion that names something unrelated, or run and compute the wrong value. native_account::upgrade belongs here too: it kept its stack and now really upgrades the account code. The second group is renames and moves (reference-block helpers, the fee asset accessor, note_tag, note_execution_hint, the MINT submodules, the fungible amount helpers, privatised low-level helpers); those fail at assembly with undefined item. Hand-written kernel offsets also shifted. The third group is for authors of custom auth and faucet components: the transaction summary layout, tx_policy, guardian, multisig, fee payment, and faucet callbacks and policies all changed shape. The fourth is the core library and the language (VM 0.29.2 → 0.35.0): precompile wrappers moved under miden::core, the in-VM verifier is verify_proof and the stark::constants accessors are gone, u64 shifts and bare exp trap on inputs they used to accept, aead::decrypt, sorted-array lookups and eval_circuit enforce new limits, and the trace instruction is back. New procedures seal output notes against further changes.

Every procedure whose body changed has a new MAST root, and every standard note script root and standard component code commitment changes. Re-assemble every note script, transaction script and account component that links these libraries (the package format itself is covered in VM & Assembler Changes).

The protocol 0.17 changelog carries a second heading labeled v0.16.0 (2026-08-06) directly below the 0.17 entries. None of its entries is in 0.16.1: it is the 0.17 fixes list under a wrong heading, and it contains several breaking MASM changes covered on this page (non-fungible mint_and_send, the foreign-procedure root check, the transfer-policy expiration cap). Several renames on this page (account_id::validate, note::execution_hint, types::MemoryAddress, the kernel_proc_offsets constants) are not in the changelog at all.

The VM 0.35 assembler reports a stale import or procedure as undefined item '...'. A use names the full module path (use miden::precompiles::hashes::keccak256 gives undefined item 'miden::precompiles::hashes::keccak256'); an exec names only the procedure (exec.tx::get_block_number gives undefined item 'get_block_number'); a syscall names the kernel path (undefined item '::$kernel::<name>'). The protocol and standards libraries are linked as packages, so importing one of their private modules also reports undefined item, not private submodule.


Procedures that kept their name but changed their stack effect​

Summary​

The VM 0.35 assembler performs no call-site type check. Almost every public protocol and standards procedure now carries a type signature (see Procedure type signatures), but signatures document the stack effect; they neither reject nor protect an old call site. The procedures below consume a different stack in 0.17 than in 0.16.

Affected Code​

Procedure0.16 stack (inputs)0.17 stack (inputs)
tx::get_block_commitment[] (returns the reference block's commitment)[block_number]
guardian::verify_signature[num_own_output_notes, MSG][is_rotation, MSG]
fee::pay_fee[num_extra_cycles, CONVERSION_INFO][num_extra_cycles, CONVERSION_INFO, serial_number_block]
fee::create_and_fund_fee_note[ASSET_ID, ASSET_VALUE][serial_number_block, ASSET_ID, ASSET_VALUE]
fee::apply_cycle_margins[num_extra_cycles, num_sponsorship_notes][num_extra_cycles]
auth::create_tx_summary[user_params(7)][user_params(6)], and the six output words come out in a new order
auth::hash_and_insert_tx_summary[ACCOUNT_DELTA, INPUT_NOTES, OUTPUT_NOTES, BLOCK_COMMITMENT, PARAMS_HEAD, PARAMS_TAIL][PARAMS_HEAD, PARAMS_TAIL, ACCOUNT_DELTA, INPUT_NOTES, OUTPUT_NOTES, BLOCK_COMMITMENT]
tx_policy::assert_no_output_notes[num_own_output_notes][]
multisig::auth_tx[user_params(7)][bound_block_num, approval_expiration_block_num, SALT]
multisig_smart::auth_tx[num_own_output_notes, user_params(7)][bound_block_num, approval_expiration_block_num, SALT]
multisig_smart::enforce_note_restrictions[num_own_output_notes, note_restrictions][note_restrictions]
faucets::non_fungible::mint_and_send[ASSET_VALUE, tag, note_type, RECIPIENT, pad(6)][ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT, pad(2)]
fungible_asset::value_into_amount[ASSET_VALUE], unchecked[ASSET_VALUE], now validates the value
native_account::upgrade[CODE_UPGRADE_COMMITMENT, STORAGE_UPGRADE_COMMITMENT], only recorded the two words[NEW_CODE_COMMITMENT, STORAGE_UPGRADE_COMMITMENT], now upgrades the account code; the storage word must be empty

tx and native_account are miden::protocol::tx and miden::protocol::native_account. The other modules live under miden::standards: auth, auth::guardian, auth::tx_policy, auth::multisig, auth::multisig_smart, fee, faucets::non_fungible and assets::fungible_asset.

What an unchanged 0.16 call site does in 0.17:

  • tx::get_block_commitment consumes whatever sits on top of the stack as a block number. A non-u32 value fails, and a value above the reference block fails. The reference block number itself returns the old value. A lower block number returns that block's commitment without any error if the transaction's partial blockchain tracks that block; otherwise execution fails with failed to lookup value in Merkle store. A zero pad reads as block 0.
  • guardian::verify_signature feeds its first element to if.true. The old argument was the count of the component's own output notes, which is 1 when the fee note is the only one. A 1 now means "rotation", which skips the guardian signature check; a count above 1 fails the condition.
  • fee::pay_fee takes the next element below CONVERSION_INFO as the serial-number block. It is u32asserted only when a non-zero fee is paid, so on a zero-fee path nothing checks it.
  • fee::create_and_fund_fee_note misreads the asset.
  • auth::create_tx_summary, fee::apply_cycle_margins and tx_policy::assert_no_output_notes leave an extra element on the stack. assert_no_output_notes also no longer tolerates the fee note.
  • auth::hash_and_insert_tx_summary hashes the words in the old order. When the host handles the auth request it rejects that preimage (failed to construct transaction summary, caused by transaction summary layout version is <value> but only version 1 is supported), and a signature made over the new order does not verify.
  • multisig::auth_tx and multisig_smart::auth_tx bind a wrong block or panic; multisig_smart::enforce_note_restrictions misreads the restrictions.
  • faucets::non_fungible::mint_and_send reads the 0.16 frame shifted by one word (the old ASSET_VALUE as ASSET_ID, recipient elements as tag and note_type), so it fails while creating the output note with failed to decode note_type into u8, or earlier in a custom mint policy that inspects the value.
  • fungible_asset::value_into_amount keeps its stack effect but now validates, so it fails on malformed values it used to accept.
  • native_account::upgrade applies the upgrade instead of only recording it. A non-empty storage word fails, and so does any call in a transaction that creates the account, or a second upgrade while one is pending. A code commitment that is neither the empty word nor the current code makes the kernel load and validate that code, which the host must provide in the advice map; the new code takes effect after the auth procedure runs. The Rust side is on Account Changes.

If your auth component calls guardian::verify_signature with the 0.16 own-notes count, a count of 1 now passes as is_rotation = 1 and the guardian signature is never verified, with nothing to report it. Call guardian::assert_rotation_policy and pass its result instead (see Custom auth components).

Migration Steps​

  1. Grep your MASM for every procedure in the table and re-derive the stack at each call site. Do not rely on the assembler or on a green build.
  2. Fix guardian::verify_signature and tx::get_block_commitment first: those two can run to completion with the wrong behaviour.
  3. Follow the per-procedure sections below for the new calling sequences.

Common Errors​

Error MessageCauseSolution
the block number must be a u32Argument-less tx::get_block_commitment consumed a non-u32 valueUse tx::get_reference_block_commitment.
the block number must not exceed the transaction reference block numberSame, with a u32 above the reference blockSame.
failed to lookup value in Merkle storeSame, with a lower block number (often a zero pad, block 0) that the transaction's partial blockchain does not trackSame.
transaction must not include output notestx_policy::assert_no_output_notes called after fee::pay_fee created the fee noteCall it with no argument, before paying the fee.
failed to decode note_type into u8Old non_fungible::mint_and_send call without ASSET_ID: the frame is read shifted by one wordPass [ASSET_ID, ASSET_VALUE, ...].
fungible asset value is not well-formedOld value_into_amount call site now validatesPass a real fungible value, or use value_into_amount_unchecked.
account storage upgrades are not supportednative_account::upgrade with a non-empty STORAGE_UPGRADE_COMMITMENTPass the empty word.
a new account cannot be upgradednative_account::upgrade in the transaction that creates the accountDeploy the account with the final code, and upgrade in a later transaction.
an account code upgrade is already pendingnative_account::upgrade after an earlier call in the same transaction set a new code commitmentUpgrade once per transaction.
transaction initialized an upgrade to account code <commitment> but the advice map did not provide the new codeThe host has no code for the requested commitmentPass the new code with TransactionArgs::with_account_code_upgrade.

tx::get_block_commitment takes a block number; reference-block helpers renamed​

Summary​

tx::get_block_commitment now reads the commitment of any block up to and including the reference block, and takes that block number on the stack. The old argument-less behaviour lives on as tx::get_reference_block_commitment, and tx::get_block_number is now tx::get_reference_block_number. The Rust contract SDK mirrors these renames (see Rust Contract SDK & Compiler).

Affected Code​

From the standard P2IDE note script:

- mem_load.TIMELOCK_HEIGHT_ITEM exec.tx::get_block_number
+ mem_load.TIMELOCK_HEIGHT_ITEM exec.tx::get_reference_block_number
# Before (0.16)
use miden::protocol::tx

exec.tx::get_block_commitment # [] -> [REF_BLOCK_COMMITMENT]
# After (0.17)
use miden::protocol::tx

exec.tx::get_reference_block_commitment # [] -> [REF_BLOCK_COMMITMENT]
push.1234 exec.tx::get_block_commitment # [block_number] -> [BLOCK_COMMITMENT]

If you call the kernel directly: kernel_proc_offsets::TX_GET_BLOCK_NUMBER_OFFSET was renamed to TX_GET_REFERENCE_BLOCK_NUMBER_OFFSET, and its value moved from 52 to 55 (see Kernel entry points).

Migration Steps​

  1. Replace every exec.tx::get_block_commitment that expects no argument with exec.tx::get_reference_block_commitment. Do this first: the old spelling still assembles and consumes the top stack element as a block number.
  2. Rename tx::get_block_number to tx::get_reference_block_number.
  3. To read an older block, push its number and call tx::get_block_commitment. The block must be authenticated by the transaction's partial blockchain: the kernel emits the new miden::protocol::tx::before_block_witness_load event, the standard host treats it as a no-op, so the client has to include that block in the transaction inputs (see Client Changes).
  4. Rename TX_GET_BLOCK_NUMBER_OFFSET if you syscall.exec_kernel_proc by hand.

On the Rust side, TransactionEventId gains TxBeforeBlockWitnessLoad, which breaks exhaustive matches.

Common Errors​

Error MessageCauseSolution
the block number must be a u32get_block_commitment consumed a non-u32 value, typically an old argument-less call siteUse get_reference_block_commitment, or push a valid block number.
the block number must not exceed the transaction reference block numberBlock number above the reference block, often a stale call site consuming an unrelated valueSame.
failed to lookup value in Merkle storeBlock number below the reference block that the transaction's partial blockchain does not track, for example a zero pad consumed by a stale call siteUse get_reference_block_commitment, or include that block in the transaction inputs.
undefined item 'get_block_number'tx::get_block_number no longer existsUse tx::get_reference_block_number.

Custom auth components: transaction summary, tx_policy, and guardian​

Summary​

The transaction summary now leads with PARAMS_HEAD = [version, metadata, user_param0, user_param1], where metadata packs the bound block number with the expiration delta shifted left by 32 bits. As a result, auth::create_tx_summary takes six user params instead of seven and returns PARAMS_HEAD, PARAMS_TAIL first, and auth::hash_and_insert_tx_summary expects that order. tx_policy::assert_no_output_notes no longer takes a count of "own" notes, and guardian::verify_signature takes an is_rotation flag produced by the new guardian::assert_rotation_policy.

The changelog mentions the six user params. It does not mention the reordered outputs of create_tx_summary, the reordered inputs of hash_and_insert_tx_summary, or the tx_policy, guardian and multisig_smart::enforce_note_restrictions signature changes.

Affected Code​

From miden::standards::auth::signature::authenticate_transaction:

# Before (0.16)
push.0.0.0.0.0.0 movup.6
# => [user_params(7), PK_COMM, scheme_id]
exec.auth::create_tx_summary
# => [ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT, OUTPUT_NOTES_COMMITMENT, BLOCK_COMMITMENT,
# PARAMS_HEAD, PARAMS_TAIL, PK_COMM, scheme_id]
exec.auth::hash_and_insert_tx_summary
# After (0.17)
push.0.0.0.0.0 movup.5
# => [user_params(6), PK_COMM, scheme_id]
exec.authenticate_transaction_with_user_params
# which runs exec.auth::create_tx_summary:
# => [PARAMS_HEAD, PARAMS_TAIL, ACCOUNT_DELTA_COMMITMENT, INPUT_NOTES_COMMITMENT,
# OUTPUT_NOTES_COMMITMENT, BLOCK_COMMITMENT, PK_COMM, scheme_id]
# and then exec.auth::hash_and_insert_tx_summary

New and changed signatures:

auth::create_tx_summary             # [user_params(6)] -> [PARAMS_HEAD, PARAMS_TAIL, ACCOUNT_DELTA, INPUT_NOTES, OUTPUT_NOTES, BLOCK_COMMITMENT]
auth::create_tx_summary_with_block # [block_number, user_params(6)] -> same six words, bound to block_number
auth::hash_and_insert_tx_summary # [PARAMS_HEAD, PARAMS_TAIL, ACCOUNT_DELTA, INPUT_NOTES, OUTPUT_NOTES, BLOCK_COMMITMENT] -> [TX_SUMMARY_COMMITMENT]
signature::authenticate_transaction_with_user_params # [user_params(6), PK_COMM, scheme_id] -> [] (new)
tx_policy::assert_no_output_notes # [] -> [] (was [num_own_output_notes] -> [])
guardian::assert_rotation_policy # [] -> [is_rotation] (new)
guardian::verify_signature # [is_rotation, MSG] -> [] (was [num_own_output_notes, MSG] -> [])
multisig_smart::enforce_note_restrictions # [note_restrictions] -> [] (was [num_own_output_notes, note_restrictions])

Migration Steps​

  1. Pass six user params, not seven, to auth::create_tx_summary. To bind a block other than the reference block, use auth::create_tx_summary_with_block.
  2. Fix any stack manipulation between create_tx_summary and hash_and_insert_tx_summary: the summary words come out in a new order. If you only chain the two, step 1 is the whole change.
  3. Prefer signature::authenticate_transaction_with_user_params over building the summary yourself.
  4. Call tx_policy::assert_no_output_notes with no argument, and call it before fee::pay_fee: the fee note is an output note.
  5. Guardian flows: call guardian::assert_rotation_policy before creating any note, keep its is_rotation result, and pass that to guardian::verify_signature.

On the Rust side, host code that parses the summary preimage from the advice map sees the new word order.

Common Errors​

Error MessageCauseSolution
transaction must not include output notesassert_no_output_notes called after pay_fee created the fee noteCall it before paying the fee.

Fee payment: serial_number_block, native fee asset only, and the removed 0.16.x helpers​

Summary​

fee::pay_fee takes a serial_number_block below CONVERSION_INFO, and fee::create_and_fund_fee_note takes it on top, so the fee note's serial number is derived from a caller-chosen block. fee::apply_cycle_margins lost its second argument. pay_fee now also pins payment to the native fee asset at rate 1/1: whenever the fee is non-zero it asserts that the committed conversion info equals fee::native_conversion_info ([fee_faucet_id_suffix, fee_faucet_id_prefix, 1, 1]). In 0.16 it converted the fee into whatever faucet and rate the conversion info named. The protocol side of that change is on Transaction Changes.

Affected Code​

From the standard single-sig auth component:

# Before (0.16)
dup exec.signature::estimate_authentication_cycles
# => [num_extra_cycles, scheme_id, CONVERSION_INFO, pad(12)]
swap movdn.5
# => [num_extra_cycles, CONVERSION_INFO, scheme_id, pad(12)]
exec.fee::pay_fee drop
# => [scheme_id, pad(12)]
# After (0.17)
dup exec.signature::estimate_authentication_cycles
# => [num_extra_cycles, scheme_id, CONVERSION_INFO, pad(12)]
swap movdn.5
# => [num_extra_cycles, CONVERSION_INFO, scheme_id, pad(12)]
exec.tx::get_reference_block_number movdn.5
# => [num_extra_cycles, CONVERSION_INFO, serial_number_block, scheme_id, pad(12)]
exec.fee::pay_fee drop
# => [scheme_id, pad(12)]

Signatures at 0.17:

fee::pay_fee                   # [num_extra_cycles, CONVERSION_INFO, serial_number_block] -> [total_sponsored_fee_amount]
fee::create_and_fund_fee_note # [serial_number_block, ASSET_ID, ASSET_VALUE] -> []
fee::apply_cycle_margins # [num_extra_cycles] -> [num_estimated_extra_cycles] (was [num_extra_cycles, num_sponsorship_notes])

The fee-estimation and fee-bound helpers that shipped in 0.16.0 and 0.16.1 do not exist in 0.17, and the 0.17 changelog does not mention their removal. Replace them as follows.

Removed (present in 0.16.0 / 0.16.1)0.17 replacement
fee::estimate_feeNo direct replacement. pay_fee now creates sponsorship notes first, then prices the transaction with fee::apply_cycle_margins + tx::compute_fee ([num_extra_cycles, EXCLUDE_NOTES_COMMITMENT] -> [fee_amount]); replicate that if you only need the amount.
fee::assert_fee_bound, fee::FeeBoundNone needed: pay_fee rejects anything but the native fee asset at rate 1/1.
fee::resolve_payment_info, fee::pay_estimated_feefee::pay_fee.
fee::pay_network_note_sponsorshipsexec.tx::get_fee_asset_id exec.fees::create_network_note_sponsorships (pay_fee already does this).
fees::estimate_network_note_sponsorshipsNone.
auth::multisig::pay_bounded_fee (0.16.1 only)exec.signature::estimate_multisig_authentication_cycles, then exec.fee::pay_fee, as the 0.17 multisig component does (see Multisig library procedures).

When the transaction creates a network note, pay_fee sponsors it before computing the fee, on fee-free chains too, and reaches the target's fee_manager::estimate_note_fee through FPI. That path caps the transaction's expiration at 20 blocks (see Faucet callbacks and policies).

Migration Steps​

  1. Before exec.fee::pay_fee, place a block number below CONVERSION_INFO. Single-sig style components use exec.tx::get_reference_block_number movdn.5; multisig components use the block their summary binds.
  2. Before exec.fee::create_and_fund_fee_note, push the block number on top of [ASSET_ID, ASSET_VALUE].
  3. Drop the second argument of fee::apply_cycle_margins.
  4. Replace any removed helper per the table above.
  5. If your component called tx_policy::assert_no_output_notes with the count returned by pay_bounded_fee, call it with no argument, before paying the fee.
  6. Commit the native conversion info in the auth args and fund the paying account with the native fee asset.

A missing serial_number_block does not fail at assembly: pay_fee consumes whatever element sits below CONVERSION_INFO, and asserts it is a u32 only when a non-zero fee is paid.

Common Errors​

Error MessageCauseSolution
undefined item '<procedure>' (for example undefined item 'estimate_fee')Call to fee::estimate_fee, fee::assert_fee_bound, multisig::pay_bounded_fee and the other removed helpersRewrite per the table.
the transaction fee must be paid in the native fee asset at rate 1/1Conversion info names another faucet or rateCommit the native conversion info.
paying a non-zero fee requires conversion info committed via the auth argsNo conversion info on a fee-charging chainCommit the native conversion info.

tx::get_fee_faucet_id is now tx::get_fee_asset_id​

Summary​

The accessor returns the fee asset ID word instead of the fee faucet ID felts. The changelog records the rename but not the change of output from two felts to a word.

Affected Code​

From miden::standards::fee:

# Before (0.16)
exec.tx::get_fee_faucet_id exec.fungible_asset::create_id
# => [FEE_ASSET_ID, ...]
# After (0.17)
exec.tx::get_fee_asset_id
# => [FEE_ASSET_ID, ...]

Where you need the faucet ID, convert the word back, as fee::native_conversion_info does:

# Before (0.16)
push.1.1
exec.tx::get_fee_faucet_id
# => [fee_faucet_id_suffix, fee_faucet_id_prefix, 1, 1]
# After (0.17)
push.1.1
exec.tx::get_fee_asset_id
exec.asset::id_into_faucet_id
# => [fee_faucet_id_suffix, fee_faucet_id_prefix, 1, 1]

Migration Steps​

  1. Replace exec.tx::get_fee_faucet_id exec.fungible_asset::create_id with exec.tx::get_fee_asset_id.
  2. Where you need the faucet ID, append exec.asset::id_into_faucet_id (from miden::protocol::asset).
  3. Rename kernel_proc_offsets::TX_GET_FEE_FAUCET_ID_OFFSET to TX_GET_FEE_ASSET_ID_OFFSET if you syscall by hand (value 60 in 0.16, 63 in 0.17).

Multisig library procedures bind a caller-chosen block​

Summary​

This section covers the MASM library side. The multisig flow itself (the bound block, MultisigAuthArgs, executing at the tip) is on Transaction Changes.

multisig::auth_tx and multisig_smart::auth_tx take [bound_block_num, approval_expiration_block_num, SALT] instead of seven user params (multisig_smart::auth_tx also took a leading num_own_output_notes). The new multisig::resolve_auth_args decodes the auth args, whose advice preimage is now [BLOCK_WORD, SALT, CONVERSION_INFO]. Approvals expire relative to the bound block, and the guardian key must not be one of the approvers. This only affects components composed from miden::standards::auth::multisig; the standard components were updated.

Affected Code​

From the standard guarded multisig component (trimmed):

# Before (0.16)
dupw exec.fee::load_conversion_info
exec.multisig::get_initial_threshold_and_num_approvers drop
add.1
exec.multisig::pay_bounded_fee # => [num_own_output_notes, AUTH_ARGS]
movdn.4
push.0.0.0
exec.multisig::auth_tx # [user_params(7)] -> [TX_SUMMARY_COMMITMENT]
dupw movup.8
exec.guardian::verify_signature # [num_own_output_notes, MSG]
exec.multisig::record_and_assert_new_tx
# After (0.17)
exec.multisig::resolve_auth_args
# => [CONVERSION_INFO, bound_block_num, approval_expiration_block_num, SALT]
dup.5 exec.tx::get_reference_block_number
exec.multisig::assert_approval_not_expired movdn.10
exec.guardian::assert_rotation_policy movdn.11
exec.multisig::get_initial_threshold_and_num_approvers drop
add.1
exec.signature::estimate_multisig_authentication_cycles
dup.5 movdn.5 # bound block doubles as the fee serial_number_block
exec.fee::pay_fee drop
exec.multisig::auth_tx # [bound_block_num, approval_expiration_block_num, SALT] -> [TX_SUMMARY_COMMITMENT]
dupw movup.9
exec.guardian::verify_signature # [is_rotation, MSG]
exec.guardian::get_guardian_public_key
exec.multisig::assert_not_approver_public_key
exec.multisig::record_and_assert_new_tx
exec.multisig::apply_approval_expiration

Migration Steps​

  1. Replace your auth-args decoding with exec.multisig::resolve_auth_args.
  2. Call multisig::assert_approval_not_expired before verifying signatures and multisig::apply_approval_expiration after record_and_assert_new_tx, as above.
  3. Replace multisig::pay_bounded_fee with signature::estimate_multisig_authentication_cycles + fee::pay_fee, passing the bound block as serial_number_block.
  4. Pass [bound_block_num, approval_expiration_block_num, SALT] to multisig::auth_tx / multisig_smart::auth_tx (drop the leading num_own_output_notes you passed to multisig_smart::auth_tx), and [note_restrictions] alone to multisig_smart::enforce_note_restrictions.

On the Rust side, the new MultisigAuthArgs builds the preimage; MultisigAuthArgs::with_approval_expiration_delta sets an expiration relative to the bound block.


active_account::compute_commitment moved to native_account​

Summary​

The procedure now lives in miden::protocol::native_account and panics when the active account is a foreign account. The new native_account::has_state_changed wraps the common "initial versus current commitment" check, and also returns 1 when a code upgrade is pending (see native_account::upgrade in the stack-effect table).

Affected Code​

From the standard no-auth component:

# Before (0.16)
exec.native_account::get_initial_commitment
# => [INITIAL_COMMITMENT, pad(16)]
exec.active_account::compute_commitment
# => [CURRENT_COMMITMENT, INITIAL_COMMITMENT, pad(16)]
exec.word::eq not
# => [has_account_state_changed, pad(16)]
# After (0.17)
exec.native_account::has_state_changed
# => [has_account_state_changed, pad(16)]

Migration Steps​

  1. Replace active_account::compute_commitment with native_account::compute_commitment.
  2. Replace the "initial commitment, compute commitment, word::eq not" sequence with native_account::has_state_changed.
  3. Remove any call from a procedure reachable through FPI; there is no foreign-account equivalent.

Common Errors​

Error MessageCauseSolution
the active account is not nativenative_account::compute_commitment or has_state_changed reached from a foreign contextOnly call it from the native account.
undefined item 'compute_commitment'active_account::compute_commitmentUse native_account::compute_commitment.

Standards modules moved: note_tag, note_execution_hint, MINT, note_creator​

Summary​

Several standards modules moved under miden::standards::note, and the per-kind MINT modules became private.

0.160.17
miden::standards::note_tag (create_account_target, create_custom_account_target, DEFAULT_TAG)miden::standards::note::note_tag
miden::standards::note::execution_hint (NONE, ALWAYS, AFTER_BLOCK, ON_BLOCK_SLOT)miden::standards::note::note_execution_hint
miden::standards::notes::mint_fungible::mint, notes::mint_non_fungible::mintPrivate (notes::mint::fungible, notes::mint::non_fungible); only the dispatcher notes::mint::main is public
Component namespace miden::standards::components::wallets::note_creatormiden::standards::components::note::note_creator

The library procedure miden::standards::note::note_creator::create_note is unchanged. The basic wallet now also re-exports it as miden::standards::wallets::basic::create_note, with the same procedure root.

The execution_hint → note_execution_hint rename is not in the changelog.

Affected Code​

From the standard PSWAP note script:

- use miden::standards::note_tag
+ use miden::standards::note::note_tag
# Before (0.16) - notes/mint.masm
use miden::standards::notes::mint_fungible
exec.mint_fungible::mint
# After (0.17) - notes/mint/mod.masm; the submodules are private to `mint`
mod fungible
exec.fungible::mint

Migration Steps​

  1. use miden::standards::note_tag → use miden::standards::note::note_tag.
  2. use miden::standards::note::execution_hint → use miden::standards::note::note_execution_hint. Update execution_hint::ALWAYS and friends accordingly, or alias the import.
  3. Stop importing notes::mint_fungible / notes::mint_non_fungible. Build MINT notes with the standard MINT script (notes::mint::main, Rust MintNote); there is no public per-kind entry point.
  4. If you referenced the note-creator component by its package namespace, switch to miden::standards::components::note::note_creator.

On the Rust side, NoteCreator moved from account::wallets to account::note_creator.

Common Errors​

Error MessageCauseSolution
undefined item '<module path>' (for example undefined item 'miden::standards::note_tag')Old module pathUse the new path from the table.
undefined item '<module path>'Importing the private notes::mint::fungible / non_fungible, or the old notes::mint_fungible / mint_non_fungibleUse the MINT dispatcher.

Fungible amount extraction renamed; value_into_amount now validates​

Summary​

The unsuffixed name is now the validating one. Because fungible_asset::value_into_amount kept its name and stack effect but changed meaning, old call sites assemble and now also validate the value.

0.160.17Stack effect
miden::protocol::asset::fungible_value_into_amountfungible_value_into_amount_unchecked[ASSET_VALUE] -> [amount]
fungible_asset::value_into_amount (unchecked)fungible_asset::value_into_amount_unchecked[ASSET_VALUE] -> [amount]
fungible_asset::to_amountfungible_asset::to_amount_unchecked[ASSET_ID, ASSET_VALUE] -> [amount, ASSET_ID, ASSET_VALUE]
fungible_asset::try_value_to_amount (validating)fungible_asset::value_into_amount[ASSET_VALUE] -> [amount]

Also new: fungible_asset::validate ([ASSET_ID, ASSET_VALUE] -> [ASSET_ID, ASSET_VALUE]).

Affected Code​

From miden::standards::faucets::fungible::receive_and_burn:

- exec.fungible_asset::to_amount movdn.8
+ exec.fungible_asset::to_amount_unchecked movdn.8

Migration Steps​

  1. Rename try_value_to_amount to value_into_amount, to_amount to to_amount_unchecked, and asset::fungible_value_into_amount to asset::fungible_value_into_amount_unchecked.
  2. Review every existing fungible_asset::value_into_amount call: keep it if validation is acceptable, otherwise rename it to value_into_amount_unchecked.

Common Errors​

Error MessageCauseSolution
fungible asset value is not well-formedOld value_into_amount call site now validates a value whose top three elements are not zeroPass a real fungible value, or use value_into_amount_unchecked.
fungible asset build operation called with amount that exceeds the maximum allowed asset amountSame, with an amount above MAX_AMOUNTSame.

Faucet MASM: mint_and_send, callbacks, and policies​

Summary​

The faucet changes themselves are on Assets, Vault & Faucet Changes. The MASM-visible parts:

  • faucets::non_fungible::mint_and_send takes the full asset, like the fungible one, and asserts the asset ID belongs to the active faucet. Non-fungible MINT note storage grew from 9 / 16+ items (private / public) to 13 / 20+. The fungible mint_and_send rejects a zero amount, and receive_and_burn validates that the burned asset is fungible.
  • Asset callbacks and transfer policies return nothing. on_before_asset_added_to_account / on_before_asset_added_to_note callbacks and the TokenPolicyManager send and receive policies return [pad(16)]; the kernel keeps the original asset value. policy_manager::invoke_send_policy / invoke_receive_policy output [pad(16)] instead of [PROCESSED_ASSET_VALUE, pad(12)]. A callback root must be a procedure of the faucet's own code, and a new account with a callback storage slot must have the callback flag enabled.
  • Mint policies may only change the asset value. policy_manager::execute_mint_policy saves tag, note_type and RECIPIENT before the policy dyncall and asserts they come back unchanged. The policy signature is unchanged ([ASSET_VALUE, tag, note_type, RECIPIENT, pad(6)] in and out).
  • Transfer policies cap the expiration at 20 blocks. expiration::apply_default sets the transaction expiration delta to at most 20 blocks. It runs in policy_manager::invoke_transfer_policy (every movement of a callback-enabled faucet asset whose send or receive policy slot is set), in the basic allowlist and blocklist policies, and in fee_manager::estimate_note_fee. The delta only ever decreases, so the tightest bound wins. See Transaction Changes for the effect on submission.

The doc comment in miden::standards::expiration says policy dispatchers leave the expiration delta to the invoked policy. The shipped policy_manager::invoke_transfer_policy calls expiration::apply_default itself.

Affected Code​

# Before (0.16) - faucets::non_fungible::mint_and_send, invoked with call
#! Inputs: [ASSET_VALUE, tag, note_type, RECIPIENT, pad(6)]
#! Outputs: [note_idx, pad(15)]
# After (0.17)
#! Inputs: [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT, pad(2)]
#! Outputs: [note_idx, pad(15)]

From the standard allow-all transfer policy:

# Before (0.16)
#! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
#! Outputs: [ASSET_VALUE, pad(12)]
@account_procedure
pub proc check_policy
dropw
end
# After (0.17)
#! Inputs: [ASSET_ID, ASSET_VALUE, custom_data, pad(7)]
#! Outputs: [pad(16)]
@account_procedure
pub proc check_policy(asset: Asset, custom_data: felt)
dropw dropw drop
end

Migration Steps​

  1. Build the non-fungible asset with non_fungible_asset::create ([faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] -> [ASSET_ID, ASSET_VALUE]) and pass both words to mint_and_send; trim the padding from 6 to 2.
  2. Custom non-fungible MINT notes: store the full asset (13 items private, 20+ public). Prefer the Rust MintNote builder.
  3. Do not mint zero amounts.
  4. Make custom callbacks and transfer policies consume [ASSET_ID, ASSET_VALUE, custom_data] and return [pad(16)]. An old callback that returns the value still runs, but its return value is ignored. Stop reading a processed value from invoke_send_policy / invoke_receive_policy.
  5. Register only callback roots that are procedures of the faucet's own code.
  6. Remove any tag, note type or recipient rewriting from custom mint policies; only adjust ASSET_VALUE.
  7. If your own FPI-callable procedure reads mutable state, call expiration::apply_default (or tx::update_expiration_block_delta) on that path.

Common Errors​

Error MessageCauseSolution
the asset stored in the MINT note does not belong to this faucetASSET_ID of another faucetDerive the ID for the minting faucet.
failed to decode note_type into u8Old mint_and_send frame without ASSET_ID, read shifted by one wordPass [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT, pad(2)].
non-fungible MINT script expects exactly 13 storage items for private or 20+ storage items for public output notesOld 9 / 16 item layoutStore the full asset.
the amount to mint is zeroFungible mint of 0Mint a positive amount.
fungible asset ID's composition must be fungiblereceive_and_burn given a non-fungible assetBurn through the matching faucet kind.
the faucet callback procedure root is not part of the faucet's account codeCallback slot points at a foreign rootStore a root of the faucet's own procedure.
an account whose storage contains an asset callback slot must have the asset callback flag enabledNew account with a callback slot but the flag offEnable the flag (the Rust AccountBuilder now derives it).
mint policy must not modify the output note tag or typePolicy changed tag or note_typeReturn them unchanged.
mint policy must not modify the output note recipientPolicy changed RECIPIENTReturn it unchanged.

Core library: precompile wrappers moved under miden::core, and claim::kernel_commitment removed​

Summary​

In 0.16 the precompile wrappers were a separate MASM package, miden-precompiles, with namespace miden::precompiles. The sources now live inside the miden-core package as the precompiles submodule, so every path gains a core:: segment. The procedure sets are unchanged apart from the path.

The 0.16 version of this page pointed custom precompile wrappers at miden::precompiles::*; in 0.17 that is miden::core::precompiles::*. Application code that only calls the miden::core::crypto::* facades (keccak256::hash_bytes, ecdsa_k256_keccak::verify, ...) needs no change. The changelog only says the package was merged into miden-core; it does not say the MASM paths moved.

0.16 module0.17 module
miden::precompiles (digest_expr, register_expr, register_value, register_mem, register_chunks_mem, register_chunks_mem_1/2/3, log_deferred, word_eq)miden::core::precompiles (same procedures)
miden::precompiles::hashes::keccak256 (hash_bytes_mem, hash_1_chunk_mem, hash_2_chunks_mem)miden::core::precompiles::hashes::keccak256
miden::precompiles::curves::secp256k1miden::core::precompiles::curves::secp256k1
miden::precompiles::fields::k1_base, ...::k1_scalarmiden::core::precompiles::fields::k1_base, ...::k1_scalar
miden::precompiles::u256miden::core::precompiles::u256

The host event names did not move: the wrappers still emit miden::precompiles::hashes::keccak256::digest and miden::precompiles::fields::field_inv, so custom host handlers keyed on those names keep working.

Separately, miden::core::sys::vm::claim::kernel_commitment was removed. It hashed a kernel-procedure digest list into the canonical kernel commitment; call poseidon2::hash_elements_in_domain with the kernel domain tag instead, which is exactly what the removed procedure did.

Affected Code​

# Before (0.16)
use miden::precompiles::hashes::keccak256
use miden::precompiles::curves::secp256k1
use miden::precompiles::fields::k1_base
use miden::precompiles::fields::k1_scalar
# After (0.17)
use miden::core::precompiles::hashes::keccak256
use miden::core::precompiles::curves::secp256k1
use miden::core::precompiles::fields::k1_base
use miden::core::precompiles::fields::k1_scalar

If your miden-project.toml declared the precompiles package as a dependency, drop it; miden-core now carries it:

 [dependencies]
-miden-precompiles = { version = "0.29", linkage = "dynamic" }

The kernel commitment, inlined:

# Before (0.16)
use miden::core::sys::vm::claim

# [kernel_ptr, num_kernel_procedures, ...]
exec.claim::kernel_commitment
# => [K, ...]
# After (0.17) - the removed procedure's body, inlined
use miden::core::crypto::hashes::poseidon2

const KERNEL_DOMAIN_TAG = 0x01000001 # miden_core::program::KERNEL_DOMAIN_TAG

# [kernel_ptr, num_kernel_procedures, ...]
swap u32assert.err="number of kernel procedures must fit in a u32"
mul.4 swap
# => [kernel_ptr, num_elements, ...]
push.KERNEL_DOMAIN_TAG movdn.2
exec.poseidon2::hash_elements_in_domain
# => [K, ...]

To copy digests from advice and hash them in one pass, use the new mem::pipe_words_to_memory_in_domain ([domain, num_words, write_ptr, ...] -> [DIGEST, write_ptr', ...]) with KERNEL_DOMAIN_TAG as the domain.

Migration Steps​

  1. Replace every use miden::precompiles::... with use miden::core::precompiles::....
  2. Remove any miden-precompiles dependency from miden-project.toml; depend on miden-core only. (The miden-precompiles Rust crate still exists; this is about the MASM package.)
  3. Replace exec.claim::kernel_commitment with the inlined sequence above.

Common Errors​

Error MessageCauseSolution
undefined item 'miden::precompiles::hashes::keccak256'Import of the old namespaceImport miden::core::precompiles::hashes::keccak256.

In-VM proof verification: verify_vm_proof is verify_proof​

Summary​

miden::core::sys::vm::verify_vm_proof, which the 0.16 guide taught, was renamed to verify_proof. It now returns a 12-element security descriptor on top of the deferred root D (16 elements in total) instead of D followed by four raw proof parameters (8 elements). The security estimator moved from sys::vm::compute_conjectured_security_level (which took [num_queries, query_pow_bits]) to stark::security::compute_conjectured_security_level, which consumes the whole descriptor unchanged.

Both recursive verifiers now run in an isolated memory context. In 0.16 verify_vm_proof ran in the caller's context and wrote its state into the caller's memory starting at address 3223322624 (2^31 + 2^30 + 2^21). In 0.17 the caller's memory is untouched and every stack element below the input word is preserved.

Affected Code​

# Before (0.16): [CLAIM_COMMITMENT, ...] on the stack, proof request already on the advice stack
use miden::core::sys::vm

exec.vm::verify_vm_proof # => [D, num_queries, query_pow_bits, deep_pow_bits, folding_pow_bits, ...]
swapw exec.vm::compute_conjectured_security_level # => [level, deep_pow_bits, folding_pow_bits, D, ...]
u32lt.96 assertz.err="proof security level is below the accepted target"
drop drop # => [D, ...]
# After (0.17)
use miden::core::sys::vm
use miden::core::stark::security

exec.vm::verify_proof # => [security_descriptor(12), D, ...]
exec.security::compute_conjectured_security_level # => [level, D, ...]
u32lt.96 assertz.err="proof security level is below the accepted target"
# => [D, ...]

The proof request key is derived from the verifier's root, so the procref used to build it changes too: procref.vm::verify_vm_proof exec.sys::build_proof_request_key becomes procref.vm::verify_proof exec.sys::build_proof_request_key.

If you discard the results, drop four words instead of two:

 exec.vm::verify_proof
-# => [D, num_queries, query_pow_bits, deep_pow_bits, folding_pow_bits]
-dropw dropw
+# => [security_descriptor, D]
+dropw dropw dropw dropw

The descriptor, top first, is [lookup_pow_bits, num_composed_constraints, max_constraint_degree, num_deep_terms, max_message_width, num_lookup_boundary_terms, lookup_fractions_per_row, log_max_height, num_queries, query_pow_bits, deep_pow_bits, folding_pow_bits]. The four old parameters are elements 8 to 11. The new estimator is not a general-purpose calculator: pass it the descriptor a verifier returned, unmodified.

New in the same area: miden::core::sys::pvm::verify_proof verifies a precompile-VM proof for a deferred root ([D, ...] -> [security_descriptor(12), ...]), and sys::pvm::request_proof emits miden::core::sys::pvm::request_proof so a host can supply the proof package on demand.

Code that builds its own verifier from the stark::* and pcs::* modules loses its helpers. Every getter, setter and pointer procedure of miden::core::stark::constants is gone; the module keeps the address constants (for example pub const NUM_QUERIES_PTR = 3223322628), assert_valid_order_tag and zeroize_stack_word. Some pointer accessors, such as claim_ptr, live in sys::vm::layout instead. Also removed: pcs::fri::helper::get_fri_remainder_codeword_max_size, pcs::fri::helper::get_fri_remainder_poly_max_degree_plus_1, stark::utils::bit_reverse_len_parallel and sys::vm::public_inputs::load_deferred_root.

# Before (0.16)
use miden::core::stark::constants

exec.constants::get_number_queries
# After (0.17)
use { NUM_QUERIES_PTR } from miden::core::stark::constants

mem_load.NUM_QUERIES_PTR

Migration Steps​

  1. Rename vm::verify_vm_proof to vm::verify_proof, including procref.vm::verify_vm_proof used to derive request keys.
  2. Add use miden::core::stark::security and replace swapw exec.vm::compute_conjectured_security_level with exec.security::compute_conjectured_security_level directly on the verifier output.
  3. Rebalance the stack: the verifier returns 16 elements, not 8; the estimator consumes 12 and returns 1.
  4. Delete any code that read verifier state back out of memory after the call: that memory now belongs to a separate context. You no longer need to keep the 3223322624.. region free.
  5. In a custom verifier, replace each stark::constants accessor call with a mem_load / mem_store of the matching address constant, or with the sys::vm::layout procedure (claim_ptr and others). Inline the four removed helpers if you used them.
  6. Delete any stack padding you added to compensate for elements the verifier consumed beyond its documented inputs; the documented stack effect is now exact.

On the Rust side, CoreLibrary::recursive_verifier_root() became vm_recursive_verifier_root() (see VM & Assembler Changes).

Common Errors​

Error MessageCauseSolution
undefined item 'verify_vm_proof' or undefined item 'compute_conjectured_security_level'Procedure renamed or movedUse vm::verify_proof and security::compute_conjectured_security_level.
undefined item '<accessor>' (for example undefined item 'get_number_queries' or undefined item 'claim_ptr')Removed stark::constants accessorUse the address constant, or the sys::vm::layout procedure.
undefined item 'load_deferred_root'One of the four removed verifier helpersInline it.

Core-library procedure roots moved​

Summary​

A procedure's MAST root changes whenever its instructions change. Between VM 0.29.2 (0.16) and 0.35.0 (0.17) many exported procedures got a new root, so any root you recorded (proof-request keys, allowlists, dynexec / dyncall targets) is stale. They include:

ProcedureRoot changed in VMWhy
crypto::dsa::ecdsa_k256_keccak::verify, verify_bytes0.30, 0.31Internal refactors
sys::vm::verify_vm_proof → sys::vm::verify_proof0.30, 0.31, 0.33, 0.34ACE registry, rename and security descriptor, context isolation, AIR soundness fixes
sys::pvm::verify_proof (new in 0.30)0.31, 0.33, 0.34, 0.35Same
sys::vm::compute_conjectured_security_level → stark::security::compute_conjectured_security_level0.31 (moved), 0.34Takes the security descriptor; 0.34 also bounds the DEEP composition term
crypto::aead::decrypt0.35Overlap check covers the tag (see below)
math::u64::shl, shr, rotl, rotr0.31Range check added (see below)
collections::sorted_array::find_word, find_key_value, find_half_key_value0.30u32 pointer checks
crypto::dsa::falcon512_poseidon2::verify, load_h_s2_and_product0.30Horner evaluation-point layout
mem::pipe_words_to_memory, mem::pipe_preimage_to_memory, collections::mmr::unpack0.30Advice pipe refactored for domain-separated hashing (unpack calls pipe_preimage_to_memory)

Also changed: crypto::hashes::keccak256::hash and merge (hash_bytes kept its root), and many of the recursive verifier's helpers under stark::*, pcs::* and sys::vm::*. VM 0.34 and 0.35 changed sys::vm::load_air_context, sys::vm::constraints_eval::execute_constraint_evaluation_check and 18 of the 20 exports under sys::pvm (including verify_proof, request_proof and every layout::*_ptr) again, and removed 79 exports: the stark::constants accessors and four verifier helpers (see In-VM proof verification). Procedures of your own that use bare exp also get a new root (see below).

The changelog announces the ECDSA verify root change in VM 0.30, but not the second change in 0.31.

Migration Steps​

  1. Derive roots at run time instead of pinning them: in MASM with procref.<module>::<proc>; in Rust with CoreLibrary::vm_recursive_verifier_root(), pvm_recursive_verifier_root(), conjectured_security_estimator_root(), or core_lib.package().get_procedure_root_by_path("::miden::core::...").
  2. Re-register recursive-verification proof packages: request keys are derived from the verifier root, so packages keyed under a 0.29 root are not found.
  3. Keep the prover, the host and the consuming program on the same VM release.

Common Errors​

Error MessageCauseSolution
procedure with root digest <root> could not be foundA pinned root of a core-library procedure that no longer exists in the loaded libraryRe-derive the root from the library you load.

Instruction and core-library behaviour changes​

Summary​

These change behaviour at run time without any assembly error.

Instruction / procedure0.16 (VM 0.29.2)0.17 (VM 0.35)
u64::shl, shr, rotl, rotrNo explicit check; rotl / rotr accepted out-of-range amountsTrap with shift amount must be in the range [0, 64) when n >= 64; 6 more cycles each
Bare expExponent up to 2^64 - 1, 73 cyclesLowers to exp.u63: exponent must be < 2^63, 72 cycles; larger exponents fail with assertion failed with error code: 0
horner_eval_baseRead two elements at alpha_addr and alpha_addr + 1, any alignmentReads one word-aligned word that must be [alpha0, alpha1, 0, 0]
horner_eval_extRead a word; elements 2 and 3 ignoredElements 2 and 3 must be zero
secp256k1 MSMs (mul_scalar, mul_scalar_generator, msm_mem, msm2, msm2_generator)Zero scalars and repeated bases rejected during deferred evaluationAccepted; the result may be the identity point
crypto::aead::decryptOverlap check ignored the tag word: a destination starting at the tag address passed, overwrote the tag, then failed tag verificationValid only if dst_ptr > src_ptr + (num_blocks + 1) * 8 + 3 or src_ptr > dst_ptr + (num_blocks + 1) * 8 - 1; otherwise fails with source and destination ranges must not overlap
collections::sorted_array::find_word, find_key_value, find_half_key_valueNo size limitFail when the array holds more than 65,536 entries (words for find_word, key-value pairs for the others)
eval_circuitUp to 2^30 - 1 wires per callAt most 32,768 wires (n_read + n_eval) per call, and at most 32 invocations while collecting a trace for proving

Details:

  • u64 shifts (miden::core::math::u64): new cycle counts are shl 21 → 27, shr 60/61 → 66/67, rotl 46 → 52, rotr 60 → 66. The VM changelog files this check under 0.29.0, but it is absent at 0.29.2 and first ships in 0.31.
  • exp: bare exp is exp.u63, so valid exponents are 0 to 2^63 - 1. A larger exponent fails at run time with assertion failed with error code: 0; the assembler cannot catch it. exp.uN (N in 0..=63, requires e < 2^N) and exp.<imm> (any field element; 0^0 is 1) are unchanged. Procedures containing bare exp get a new MAST root.
  • Sorted-array lookups: the cap lives in the host handler of the lowerbound_array / lowerbound_key_value events, not in the MASM procedures.
  • eval_circuit: plain execution enforces only the per-call wire limit. Both limits fit the ACE circuits of the VM and PVM recursive verifiers.
  • secp256k1 MSMs (miden::core::precompiles::curves::secp256k1): an identity value used as an MSM base is still rejected.
  • Merkle depth: mtree_verify now rejects a depth of 0 or above 64 before touching the advice provider. mtree_get and mtree_set fetch the node from the advice provider first: a depth above 64 fails that lookup (provided node index <index> is out of bounds for a merkle tree node at depth <depth>), and a depth of 0 on a root in the store reaches the new depth check. Details are on VM & Assembler Changes.

Affected Code​

The Horner evaluation point:

# Before (0.16): any address for horner_eval_base; ext ignored elements 2..3
push.ALPHA_1 push.ALPHA_0 push.ALPHA_ADDR mem_store push.ALPHA_ADDR add.1 mem_store
# After (0.17): ALPHA_ADDR word-aligned, word = [alpha0, alpha1, 0, 0]
push.0.0 push.ALPHA_1 push.ALPHA_0 # => [alpha0, alpha1, 0, 0]
push.ALPHA_ADDR mem_storew_le dropw

Migration Steps​

  1. Reduce shift and rotation amounts modulo 64 (or bounds-check them) before calling u64::shl, shr, rotl or rotr, and update cycle budgets.

  2. Use exp.<imm> for constant exponents: it takes any field element. If a dynamic exponent can reach 2^63, split it at 2^32 (131 cycles instead of 72; it keeps 0^(p-1) = 0):

    # [e, b, ...] -> [b^e, ...], any felt e
    u32split # [e_lo, e_hi, b, ...]
    dup.2 swap exp.u32 # [b^e_lo, e_hi, b, ...]
    movup.2 exp.4294967296 # [b^(2^32), b^e_lo, e_hi, ...]
    movup.2 exp.u32 # [(b^(2^32))^e_hi, b^e_lo, ...]
    mul # [b^e, ...]

    Reducing the exponent modulo p - 1 does not help: a field element is already below p, so the reduction only changes e = p - 1 (turning 0^(p-1) = 0 into 0^0 = 1) and exponents from 2^63 to p - 2 still fail. Update any pinned root of a procedure that uses bare exp.

  3. Store the Horner evaluation point at a word-aligned address as [alpha0, alpha1, 0, 0], and clear elements 2 and 3 if you kept other data there.

  4. If you relied on secp256k1 MSMs failing for zero scalars or duplicate bases, check those conditions explicitly, and handle an identity result (for example with secp256k1::is_identity or assert_not_identity).

  5. Keep Merkle depths in 1..=64.

  6. aead::decrypt: place the plaintext buffer so it overlaps neither the ciphertext nor the tag word that follows it.

  7. Split sorted arrays above 65,536 entries before calling sorted_array::find_*, and keep custom eval_circuit circuits within 32,768 wires.

Common Errors​

Error MessageCauseSolution
assertion failed with error message: shift amount must be in the range [0, 64)u64 shift or rotation with n >= 64Reduce or check n first.
Horner evaluation point at memory address <addr> in context <ctx> must be encoded as [alpha0, alpha1, 0, 0]Non-zero elements 2 and 3Zero them.
word access at memory address <addr> in context <ctx> is unaligned: word accesses require addresses that are multiples of 4Unaligned alpha_addrAlign it to 4.
Merkle tree depth must be in the range 1..=64, but was <depth>mtree_verify with depth 0 or above 64, or mtree_get / mtree_set with depth 0Use a depth in 1..=64.
provided node index <index> is out of bounds for a merkle tree node at depth <depth>mtree_get / mtree_set with a depth above 64 (the advice lookup fails before the depth check)Same.
assertion failed with error code: 0Bare exp (or exp.u63) with an exponent >= 2^63Use exp.<imm> for constants, or split the exponent at 2^32 as above.
assertion failed with error message: source and destination ranges must not overlapaead::decrypt destination overlaps the source or its tag wordStart the destination after src_ptr + (num_blocks + 1) * 8 + 3, or end it before src_ptr.
error during processing of event 'miden::core::collections::sorted_array::lowerbound_array' (ID: <id>), caused by sorted array entry count <n> exceeds maximum of 65536find_word on more than 65,536 words (find_key_value / find_half_key_value report lowerbound_key_value)Split the array.
failed to execute arithmetic circuit evaluation operation: ace circuit evaluation failed: num of wires cannot exceed 32768 but was <n>eval_circuit with n_read + n_eval > 32768Shrink the circuit.
failed to execute arithmetic circuit evaluation operation: ace circuit evaluation failed: number of recorded eval_circuit invocations cannot exceed 32More than 32 eval_circuit calls while building a trace for provingReduce the number of circuit evaluations per program.

Trace events are back​

Summary​

The 0.16 version of this page said trace.<n> was removed with no replacement. That was too strong: in 0.16 you could still emit a trace event by hand (below). In 0.17, trace is an instruction again: trace, trace.CONST and trace.event("...") emit optional, read-only trace events that the host handles through on_trace / DefaultHost::register_trace_handler (that host API already existed in 0.16). Trace IDs must come from an event("...") constant or the inline form; a bare numeric immediate such as trace.5 is not accepted.

Affected Code​

# Before (0.16): manual form
const SYS_EVENT = event("sys::trace_event")
push.MY_TRACE
emit.SYS_EVENT
drop
# After (0.17)
const MY_TRACE = event("miden_debug::println")
trace.MY_TRACE # stack-neutral, 5 cycles
trace.event("miden_debug::println") # inline form
push.<felt> trace # stack form, 3 cycles, id left on stack

Migration Steps​

  1. Replace the manual push / emit / drop sequence with trace.CONST or trace.event("...").
  2. Define trace IDs with event("..."); do not port a numeric trace.<n> literally.

MASM language: built-in u256, @bigendian removed, ABI attributes imply @callconv​

Summary​

  • u256 is a built-in type. The core library used to declare pub type u256 = struct { lo: u128, hi: u128 } in miden::core::math::u256 because the parser had no u256 keyword. u256 is now a built-in type name and the alias was deleted. This is not in the changelog.
  • @bigendian struct representation removed. type T = struct @bigendian { ... } no longer parses. The remaining struct annotations are @packed, @packed(N), @transparent and @align(N). This is not in the changelog.
  • Protocol ABI attributes imply the component-model calling convention. A procedure annotated @account_procedure, @auth_script, @note_script or @transaction_script now gets @callconv("component-model") automatically. Combining one of these with a different explicit @callconv(...), or putting two different protocol ABI attributes on one procedure, is a parse error.

Affected Code​

# Before (0.16)
use miden::core::math::u256
proc my_add(a: u256::u256, b: u256::u256) -> u256::u256
# After (0.17)
proc my_add(a: u256, b: u256) -> u256
# Before (0.16)
type BeWord = struct @bigendian { a: felt, b: felt, c: felt, d: felt }
# After (0.17)
type BeWord = struct { a: felt, b: felt, c: felt, d: felt }
# Rejected in 0.17:
@note_script
@callconv("fast")
pub proc main
...
end

@auth_script
@account_procedure
pub proc auth
...
end
# Accepted: omit @callconv, or state the matching one
@note_script
pub proc main
...
end

Migration Steps​

  1. Replace u256::u256 (or any alias of miden::core::math::u256::u256) in type positions with the built-in u256.
  2. Remove @bigendian from struct declarations.
  3. Remove any @callconv(...) other than "component-model" from procedures that carry a protocol ABI attribute, and keep one protocol ABI attribute per procedure.

Common Errors​

Error MessageCauseSolution
conflicting attributes for procedure definition@callconv conflicts with the ABI attribute, or two ABI attributes on one procedureDrop the conflicting attribute.
invalid struct annotation (help: expected one of: '@packed', '@packed(N)', '@transparent', or '@align(N)')@bigendianRemove it.

Procedure type signatures and typed pointers​

Summary​

Almost every public procedure in the protocol and standards libraries now carries a type signature, pointer arguments use ptr<T>, and the shared types moved to miden::protocol_utils::types (still re-exported from miden::protocol::types). Signatures do not change how you invoke a procedure: the assembler performs no call-site type check, so they neither break nor protect existing call sites (see the stack-effect table).

0.160.17
miden::protocol::types::MemoryAddress (u32)Removed; use ptr<felt>, ptr<word>, ptr<Asset> and so on
miden::protocol::types::DoubleWord = struct { word_lo: word, word_hi: word }struct { lo: word, hi: word }
double_word_array::DoubleWord (8 felt fields a to h)Re-export of miden::protocol::types::DoubleWord
miden::protocol::types::NoteType = u8enum NoteType : u8 { PRIVATE = 0, PUBLIC = 1 } (values unchanged)
(new)AssetClass, AssetMetadata, AssetComposition, BlockCommitment, NoteArgs, NoteSerialNumber, Note*Commitment in miden::protocol::types; miden::standards::types (AuthArgs, Commitment, Uint64/128/256, ...)

Migration Steps​

  1. Replace MemoryAddress in your own signatures with a ptr<...> type.
  2. Update struct field names if your signatures destructure DoubleWord.

Low-level protocol helpers removed or made private​

Summary​

Internal helpers that were public in 0.16.1 are now private or gone. The input_note::*_raw and note::write_* helpers now live in input_note_internal and note_internal, new private submodules of miden::protocol that cannot be imported.

Removed in 0.17Use instead
input_note::get_initial_assets_raw, get_initial_assets_info_raw, get_asset_raw, remove_asset_raw, remove_all_assets_raw, get_note_id_raw, get_attachments_commitment_rawThe indexed input_note::* or active_note::* procedures (input_note::remove_asset takes [ASSET_ID, ASSET_VALUE, note_index]).
note::write_assets_to_memory, note::write_attachment_commitments_to_memory, note::write_attachment_to_memory, note::write_indexed_attachment_to_memoryactive_note::get_initial_assets, input_note::get_initial_assets, output_note::get_assets, and the write_attachment_to_memory / write_attachment_commitments_to_memory procedures of active_note, input_note and output_note.
miden::protocol::account_id::validateaccount_id::validate_structure (unlike validate, it does not check that the ID version is supported, only that it is non-zero)
miden::protocol::types::MemoryAddressTyped pointers such as ptr<felt>.
miden::standards::tx_scripts::send_notes::common::WORD_NUM_ELEMENTSmiden::protocol::constants::WORD_NUM_ELEMENTS

The changelog also names active_note, but no active_note procedure was removed: the removed ones are the seven input_note::*_raw procedures and the four note::write_* helpers.

Migration Steps​

  1. Replace each removed item per the table.

Common Errors​

Error MessageCauseSolution
undefined item '<name>' (for example undefined item 'get_asset_raw')Any removed item abovePer the table.

Asset ID and note metadata encodings gained version bits​

Summary​

The asset metadata byte in an asset ID is now [reserved(2) | composition(2) | version(4)], with version 1 in bits 0-3 and composition in bits 4-5. The first note metadata felt ends in [reserved(1) | note_type(1) | version(6)], so the note type is at bit 6. The kernel validates both versions. fungible_asset::create_id, non_fungible_asset::create and note::metadata_into_note_type handle this for you. See Assets, Vault & Faucet Changes and Note Changes for the Rust side.

Non-fungible metadata is 0x01 in 0.17 (ASSET_METADATA_NONE), which is the value a 0.16 fungible ID used. A 0.16 non-fungible ID (metadata 0x00) fails the version check.

Affected Code​

# Before (0.16) - assets/fungible_asset.masm create_id
push.COMPOSITION_FUNGIBLE # metadata byte 0x01
add
# After (0.17) - assets/fungible_asset.masm create_id
add.ASSET_METADATA_FUNGIBLE # 0x11: composition fungible (bits 4-5) | version 1 (bits 0-3)
# Before (0.16) - note::metadata_into_note_type
u32shr.4
# After (0.17)
u32shr.6

Migration Steps​

  1. Build asset IDs with fungible_asset::create_id / fungible_asset::create / non_fungible_asset::create, never by adding composition constants by hand.
  2. Decode the note type with note::metadata_into_note_type, not by shifting.

Common Errors​

Error MessageCauseSolution
unknown asset ID versionMetadata byte whose version bits are not 1 (for example a 0.16 non-fungible ID, metadata 0x00)Use the standards builders.
note metadata has an unsupported versionInput note metadata with a version other than 1, for example a 0.16 public note (low byte 0x11 reads as version 17)Use notes created by the 0.17 kernel.
note metadata has a non-zero reserved bitHand-built metadata with bit 7 setSame.

Kernel entry points: syscall and foreign procedure roots​

Summary​

  • Only exec_kernel_proc is a valid syscall target. The kernel root module is now lib/dispatcher.masm, which exports only exec_kernel_proc; the 64 API procedures moved to the api submodule and are no longer kernel exports. Going through miden::protocol::*, or through syscall.exec_kernel_proc with a kernel_proc_offsets constant, is unaffected.
  • Kernel procedure offsets shifted. The kernel has 64 procedures instead of 61. The new ones are ACCOUNT_GET_CODE_UPGRADE_COMMITMENT_OFFSET = 23, OUTPUT_NOTE_SEAL_OFFSET = 48 and OUTPUT_NOTE_IS_SEALED_OFFSET = 49. The 0.16 offsets 23 to 46 move up by one, and the TX_* offsets (47 to 60 in 0.16) move up by three. Code that names the kernel_proc_offsets constants only needs re-assembly; a hard-coded number now dispatches to a different procedure.
  • A foreign procedure must belong to the foreign account's code. tx::execute_foreign_procedure now fails unless FOREIGN_PROC_ROOT is a procedure the foreign account exports; with the standard host, the procedure-index lookup rejects the root before the kernel's own assertion runs. In 0.16 the kernel only checked that the root was non-zero, so a caller could run an arbitrary MAST root, for example a library procedure that reads the foreign account's storage, under that account's identity. Faucet callback roots get the same check (see Faucet callbacks and policies).

Migration Steps​

  1. Replace any syscall.<kernel api procedure> with the matching miden::protocol::* procedure.
  2. For FPI, pass the root of a procedure the foreign account exports; a library procedure it does not export is rejected.
  3. Replace any hard-coded kernel offset with its kernel_proc_offsets constant.

Common Errors​

Error MessageCauseSolution
undefined item '::$kernel::<name>'syscall.<name> to a kernel API procedure (for example syscall.account_get_id)Use the protocol library.
account procedure with procedure root <root> is not in the account procedure index mapFPI to a root the foreign account does not export; reported as the cause of an event error for miden::protocol::account::push_procedure_indexCall a procedure the foreign account exports.

Other MASM changes​

  • New procedures and constants worth adopting (additive):
    • active_note::get_storage_info ([] -> [NOTE_STORAGE_COMMITMENT, num_storage_items]) and active_note::get_bounded_storage ([dest_ptr, max_num_storage_items] -> [num_storage_items]).
    • miden::protocol::constants::MAX_ASSETS_PER_NOTE (16; in 0.16.1 only the kernel had it) and miden::protocol::tx::MAX_EXPIRATION_BLOCK_DELTA (65535).
    • native_account::has_state_changed, tx::get_reference_block_commitment, fungible_asset::validate.
    • native_account::get_code_upgrade_commitment ([] -> [NEW_CODE_COMMITMENT], the empty word when no upgrade is pending).
    • output_note::seal ([note_index] -> []) and output_note::is_sealed ([note_index] -> [is_sealed]).
    • miden::standards::expiration::apply_default and DEFAULT_EXPIRATION_BLOCK_DELTA (20).
    • miden::standards::auth::eip712, auth::eip712_transaction_summary, access::role_symbol, and the tx_fee_collector auth component (miden::standards::components::auth::tx_fee_collector).
  • EVM-style ECDSA public-key recovery: ecdsa_k256_keccak::recover ([MSG_WORD, SIG_PTR, ...] -> [QX[8], QY[8], ...]) and recover_bytes ([MSG_PTR, MSG_LEN_BYTES, SIG_PTR, ...]) read an R || S || V witness from memory. They accept high-s, and a wrong message recovers a different valid key, so authenticate the returned key against trusted state. CoreLibrary::handlers() registers the host handler; a host that builds its handler list by hand must add it. The verify / verify_bytes advice layout is unchanged from 0.16.
  • P2ID storage has four items: [target_id_suffix, target_id_prefix, salt_0, salt_1]. p2id::prepare_note and p2id::create_output_note keep their signatures and write a zero salt. If you compute a P2ID recipient by hand, write four items and pass num_storage_items = 4, or it fails with P2ID note expects exactly 4 note storage items. See Note Changes.
  • Output note sealing: output_note::seal freezes an output note's assets and attachments so its final note ID cannot change. It is idempotent and only the native account can call it. After it, output_note::add_asset and the attachment procedures fail with sealed output notes cannot be modified. The standard PSWAP note seals the notes it creates after checking their asset, and fails with PSWAP output was altered before sealing if the asset no longer matches (see Note Changes).
  • Multisig procedure thresholds: in an account using the standard multisig auth, a per-procedure threshold below the default also lets that smaller group of approvers add assets and attachments to the procedure's output notes from a transaction script. A procedure with a lowered threshold should seal every note it creates, after adding its own assets and attachments.
  • Standard note scripts check targeting through miden::standards::note::note_target and reclaim through note::note_reclaim, their error constants were renamed, config notes must be public, and every standard script root changed. See Note Changes.

Common Errors​

Error MessageCauseSolution
the block number must be a u32Argument-less tx::get_block_commitment call consumed an unrelated valueUse tx::get_reference_block_commitment.
the block number must not exceed the transaction reference block numberSame, or a block number above the reference blockSame, or push a valid block number.
failed to lookup value in Merkle storeSame, with a lower block number (often a zero pad) that the transaction's partial blockchain does not trackSame, or include that block in the transaction inputs.
undefined item '<procedure>'Renamed or removed procedure; the message names only the procedure (get_block_number, compute_commitment, verify_vm_proof, estimate_fee, ...)Use the 0.17 name from this page.
undefined item '<module path>'Stale or private module in a use: miden::precompiles::..., miden::standards::note_tag, note::execution_hint, notes::mint_fungible, notes::mint::fungibleUse the new module path, or the MINT dispatcher.
undefined item '::$kernel::<name>'syscall.<name> to a kernel API procedureUse miden::protocol::*.
transaction must not include output notestx_policy::assert_no_output_notes after fee::pay_feeCall it with no argument, before paying the fee.
the transaction fee must be paid in the native fee asset at rate 1/1Conversion info names another faucet or rateCommit the native conversion info.
the active account is not nativenative_account::compute_commitment / has_state_changed in a foreign contextCall it only from the native account.
fungible asset value is not well-formedOld value_into_amount call site now validatesPass a real fungible value, or use value_into_amount_unchecked.
failed to decode note_type into u8Old non-fungible mint_and_send call without ASSET_ID, read shifted by one wordPass the full asset of the minting faucet.
the asset stored in the MINT note does not belong to this faucetNon-fungible mint_and_send or MINT note whose ASSET_ID belongs to another faucetSame.
account procedure with procedure root <root> is not in the account procedure index mapFPI to a root the foreign account does not exportCall a procedure the foreign account exports.
unknown asset ID versionHand-built asset ID with the 0.16 metadata byteUse the standards asset builders.
procedure with root digest <root> could not be foundPinned core-library root that changed, or a custom MAST store that does not serve the dynamically linked standards libraryDerive roots with procref or the CoreLibrary accessors; load StandardsLib into the store.
assertion failed with error message: shift amount must be in the range [0, 64)u64 shift or rotation with n >= 64Reduce n modulo 64 first.
assertion failed with error code: 0Bare exp with an exponent >= 2^63Use exp.<imm> for constants, or split the exponent at 2^32.
sealed output notes cannot be modifiedAsset or attachment added to an output note after output_note::sealAdd everything before sealing.
a new account cannot be upgraded, account storage upgrades are not supportednative_account::upgrade in an account-creating transaction, or with a non-empty storage wordUpgrade in a later transaction, with an empty storage word.
conflicting attributes for procedure definition@callconv other than component-model on a protocol ABI procedureDrop the explicit @callconv.