Skip to content

docs(migration): add the v0.16 migration guide - #357

Open
WiktorStarczewski wants to merge 5 commits into
mainfrom
docs/migration-guide-0.16
Open

docs(migration): add the v0.16 migration guide#357
WiktorStarczewski wants to merge 5 commits into
mainfrom
docs/migration-guide-0.16

Conversation

@WiktorStarczewski

@WiktorStarczewski WiktorStarczewski commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Miden Testnet 0.16.0

This guide covers all breaking changes you need to migrate an application to Miden 0.16.0. Like the 0.15 guide, it is intentionally user-facing: you do not need to know or care which internal crate (VM, protocol, client) a change came from. If you are:

  • building accounts, notes, or transactions
  • running a client, web client or React SDK
  • writing or compiling MASM
  • writing Rust smart contracts with the miden SDK
  • interacting with storage, auth, or RPCs

this document is for you. It folds together the breaking changes from the protocol crates (0.15.30.16.0), the VM crates (miden-vm, 0.230.29.1), miden-client (0.150.16.0), the Web SDK (@miden-sdk/* 0.150.16.0), and the miden Rust contract SDK / compiler (0.130.14).


Quick Upgrade

Try upgrading first — most projects can start with a dependency update:

Cargo.toml

# Replace these
miden-client              = "0.15"
miden-client-sqlite-store = "0.15"
miden-protocol            = "0.15.3"
miden-standards           = "0.15.3"
miden-tx                  = "0.15.3"
miden-tx-batch-prover     = "0.15.3"
miden-assembly            = "0.23"
miden-core                = "0.23"
miden-core-lib            = "0.23"
miden-processor           = "0.23"
miden-prover              = "0.23"
miden-crypto              = "0.25"

# With these
miden-client              = "0.16.0-rc.1"
miden-client-sqlite-store = "0.16.0-rc.1"
miden-protocol            = "0.16.0-rc.6"
miden-standards           = "0.16.0-rc.6"
miden-tx                  = "0.16.0-rc.6"
miden-tx-batch            = "0.16.0-rc.6"   # renamed from miden-tx-batch-prover
miden-assembly            = "0.29.1"
miden-core                = "0.29.1"
miden-core-lib            = "0.29.1"
miden-processor           = "0.29.1"
miden-prover              = "0.29.1"
miden-crypto              = "0.29.1"

package.json (Web SDK)

{
  "@miden-sdk/miden-sdk": "0.16.0-rc.2",
  "@miden-sdk/react": "0.16.0-rc.2"
}

Then run:

cargo update && cargo build

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

0.15 artifacts do not round-trip

The MAST wire format moved 0.0.30.0.4, the package format 4.0.06.0.0, and the .masl library format was removed entirely. Several commitment preimages changed as well. Re-assemble every package from source and re-sync into a fresh store.

Your local store must be recreated, and your node must be upgraded with your client

Every pre-0.16 SQLite store is rejected — there is no migration path. Browser applications reset their IndexedDB store automatically. Separately, 0.16 clients seal (encrypt) transaction inputs before submission, so a 0.16 client cannot talk to an older node and vice versa.


Who should read this?

This guide is for:

  • Rust client developers migrating from v0.15 → v0.16
  • Web SDK developers using the JavaScript/TypeScript SDK
  • Smart contract authors writing MASM or using protocol APIs
  • App developers using the protocol, standards, or client crates

If you're starting fresh on v0.16, you can skip this guide and go directly to the Get Started guide.


At a Glance

Big themes in 0.16:

Change Summary
Fees moved into the auth procedure The kernel no longer burns the fee automatically. The auth procedure reads FeeConversionInfo from the transaction's auth args and emits a TX_FEE note. On a fee-charging chain, requests signed by AuthSingleSig/AuthMultisig must call TransactionRequestBuilder::fee_conversion_info(info, salt).
MASM gained an explicit module tree A .masm file is only included if its parent declares it with mod/pub mod — an undeclared file is silently dropped. use split into module imports and braced item imports, aliases moved from -> to as, and imports resolve globally.
Account updates became absolute AccountDeltaAccountPatch for account updates (ExecutedTransaction, AccountUpdateDetails, client results). TransactionSummary::account_delta() deliberately stays relative.
Auth is no longer a special builder slot AccountBuilder::with_auth_component is gone; auth components pass through with_component(s) and are found by their @auth_script attribute. Keys are wrapped in a new Approver / ApproverSet. AuthMethod and AuthSingleSigAcl are removed.
Asset identity renamed one level down AssetVaultKeyAssetId, and the old AssetIdAssetClass. Because AssetId survives with a new meaning, careless renaming compiles and is wrong.
Library is gone; Package is the only artifact Library/KernelLibrary were deleted, link_*_library collapsed into link_package, *_from_dir became *_from_root, and .masl no longer exists. MAST 0.0.4 / package 6.0.0 are not backward compatible.
Notes use typed builders, and carry fewer assets XNote::create(..)XNote::builder()…build()? + .into(). MAX_ASSETS_PER_NOTE dropped 64 → 16. Mint and burn scripts were unified across faucet kinds, changing their roots.
Debug decorators removed debug.* and trace are gone from the language, replaced by miden::core::debug procedures — which, unlike the decorators, print unconditionally. The client and CLI debug-mode toggles were removed with them.
Commitment preimages changed ECDSA public-key commitments, MMR peak commitments, and domain-separated empty-input hashes all changed value. Nothing fails to compile; stored values simply stop matching.
Store and node compatibility both break Every pre-0.16 SQLite store must be recreated, and transaction inputs are now sealed, so client and node must be upgraded together.

If you only skim a few sections, skim Transaction Changes, Account Changes, MASM Changes, and Client Changes.


Compatibility

Component Required Tested With
Miden VM crates 0.29+ 0.29.1
miden-crypto 0.29+ 0.29.1
miden-protocol 0.16+ 0.16.0-rc.6
miden-standards 0.16+ 0.16.0-rc.6
miden-client 0.16+ 0.16.0-rc.1
Web SDK (@miden-sdk/*) 0.16+ 0.16.0-rc.2
miden contract SDK 0.14+ 0.14.0-rc.1
midenc compiler 0.10+ 0.10.0-rc.1
Rust (client) 1.96+ 1.96
Rust (protocol / VM) 1.96.1+ 1.96.1
Rust (contract SDK / compiler) 1.97+ 1.97

Pin the exact pre-release version

The 0.16 protocol and client crates currently publish as 0.16.0-rc.N. Cargo does not match a pre-release against a plain "0.16" requirement, so pin the exact string until the final release is published.

The contract toolchain lags the rest of the line

midenc and the miden contract SDK build against protocol 0.16.0-alpha.4 and VM 0.25, not the protocol 0.16.0-rc and VM 0.29.1 used by the client and node. Artifacts still load — the MAST and package formats are compatible across those VM versions — but the protocol API surface the compiler sees is an earlier snapshot. Its MSRV is also higher, at 1.97.



Table of Contents

  1. Imports & Dependencies
  2. Hashing & Crypto Changes
  3. Account Changes
  4. Note Changes
  5. Assets, Vault & Faucet Changes
  6. Transaction Changes
  7. Client Changes
  8. MASM Changes
  9. VM & Assembler Changes
  10. Rust Contract SDK & Compiler

Final Checklist · Need Help?


Imports & Dependencies

Every layer of the stack moves:

  • The protocol crates (miden-protocol, miden-standards, miden-tx, miden-testing) go 0.15.30.16.0.
  • The VM crates (miden-assembly, miden-core, miden-core-lib, miden-processor, miden-prover, miden-mast-package) go 0.230.29.1. This is a much larger jump than previous releases and carries breaking MASM language changes — see VM & Assembler Changes.
  • miden-crypto goes 0.250.29.1. It is no longer an independent crate line: it was imported into the Miden VM workspace and now shares the VM version number.
  • miden-client and miden-client-sqlite-store go 0.150.16.0.
  • The Web SDK packages go 0.150.16.0.

Two crates changed identity: miden-tx-batch-prover is now miden-tx-batch, and a new miden-protocol-build-utils crate provides MASM assembly helpers. On the VM side the core package was split, adding a miden-precompiles package alongside miden-core.


Breaking Change

The protocol crates move from 0.15.3 to 0.16.0, miden-client from 0.15 to 0.16.0, and the VM crates jump 0.23 → 0.29.1 — six minor versions, not one. miden-crypto was absorbed into the Miden VM workspace and now shares its version number (0.25 → 0.29.1). The MSRV is Rust 1.96. Because the MAST wire format, the package format, and several commitment preimages changed, 0.15 artifacts do not round-trip: re-assemble every package from source, recreate your local store, and upgrade your node in lockstep with your client.

Quick Fix

Cargo.toml

# Replace these
miden-client              = "0.15"
miden-client-sqlite-store = "0.15"
miden-protocol            = "0.15.3"
miden-standards           = "0.15.3"
miden-tx                  = "0.15.3"
miden-tx-batch-prover     = "0.15.3"
miden-assembly            = "0.23"
miden-core                = "0.23"
miden-core-lib            = "0.23"
miden-processor           = "0.23"
miden-prover              = "0.23"
miden-crypto              = "0.25"

# With these
miden-client              = "0.16.0-rc.1"
miden-client-sqlite-store = "0.16.0-rc.1"
miden-protocol            = "0.16.0-rc.6"
miden-standards           = "0.16.0-rc.6"
miden-tx                  = "0.16.0-rc.6"
miden-tx-batch            = "0.16.0-rc.6"   # renamed from miden-tx-batch-prover
miden-assembly            = "0.29.1"
miden-core                = "0.29.1"
miden-core-lib            = "0.29.1"
miden-processor           = "0.29.1"
miden-prover              = "0.29.1"
miden-crypto              = "0.29.1"

package.json (Web SDK)

{
  "@miden-sdk/miden-sdk": "0.16.0-rc.2",
  "@miden-sdk/react": "0.16.0-rc.2"
}

Then run:

cargo update && cargo build

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

Pin the exact version

The 0.16 protocol and client crates currently publish as 0.16.0-rc.N pre-releases. Cargo does not match a pre-release against a plain requirement, so miden-protocol = "0.16" will fail to resolve. Pin the exact string as shown above until the final release is published.

0.15 artifacts do not round-trip

The MAST wire format moved 0.0.30.0.4 and the package format 4.0.06.0.0, so serialized packages and MastForest blobs from 0.15 will not load. The .masl library format no longer exists at all. Several commitment preimages also changed (ECDSA public keys, MMR peaks, empty domain-separated hashes), so derived values must be recomputed. Re-assemble from source and re-sync into a fresh store.


Version Bumps

Crate v0.15 v0.16
miden-client 0.15 0.16.0
miden-client-sqlite-store 0.15 0.16.0
miden-protocol 0.15.3 0.16.0
miden-standards 0.15.3 0.16.0
miden-tx 0.15.3 0.16.0
miden-testing 0.15.3 0.16.0
miden-tx-batch-prover 0.15.3 renamed to miden-tx-batch 0.16.0
miden-protocol-build-utils 0.16.0 (new)
miden-assembly 0.23 0.29.1
miden-core 0.23 0.29.1
miden-core-lib 0.23 0.29.1
miden-processor 0.23 0.29.1
miden-prover 0.23 0.29.1
miden-verifier 0.23 0.29.1
miden-mast-package 0.23 0.29.1
miden-precompiles 0.29.1 (new)
miden-crypto 0.25 0.29.1
npm package v0.15 v0.16
@miden-sdk/miden-sdk 0.15.x 0.16.0
@miden-sdk/react 0.15.x 0.16.0
@miden-sdk/vite-plugin 0.16.0

miden-idxdb-store is not a package

Earlier guidance listed a miden-idxdb-store npm dependency. No such package exists on the public registry — the IndexedDB store ships inside @miden-sdk/miden-sdk. Remove it from your package.json if you carried it over.


Affected Code

Cargo.toml:

- miden-client              = "0.15"
- miden-client-sqlite-store = "0.15"
- miden-protocol            = "0.15.3"
- miden-standards           = "0.15.3"
- miden-tx                  = "0.15.3"
- miden-tx-batch-prover     = "0.15.3"
- miden-assembly            = "0.23"
- miden-core                = "0.23"
- miden-core-lib            = "0.23"
- miden-processor           = "0.23"
- miden-prover              = "0.23"
- miden-crypto              = "0.25"
+ miden-client              = "0.16.0-rc.1"
+ miden-client-sqlite-store = "0.16.0-rc.1"
+ miden-protocol            = "0.16.0-rc.6"
+ miden-standards           = "0.16.0-rc.6"
+ miden-tx                  = "0.16.0-rc.6"
+ miden-tx-batch            = "0.16.0-rc.6"
+ miden-assembly            = "0.29.1"
+ miden-core                = "0.29.1"
+ miden-core-lib            = "0.29.1"
+ miden-processor           = "0.29.1"
+ miden-prover              = "0.29.1"
+ miden-crypto              = "0.29.1"

package.json (Web SDK):

- "@miden-sdk/miden-sdk": "^0.15.0",
- "@miden-sdk/react": "^0.15.0",
- "miden-idxdb-store": "^0.15.0"
+ "@miden-sdk/miden-sdk": "0.16.0-rc.2",
+ "@miden-sdk/react": "0.16.0-rc.2"

MSRV (Minimum Supported Rust Version)

The MSRV rose across the board. Update your rust-toolchain.toml to Rust 1.96:

rust-toolchain.toml

[toolchain]
channel = "1.96"
Component v0.15 v0.16
protocol crates 1.90 1.96.1
miden-client 1.93 1.96
Miden VM 1.90 1.96

Migration Steps

  1. Bump every Miden crate per the table above, pinning the exact 0.16.0-rc.N strings for the protocol and client crates, and run cargo update.
  2. Rename the miden-tx-batch-prover dependency to miden-tx-batch if you used it.
  3. Set your toolchain to at least Rust 1.96.
  4. Bump @miden-sdk/miden-sdk and @miden-sdk/react together — mixing 0.15 and 0.16 packages will not link against the shared WASM ABI. Drop any miden-idxdb-store dependency.
  5. Re-assemble every .masp package from source under the new toolchain, and delete cached MastForest blobs. The .masl format is gone entirely.
  6. Recreate your local store. The SQLite store's schema fingerprint changed and existing databases are rejected; browser users have their IndexedDB store cleared automatically on the version bump. See Client Changes.
  7. Upgrade your node together with your client. 0.16 clients seal transaction inputs before submission; a 0.16 node rejects plaintext submissions and an older node rejects sealed ones, so the two cannot be mixed.

Common Errors

Error Message Cause Solution
failed to select a version for miden-protocol A plain "0.16" requirement will not match a 0.16.0-rc.N pre-release Pin the exact version string, e.g. "0.16.0-rc.6".
failed to select a version for miden-tx-batch-prover Crate renamed in 0.16 Depend on miden-tx-batch instead.
MastForest deserialization failed: unexpected version MAST wire format moved to 0.0.4 Re-assemble every package from source under VM 0.29.1.
package fails to load with a version mismatch Package format moved to 6.0.0 Rebuild the .masp; .masl is no longer supported at all.
Migration error: Attempt to migrate a database with a migration number that is too high Existing SQLite store predates the 0.16 schema Delete and recreate the store, then re-sync.
Node rejects a submitted transaction Client and node versions are mixed Upgrade both to 0.16; sealed and plaintext submissions are mutually incompatible.
rustc version error during build MSRV raised to 1.96 Update rust-toolchain.toml.

Hashing & Crypto Changes

Unlike most of this release, nothing here breaks your build. These are value changes, so the symptom is a proof that fails to verify, an account whose storage no longer matches, or an advice-map lookup that misses — all at runtime, all without a compiler error pointing at the cause.

The rule of thumb: if you stored a hash, recompute it. If you only ever compute hashes on the fly from current inputs, you are unaffected.


Breaking Change

Four commitment preimages changed. Any value you have persisted — in account storage, note storage, an advice map key, or your own database — that was derived from an ECDSA public key, an MMR peak set, a domain-separated empty input, or hash_bytes(&[]) is now wrong and must be recomputed. These changes are silent: nothing fails to compile, and the old values simply no longer match.

Quick Fix

// Recompute every stored ECDSA public-key commitment
use miden_crypto::dsa::ecdsa_k256_keccak::PublicKey;
let commitment: Word = public_key.to_commitment();

Then re-derive anything downstream: account storage slots, note storage, and advice-map keys built from those commitments.

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


ECDSA k256 public-key commitment format changed

The ECDSA-k256/Keccak public-key commitment now hashes the native affine coordinate limbs (qx || qy as little-endian u32 limbs) instead of the compressed SEC1 public-key bytes. Compressed SEC1 serialization of the key itself is unchanged — only the commitment value changed (#3342, crypto#1075).

Affected Code

v0.15:  PK_COMM = Poseidon2::hash_elements( 33 compressed SEC1 bytes packed as 9 felts )
v0.16:  PK_COMM = Poseidon2::hash_elements( QX[8] || QY[8] )   # native LE u32 limbs
// After (0.16) — regenerate every stored commitment
use miden_crypto::dsa::ecdsa_k256_keccak::PublicKey;
let commitment: Word = public_key.to_commitment();

Migration Steps

  1. Recompute every stored ECDSA public-key commitment with PublicKey::to_commitment().
  2. Re-derive anything downstream of that commitment — account storage slots, note storage, advice-map keys.
  3. No MASM call-site changes are needed. The operand-stack contract of ecdsa_k256_keccak::verify is still [PK_COMM, MSG_WORD, ...]; only the value of PK_COMM moved. The advice layout did change, though — see MASM Changes.

MMR peak commitments now bind the leaf count

MMR peak commitments are computed over [num_leaves, 0, 0, 0] || padded_peaks instead of padded_peaks alone, on both the Rust and MASM sides. All MMR peak commitments change (#3388).

Affected Code

v0.15:  hash_peaks() = Poseidon2::hash_elements( padded_peaks )
v0.16:  hash_peaks() = Poseidon2::hash_elements( [num_leaves, 0, 0, 0] || padded_peaks )

The miden::core::collections::mmr pack and unpack procedures were updated to the same preimage. In 0.15, pack hashed the range starting at mmr_ptr + 4, skipping the leaf-count word; in 0.16 it hashes from mmr_ptr, so the leaf count is absorbed first. The MASM stack contracts ([mmr_ptr, ...] -> [HASH, ...]) are unchanged.

Migration Steps

  1. Recompute and re-persist every stored MMR peak commitment.
  2. Invalidate any cached chain-MMR commitment, advice-map entry keyed by an MMR commitment, or proof whose witness depends on one.
  3. No MASM call-site changes — mmr::pack and mmr::unpack keep their signatures.

Domain-separated empty-input hashing changed

hash_elements_in_domain(&[], d) for a nonzero domain d used to collide with other inputs. The fix marks the empty-input case in the third capacity element and applies a permutation, so the result is now a distinct, nonzero digest (#3447, refining #3366).

Related and also digest-changing: hash_bytes(&[]) no longer returns Word::default(). The empty-bytes input now absorbs a padding marker and permutes, producing a nonzero digest consistent with the 10* sponge padding rule (#3366).

Affected Code

// After (0.16) — the empty-input branch, from the algebraic sponge implementation
} else if total_len == 0 && state[CAPACITY_RANGE.start + 1] != ZERO {
    // Mark an empty domain-separated input in an otherwise unused capacity element.
    state[CAPACITY_RANGE.start + 2] = Felt::ONE;
    S::apply_permutation(&mut state);
}

Migration Steps

  1. Re-derive any commitment computed as hash_elements_in_domain over an empty element list with a nonzero domain — typically "empty collection" sentinel values.
  2. Re-derive any value computed as hash_bytes(&[]). A stored zero word is no longer the right answer.
  3. merge_in_domain and non-empty hash_elements_in_domain inputs are unaffected.

AeadPoseidon2 key derivation restored to canonical decoding

AeadPoseidon2::key_from_bytes was restored to canonical-Felt decoding (#3366). Keys persisted under the brief SHA-256 KDF contract must be re-derived.

Migration Steps

  1. If you persisted AEAD keys derived with key_from_bytes during the 0.16 pre-release window, re-derive them.
  2. Data encrypted under a key derived by the interim contract cannot be decrypted with a canonically-derived key — re-encrypt it.

Common Errors

These changes do not produce compile errors. Expect runtime symptoms instead:

Symptom Cause Solution
Signature verification traps for a key that worked in 0.15 Stored PK_COMM uses the old preimage Recompute with PublicKey::to_commitment().
Advice-map lookup misses for a key you know you inserted The key is a changed commitment Re-derive the key.
Chain-MMR commitment mismatch after upgrading Peak commitment now binds the leaf count Recompute and re-persist.
An "empty" sentinel commitment no longer matches Empty-input hashing changed Re-derive the sentinel.
Previously encrypted data fails to decrypt AEAD key derivation changed Re-derive the key and re-encrypt.

Account Changes

Three independent shifts land on accounts in this release.

Auth stops being special. In 0.15 the builder had a dedicated auth slot; in 0.16 the auth component is just a component, and the builder finds it by looking for the @auth_script attribute in its MASM. This is what makes the fee change possible — the auth procedure is now also where fees get paid, so it needed to compose with everything else.

Keys are wrapped in an Approver. AuthSingleSig::new took a public-key commitment and a scheme; it now takes a single Approver carrying both. Multi-signature components take an ApproverSet with a threshold. This is a mechanical rewrite, but it touches every account you construct.

Component names were normalised, which changes commitments. Every standard component's NAME dropped its components:: segment. Since the name feeds component metadata, and metadata feeds the storage schema commitment, this silently changes account commitments even when nothing else about your account changed.

AccountType did not change

AccountType has been Private / Public since 0.15, and there is no separate AccountStorageMode. If you are coming from an older release, that collapse is covered in the 0.15 guide.


Breaking Change

AccountBuilder::with_auth_component was removed — auth components are now passed through with_component like any other, and identified by their MASM @auth_script attribute. Auth components take an Approver instead of a raw key and scheme. Every standard component's NAME constant changed, and because component metadata feeds the storage schema commitment, accounts rebuilt from the same seed will have different commitments.

Quick Fix

// Before (0.15)
let account = AccountBuilder::new(init_seed)
    .account_type(AccountType::Public)
    .with_auth_component(AuthSingleSig::new(pub_key, auth_scheme))
    .with_component(BasicWallet)
    .build()?;

// After (0.16)
let account = AccountBuilder::new(init_seed)
    .account_type(AccountType::Public)
    .with_components([
        AuthSingleSig::new(Approver::new(pub_key, auth_scheme)).into(),
        BasicWallet.into(),
    ])
    .build()?;

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


AccountBuilder::with_auth_component removed

AccountBuilder now takes all components uniformly through with_component and with_components, and identifies the auth component by its @auth_script MASM attribute.

Affected Code

  let account = AccountBuilder::new(init_seed)
      .account_type(AccountType::Public)
-     .with_auth_component(auth_component)
-     .with_component(BasicWallet)
+     .with_components([auth_component.into(), BasicWallet.into()])
      .build()?;

AccountBuilder also gained with_asset_callbacks(AssetCallbackFlag). Whether a faucet's assets trigger callbacks is now encoded in the account ID rather than in separate storage, so this is set at construction time.

Migration Steps

  1. Drop with_auth_component and pass the auth component through with_component or with_components.
  2. If you author a custom auth component, make sure its entry procedure carries the @auth_script attribute — that is how the builder recognises it.
  3. If you build a faucet whose assets should trigger callbacks, set with_asset_callbacks.

Common Errors

Error Message Cause Solution
no method named with_auth_component Method removed Use with_component / with_components.
Build fails reporting no auth component Custom component lacks the attribute Annotate the entry procedure with @auth_script.

Approver and ApproverSet replace raw key arguments

Approver bundles a public-key commitment with its signature scheme; ApproverSet bundles a list of approvers with a threshold. Both are new in 0.16. The AuthMethod enum was removed, and AuthSingleSigAcl / AuthSingleSigAclConfig were removed outright.

Affected Code

// Before (0.15)
AuthSingleSig::new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self
// After (0.16)
Approver::new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Approver
AuthSingleSig::new(approver: Approver) -> Self
ApproverSet::new(approvers: Vec<Approver>, threshold: u32) -> Result<Self, AccountError>
AuthMultisig::new(approver_set: ApproverSet) -> Self

The convenience constructors are unchanged and remain the shortest path when you have a concrete key:

// Identical in 0.15 and 0.16
AuthSingleSig::falcon512_poseidon2(pub_key)
AuthSingleSig::ecdsa_k256_keccak(pub_key)
AuthSingleSig::from_public_key(pub_key)

New accessors: AuthSingleSig::approver(), ApproverSet::approvers(), and ApproverSet::threshold().

Migration Steps

  1. Wrap existing AuthSingleSig::new(pub_key, scheme) arguments in Approver::new(pub_key, scheme).
  2. Replace multi-signature construction with ApproverSet::new(approvers, threshold)? — note it is fallible.
  3. Remove any use of AuthMethod; the scheme now travels inside the Approver.
  4. If you used AuthSingleSigAcl, there is no drop-in replacement. Rebuild the access-control policy using the components under miden::standards::access (for example RoleBasedAccessControl or Authority).

Common Errors

Error Message Cause Solution
this function takes 1 argument but 2 were supplied on AuthSingleSig::new Signature changed Wrap the arguments in Approver::new.
cannot find type AuthMethod Removed Use Approver / AuthScheme.
cannot find type AuthSingleSigAcl Removed Rebuild with an access-control component.

Component names changed, and account commitments with them

Every standard component's NAME constant was normalised by dropping the components:: segment. Component metadata feeds the storage schema commitment, so the commitment of an account built from the same seed and the same components differs between 0.15 and 0.16.

Affected Code

- "miden::standards::components::auth::singlesig"
+ "miden::standards::auth::singlesig"

- "miden::standards::components::wallets::basic_wallet"
+ "miden::standards::wallets::basic_wallet"

- "miden::standards::components::access::rbac"
+ "miden::standards::access::rbac"

- "miden::standards::components::faucets::fungible_faucet"
+ "miden::standards::faucets::fungible"

A few names already lacked the segment in 0.15 — miden::standards::auth::network_account and miden::standards::access::ownable2step are unchanged. Note that the fungible faucet also lost its _faucet suffix, so it is not a pure prefix change.

The miden::standards::account::metadata module was also renamed to miden::standards::account::inspection, both in MASM and in Rust.

Migration Steps

  1. Update any hard-coded component name strings.
  2. Expect new account IDs and commitments for accounts rebuilt from the same seed. If you have persisted an account ID derived under 0.15, it will not be reproduced by 0.16 construction.
  3. Update references to account::metadata to account::inspection.

Component MASM must annotate exported procedures

Account component MASM must annotate every exported procedure with @account_procedure. Un-annotated procedures are not exported. This attribute is new in 0.16 — the protocol's own MASM went from zero uses to 99.

@auth_script and @note_script already existed in 0.15 and are unchanged.

Affected Code

# After (0.16)
@account_procedure
pub proc receive_asset(asset: word)
    # …
end

@auth_script
pub proc auth_tx(auth_args: word)
    # …
end

Migration Steps

  1. Add @account_procedure to every procedure your component intends to export.
  2. Re-check the resulting AccountCode procedure list — a missing annotation shows up as a procedure that silently is not callable, not as a compile error.

AccountCode::from_parts is now fallible

AccountCode::from_parts validated its procedure count with assert! in 0.15 and now returns a Result instead of panicking.

Affected Code

// Before (0.15)
pub fn from_parts(mast: Arc<MastForest>, procedures: Vec<AccountProcedureRoot>) -> Self

// After (0.16)
pub fn from_parts(
    mast: Arc<MastForest>,
    procedures: Vec<AccountProcedureRoot>,
) -> Result<Self, AccountError>

Migration Steps

Add ? or explicit error handling at every call site.


AccountId no longer converts into [Felt; 2]

The impl From<AccountId> for [Felt; 2] was removed. Use the prefix() and suffix() accessors instead. Conversions to [u8; 15] and u128 are unchanged.

Affected Code

// Before (0.15)
let felts: [Felt; 2] = account_id.into();
// After (0.16)
let prefix: AccountIdPrefix = account_id.prefix();
let suffix: Felt = account_id.suffix();

Migration Steps

Replace the into() conversion with the two accessors. Note prefix() returns an AccountIdPrefix, not a bare Felt.


Account updates move from AccountDelta to AccountPatch

Account updates moved from the relative AccountDelta to the absolute AccountPatch. ExecutedTransaction and AccountUpdateDetails now carry a patch, and Account::apply_delta was replaced by applying a patch.

AccountDelta still exists

This is not a wholesale removal. AccountDelta remains, and TransactionSummary::account_delta() deliberately still returns one — the signed transaction summary binds a relative delta. Only account update representation moved to the absolute patch model. The same split exists on the TypeScript side, where TransactionSummary.accountDelta() is unchanged while the result's accountDelta() became accountPatch().

Affected Code

// Before (0.15)
let delta = executed_tx.account_delta();
account.apply_delta(&delta)?;
// After (0.16)
let patch = executed_tx.account_patch();
account.apply_patch(&patch)?;

// Or build a fresh account from the patch instead of mutating one:
let account = Account::try_from(&patch)?;

apply_patch is the direct replacement for apply_delta and keeps the same in-place shape, so it is the smaller edit for existing code. The same rename applies further down: AssetVault::apply_delta became apply_patch, taking an AccountVaultPatch.

Migration Steps

  1. Replace account_delta() with account_patch() on ExecutedTransaction and on client transaction results.
  2. Replace Account::apply_delta(&delta) with Account::apply_patch(&patch), or construct a new account with Account::try_from(&patch).
  3. Leave TransactionSummary::account_delta() call sites alone — that one is intentionally still a delta.

Network accounts require a fee policy

AuthNetworkAccount::new now takes a FeePolicyManager alongside the allowed-note set. The manager carries the fee faucet and the active fee policy, and expands into the policy's components when the auth component is installed — so you do not install policy components separately.

This applies to network accounts and faucets, not ordinary accounts

Fee policies describe how an account that sponsors or charges fees estimates them. An ordinary user account paying a fee does not install one; it supplies fee conversion info per transaction instead. See Transaction Changes.

Affected Code

// After (0.16)
let manager = FeePolicyManager::builder()
    .fee_faucet_id(fee_faucet_id)
    .active_fee_policy(FeePolicy::from(BasicConstantFeePolicy::new()))
    .build();

let auth = AuthNetworkAccount::new(allowed_notes, manager)?;

AuthNetworkAccount no longer converts into a single AccountComponent — it expands into several, so install it through with_components.

Migration Steps

  1. Build a FeePolicyManager with the fee faucet and an active fee policy, registering any alternatives with allowed_fee_policy for runtime switching.
  2. Pass it to AuthNetworkAccount::new, which is fallible.
  3. Install the auth component with with_components, not with_component, since it expands to several components.

Common Errors

Error Message Cause Solution
no method named with_auth_component Removed Use with_component(s).
this function takes 1 argument but 2 were supplied AuthSingleSig::new takes an Approver Wrap in Approver::new.
cannot find type AuthMethod / AuthSingleSigAcl Removed See the auth section above.
expected Result, found AccountCode from_parts is fallible Add ?.
the trait From<AccountId> is not implemented for [Felt; 2] Conversion removed Use prefix() / suffix().
Account commitment differs from 0.15 for the same seed Component names changed Expected; re-record the new ID.
Component procedure is not callable but compiles Missing @account_procedure Annotate the procedure.

Note Changes

In 0.15 each standard note was a unit struct — pub struct P2idNote; — with a create associated function that took every parameter positionally and returned a finished Note. In 0.16 each is a real struct holding its fields, built through a bon builder and converted to a Note with Into.

The practical benefits are that optional parameters are now actually optional rather than positional, and that the intermediate typed value is inspectable before you convert it. The practical cost is that every call site changes.

The asset limit change is the one to watch, because it is a runtime error rather than a compile error, and it only triggers for notes carrying more than 16 assets.


Breaking Change

Every standard note changed shape. P2idNote, P2ideNote, SwapNote, MintNote, and BurnNote were marker types with a create(..) associated function returning a Note; they are now real structs built with a typed builder and converted with .into(). Separately, MAX_ASSETS_PER_NOTE dropped from 64 to 16, so any note packing more than 16 assets now fails to build.

Quick Fix

// Before (0.15)
let note = P2idNote::create(
    sender, target, vec![asset], NoteType::Public, attachments, &mut rng,
)?;

// After (0.16)
use miden_standards::note::P2idNote;

let note: Note = P2idNote::builder()
    .sender(sender)
    .target(target)
    .assets(vec![asset])
    .note_type(NoteType::Public)
    .generate_serial_number(&mut rng)
    .build()?
    .into();

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


Standard notes are built with typed builders

Each standard note struct now exposes builder() and converts into Note via From / Into.

Affected Code

// Before (0.15)
pub fn create<R: FeltRng>(
    sender: AccountId,
    target: AccountId,
    assets: Vec<Asset>,
    note_type: NoteType,
    attachments: NoteAttachments,
    rng: &mut R,
) -> Result<Note, NoteError>
// After (0.16)
let p2id = P2idNote::builder()
    .sender(sender)
    .target(target)
    .assets(vec![asset])          // or .asset(x), repeatable
    .note_type(NoteType::Public)
    .generate_serial_number(&mut rng)   // or .serial_number(word)
    .build()?;
let note: Note = p2id.into();

Note two naming details that are easy to get wrong: the setter is serial_number, not serial_num, and generate_serial_number(&mut rng) is the direct replacement for the old rng parameter.

P2ideNote takes its optional parameters as optional setters rather than positionally:

let p2ide = P2ideNote::builder()
    .sender(sender)
    .target(target)
    .assets(vec![asset])
    .note_type(NoteType::Private)
    .serial_number(serial_number)
    .reclaimer(reclaimer_account_id)        // optional; defaults to the sender
    .reclaim_height(BlockNumber::from(n))   // optional
    .timelock_height(BlockNumber::from(m))  // optional
    .build()?;

SwapNote follows the same pattern. In 0.15 SwapNote::create returned a (Note, NoteDetails) tuple carrying the payback details; in 0.16 you build the SwapNote and read its parts from the typed value.

The same builder treatment applies to MintNote, BurnNote, PswapNote, and TxFeeNote, along with the configuration notes (AllowlistConfigNote, OwnerConfigNote, FaucetMetadataConfigNote, NetworkAccountConfigNote, FaucetPolicyConfigNote, MinBurnAmountConfigNote).

Migration Steps

  1. Replace every XNote::create(..) call with the corresponding XNote::builder() chain, ending in .build()? and .into().
  2. Replace the trailing rng argument with .generate_serial_number(&mut rng).
  3. Use .assets(..) for a collection or .asset(..) repeatedly for individual assets.
  4. For P2ideNote, set only the optional parameters you actually need. reclaimer still defaults to the sender, so existing "sender can reclaim" behaviour is preserved without changes.

Common Errors

Error Message Cause Solution
no function or associated item named create Replaced by the builder Use XNote::builder().
no method named serial_num Setter renamed Use serial_number or generate_serial_number.
expected Note, found P2idNote The builder yields the typed note Add .into().
a P2ID note must contain at least one asset Built with no assets Add at least one asset.

MAX_ASSETS_PER_NOTE dropped from 64 to 16

The protocol limit on assets carried by a single note fell from 64 to 16.

Affected Code

- pub const MAX_ASSETS_PER_NOTE: usize = 64;
+ pub const MAX_ASSETS_PER_NOTE: usize = 16;

This is enforced by NoteAssets::new, so it surfaces as a NoteError at build time rather than a compile error.

Migration Steps

  1. Audit any code path that batches assets into a single note and cap it at 16.
  2. If you previously relied on packing up to 64 assets, split the payload across multiple notes.
  3. If you compute a batch size from the constant rather than hard-coding it, no change is needed beyond a rebuild.

MINT and BURN are unified across faucet kinds

One mint.masm and one burn.masm script now serve both fungible and non-fungible faucets, with the variant carried in the note storage. The script roots changed, so any hard-coded or cached root is now wrong.

Migration Steps

  1. Recompute and re-store any cached standard note script roots.
  2. Remove per-faucet-kind branching that selected between separate mint or burn scripts.

Other note changes

  • PswapNote (partial swap) gained a minimum-fill parameter, and its fields were renamed.
  • NoteFile was reworked and moved to miden-standards, with variants keyed on NoteId, ExpectedNote, and Committed. This mostly affects client code — see Client Changes.
  • NoteTag moved under miden::standards::note::note_tag in MASM. In the released 0.16.0-rc line it is still reachable at miden::standards::note_tag.

Common Errors

Error Message Cause Solution
no function or associated item named create Notes use builders now Rewrite with XNote::builder().
NoteError about exceeding asset limits Limit is now 16 Split across multiple notes.
Note script root mismatch for mint or burn Scripts were unified Recompute the roots.
expected Note, found MintNote Builder returns the typed note Add .into().

Assets, Vault & Faucet Changes

The rename reflects a conceptual correction. The per-asset vault key is the thing that actually identifies an asset, so it took the name AssetId; the faucet-level identifier it used to share a name with describes a class of assets, so it became AssetClass.

This is the most dangerous rename in the release precisely because it is not a removal. AssetId still exists after the upgrade, so code referring to it keeps compiling while silently meaning something different. Rename in the right order and let the compiler find the rest.

Several related types did not change

AssetAmount, AssetComposition, and AssetCallbackFlag all existed in 0.15 and are unchanged. AssetVault::get_balance already returned AssetAmount in 0.15 — only its parameter type was renamed. If you saw AssetAmount described as new, that applies to the client surface, not the protocol.


Breaking Change

The asset model was renamed one level down. What was AssetVaultKey is now AssetId, and what was AssetId is now AssetClass. Because the name AssetId survives with a different meaning, a careless search-and-replace will compile and be wrong — do the AssetIdAssetClass rename first. Separately, the single create_fungible_faucet factory split into six auth-specific factories.

Quick Fix

// Before (0.15)
let key: AssetVaultKey = asset.vault_key();
let key = AssetVaultKey::new_fungible(faucet_id, callback_flag);

// After (0.16)
let id: AssetId = asset.id();
let id = AssetId::new_fungible(faucet_id);   // callback flag now lives on the AccountId

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


AssetVaultKeyAssetId, and AssetIdAssetClass

The vault key type was renamed to AssetId, the previous AssetId became AssetClass, and Asset::vault_key() became Asset::id(). AssetIdHash is the corresponding hash type.

Affected Code

// Before (0.15)
let key: AssetVaultKey = asset.vault_key();
let balance: AssetAmount = vault.get_balance(vault_key)?;
let key = AssetVaultKey::new_fungible(faucet_id, callback_flag);
// After (0.16)
let id: AssetId = asset.id();
let balance: AssetAmount = vault.get_balance(asset_id)?;
let id = AssetId::new_fungible(faucet_id);
// or, fully explicit:
let id = AssetId::new(asset_class, faucet_id, composition);

Note that AssetId::new_fungible no longer takes a callback flag. Whether a faucet's assets trigger callbacks is encoded in the account ID itself, set at construction time via AccountBuilder::with_asset_callbacks.

Asset itself is unchanged in shape — still an enum with Fungible and NonFungible variants — and FungibleAsset::new(faucet_id, amount) keeps its signature.

Migration Steps

  1. Rename AssetIdAssetClass first, throughout your codebase.
  2. Then rename AssetVaultKeyAssetId.
  3. Replace asset.vault_key() with asset.id().
  4. Drop the callback-flag argument from new_fungible calls; set it on the account instead with with_asset_callbacks.
  5. Re-index any persisted vault or asset data. Serialized asset identifiers are not compatible across the rename.

Common Errors

Error Message Cause Solution
cannot find type AssetVaultKey Renamed Use AssetId.
no method named vault_key Renamed Use id().
this function takes 1 argument but 2 were supplied on new_fungible Callback flag removed Drop it; set with_asset_callbacks on the account.
Type mismatch where AssetId used to work AssetId now means the vault key The old meaning is AssetClass.

Faucet factories split by authentication scheme

create_fungible_faucet took an AuthMethod and an AccessControl argument and dispatched internally. Since AuthMethod was removed (see Account Changes), the factory split into one function per authentication scheme, each taking a concrete auth component.

Affected Code

// Before (0.15)
pub fn create_fungible_faucet(
    init_seed: [u8; 32],
    faucet: FungibleFaucet,
    account_type: AccountType,
    auth_method: AuthMethod,
    access_control: AccessControl,
    token_policy_manager: TokenPolicyManager,
) -> Result<Account, FungibleFaucetError>
// After (0.16)
pub fn create_singlesig_user_fungible_faucet(
    init_seed: [u8; 32],
    faucet: FungibleFaucet,
    auth_component: AuthSingleSig,
    token_policy_manager: TokenPolicyManager,
    account_type: AccountType,
) -> Result<Account, FungibleFaucetError>

Note that the parameter order changed as well as the parameter list — account_type moved to the end.

The full set of factories:

Faucet kind v0.16 factory
Fungible, single signature create_singlesig_user_fungible_faucet
Fungible, multisig create_multisig_user_fungible_faucet
Fungible, guarded multisig create_guarded_user_fungible_faucet
Fungible, network account create_network_fungible_faucet
Non-fungible, user account create_user_non_fungible_faucet
Non-fungible, network account create_network_non_fungible_faucet

Non-fungible faucet factories are new in this release; 0.15 shipped only the fungible factory.

Migration Steps

  1. Choose the factory matching your authentication scheme and pass a concrete auth component instead of an AuthMethod.
  2. Drop the access_control argument. The factories install Authority::AuthControlled and the pausable components for you.
  3. Check the argument order — account_type is now last.
  4. Expect a different account ID for a faucet rebuilt from the same seed, since the component set and names changed.

Common Errors

Error Message Cause Solution
cannot find function create_fungible_faucet Split into per-scheme factories Use the matching factory from the table.
cannot find type AuthMethod Removed Pass a concrete auth component.
Arguments of the wrong type Parameter order changed account_type moved to the end.

Common Errors

Error Message Cause Solution
cannot find type AssetVaultKey Renamed to AssetId Rename, after renaming old AssetId to AssetClass.
Silent behaviour change around asset identity AssetId kept its name with a new meaning Audit every AssetId reference.
Persisted vault lookups miss after upgrading Asset identifier serialization changed Re-index persisted vault data.
cannot find function create_fungible_faucet Factories split Use the auth-specific factory.

Transaction Changes

The fee change is the largest behavioural change in the release, and it is easy to under-estimate because it does not necessarily break your build. If you target a chain that charges no fee, nothing changes. If you target a chain that does, transactions that used to succeed now fail unless the request declares fee conversion info.

The reason for the move is visible in the standard auth component's own documentation: paying the fee before the transaction summary is created means the fee note and the vault withdrawal funding it are covered by the signature. Under the old model the kernel deducted the fee outside anything the user signed.

The sealing change is a hard compatibility boundary rather than an API change — it mostly costs you a coordinated upgrade rather than a code edit.


Breaking Change

Transaction fees moved out of the kernel epilogue and into the authentication procedure. On a chain with a non-zero verification_base_fee, transactions signed by AuthSingleSig or AuthMultisig must commit fee conversion info through the transaction's auth args, and the paying account must hold the fee asset. Separately, transaction inputs are now sealed (encrypted) before submission, so a 0.16 client cannot submit to a 0.15 node or vice versa.

Quick Fix

// After (0.16) — declare how the fee is paid
use miden_client::account::component::FeeConversionInfo;

let info = FeeConversionInfo::one_to_one(fee_faucet_id);
let request = TransactionRequestBuilder::new()
    .fee_conversion_info(info, salt)   // salt: Word
    .build()?;

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


Transaction fees are paid by the auth procedure

In 0.15 the kernel computed and burned the fee from the native account's vault automatically. In 0.16 the auth procedure reads a FeeConversionInfo blob out of the auth args, computes the fee, and emits a TX_FEE note to the fee faucet.

The standard auth components already do this for you — AuthSingleSig's MASM calls fee::load_conversion_info and fee::pay_fee before authenticating. What you must supply is the auth args.

Affected Code

// Before (0.15)
// Nothing to declare: the kernel handled the fee.
let request = TransactionRequestBuilder::new().build()?;
let fee = executed_tx.fee();
// After (0.16)
use miden_client::account::component::FeeConversionInfo;

// Pay in the chain's native fee asset at rate 1/1:
let info = FeeConversionInfo::one_to_one(fee_faucet_id);
// or specify an explicit conversion rate:
let info = FeeConversionInfo::new(fee_faucet_id, rate_num, rate_den)?;

let request = TransactionRequestBuilder::new()
    .fee_conversion_info(info, salt)
    .build()?;

The exact signatures:

FeeConversionInfo::new(faucet_id: AccountId, rate_num: u64, rate_den: u64) -> Result<Self, NoteError>
FeeConversionInfo::one_to_one(faucet_id: AccountId) -> Self
commit_fee_conversion_info(conversion_info: FeeConversionInfo, salt: Word) -> (Word, Vec<Felt>)

// on the client builder:
pub fn fee_conversion_info(self, conversion_info: FeeConversionInfo, salt: Word) -> Self

The salt argument is mandatory and undocumented upstream

fee_conversion_info takes a second salt: Word parameter that the changelog does not mention. Code written from the changelog alone will not compile.

FeeConversionInfo is reachable at miden_client::account::component::FeeConversionInfo — it is not exported from miden_client::auth, where you would naturally look first. Adding a direct miden-standards dependency also works.

When it is required, and when it is rejected

Only auth components that actually read the auth args honour the conversion info. The client validates this before execution rather than silently paying in the native asset:

// miden_client::transaction::TransactionRequestError
FeeConversionInfoUnsupported(String)
// "the request declares fee conversion info but the account's auth component {0} does not read it"

The check passes only for AuthSingleSig and AuthMultisig. Declaring fee conversion info for any other auth component — NoAuth, for example — is rejected. If the request does not declare fee conversion info at all, the check is skipped, so existing code that never calls the builder method is unaffected by this validation.

Because fee_conversion_info consumes the auth arg, it conflicts with a manually set auth_arg — whichever is applied last wins.

Migration Steps

  1. On a chain with a non-zero verification_base_fee, call fee_conversion_info(info, salt) on every request signed by an AuthSingleSig or AuthMultisig account. Use FeeConversionInfo::one_to_one(fee_faucet_id) for the native fee asset.
  2. Ensure the paying account holds a balance of the fee asset — the auth procedure debits it.
  3. Do not call fee_conversion_info for accounts using any other auth component.
  4. If you set auth_arg manually, pick one or the other.
  5. Replace executed_tx.fee() with inspection of the TX_FEE output note. ExecutedTransaction::compute_fee() still exists but only under the testing feature — do not use it in production.
  6. If you wrote a custom auth component in MASM, it must now call miden::standards::fee::load_conversion_info followed by miden::standards::fee::pay_fee, or the transaction will fail fee validation.
  7. On a zero-fee chain, no change is required.

Common Errors

Error Message Cause Solution
this function takes 2 arguments but 1 was supplied The salt parameter Pass a Word salt.
FeeConversionInfoUnsupported Auth component does not read auth args Only declare it for AuthSingleSig / AuthMultisig.
cannot find FeeConversionInfo in miden_client::auth Exported elsewhere Use miden_client::account::component::FeeConversionInfo.
Transaction aborts on a fee-charging chain No fee conversion info declared, or no fee asset balance Declare the info and fund the account.
Custom auth component transaction fails fee validation MASM does not pay the fee Call fee::load_conversion_info then fee::pay_fee.

Transaction inputs are sealed before submission

Transaction inputs are encrypted ("sealed") before being submitted. The RPC layer gained a get_transaction_encryption_key method plus a miden_client::rpc::encryption module.

This is a hard compatibility boundary. A 0.16 node rejects plaintext submissions and an older node rejects sealed ones, so the client and node must be upgraded together.

Most applications do not change any code here

Client::submit_proven_transaction keeps its 0.15 signature exactly — it still takes impl Into<TransactionInputs>, and sealing happens beneath it. Only the NodeRpcClient trait methods changed to take SealedTransactionInputs, so this is a source-breaking change solely for code that implements that trait.

The requirement this does impose on every application is a sync before submitting: sealing resolves an encryption key against the chain state, so a client that has not synced the genesis and chain-tip headers fails with ClientError::ChainValidationError.

Migration Steps

  1. Upgrade your node and client together. There is no configuration that makes a 0.16 client talk to a 0.15 node.
  2. Ensure the client has synced before submitting, or key resolution fails with ChainValidationError.
  3. If you implement NodeRpcClient yourself, update submit_proven_transaction and submit_proven_batch to take SealedTransactionInputs, and add get_transaction_encryption_key.

Key types are not re-exported from the encryption module

The changelog states that miden_client::rpc::encryption re-exports the validator DSA key types. It does not — that module contains no pub use statements. Reach them via miden_client::crypto::{ecdsa_k256_keccak, eddsa_25519_sha512}.


TransactionSummary binds the reference block, expiration, and user params

TransactionSummary::new replaced its single salt parameter with a block commitment, an expiration delta, and a structured user-parameters value.

Affected Code

// Before (0.15)
pub fn new(
    account_delta: AccountDelta,
    input_notes: InputNotes<InputNote>,
    output_notes: RawOutputNotes,
    salt: Word,
) -> Self
// After (0.16)
pub fn new(
    account_delta: AccountDelta,
    input_notes: InputNotes<InputNote>,
    output_notes: RawOutputNotes,
    block_commitment: Word,
    expiration_delta: u16,
    user_params: TransactionSummaryUserParams,
) -> Self

TransactionSummaryUserParams carries seven field elements. On the TypeScript side the corresponding accessor renamed from TransactionSummary.salt() to TransactionSummary.userParams().

The reference block commitment is included because it determines the fee parameters, and therefore the fee amount deducted; the expiration delta is included so the signature covers it.

TransactionSummary still uses AccountDelta

Note the first parameter. While account updates moved to the absolute AccountPatch model (see Account Changes), the signed transaction summary deliberately still binds a relative AccountDelta. Do not rewrite these call sites.

Migration Steps

  1. Replace the salt argument with the block commitment, expiration delta, and user params.
  2. In TypeScript, replace summary.salt() with summary.userParams().

Other transaction changes

  • Proving is synchronous; execution stays asynchronous. Adjust any code that awaited the proving step.
  • ExecutedTransaction::account_delta() became account_patch(), matching the account update model. See Account Changes.
  • ExecutedTransaction::compute_fee() is gated behind the testing feature. Production fee figures come from the TX_FEE note.

Common Errors

Error Message Cause Solution
Node rejects a submitted transaction Client and node versions mixed Upgrade both to 0.16.
this function takes 6 arguments but 4 were supplied TransactionSummary::new changed Pass block commitment, expiration delta, and user params.
no method named salt on a summary Renamed Use userParams().
no method named fee on an executed transaction Fees now flow through the TX_FEE note Inspect the output note.
no method named account_delta on an executed transaction Renamed Use account_patch().

Client Changes

The client changes fall into four groups. The store break is the one that costs users data, and it is unavoidable. The fee and sealing changes are covered in Transaction Changes — they surface here as a new builder method and a node-version requirement. The rename churn (account_deltaaccount_patch, sendtransfer, and friends) is mechanical. And a handful of silent behavioural changes — the call argument counting, the transaction summary display, notes.sendPrivate requiring a scan height — will not fail your build but will change what your application does.


Your local store must be recreated

Every pre-0.16 SQLite store is rejected. There is no migration path: delete the database and re-sync. Browser applications are handled automatically — the IndexedDB store detects the version bump and wipes itself on first open. In both cases any state that existed only locally is lost, including records for accounts not yet committed on-chain.

Client and node must be upgraded together

0.16 clients seal (encrypt) transaction inputs before submission. A 0.16 node rejects plaintext submissions and an older node rejects sealed ones, so the two cannot be mixed. Upgrade both.

Quick Fix

# CLI: the send subcommand was renamed
miden-client transfer -t <TARGET> -a 100::<FAUCET> -n private
// Rust: account updates are absolute patches now
let patch = tx_result.account_patch();
// Web: same split on the TypeScript side
const patch = txResult.accountPatch();

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


(Store) Every pre-0.16 SQLite store must be recreated

The store schema changed substantially: account SMT forest tables were added, account IDs and all digest columns were retyped from hex TEXT to BLOB, a script_root index was added, and the migrations table was dropped in favour of a schema fingerprint.

Affected Code

A store written by miden-client 0.15.5 fails to open with:

Migration error: Attempt to migrate a database with a migration number that is too high

This is not the error the changelog names

The changelog says opening a pre-0.16 database fails with SchemaHashMismatch. In practice a 0.15.5 store sits at user_version = 2, and because 0.16 defines only one migration the fingerprint check is skipped entirely — the failure surfaces from the migration layer instead. SchemaHashMismatch is only reached by a store at user_version = 1. Both paths fail; only the message differs.

Beyond the account ID retyping, the schema diff also shows the latest_account_assets and historical_account_assets column vault_key renamed to asset_id (following the protocol rename), a new unique index on tags(tag, source), and all digest columns retyped to BLOBaccount_commitment, note_id, nullifier, script_root, recipient_digest, and storage keys and values.

Migration Steps

  1. Delete the store database and let the client recreate it, then re-sync.
  2. Export anything you need to keep before upgrading — private note files in particular.
  3. Browser applications need no action; the IndexedDB store resets itself when the client's minor version increases.
  4. If you implement a custom Store, note that insert_block_header now takes a nodes argument, insert_partial_blockchain_nodes was removed, and the new NoteFilter::ScriptRoots variant makes existing exhaustive matches fail to compile.

(Rust) Account updates use AccountPatch

TransactionResult::account_delta() became account_patch(), and Account::apply_delta was replaced by construction from a patch. TransactionSummary::account_delta() is deliberately unchanged. This is covered in full under Account Changes.

One import detail specific to the client: in 0.15 AccountStorageDelta lived in miden_client::asset; the 0.16 replacement AccountStoragePatch lives in miden_client::account. The module moved as well as the name. StorageMapDelta and StorageSlotDelta were dropped from miden_client::asset alongside it, while AccountVaultDelta remains there.


(Rust) Fee conversion info on the transaction request

TransactionRequestBuilder::fee_conversion_info(info, salt) is new and required on fee-charging chains for AuthSingleSig and AuthMultisig accounts. See Transaction Changes for the full flow, including the mandatory salt argument that the changelog omits.


(Rust) Fungible amounts use AssetAmount

The client surface switched from raw u64 to AssetAmount for fungible amounts. AccountReader::get_balance returns AssetAmount, and the token conversion helpers (tokens_to_base_units, base_units_to_tokens) and build_pswap_consume follow.

Migration Steps

  1. Wrap raw amounts with AssetAmount, or unwrap with the provided accessor where you need a u64.
  2. Handle TokenParseError::InvalidAmount where you parse user-supplied amounts.

(Rust) Auth and faucet re-exports changed

AuthMethod and AuthSingleSigAcl were removed, and the single fungible faucet factory split into auth-specific factories — see Assets, Vault & Faucet Changes. Note that the client re-exports only two of the six upstream factory functions; for the rest, depend on miden-standards directly.

The account policy components were also renamed, a change absent from the changelog and found by diffing the re-export lists:

- AllowlistOwnerControlled
+ AllowlistManager
- BlocklistOwnerControlled
+ BlocklistManager

(Rust) Note screening methods renamed

NoteScreener::can_consume became get_consumability, and can_consume_batch became get_batch_consumability. A new get_batch_consumability_for_account was added. Client::get_consumable_notes keeps its signature — passing a single account is now screened more efficiently, but nothing about the call changes.

The rename is cosmetic

The changelog justifies it by saying the methods now return a consumption status per account rather than a boolean. They never returned a boolean — the return type is identical in 0.15 and 0.16. Rename the call sites; do not change how you handle the result.


(Rust) Debug mode removed

DebugMode, ClientBuilder::in_debug_mode, Client::in_debug_mode, and the MIDEN_DEBUG environment variable were all removed. The VM replaced the flag-gated debug.* decorators with miden::core::debug procedures that print unconditionally, so there is nothing left to gate. See MASM Changes.


(Rust) Other library changes

  • StateSyncUpdate is immutable — construct with from_parts, read through accessors, and destructure with into_parts. PartialBlockchainUpdates::insert lost its nodes argument, and extend_authentication_nodes was added.
  • miden_client::assembly::Library was removed. Use miden_client::vm::Package. Note that Package is not new — it was already re-exported in 0.15; only the Library removal is a 0.16 change.
  • Client::fetch_all_private_notes was removed, replaced by note transport syncing.
  • TransactionRecord gained a private field, so struct literal construction no longer compiles.
  • send_notes reads its payload from the advice provider and requires a payload-commitment script argument. A script_arg passed alongside a SendNotes template is ignored.
  • AccountSmtForest is generic over its backend, and the root-staging API was removed.
  • Response verification moved into VerifyingRpcClient. The built-in gRPC constructors now wrap the transport in it automatically, but ClientBuilder::rpc does not — passing your own NodeRpcClient compiles and runs while silently losing response verification. Wrap it yourself with VerifyingRpcClient::new(..).

(Web) Package and API changes

Bump @miden-sdk/miden-sdk and @miden-sdk/react together — mixing 0.15 and 0.16 packages will not link against the shared WASM ABI.

Change Migration
ClientOptions.debugMode removed; createClient* drops the trailing debugMode argument Delete the option and the argument.
accountDelta()accountPatch(); AccountStorageDelta removed Rename. TransactionSummary.accountDelta() is unchanged.
TransactionSummary.salt()userParams() Rename; the value is now seven field elements.
transactions.preview(..) returns only a summary while authorization is pending Do not expect full transaction details from a preview.
notes.sendPrivate requires scanAfterBlockNum; new notes.sendPrivateOutput Pass a scan height.
notes.fetchPrivate({ mode: "all" }) removed Use note transport syncing.
AccountComponent.createNetworkAuthcreateNetworkAuthComponents Rename; it now returns several components.
FungibleAsset.withCallbacks(flag) removed Set callbacks on the account at construction.
P2ID and P2IDE notes must carry at least one asset Building an empty note now throws.
Production WASM strips MASM debug metadata Expect less detail in production stack traces.
Notes carrying a NetworkAccountTarget are priced via a foreign procedure invocation into the target Behavioural; see the note below.

Additive: notes.list({ scriptRoots }), NoteScript.networkAccountConfig(), NoteScript.feeSponsorship(), and compile.component({ namespace }).

If you author MASM through the Web SDK, the language changes apply to you as well — @account_procedure annotations, mod declarations, and the new import syntax. See MASM Changes.

Unverified

The NetworkAccountTarget foreign-procedure-invocation requirement is reported from the changelog. We were not able to locate the enforcing call site in source, so treat it as a lead rather than a confirmed behaviour.


(React) Send hooks relay through sendPrivateOutputNote

useSend, useTransaction, and useMultiSend now relay private note output via sendPrivateOutputNote, following the notes.sendPrivate change above. If you wrapped these hooks, re-check the relay path.


(CLI) send renamed to transfer

The send subcommand is now transfer. Nothing else changed — every flag, short form, and default is identical. send is not kept as an alias, so existing scripts fail with an unknown-subcommand error.

Affected Code

# Before (0.15)
miden-client send -s <SENDER> -t <TARGET> -a 100::<FAUCET> -n private

# After (0.16)
miden-client transfer -s <SENDER> -t <TARGET> -a 100::<FAUCET> -n private

Migration Steps

Replace miden-client send with miden-client transfer in scripts, aliases, and CI jobs. Change nothing else.


(CLI) account --with-code replaced by account --inspect

--with-code, which dumped the account code as one pretty-printed blob, is gone. account --inspect <ID>[:<PROCEDURE>] lists the procedures an account exposes, split into resolved procedures (name, signature, originating package) and unresolved ones listed by MAST root.

Affected Code

# Before (0.15)
miden-client account --show <ID> --with-code

# After (0.16)
miden-client account --inspect <ID>                     # list procedures
miden-client account --inspect <ID> --verbose           # with MASM disassembly
miden-client account --inspect <ID>:receive_asset       # a single procedure
miden-client account --inspect <ID> -p ./component.masp # resolve names from extra packages

Migration Steps

  1. Replace account --show <ID> --with-code with account --inspect <ID> --verbose.
  2. --inspect is mutually exclusive with --list, --show, and --default.
  3. --package and --verbose both require --inspect.
  4. Expect <unresolved> entries for procedures whose package the CLI cannot find; pass --package to resolve them.

(CLI) call counts arguments in field elements

call validates argument count against the procedure's signature. In 0.15 it compared against the number of parameters; in 0.16 it compares against the total stack width in field elements. A procedure taking one Word now needs four --args values.

This change is not in the changelog.

Affected Code

# A procedure with signature `set_item(Word) -> ()`

# Before (0.15): one parameter, one argument
miden-client call <ID>:set_item -p component.masp --args 0x1234

# After (0.16): a Word is four felts wide
miden-client call <ID>:set_item -p component.masp --args <f0> <f1> <f2> <f3>

Migration Steps

  1. Re-check every scripted call whose procedure takes or returns anything wider than one field element.
  2. Expand each wide argument into one value per field element, in signature order.
  3. Read the Raw Signature: line the command prints — it is now the authoritative stack layout.

A mismatched count fails with a clear error rather than executing with a mis-shaped stack, so this one fails loudly.


(CLI) token_symbol_map.toml: id renamed to address

The per-symbol entry key changed from id to address. The value format is unchanged — it was already a bech32 address — so this is a pure key rename. A file still using id fails to parse rather than falling back.

Affected Code

# Before (0.15)
BTC = { id = "mlcl1qru2e5yvx40ndgqqqzusrryr0ucyd0uj", decimals = 8 }

# After (0.16)
BTC = { address = "mlcl1qru2e5yvx40ndgqqqzusrryr0ucyd0uj", decimals = 8 }

Migration Steps

  1. Rename id = to address = on every entry. Leave the values alone.
  2. The file lives in the .miden directory alongside miden-client.toml. If you have both a local and a global .miden directory, update both.

(CLI) init writes a different package set

init now writes nine bundled .masp component packages instead of seven.

# Added in 0.16
basic-non-fungible-faucet.masp
auth/guarded-multisig-auth.masp
auth/network-account-auth.masp

# Removed in 0.16
auth/acl-auth.masp

The removal is the CLI-side consequence of dropping AuthSingleSigAcl, and it is the one most likely to break an existing setup. The changelog mentions only the additions.

Two error-reporting changes also landed: running init where a config already exists now names the configured network and points at clear-config, and an unparseable --remote-prover-endpoint is a hard error instead of being silently discarded.


(CLI) Other changes

  • --debug and MIDEN_DEBUG removed. Passing --debug is now a usage error; setting MIDEN_DEBUG is silently ignored.
  • The pre-confirmation transaction summary shows absolute values, not deltas — including a column rename and Nonce incremented by: N becoming New account nonce: N. This follows from the AccountPatch move but changes what users read before approving a transaction.
  • swap gained --payback-note-type <private|public>, defaulting to private (0.15 hardcoded private). Note that pswap already had this flag in 0.15 with the same default. The tag the command tells you to track also changed, from a swap-specific tag to an account-target tag derived from the sender's account ID.
  • consume-notes gained --start-debug-adapter <ADDR> and --record <FILE>; exec gained --record. exec --start-debug-adapter already existed in 0.15. Both require a build with the dap feature, which is not enabled by default.

Common Errors

Error Message Cause Solution
Migration error: Attempt to migrate a database with a migration number that is too high Pre-0.16 store Delete and recreate the store.
error: unrecognized subcommand 'send' Renamed Use transfer.
error: unexpected argument '--with-code' Removed Use --inspect.
Procedure '<name>' expects 4 value(s), got 1 Arguments counted in field elements Expand wide arguments.
missing field 'address' parsing the token map Key renamed Rename id to address.
error: unexpected argument '--debug' Removed Delete the flag.
no method named account_delta on a transaction result Renamed Use account_patch().
Node rejects a submission Mixed client and node versions Upgrade both to 0.16.
A component package is missing after init auth/acl-auth.masp was removed Migrate off AuthSingleSigAcl.

MASM Changes

The largest change is structural rather than syntactic. In 0.15 the assembler discovered modules by walking directories; in 0.16 it follows an explicit tree of mod declarations rooted at your project's root module. This is why the assembler's directory-based entry points disappeared (see VM & Assembler Changes) and why miden-project.toml now requires an explicit path to that root.

The failure mode is worth internalising: forgetting a mod declaration is not an error at the declaration site. The module simply is not part of the artifact, and you discover it later as an undefined-symbol error at the call site — or, worse, not at all if nothing calls it.

Everything else on this page is mechanical: import rewrites, decorator replacements, and renamed protocol procedures.


Breaking Change

Miden Assembly gained an explicit module tree. A .masm file is no longer picked up because it sits in the right directory — its parent must declare it with mod or pub mod, and an undeclared file is silently dropped from the artifact rather than silently included. The use form was split into module imports and braced item imports, alias syntax changed from -> to as, and the debug.* and trace decorators were removed. On the protocol side, asset helpers, note creation, and several account procedures moved to new paths.

Quick Fix

# Before (0.15)
use miden::standards::wallets::basic->basic_wallet
pub use miden::core::stark::verifier

# After (0.16)
use miden::standards::wallets::basic as basic_wallet
pub mod verifier
pub use {verify} from self::verifier
# Every directory of .masm files now needs a mod.masm declaring its children
pub mod account
pub mod asset
mod callbacks     # private to the parent

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


Every module must be declared with mod / pub mod

A submodule's source is resolved as either <dir>/<name>.masm or <dir>/<name>/mod.masm, relative to the declaring module's directory. The assembler includes only modules reachable through these declarations (#3220).

Affected Code

In 0.15 the protocol's kernel root module was a comment; the directory tree was walked implicitly. In 0.16 it enumerates its children, and every intermediate directory gained its own mod.masm — the protocol repo went from 9 mod.masm files to 35:

# After (0.16) — kernels/transaction-core/src/mod.masm
pub mod account
pub mod account_update
pub mod asset
pub mod asset_vault
mod callbacks           # private: not reachable from outside this module
pub mod constants
pub mod epilogue
# … one line per child module

Declarations may be interleaved with use statements and appear anywhere among the top-level forms. Two other top-level forms landed alongside mod: an optional namespace <path> declaration that names the module explicitly, and extern package "<name>@<version>".

# After (0.16) — the full top-level form vocabulary
namespace app::accounts
extern package "miden/base@0.1.0"
mod internal
pub mod api

Migration Steps

  1. For every directory of .masm files, add a mod.masm (or a sibling <dir>.masm) that declares each child with pub mod <name>. Use plain mod <name> for modules that should not be reachable from outside the parent.
  2. Walk your project root downward and confirm every .masm file is reachable from the root through a chain of declarations.
  3. Do not declare a submodule with the same name as its parent, and do not declare the same source file from two different parents — both are hard errors.

Common Errors

Error Message Cause Solution
invalid submodule declaration '<name>': could not find module sources at '<dir>/<name>.masm' or '<dir>/<name>/mod.masm' mod <name> with no matching file Create the file or remove the declaration.
invalid submodule declaration '<name>': submodules must not have the same name as their parent e.g. mod foo inside foo/mod.masm Rename the child.
conflicting submodule paths detected: '<name>' can be parsed from either '<a>' and '<b>', but not both Both <name>.masm and <name>/mod.masm exist Delete one.
invalid submodule declaration '<name>': module source '<uri>' is already reachable through another submodule declaration Two parents declare the same file Declare it once.
undefined item '<path>' on a call that used to work The callee's module is not declared Add the missing mod declaration.

Import syntax: item imports, as aliases, and global resolution

The use form was split into two explicitly distinguished shapes, and import resolution became strictly global (#3220):

  • Module importuse some::module or use some::module as alias. Brings a module into scope under a local name. May not be pub.
  • Item importuse {item} from some::module or use {a, b as c} from some::module. Brings individual procedures, constants, or types into scope. May be pub, which is how you re-export.

Four consequences follow:

  1. pub use <path> for re-exporting a module is gone. pub use is valid only in the braced item form, so you can no longer re-export a module, only named items.
  2. The alias separator changed from -> to as.
  3. An import path may no longer begin with another import's alias — imports resolve in the global namespace, as if every path were absolute.
  4. Submodule-relative imports need an explicit self:: prefix.

Source-level digest imports (use 0x<digest>->name) were removed. Direct digest invocation targets (exec.0x…) still work.

Affected Code

The alias change, from the protocol's own P2IDE note script:

- use miden::standards::wallets::basic->basic_wallet
+ use miden::standards::wallets::basic as basic_wallet

Re-exporting a procedure from another package:

- pub use ::miden::utils::panic
+ pub use {panic} from ::miden::utils

Re-exporting from your own submodule, from the core library's stark/mod.masm:

# Before (0.15)
use miden::core::stark::verifier
pub use verifier::verify
# After (0.16)
pub mod verifier
pub use {verify} from self::verifier

Note both halves of that change: verifier is now a declared submodule, and the re-export path is self::verifier rather than the bare alias. In 0.15 the second use resolved verifier through the first — that chaining is exactly what was removed.

Plain module imports are unchanged and remain the common case:

# Identical in 0.15 and 0.16
use miden::core::crypto::hashes::poseidon2
use miden::protocol::active_note

Migration Steps

  1. Rewrite every pub use a::b::c re-export as pub use {c} from a::b.
  2. Replace every use path->alias with use path as alias.
  3. Rewrite any use whose path begins with an alias introduced by an earlier use in the same file to use the full global path.
  4. To import from a submodule of the current module, prefix with self::. You cannot use a submodule you declared yourself — it is already in scope via mod, so reference it by name.
  5. Delete any use 0x<digest>->name source-level digest imports.

Common Errors

Error Message Cause Solution
`pub use` is only supported for braced item imports pub use some::module Use pub use {item} from some::module.
import aliases use `as`; `->` is no longer supported use foo->bar use foo as bar.
import target '<path>' cannot be resolved through import '<alias>' Path starts with another import's alias Use the full global path.
cannot import submodule '<path>' declared in the same module use of your own mod-declared child Drop the use.
item import target '<path>' resolved to a module use {x} from … where x is a module Use the module-import form.
digest imports are not supported use 0x1234->entry Remove it; use exec.0x… directly.

debug.* and trace decorators removed

The debug.* decorator family and the trace decorator were removed from the language, along with the CLI --trace flag and the decorator wire slots in the MAST format. Print-style debugging now goes through the new miden::core::debug module, whose procedures are ordinary emit events handled host-side (#3169, #3201, #3208).

These print in production

Because they are events rather than decorators, they carry no MAST cost — but unlike debug.*, which only fired when the VM ran in debug mode, they print whenever invoked. Leaving one in production code will print, and will disclose private values if your program has moved witness data onto the stack or into memory.

Affected Code

v0.15 decorator v0.16 replacement
debug.stack exec.debug::print_stack
debug.stack.<n> exec.debug::print_stack (prints the whole stack; there is no top-n form)
debug.mem exec.debug::print_mem_all
debug.mem.<n> push.<n> exec.debug::print_mem_addr
debug.mem.<n>.<m> push.<m> push.<n> exec.debug::print_mem — takes [start, end], end-exclusive
debug.local, debug.local.<n>, debug.local.<n>.<m> locaddr.<n> exec.debug::print_mem_addr
debug.adv_stack.<n> push.<n> push.0 exec.debug::print_adv_stack, or exec.debug::print_adv_stack_all
trace.<n> Removed with no replacement.
# After (0.16)
use miden::core::debug

begin
    exec.debug::print_stack                 # []                -> []
    exec.debug::print_mem_all               # []                -> []
    push.16 push.0 exec.debug::print_mem    # [start=0, end=16] -> []
    locaddr.0 exec.debug::print_mem_addr    # [addr]            -> []
    exec.debug::print_adv_stack_all         # []                -> []
    exec.debug::print_adv_map_all           # []                -> []
    exec.debug::print_adv_map_item          # [KEY]             -> []   (consumes the key)
end

The full export list of miden::core::debug is print_stack, print_mem, print_mem_addr, print_mem_all, print_adv_stack, print_adv_stack_all, print_adv_map_all, and print_adv_map_item.

On the Rust side, DebugOptions, Instruction::Debug(..), and Instruction::Trace(..) no longer exist.

Migration Steps

  1. Search your MASM for debug. and trace. and replace per the table. Remember print_mem takes [start, end] with end exclusive, and both operands are consumed.
  2. Add use miden::core::debug to any module that now calls these.
  3. Remove --trace from any miden-vm invocation.
  4. Register the handlers. CoreLibrary::handlers() includes the stack and memory debug handlers by default; the advice handlers are opt-in, so extend the handler set with miden_core_lib::handlers::debug::advice_debug_handlers to enable print_adv_stack* and print_adv_map*.
  5. Strip these calls from production code.

Core library: the miden::precompiles namespace, and removals

The core MASM package was split into miden::core and a new miden::precompiles namespace (#3459, #3222). Some procedures that used to live under miden::core::crypto are now internal precompile support under miden::precompiles.

miden::core::crypto::hashes::keccak256 still exists and still exports hash_bytes, hash, and merge — it now delegates to miden::precompiles::hashes::keccak256. Application code should keep calling the miden::core::… facade; reach for miden::precompiles::* only if you are writing your own precompile wrapper.

Several modules and procedures were removed outright. EdDSA and SHA-512 are documented as temporarily removed pending precompiles-prover support.

Removed in v0.16 Replacement
miden::core::crypto::dsa::eddsa_ed25519 (whole module) None in this line.
miden::core::crypto::hashes::sha512 (whole module) None in this line.
miden::core::crypto::dsa::ecdsa_k256_keccak::verify_prehash verify, or the new verify_bytes.
miden::core::sys::log_precompile_request miden::core::sys::build_proof_request_key — a different operation, not a rename.
miden::core::pcs::fri::frie2f4::preprocess Test-only helper; no replacement.

Additions in the same area: ecdsa_k256_keccak::verify_bytes, for verifying a signature over a variable-length Keccak256 message held in VM memory (#3563), and miden::core::math::u256 reaching parity with the u64 and u128 modules (#3167).

Migration Steps

  1. If you verify Ed25519 signatures or hash with SHA-512 in MASM, there is no in-VM path in 0.16. Move that work off-chain or defer the upgrade.
  2. Replace ecdsa_k256_keccak::verify_prehash with verify (word-sized message) or verify_bytes (variable-length message in memory) — and see the ABI change below, which you need either way.
  3. If you wrote a custom precompile wrapper against sys::log_precompile_request, rewrite it against the deferred-DAG helpers in miden::precompiles.

ECDSA advice and signature ABI changed

The advice-stack layout consumed by miden::core::crypto::dsa::ecdsa_k256_keccak::verify changed from PK[9] | SIG[17] — a 33-byte compressed public key and a 65-byte recoverable signature, byte-packed — to QX[8] | QY[8] | SIG_R[8] | SIG_S[8], native little-endian u32 limbs with no recovery byte (#3222). The public-key commitment preimage changed too; see Hashing & Crypto Changes.

Affected Code

# Before (0.15)
#!   Operand stack: [PK_COMM, MSG, ...]
#!   Advice stack:  [PK[9] | SIG[17] | ...]
exec.ecdsa_k256_keccak::verify
# After (0.16)
#!   Operand stack: [PK_COMM, MSG_WORD, ...]
#!   Advice stack:  [QX[8] | QY[8] | SIG_R[8] | SIG_S[8] | ...]
exec.ecdsa_k256_keccak::verify

# New: variable-length message held in memory
#!   Operand stack: [PK_COMM, MSG_PTR, MSG_LEN_BYTES, ...]
exec.ecdsa_k256_keccak::verify_bytes

Two behavioural notes carried in the 0.16 source docs: verify accepts high-s signatures, because it proves that some witness satisfies the ECDSA equation and (r, s) and (r, n-s) are equivalent witnesses; and it pushes no result word — it traps on failure.

Migration Steps

  1. Rewrite the host code that populates the advice stack to emit QX[8], QY[8], SIG_R[8], SIG_S[8] as little-endian u32 limbs.
  2. Drop the recovery byte — it is not part of the new ABI.
  3. If you rely on canonical Ethereum-style signatures, add your own low-s check; verify will not reject high-s.
  4. For messages longer than one word, switch from manual chunking to verify_bytes.

do .. while .. end loops added

A tail-controlled loop form was added (#3232). This is additive — while.true is unchanged and still performs an entry check.

# New in 0.16
do
    <body>          # always runs at least once
while
    <condition>     # must leave one boolean on top of the stack
end

Use it wherever you previously wrote push.1 while.true … end to force a first iteration.


Protocol procedure moves and renames

The protocol MASM surface was reorganised: asset helpers moved from the protocol library into miden::standards::assets, note creation moved behind miden::standards::note::note_creator, and several active_account procedures moved to native_account.

Affected Code

v0.15 v0.16
miden::protocol::asset::* (build/validate helpers) miden::standards::assets::*
miden::protocol::faucet::create_fungible_asset / create_non_fungible_asset Removed — use the miden::standards::assets builders
miden::protocol::output_note::create (callable from note scripts) Account context only; note scripts must call miden::standards::note::note_creator::create_note
miden::protocol::active_account::get_initial_* miden::protocol::native_account::get_initial_*
miden::protocol::active_account::has_non_fungible_asset has_asset
miden::protocol::active_note::get_assets get_initial_assets, plus explicit removal procedures
basic_wallet::add_assets_to_account basic_wallet::move_note_assets_to_account
miden::standards::account::metadata miden::standards::account::inspection
# Before (0.15) — note script moving assets into the account
use miden::standards::wallets::basic->basic_wallet

@note_script
pub proc main
    call.basic_wallet::add_assets_to_account
end
# After (0.16)
use miden::standards::wallets::basic as basic_wallet

@note_script
pub proc main
    call.basic_wallet::move_note_assets_to_account
end

Migration Steps

  1. Update every use path in your MASM per the table above.
  2. Replace output_note::create in note scripts with note_creator::create_note.
  3. Rename add_assets_to_account to move_note_assets_to_account.

Input-note assets are now stateful

In 0.15 a note script read the note's asset list and the kernel reconciled it at the end of execution. In 0.16 the note's initial assets are read with active_note::get_initial_assets, and assets must be explicitly removed as they are consumed. Partially-consumed notes are representable, so the kernel no longer drains the note for you.

Affected Code

The active_note asset surface in 0.16, verified from asm/protocol/src/active_note.masm:

pub proc get_initial_assets       # [dest_ptr] -> [num_assets]
pub proc get_initial_assets_info
pub proc get_initial_num_assets
pub proc get_asset
pub proc remove_asset
pub proc remove_all_assets

active_note::get_storage and the new active_note::write_storage_to_memory are the storage-side counterparts.

Migration Steps

  1. Replace active_note::get_assets with active_note::get_initial_assets.
  2. Add an explicit removal call for each asset you move out of the note — remove_asset for individual assets, or remove_all_assets if you consume the note fully.
  3. Do not assume the kernel drains the note. If you leave assets in place, the note is treated as partially consumed.

Common Errors

Error Message Cause Solution
undefined item '<path>' for a procedure that exists on disk Its module is not declared with mod Add the declaration to the parent module.
import aliases use `as`; `->` is no longer supported Old alias syntax Rewrite with as.
`pub use` is only supported for braced item imports Re-exporting a module Re-export named items instead.
undefined instruction debug.stack Decorator removed Use exec.debug::print_stack.
undefined item 'add_assets_to_account' Procedure renamed Use move_note_assets_to_account.
Note consumption fails with assets remaining Assets are no longer drained implicitly Call remove_asset / remove_all_assets.

VM & Assembler Changes

The assembler was rebuilt around a single artifact type. In 0.15 there were three — Program, Library, and KernelLibrary — serialized as .masl for libraries. In 0.16 everything is a Package serialized as .masp, linking goes through one link_package(package, linkage) method, and the directory-walking *_from_dir entry points became *_from_root entry points that take the root module file. This follows directly from the new explicit module tree: the assembler no longer discovers modules by walking directories, so a directory is no longer a meaningful input.

Three wire formats changed at the same time and none are backward compatible, which means every artifact must be rebuilt from source rather than migrated.

Verification was also reshaped: the free verify(program_info, stack_inputs, stack_outputs, proof) function became verify(proof, claim) over a single ExecutionClaim, and the caller-managed precompile registry disappeared entirely — deferred proofs are now rehydrated and bound automatically.


Breaking Change

The VM jumps 0.23 → 0.29.1. Library and KernelLibrary no longer exist — Package is the only artifact type, and the .masl format is gone. The MAST wire format moved 0.0.30.0.4 and the package format 4.0.06.0.0, so no 0.15 artifact or serialized proof loads under 0.16. Verification now takes a single ExecutionClaim, and miden-project.toml requires an explicit path on every target.

For the MASM language changes that ship with this VM version — the new mod declarations, the rewritten use syntax, and the removal of the debug.* decorators — see MASM Changes. For the changed commitment preimages, see Hashing & Crypto Changes.

Quick Fix

// Before (0.15)
let mut assembler = Assembler::default();
assembler.link_dynamic_library(CoreLibrary::default())?;
let program: Program = assembler.assemble_program(source)?;

// After (0.16)
let mut assembler = Assembler::new(source_manager);
assembler.link_package(CoreLibrary::default().package(), Linkage::Dynamic)?;
let package: Box<Package> = assembler.assemble_program("program", source)?;
let program: Program = package.unwrap_program();

miden-project.toml

  [lib]
  namespace = "my::app"
+ path = "mod.masm"

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


LibraryPackage throughout the assembler

Library and KernelLibrary were deleted. Every entry point that produced or consumed a Library now produces or consumes a Package, the link_*_library family collapsed into link_package(package, linkage), and assemble_program returns a Box<Package> rather than a Program. Every assemble entry point now takes a package name (#3216, #3220).

Affected Code

// Before (0.15)
use miden_assembly::Assembler;
use miden_core_lib::CoreLibrary;

let mut assembler = Assembler::default();
assembler.link_dynamic_library(CoreLibrary::default())?;
let program: Program = assembler.assemble_program(source)?;
// After (0.16)
use miden_assembly::{Assembler, Linkage};
use miden_core_lib::CoreLibrary;

let mut assembler = Assembler::new(source_manager);
assembler.link_package(CoreLibrary::default().package(), Linkage::Dynamic)?;
for library in libraries {
    assembler.link_package(library, Linkage::Dynamic)?;   // Arc<Package>
}
let package: Box<Package> = assembler.assemble_program("program", source)?;
let program: Program = package.unwrap_program();          // or try_into_program()

The complete mapping:

v0.15 v0.16
Assembler::with_kernel(sm, kernel_lib: KernelLibrary) -> Self Assembler::with_kernel(sm, kernel: Arc<Package>) -> Result<Self, Report>
link_library(lib, linkage) / link_dynamic_library(lib) / link_static_library(lib) link_package(package: Arc<Package>, linkage: Linkage)
with_dynamic_library(lib) / with_static_library(lib) with_package(package: Arc<Package>, linkage: Linkage)
compile_and_statically_link_from_dir(dir, namespace) compile_and_statically_link_from_root(root, namespace: Option<&Path>)
assemble_library(modules) -> Arc<Library> assemble_library(name, root, support) -> Box<Package>
assemble_library_from_dir(dir, namespace) -> Arc<Library> assemble_library_from_root(root, namespace: Option<&Path>) -> Box<Package>
assemble_kernel(module) -> KernelLibrary assemble_kernel(name, root, support) -> Box<Package>
assemble_kernel_from_dir(sys_path, lib_dir) -> KernelLibrary assemble_kernel_from_root(name, sys_module_path) -> Box<Package>
assemble_program(source) -> Program assemble_program(name, source) -> Box<Package>
kernel() -> &Kernel kernel() -> &KernelDescriptor
with_profile(&miden_project::Profile) (new)

Also removed from the miden_assembly re-export surface: Library, KernelLibrary, Parse, ParseOptions, LinkLibraryKind, and the library module. Added: Linkage, the module module, and the project-assembly types (ProjectSourceProvider, MasmSourceProvider, ResolvedPackage, AssemblyInterrupted).

Migration Steps

  1. Replace every Library / KernelLibrary binding with PackageArc<Package> for linking, Box<Package> from the assemble methods.
  2. Collapse link_dynamic_library(x) / link_static_library(x) / link_library(x, l) into link_package(x, Linkage::Dynamic) or Linkage::Static.
  3. Rename *_from_dir calls to *_from_root and pass the root module file instead of the directory. Their namespace parameter is now Option<&Path> rather than a required impl AsRef<Path>.
  4. Thread a package name through assemble_program, assemble_library, and assemble_kernel. Any string works; the CLI uses the literal "program".
  5. After assemble_program, call .unwrap_program() (panics on a non-executable package) or .try_into_program() to get the Program the processor expects.
  6. Add ? to Assembler::with_kernel — it is now fallible.

Common Errors

Error Message Cause Solution
cannot find type Library in miden_assembly Type removed Use Package.
no method named link_dynamic_library Collapsed into one method link_package(pkg, Linkage::Dynamic).
expected Program, found Box<Package> assemble_program return type changed Call .unwrap_program() or .try_into_program().
this function takes 2 arguments but 1 was supplied Assemble entry points take a package name Pass a name as the first argument.

Core package split into miden::core + miden::precompiles

The single core MASM package was split into miden-core (namespace miden::core) and miden-precompiles (namespace miden::precompiles), freeing the bare miden namespace for sibling packages such as miden-protocol (#3459, #3222). Both must be linkedmiden-core has a runtime dependency on miden-precompiles.

Affected Code

// Before (0.15)
let mut assembler = Assembler::default();
assembler.link_dynamic_library(CoreLibrary::default())?;
let lib = CoreLibrary::default().library();     // &Library
// After (0.16)
let core_lib = CoreLibrary::default();
let mut assembler = Assembler::new(source_manager);
for package in core_lib.packages() {                 // [Arc<Package>; 2]
    assembler.link_package(package, Linkage::Dynamic)?;
}

// Or individually:
let core: Arc<Package>        = core_lib.package();
let precompiles: Arc<Package> = core_lib.precompiles_package();
let mast: &Arc<MastForest>    = core_lib.mast_forest();   // merged, for execution

CoreLibrary::SERIALIZED now holds the miden-core.masp bytes and a new CoreLibrary::PRECOMPILES_SERIALIZED holds miden-precompiles.masp; in 0.15 SERIALIZED was core.masl. CoreLibrary::library() and CoreLibrary::verifier_registry() are gone, and CoreLibrary::recursive_verifier_root() is new.

Migration Steps

  1. Replace the single link call with a loop over CoreLibrary::default().packages(), or link package() and precompiles_package() explicitly.
  2. Replace CoreLibrary::default().library() with .package().
  3. Drop CoreLibrary::verifier_registry(). The deferred-precompile registry now lives in the miden-precompiles crate as miden_precompiles::registry() and is applied by the verifier automatically.

MAST wire format 0.0.4, package format 6.0.0, and .masl removed

Three artifact-format changes land together, none backward compatible:

  • The MAST wire format bumped [0,0,3][0,0,4], removing inline metadata slots. Assembly-op and debug-variable metadata now live in a separate indexed DebugInfo section (#3201, #3208, #3221). The stripped serialization mode was removed (#3268).
  • The package (.masp) format bumped [4,0,0][6,0,0], from consolidating debug sections into PackageDebugInfo (#3398) and binding dense forest and package digests to stored roots and dependencies (#3334).
  • The .masl library format no longer exists. Library::LIBRARY_EXTENSION is gone along with the type; .masp is the only artifact format.

Affected Code

// Any 0.15 blob fails to read under 0.16:
let forest  = MastForest::read_from_bytes(&old_bytes)?;  // Err: unexpected version [0,0,3]
let package = Package::read_from_bytes(&old_masp)?;      // Err: unexpected version [4,0,0]

Package deserialization is now tiered by trust level. In 0.15 Package implemented only the plain Deserializable::read_from:

// After (0.16) — three trust levels
Package::read_from(&mut r)?            // untrusted: validates MAST, drops debug sections
Package::read_from_bytes(bytes)?
Package::read_from_trusted(&mut r)?    // local cache: validates MAST, keeps debug sections
Package::read_from_bytes_trusted(bytes)?
Package::read_from_unchecked(&mut r)?  // skips MAST validation; only for self-produced bytes
Package::read_from_bytes_unchecked(bytes)?

Migration Steps

  1. Re-assemble every .masp package from source under 0.16, and re-serialize every cached MastForest blob. Invalidate on-disk and database-persisted copies.
  2. Delete .masl artifacts and any code that reads them.
  3. Discard serialized proofs from 0.15 — the proof envelope changed too.
  4. Choose the reader that matches your trust boundary: read_from_bytes for anything from a registry, the network, or a user; read_from_bytes_trusted for your own build cache when you want debug info retained.

ExecutionProof reworked; Verifier replaces the free verify_* functions

ExecutionProof was restructured from { proof, hash_fn, pc_requests } into two envelopes, StarkProof and DeferredProof, and proof serialization changed (#3222). The legacy proof-bound precompile request model was replaced by the deferred-DAG framework in miden_core::deferred.

On the verification side, verify(program_info, stack_inputs, stack_outputs, proof) and verify_with_precompiles(..) were replaced by a Verifier type and a free verify(proof, claim) taking a single ExecutionClaim that bundles what used to be three arguments (#3422, #3447).

Affected Code

// Before (0.15)
let security_level = miden_verifier::verify(
    program_info, stack_inputs, stack_outputs, proof,
)?;
let (level, commitment) = miden_verifier::verify_with_precompiles(
    program_info, stack_inputs, stack_outputs, proof, &registry,
)?;
// After (0.16)
use miden_core::program::ExecutionClaim;
use miden_verifier::{Verifier, verify};

let claim = ExecutionClaim::from_program_info(program_info, stack_inputs, stack_outputs);

let security_level: u32 = verify(proof, claim)?;                 // free fn, default config
let security_level: u32 = Verifier::new().verify(proof, claim)?; // equivalent

// Partial (delegable) verification returns a #[must_use] obligation:
let (level, unsettled) = Verifier::new()
    .with_max_deferred_elements(n)
    .verify_partial(proof, claim)?;
let root: Word = unsettled.root();

ExecutionProof now exposes miden_proof() -> &StarkProof and deferred_proof() -> &DeferredProof, with constructors ExecutionProof::new(miden, deferred) and from_parts(bytes, hash_fn, deferred). The 0.15 public fields, the three-argument new, stark_proof(), deferred_state(), and into_parts() are gone.

verify_with_precompiles and verify_with_max_deferred_elements are both removed. Precompile verification is no longer wired up by the caller: the deferred wire is rehydrated under the built-in miden_precompiles::registry() and bound to the STARK public inputs automatically.

prove and prove_sync keep their 0.15 signatures. New in this line: prove_partial, prove_partial_sync, and prove_partial_from_trace_sync.

Migration Steps

  1. Build an ExecutionClaim — usually ExecutionClaim::from_program_info(info, inputs, outputs) — and pass (proof, claim) to verify.
  2. Delete PrecompileVerifierRegistry plumbing and calls to verify_with_precompiles / verify_with_max_deferred_elements. Use Verifier::with_max_deferred_elements(n) if you need a non-default budget.
  3. Replace field access on ExecutionProof with miden_proof() / deferred_proof().
  4. Discard serialized proofs from 0.15 — they will not deserialize.
  5. If you use verify_partial, do not drop the returned Unsettled. It is #[must_use] and represents a deferred obligation you must settle or re-expose.

AdviceInputs.stack replaced by the AdviceStack type

AdviceInputs's public stack: Vec<Felt> field was replaced by a private AdviceStack, and the with_stack / with_stack_values / extend_stack helpers were removed in favour of with_advice_stack(AdviceStack) and the advice_stack() accessor (#3423).

Affected Code

// Before (0.15)
let advice = AdviceInputs::default()
    .with_stack(vec![a, b, c])
    .with_stack_values([1u64, 2, 3])?
    .with_map(entries);
let raw: &Vec<Felt> = &advice.stack;
// After (0.16)
use miden_core::advice::{AdviceInputs, AdviceStack};

let mut stack = AdviceStack::new();
stack.append_word(word).append_elements([a, b, c]);
// or, from raw u64s, validating each against the field modulus:
let stack = AdviceStack::try_from_values([1u64, 2, 3])?;

let advice = AdviceInputs::default()
    .with_advice_stack(stack)
    .with_map(entries);

let stack: AdviceStack = advice.advice_stack();   // clone of the stack
let (stack, map, store) = advice.into_parts();    // new in 0.16

AdviceStack distinguishes append (bottom) from prepend/push (top) and names the MASM instruction each targets: append_element, append_elements, append_word, append_dword, append_for_adv_push, append_for_adv_pipe, prepend_elements, prepend_word, prepend_stack, push_element, plus consume_element / consume_word / consume_dword and into_elements. AdviceInputs::map and AdviceInputs::store remain public fields.

Migration Steps

  1. Replace with_stack(iter) with with_advice_stack(AdviceStack::…), building the stack with the append/prepend methods.
  2. Replace with_stack_values(u64s)? with AdviceStack::try_from_values(u64s)?.
  3. Replace direct reads of advice_inputs.stack with advice_inputs.advice_stack(), or destructure with into_parts().
  4. Mind the ordering vocabulary: append_* adds below (consumed later), prepend_* and push_element add on top (consumed first).

New resource bounds

The live advice map is now bounded by total field-element count and the advice Merkle store by internal node count, both during setup and execution (#3264). FastProcessor memory growth is bounded by a configurable ExecutionOptions::max_memory_elements (#3226). If you seed very large advice inputs, expect a setup-time error rather than silent success.


ModuleInfoModuleDescriptor, KernelKernelDescriptor

The module and kernel metadata types were renamed and relocated (#3356).

Affected Code

- use miden_core::program::Kernel;
- use miden_assembly::library::ModuleInfo;
- let k: &Kernel = assembler.kernel();
+ use miden_core::program::KernelDescriptor;
+ use miden_assembly::module::ModuleDescriptor;
+ let k: &KernelDescriptor = assembler.kernel();

The module path moved as well: the library module is gone from miden_assembly's re-exports, and ModuleDescriptor lives under module.

Migration Steps

  1. Rename KernelKernelDescriptor and ModuleInfoModuleDescriptor at every import and binding.
  2. Update the import path from miden_assembly::library to miden_assembly::module.
  3. Re-check descriptor method names against the new type — several were renamed alongside it.

miden-project.toml: path is mandatory on every target

The path key on [lib] and [[bin]] targets changed from optional to required. It may point at files with extensions other than .masm — a Rust project's source root, for example — which is why the implicit default was dropped (#3216). In the Rust AST, LibTarget::path and BinTarget::path moved from Option<Span<Uri>> to Span<Uri>.

A project with neither a [lib] nor any [[bin]] still gets an implicit library target defaulting to mod.masm; that inference is unchanged.

Affected Code

miden-project.toml

  [lib]
  namespace = "miden::protocol"
+ path = "mod.masm"

  [[bin]]
  name = "entry"
+ path = "bin/main.masm"

Migration Steps

  1. Add an explicit path to every [lib] and [[bin]] in every miden-project.toml.
  2. If you construct LibTarget / BinTarget in Rust, drop the Some(..) wrapper around path.

miden-vm bundle reworked

miden-vm bundle now takes the path to a root .masm module instead of a directory, --kernel is a boolean flag instead of taking a path, and the output is a .masp package instead of a .masl library. With --kernel set, the kernel's support modules are derived from the explicit mod declarations in the root module (#3216, #3220).

Affected Code

# Before (0.15)
miden-vm bundle --namespace mylib ./src            # directory  -> out.masl
miden-vm bundle --kernel ./kernel.masm ./src       # --kernel takes a path

# After (0.16)
miden-vm bundle --namespace mylib ./src/mod.masm   # root module -> out.masp
miden-vm bundle --kernel ./kernel/mod.masm         # --kernel is a flag

--namespace is now optional in the non-kernel case: if omitted, the assembler expects a namespace declaration in the root module, where 0.15 fell back to the directory name. For --kernel the namespace defaults to $kernel. A new -r / --release flag disables debug symbols.

Migration Steps

  1. Change the positional argument from a directory to the root module file.
  2. Change --kernel <path> to a bare --kernel with the kernel's root module as the positional argument.
  3. Update the expected output filename from out.masl to out.masp.
  4. Ensure the root module declares its submodules with mod / pub mod — that is now how support modules are discovered.
  5. Either pass --namespace or add a namespace declaration to the root module.

ProjectAssembler::assemble_with_sources removed

ProjectAssembler::assemble_with_sources(target, profile, sources) was removed — projects must be assembled from the filesystem (#3216). In its place, project assembly is extensible through the ProjectSourceProvider trait, which lets non-MASM source languages participate (#3375, #3383).

Affected Code

// Before (0.15)
let pkg = project_assembler.assemble_with_sources(target, profile, sources)?;
// After (0.16)
let pkg: Arc<MastPackage> = project_assembler.assemble(target_selector, profile_name)?;

// Register a provider for a non-MASM source language:
let mut pa = Assembler::new(sm)
    .for_project_at_path_with_providers(manifest_path, &mut store, [my_provider])?;

// A provider can interrupt assembly:
match pa.assemble_interruptible(target_selector, profile_name)? {
    ControlFlow::Continue(pkg) => { /* … */ },
    ControlFlow::Break(interrupted) => { /* … */ },
}

