Persistent Full-Range Hashing into the Native Field
| MPS | 0045 |
|---|---|
| Status | Proposed |
| Category | Libraries and Tooling |
| Authors | Iskander Andrews (0xisk) |
| Created | 14-Oct-2026 |
Abstract
Compact has no persistent hash that returns a full native Field.
persistentHash returns 32 bytes. The standard library bridge to a Field,
degradeToTransient, keeps the low 31 bytes and drops the 32nd, so its
output is below 2^248 in a 255-bit field: not full-range, not uniform. The
only hash that returns a Field, transientHash, is documented as not
guaranteed to survive upgrades, so it must not derive state. Casts do not
help either: Bytes<32> as Field is range-checked and fails at or above the
modulus, while the same cast to every foreign field type reduces.
Our case is OpenZeppelin’s Confidential Note Fungible Token, a Compact module. Below, “the contract” is the program that imports it.
- A balance is a set of
notes,
(value: Uint<128>, nonce: Field), committed into a Merkle tree. - A note is spent by publishing its
nullifier,
persistentHash(domain, nonce). Whoever knows the nonce can spend the note first. _mintderives the nonce throughfreshNonce,degradeToTransient(persistentHash(...)), so it is 248 bits._mintNotetakes a note the contract built, so its nonce can be anyField. Every commit goes through it:_mint,_transferand_burnall call it with theNotevalues the contract passes in.- The
Delivery extension
publishes each output note’s
(value, nonce)encrypted for the recipient asnonce + mask. The mask is theEcdhMaskKDF, againdegradeToTransient(persistentHash(...)), so below2^248.
Since the mask is below 2^248, nonce + mask never wraps, and the
ciphertext places the nonce in a window of size 2^248. For a _mint nonce
the window is as wide as the nonce, so only the edge leaks: about 0.7 bits
on average, and a (nonce, mask) pair that makes the nonce searchable has
probability around 2^-337. For a contract-chosen nonce above 2^248 there is no
probability in it, the top bits are simply visible in the ciphertext.
We had two ways out:
- A full-field pad,
compact-contracts#913,
that reimplements RFC 9380
hash_to_fieldon SHA-256 with hand-written field arithmetic in Compact, at several times the rows. - Typing the plaintext as
Uint<248>under the existing pad, compact-contracts#1150. The leak on the token’s own nonces is negligible, and the range-checked cast refuses a wider nonce instead of leaking it.
The token took the second.
The ledger has a Poseidon hash_to_field, ZKIR has a reducing FromBytes32
and a wide-input reduction. None is reachable from Compact. Nobody is blocked
today, so this MPS is low priority. It writes the gap down so the next
library needing a uniform Field does not rediscover it.
Vision
An author who needs a Field from a hash picks a standard library circuit by
persistence and output range and gets exactly that. The docs of every
bytes-to-field conversion say what is kept, what is dropped, and whether the
output is uniform. A mask, a key, or a nonce is one library call, with no
field arithmetic written in Compact.
Problem
degradeToTransienttruncates. The ledger returns the 31-byte chunk of the input’s field representation and the compiler lowers the call to acopy. The docs never say 8 bits are dropped, and the native declaration calls its disclosure “a modulus of” the input, though nothing is reduced.- No persistent
Fieldhash.transientHashis the only hash returning aFieldand the only circuit-cheap one. Contracts that derive state fall back to SHA-256 and pay in rows (compact#183). - Casts are inconsistent.
Bytes<32> as Fieldrange-checks, in the runtime and in circuit.Bytes<32> as Secp256k1Base, the other foreign fields, andBytes<64> as Curve25519Scalarreduce. A random 32-byte digest fails the native cast more than half the time. - The primitive exists below Compact. ZKIR v3 has a reducing
FromBytes32overNative(implementation), the ledger has a Poseidonhash_to_fieldand a 64-bytefrom_uniform_bytes. The compiler emits none of them for the native field. - Reducing 32 bytes is not enough. A 256-bit integer modulo a 255-bit prime has a visible bias. Uniform output needs a wider input, as in RFC 9380.
Use Cases
- A
Fieldplaintext under a one-time pad. The Delivery case above. The nonce type isFieldand_mintNoteaccepts any value of it. A pad below2^248cannot hide the top 7 bits of a wider nonce. Hiding what the type allows needs a pad uniform over the wholeField(compact-contracts#735). Today the type is narrowed instead. - Persistent key derivation. A
Fieldsecret or nonce derived from a seed must come back the same after an upgrade. Today it is truncated or depends ontransientHash. - Commitments in the field. A
FieldMerkle leaf or commitment comes fromtransientCommit, with no persistence guarantee, or from a truncated SHA-256 digest. -
The workaround. compact-contracts#913 (closed, branch kept) reimplements RFC 9380
hash_to_fieldon SHA-256 with a hand-writtenFqmodule. Cost on that branch with the toolchain in the references (k, rows):Circuit Standard-library path Full-field workaround ECDH mask derivation 14, 8700 14, 14055 SHA-256 of 32 bytes, then to Field13, 2305 14, 9806 Wide-bytes reduction into the field alone n/a 11, 2006
Goals
- A standard library circuit from a hash to a
Fieldthat is uniform over the whole field and persists across upgrades. - A stable, named, circuit-cheap hash into
Fieldthat may derive state. - Docs for every bytes-to-field path: what is kept, what is dropped, uniform or not, persistent or not.
Expected Outcomes
Libraries stop reimplementing hash-to-field in Compact. A mask or a
persistent Field key is one call at a fraction of today’s rows. The docs
alone tell an author whether a conversion loses bits or has a bias.
Open Questions
- Should the full-range map be a new circuit, a reducing cast from a wide
Bytes<n>toFieldlike the foreign-field casts, or both? And which input width: RFC 9380Lfor a 255-bit modulus, or something else? - Can the ledger’s Poseidon
hash_to_fieldbe the persistent circuit-cheap option, and under which guarantee?
Recommended MIPs
- Persistent hash to the native field. A standard library circuit from
any input to a uniform
Fieldwith a persistence guarantee: input width, expansion, domain separation, ZKIR lowering. - Stable named hashes.
poseidonandsha256as named circuits with stated guarantees,transientHashandpersistentHashkept as aliases, rename done in the on-chain runtime first (compact#183). - Conversion docs. Document the truncation in
degradeToTransientand the range check inBytes<32> as Field.
References
- Compact standard library exports,
degradeToTransient,transientHash,persistentHash: https://docs.midnight.network/compact/standard-library/exports - Compact compiler
compactc-v0.35.0: native declarations https://github.com/LFDT-Minokawa/compact/blob/compactc-v0.35.0/compiler/midnight-natives.ss, ZKIR lowering ofdegradeToTransientand byte casts https://github.com/LFDT-Minokawa/compact/blob/compactc-v0.35.0/compiler/zkir-v3-passes/reduce-to-zkir.ss, cast rules https://github.com/LFDT-Minokawa/compact/blob/compactc-v0.35.0/doc/compact-reference.mdx - Midnight ledger, branch
ledger-8atc38ea00f:degrade_to_transient,hash_to_field,transient_hashhttps://github.com/midnightntwrk/midnight-ledger/blob/c38ea00f/transient-crypto/src/hash.rs, byte-to-field representation https://github.com/midnightntwrk/midnight-ledger/blob/c38ea00f/transient-crypto/src/repr.rs, ZKIR v3FromBytes32,Bytes32IntoLowHigh,ReconstituteFieldhttps://github.com/midnightntwrk/midnight-ledger/blob/c38ea00f/zkir-v3/src/ir.rs, https://github.com/midnightntwrk/midnight-ledger/blob/c38ea00f/zkir-v3/src/ir_instructions/from_bytes32.rs - Midnight ZK:
midnight-circuitsPoseidon and SHA-256 chips,midnight-zk-stdlibposeidon,sha2_256,assigned_from_le_bytes: https://github.com/midnightntwrk/midnight-zk - Compact issue, expose Poseidon from Compact: https://github.com/LFDT-Minokawa/compact/issues/183
- OpenZeppelin compact-contracts: Confidential Note Fungible Token
https://github.com/OpenZeppelin/compact-contracts/issues/722, core
https://github.com/OpenZeppelin/compact-contracts/pull/743, Delivery
extension https://github.com/OpenZeppelin/compact-contracts/issues/727,
Fieldpad forEcdhMaskhttps://github.com/OpenZeppelin/compact-contracts/issues/735, full-field pad and row measurements (closed, branch kept) https://github.com/OpenZeppelin/compact-contracts/pull/913, theUint<248>route taken instead https://github.com/OpenZeppelin/compact-contracts/pull/1150 - RFC 9380, Hashing to Elliptic Curves,
hash_to_field: https://www.rfc-editor.org/rfc/rfc9380#section-5.2 - MPS-0027, Domain Separation for Midnight Hash Constructions; MPS-0008, Keccak-256 hashing; MPS-0011, native cryptographic primitives.
Acknowledgements
Copyright
This MPS is licensed under CC-BY-4.0.