Skip to main content
Version: 0.16

Hashing & Crypto Changes

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.


Summary​

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.


ECDSA k256 public-key commitment format changed​

Summary​

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​

Summary​

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​

Summary​

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​

Summary​

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:

SymptomCauseSolution
Signature verification traps for a key that worked in 0.15Stored PK_COMM uses the old preimageRecompute with PublicKey::to_commitment().
Advice-map lookup misses for a key you know you insertedThe key is a changed commitmentRe-derive the key.
Chain-MMR commitment mismatch after upgradingPeak commitment now binds the leaf countRecompute and re-persist.
An "empty" sentinel commitment no longer matchesEmpty-input hashing changedRe-derive the sentinel.
Previously encrypted data fails to decryptAEAD key derivation changedRe-derive the key and re-encrypt.