ProjectAssembler::assemble(target_selector, profile_name) keeps its 0.15 signature.

Migration Steps

  1. Drop assemble_with_sources; write your sources to disk and use assemble, or implement a ProjectSourceProvider.
  2. If you need to react to a provider interrupting assembly, use assemble_interruptible and match on the ControlFlow.

Changelog correction

The 0.25.4 changelog names the new method ProjectAssembler::assemble_source_project. The method that actually exists in the released code is assemble_source_package.


Smaller Rust API removals

These are lower-impact, but each will break a build if you touch it.

Removed / changed PR
MastForest::compact removed — deduplicate through builders or explicit MastForest::merge #3318
Stripped MastForest serialization mode removed #3268
Dense forest construction moved to DenseMastForestBuilder; non-canonical dense payloads rejected #3334
MastForestBuilder simplified around builder-local refs and immutable finalized forests #3139
prettier::pretty_print_csv, MastNodeId::from_usize_safe, DecoratorId::from_u32_bounded, OpBatch::end_indices removed #3197
Processor trait methods moved into their sub-interfaces #3202
ExecutionOptions::with_overlapped_trace_build added, on by default #3407
miden-vm run / miden-vm prove now fail when the inferred .inputs file is missing instead of proceeding #3236
ResumeContext exposes its debug info outside miden-processor and can be built from a Package #3355
Proof serialization switched from bincode to wincode; verifier-side STARK proof deserialization bounded to 64 MiB #3148
AeadPoseidon2::key_from_bytes restored to canonical-Felt decoding; keys persisted under the brief SHA-256 KDF contract must be re-derived #3366
Felt::from_{u8,u16,u32} are now const; Felt::MAX added crypto#1081
Assembling a procedure with more locals than the frame pointer can represent is a diagnostic error rather than a panic #3332

The miden-crypto 0.26 and 0.27 breaking changes are almost entirely in LargeSmt / LargeSmtForest storage backends and prover internals, which application code does not call. The one exception worth knowing: the RustCrypto and dalek stack (k256, sha2, sha3, curve25519-dalek, ed25519-dalek, x25519-dalek, hkdf, der) was upgraded (crypto#1045) and rand moved to 0.10 (crypto#995). Expect version-unification pressure if you depend on those crates directly.


Common Errors

Error Message Cause Solution
unexpected version [0,0,3] reading a MastForest MAST wire format is now 0.0.4 Re-assemble from source.
unexpected version [4,0,0] reading a package Package format is now 6.0.0 Rebuild the .masp.
Cannot open a .masl file Format removed entirely Rebuild as .masp.
cannot find function verify_with_precompiles Replaced by automatic deferred verification Use verify(proof, claim).
no field stack on type AdviceInputs Field is now private Use advice_stack() or into_parts().
missing field path parsing miden-project.toml path is mandatory Add it to every [lib] and [[bin]].
Serialized proof fails to deserialize Proof envelope and serialization changed Regenerate the proof.

Rust Contract SDK & Compiler

Which "Rust SDK"?

Two different things get called the Rust SDK. This page is about the miden crate and midenc, used to write account components, notes, and transaction scripts in Rust and compile them to MASM. The miden-client library — used to build applications that talk to a Miden node — is covered in Client Changes.

Breaking Change

Component trait methods must now be marked #[account_procedure] to be part of the account interface, and #[account(..)] generates one trait per interface instead of inherent methods. Note also that the contract toolchain lags the rest of the 0.16 line: it builds against protocol 0.16.0-alpha.4 and VM 0.25, not the protocol 0.16.0-rc and VM 0.29.1 that the client and node use.

Quick Fix

// Before
#[component]
trait BasicWallet {
    fn receive_asset(&mut self, asset: Asset);
}

// After
#[component]
trait BasicWallet {
    #[account_procedure]
    fn receive_asset(&mut self, asset: Asset);
}

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


Versions

The contract toolchain versions independently of the rest of the stack, and in this release it is genuinely behind.

Component Version
midenc / compiler workspace 0.10.0
miden contract SDK crate (and miden-base-sys, miden-stdlib-sys, miden-sdk-alloc) 0.14.0
Protocol it builds against 0.16.0-alpha.4
VM crates it builds against 0.25
MSRV 1.97 (plus a nightly toolchain)

Two consequences worth planning around:

  • The MSRV is 1.97, higher than the 1.96 the rest of the stack requires. Your toolchain must satisfy the highest of the two.
  • Because the toolchain pins protocol 0.16.0-alpha.4 and VM 0.25, contract code compiled with it sees an earlier snapshot of the 0.16 protocol surface than your client does. The MAST and package wire formats are compatible across VM 0.25 and 0.29.1, so artifacts still load; the skew is in the protocol API surface, not serialization.

Component methods must be marked #[account_procedure]

A #[component] trait's methods are no longer implicitly part of the account interface. Every method that must be callable from a note script, a transaction script, a foreign procedure invocation, or a sibling component now needs #[account_procedure] on the trait declaration, not on the impl.

#[auth_script] and #[account_procedure] cannot be combined in one component. An authentication component keeps using #[auth_script] alone; mixing them is a compile error. Like #[auth_script], #[account_procedure] is recognised by the enclosing #[component] macro and needs no import.

Affected Code

// Before
use miden::{Asset, NoteIdx, component, component_storage, output_note};

#[component]
trait BasicWallet {
    fn receive_asset(&mut self, asset: Asset);
    fn move_asset_to_note(&mut self, asset: Asset, note_idx: NoteIdx);
}
// After
use miden::{Asset, NoteIdx, NoteType, Recipient, Tag, component, component_storage, output_note};

#[component]
trait BasicWallet {
    #[account_procedure]
    fn receive_asset(&mut self, asset: Asset);

    #[account_procedure]
    fn move_asset_to_note(&mut self, asset: Asset, note_idx: NoteIdx);

    #[account_procedure]
    fn create_note(&mut self, tag: Tag, note_type: NoteType, recipient: Recipient) -> NoteIdx;
}

The impl block is unchanged — the attribute is not repeated there.

Migration Steps

  1. For each #[component] trait, add #[account_procedure] above every method called from a note, a transaction script, FPI, or a sibling component.
  2. Leave authentication components alone. They keep #[auth_script] and must not gain #[account_procedure].
  3. Purely internal helper methods can stay unmarked.

The shipped templates disagree with this rule

The cargo miden new account template declares its method without #[account_procedure] while the sibling note and tx-script templates call it, and the full-project scaffold has the same gap. The repository's own examples/counter-contract does mark them. The template tests only build, never execute, so the gap is not caught by CI. If you scaffold a new project, add the attribute yourself rather than trusting the generated code.


#[account(..)] generates one trait per interface

#[account(..)] used to generate the referenced component's methods as inherent methods on the wrapper struct. It now generates one trait per referenced interface, named after the interface, and implements it for the wrapper. This lets two components exporting the same method name coexist on one account.

Most single-component call sites are unchanged, but two situations break.

Affected Code

The wrapper struct may no longer share its name with a generated trait:

// Before — compiled
#[account(counter_contract::CounterContract)]
struct CounterContract;

// After — rename the wrapper
#[account(counter_contract::CounterContract)]
struct Counter;

let counter = Counter::new(counter_account_id);
let count = counter.get_count();

Cross-module call sites need the generated trait in scope. A #[note] or #[tx_script] entrypoint in the same module sees it automatically; a call site in a different module needs to import the trait, which is named after the interface:

use crate::BasicWallet;   // the generated trait, not the wrapper struct

Migration Steps

  1. Rename any wrapper struct that collides with its interface name.
  2. Import the generated trait at cross-module call sites.

Other changes

  • Transaction-kernel bindings were renamed and moved to track the protocol 0.16 surface, and several were removed.
  • Kernel scalars are typed rather than raw Felt, so values that used to be interchangeable now need explicit conversion.
  • AssetAmount is a validated fungible-amount type, matching the protocol and client surfaces.
  • miden-project.toml requires an explicit path on [lib] and every [[bin]]. See VM & Assembler Changes.
  • #[note] reserves get_entrypoint_root, so a note struct cannot define a method with that name, and note structs now implement ToFeltRepr.
  • cargo miden new fetches templates from a release bundle rather than embedding them.

Additive in this line: typed transaction-script arguments, note constructors, and println!-style formatting.


Common Errors

Error Message Cause Solution
A component method is not callable from a note or script Missing #[account_procedure] Add it to the trait method declaration.
Compile error combining auth and account attributes They are mutually exclusive Auth components keep #[auth_script] only.
Name collision between a wrapper struct and a trait #[account(..)] now generates traits Rename the wrapper.
no method named .. at a cross-module call site The generated trait is not in scope Import the trait named after the interface.
missing field path in miden-project.toml Now mandatory Add path to every target.
Toolchain version error MSRV is 1.97 here Use the higher of the stack's requirements.

Final Checklist

Complete these steps to verify your migration:

  • Bump all Miden crate versions per section 1, pinning the exact 0.16.0-rc.N strings, and rename miden-tx-batch-prover to miden-tx-batch
  • Bump @miden-sdk/miden-sdk and @miden-sdk/react together; drop any miden-idxdb-store dependency
  • Update the toolchain to Rust 1.96 (1.97 if you also build Rust contracts)
  • Re-assemble every .masp from source and delete cached MastForest blobs; .masl no longer exists
  • Delete and recreate your local store, then re-sync — export private note files first
  • Upgrade your node together with your client — sealed and plaintext submissions are mutually incompatible
  • Add mod / pub mod declarations so every .masm file is reachable from your project root
  • Rewrite pub use a::b::c as pub use {c} from a::b, and use x->y as use x as y
  • Replace debug.* / trace decorators with miden::core::debug procedures, and strip them from production code
  • Declare fee conversion info on transactions if your chain charges a fee, and fund the paying account with the fee asset
  • Move auth components out of with_auth_component and wrap keys in Approver / ApproverSet
  • Rename AssetIdAssetClass first, then AssetVaultKeyAssetId
  • Replace account_delta() with account_patch() — but leave TransactionSummary::account_delta() alone
  • Rewrite XNote::create(..) calls as builders, and cap notes at 16 assets
  • Recompute stored ECDSA public-key commitments, MMR peak commitments, and empty domain-separated hashes
  • Replace Library/KernelLibrary with Package, and link_*_library with link_package
  • Add an explicit path to every [lib] and [[bin]] in miden-project.toml
  • Build an ExecutionClaim and call verify(proof, claim); discard proofs serialized under 0.15
  • CLI: rename send to transfer, --with-code to --inspect, and id to address in token_symbol_map.toml
  • CLI: re-check every call invocation — arguments are now counted in field elements
  • (If you write Rust contracts) mark component trait methods with #[account_procedure] and import the traits generated by #[account(..)]
  • Run cargo buildno errors
  • Run cargo testall tests pass

You're done!

If your project builds and all tests pass, you've successfully migrated to v0.16.



Need Help?

  • Telegram: Build on Miden — technical discussion and support.
  • Forum: Miden discussions — longer-form questions and design discussion.
  • GitHub issues: file against the relevant repo — rust-sdk, web-sdk, protocol, miden-vm, or compiler.
  • Changelogs: the per-repo CHANGELOG.md files carry the full list of changes, including non-breaking features and fixes omitted from this guide.

WiktorStarczewski and others added 2 commits August 19, 2026 16:02
Replaces the 0.15 guide in the current docs version with a 0.16 guide
covering the protocol crates (0.15.3 -> 0.16.0), the VM (0.23 -> 0.29.1),
miden-client, the Web SDK, and the Rust contract SDK / compiler.

The 0.15 guide remains available under versioned_docs/version-0.15.

Every API, signature, flag and error string was verified against source
at the release tags rather than taken from the changelogs, which turned
out to be unreliable: several entries describe APIs that do not exist in
the shipped code, and several real breaking changes are missing from them
entirely. Notable corrections carried into the guide:

- AccountDelta -> AccountPatch applies to account updates only;
  TransactionSummary::account_delta() deliberately stays relative.
- TransactionRequestBuilder::fee_conversion_info takes an undocumented
  second `salt: Word` argument; code written from the changelog alone
  will not compile.
- Opening a pre-0.16 store surfaces a migration error, not the
  SchemaHashMismatch the changelog names.
- `miden-client call` now counts arguments in field elements rather than
  parameters, and `init` dropped auth/acl-auth.masp. Neither is in the
  changelog.
- Claims that AuthNetworkAccount::with_allowed_tx_scripts was removed and
  that RoleBasedAccessControl gained a builder() are contradicted by
  source, so they are omitted.

Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up verification against the pinned trees turned up four fixes:

- Client::submit_proven_transaction keeps its 0.15 signature exactly.
  Only the NodeRpcClient trait methods take SealedTransactionInputs, so
  this is source-breaking solely for trait implementers, not for the
  applications the guide addresses. The real requirement it imposes on
  everyone is syncing before submitting, which now fails with
  ChainValidationError rather than silently.
- Account::apply_patch is the direct replacement for apply_delta and is
  the smaller edit; the guide previously showed only Account::try_from.
- ClientBuilder::rpc does not wrap a custom client in VerifyingRpcClient
  while the built-in gRPC constructors now do, so passing your own RPC
  client silently loses response verification.
- Client::get_consumable_notes keeps its signature; the single-account
  path is a performance change, not an API change.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread docs/builder/migration/09-vm-assembler.md Outdated
Comment thread docs/builder/migration/09-vm-assembler.md Outdated
Comment thread docs/builder/migration/09-vm-assembler.md Outdated
WiktorStarczewski and others added 3 commits August 20, 2026 04:07
Co-authored-by: François Garillot <4142+huitseeker@users.noreply.github.com>
Co-authored-by: François Garillot <4142+huitseeker@users.noreply.github.com>
Co-authored-by: François Garillot <4142+huitseeker@users.noreply.github.com>
Comment on lines +26 to +29
.with_components([
AuthSingleSig::new(Approver::new(pub_key, auth_scheme)).into(),
BasicWallet.into(),
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: if you add individual components, use with_component, it reads better. If you have a type that implements IntoIterator<Item = AccountComponent> like AuthNetworkAccount use with_components.

Comment on lines +47 to +49
:::note `AccountType` did not change
`AccountType` has been `Private` / `Public` since 0.15, and there is no separate `AccountStorageMode`. If you are coming from an older release, that collapse is covered in the [0.15 guide](https://docs.miden.xyz/0.15/builder/migration/account-changes).
:::

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: not sure if this is relevant for the 0.15 -> 0.16 migration?

In 0.15 each standard note was a unit struct — `pub struct P2idNote;` — with a `create` associated function that took every parameter positionally and returned a finished `Note`. In 0.16 each is a real struct holding its fields, built through a `bon` builder and converted to a `Note` with `Into`.

### Migration Steps
The practical benefits are that optional parameters are now actually optional rather than positional, and that the intermediate typed value is inspectable before you convert it. The practical cost is that every call site changes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

and that the intermediate typed value is inspectable before you convert

I think this undersells the value of a proper type to represent each standard note. E.g. you can write an API now that takes a P2IdNote rather than Note, which gives you type safety. Worth mentioning here to give downstream users the chance that their agent recognizes the opportunity for a better API rather than just a make-it-work migration.

2. Replace `NoteId::new(recipient, asset_commitment)` with `NoteDetailsCommitment::new(&recipient, &assets)`.
3. To obtain the public `NoteId`, call `note.id()`, or `NoteId::new(details_commitment, &metadata)`.
4. Recompute and re‑persist any stored note IDs — 0.14 ids do not roundtrip, and an id now changes if metadata changes.
The asset limit change is the one to watch, because it is a runtime error rather than a compile error, and it only triggers for notes carrying more than 16 assets.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: Makes it sound like this used to be a compile error, but it was always runtime. Not sure this sentence carries real value.

@PhilippGackstatter PhilippGackstatter left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good - I reviewed chapters 2 to 7.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants