Four functions that look like twins
Imagine you want to run a sealed-bid auction on a public blockchain. Everyone must be able to see that a bid was placed, but nobody should learn the amount until the bidder chooses to open it. Later, the bidder must be able to prove that the amount they open is the one they originally sealed, and no other.
Midnight's smart contract language, Compact, gives developers four built-in tools for this kind of job: persistentHash, persistentCommit, transientHash and transientCommit. Their names are almost the same, and a new developer can easily treat them as interchangeable. They are not. Two simple questions decide which one you need:
Do I need a hash or a commitment?
Do I need the result to be persistent or transient?
This article answers both, using Midnight's official documentation as the source for every claim. Where I tested something myself, I say so.
Two ideas first: hash and commitment
A hash function takes any input and turns it into a short, fixed-size code. The same input always gives the same code, and you cannot work backward from the code to the input. Midnight's documentation describes these functions as one-way and deterministic [2]. You can think of a hash as a fingerprint of your data.
A fingerprint has one weakness. If the thing you are fingerprinting has only a few possible values, like a yes or no answer, or a number between 0 and 100, anyone can fingerprint every possibility themselves and compare. The documentation points to this problem directly: simple values that are easy to guess by brute force are not safe under a plain hash [2].
A commitment fixes that. Before hashing, you mix your value with a random secret number called a salt. The documentation explains that the salt makes the simple value impossible to guess unless the salt itself is also guessed [2]. A commitment is like putting your value in a sealed envelope together with a random note that only you know, then publishing a fingerprint of the whole envelope.
Midnight's documentation names two promises a good commitment makes [2]:
Hiding: the published result reveals nothing about the original value.
Binding: once you have committed, you cannot change your mind. The commitment is locked to the original value.
To check a commitment later, the owner provides the same value and the same salt, the contract runs them through the same function, and the contract compares the result with the stored one. If they match, the claim is verified. If not, the owner provided something different [2].
The four functions in one grid
Here is how the four functions line up, with their exact signatures from the standard library reference [1]:
Hash | Commitment | |
|---|---|---|
Persistent |
|
|
Transient |
|
|
A few terms in that table need explaining:
<T>means the function accepts a value of almost any type.Bytes<32>is a block of 32 bytes, which is 256 bits. Persistent functions return this.Fieldis a number type that Midnight's proof system works with natively. The documentation calls these values field elements, and the transient functions return them [1].randis the salt. Notice that its type matches the output type:Bytes<32>for the persistent version,Fieldfor the transient one.
Question 1: hash or commitment?
The difference between a hash and a commitment is not only about guessing. It also changes what the Compact compiler demands from you.
Compact follows a "private by default" rule. Circuit inputs and anything derived from witness functions are treated as private. A witness is a function that runs on the user's own machine and supplies private data to a circuit [2]. Before such a value can be stored on the public ledger, or returned from an exported circuit, the developer must wrap it in disclose(). The documentation adds an important note: disclose() does not make a value public by itself. It tells the compiler that you have checked the value and accept that it can appear publicly [2].
Here is where the two tools differ [1][2]:
A hash does not count as protection. The documentation states that a hash is not sufficient to protect its input from disclosure, so if the input came from a witness and the result will be stored or returned, you must use
disclose().A commitment does count as protection, as long as the salt is sufficiently random. In that case you do not need
disclose().
I tested this myself with the Compact compiler, version 0.35.0 [5]. Storing the persistentHash of a witness value in a ledger field was rejected, with a message saying the ledger operation might disclose a hash of the witness value. Storing the result of persistentCommit or transientCommit for the same kind of value compiled without any disclose(). Wrapping the hash in disclose() made the first case compile. This matches the documentation exactly. If you want to confirm it, the experiment takes only a few minutes.
The practical rule: if the input is secret and could be guessed, use a commitment. Use a plain hash when the input already has a unique, hard-to-guess fingerprint, for example a long random secret key [2].
Question 2: persistent or transient?
This second choice is about time. The two families differ in what they promise about the future [1][2].
Persistent functions are guaranteed to give consistent results even when the Midnight protocol is upgraded. They are built on the SHA-256 algorithm, which is a published standard from the United States' National Institute of Standards and Technology [1][4]. The documentation recommends them for anything stored in ledger state and for authentication. The trade-off is that they are not optimized for use inside zero-knowledge circuits, so the documentation says to avoid using them for simple consistency checks where possible [1].
Transient functions are optimized to be efficient inside circuits. The trade-off is that they are explicitly not guaranteed to persist between upgrades. The documentation says they should never be used to derive state data, but they can be used for consistency checks [1].
In plain language: a persistent function is like an engraved plaque, and a transient function is like a pencil note on scratch paper. If the result needs to still be valid after Midnight upgrades, use the engraved version. If the value only has to survive the length of one interaction, the pencil note is cheaper.

Midnight's own summary table captures all four choices [2]:
Function | Best for | Lasts across upgrades | Protects its input from disclosure |
|---|---|---|---|
| Temporary checks | No guarantee | No |
| Temporary hiding | No guarantee | Yes |
| State derivation, authentication | Guaranteed | No |
| Long-term hiding | Guaranteed | Yes |
Moving between the two
Sometimes a value starts in one family and needs to move to the other. The standard library provides two bridge functions for this: degradeToTransient turns the output of a persistent hash or commitment into a field element, and upgradeFromTransient turns a field element into the persistent output type [1]. Note the names. "Degrade" is a reminder that the result loses the persistence guarantee.

A working example: a sealed bid
The example below follows the commit and reveal pattern from Midnight's documentation, using different names [2]. A bidder first seals an amount, and later opens it. I compiled it with the Compact compiler 0.35.0 to confirm that it is valid [5].
compact
pragma language_version >= 0.23;
import CompactStandardLibrary;
export ledger sealedBid: Bytes<32>;
export circuit placeBid(amount: Uint<64>, salt: Bytes<32>): [] {
const commitment = persistentCommit<Uint<64>>(amount, salt);
sealedBid = commitment;
}
export circuit openBid(amount: Uint<64>, salt: Bytes<32>): [] {
const commitment = persistentCommit<Uint<64>>(amount, salt);
assert(commitment == sealedBid, "Opening does not match the commitment");
}What happens here:
Sealing.
placeBidtakes the amount and a salt, computes the commitment, and stores only the commitment on the ledger. The amount and the salt are circuit inputs, which are private by default [2]. Observers see a 32-byte value that tells them nothing about the amount.Opening. Later,
openBidtakes the amount and salt again, recomputes the commitment, and usesassertto require that it matches the stored one. If the bidder tries a different amount, the assertion fails and the transaction is rejected. That is the binding promise at work.
Why persistentCommit and not transientCommit? The commitment is stored on the ledger and has to match a recomputation that may happen long after it was created, so the persistence guarantee matters. The commitment is also not wrapped in disclose(), because it is a commitment with a salt, which the documentation says the compiler can treat as sufficiently private to store publicly [2].
Two cautions. First, choose a fresh, unpredictable salt for every commitment and never reuse one. The documentation warns that reusing the same random value across different commitments lets an observer link them, which weakens privacy [2]. Second, this example is a teaching sketch. It does not decide who is allowed to place or open a bid, and a real contract would need access control, which Midnight's documentation describes as the developer's responsibility [2].

Where persistentHash shines: keys and nullifiers
Midnight's documentation shows two common uses of persistentHash that are worth knowing [2].
A hash-based public key. A contract can derive a "DApp-specific public key" by hashing a private secret key together with a short label. Only someone who knows the secret can recompute that value, which allows a contract to check authority without the secret ever appearing on-chain. The result is stored using disclose(), because a plain hash does not protect its input.
A nullifier. A nullifier is a one-way hash that marks a resource as used without revealing which resource it was. The contract keeps a set of used nullifiers, and before accepting a spend it checks that the new one is not already in the set. Outsiders can see that something was spent, but not what.
Both examples rely on the same habit: domain separation. Add a different text label to each purpose, for example one label for keys and another for nullifiers, so that a hash made for one purpose can never be mistaken for a hash made for another [2]. In Compact, the examples use pad(32, "some-label"), which turns a short text label into a 32-byte value that can be hashed together with the secret.
Common mistakes
Hashing a value that is easy to guess and publishing it. A birth year, a yes or no answer, or a small amount can be recovered by trying every possibility. Use a commitment with a random salt instead [2].
Reusing a salt. Each commitment needs its own fresh random value [2].
Using a transient function for data that will be stored. Transient results are not guaranteed to persist between upgrades, and the documentation says not to derive state data with them [1].
Using a persistent function for throwaway checks. It works, but it is not optimized for circuits, and the documentation recommends avoiding it for consistency checks when possible [1].
Forgetting
disclose()on a hash. The compiler will stop you, as my test confirmed. Do not adddisclose()blindly. Ask first whether the value is safe to make public, or whether a commitment is what you really wanted [2].Skipping domain separation. Use a different label for each purpose [2].
Quick decision guide
Is the value secret and guessable? Use a commitment. Is it already a long, unique secret? A hash may be enough.
Will the result be stored on the ledger, or used for authentication? Use persistent.
Is it only a short-lived check inside one interaction? Transient is an option.
When in doubt, choose persistent. It costs more inside a circuit, but it stays valid across upgrades [1].
Terms in plain words
Hash: a one-way fingerprint of some data.
Commitment: a sealed envelope, made from a value plus a random salt, that hides the value and locks you to it.
Salt (rand): a random secret number mixed into a commitment.
Circuit: a function in Compact whose execution is proven with zero-knowledge proofs.
Witness: a function that runs on the user's own machine and supplies private data to a circuit.
Ledger: the public on-chain state of a contract.
Disclose: a wrapper that tells the compiler a value is safe to appear publicly.
Nullifier: a one-way hash that marks something as used without revealing what it was.
Domain separation: adding a distinct label to a hash so it cannot be confused with another use.
Sources
Compact standard library, detailed API reference: official signatures and behavior of
transientHash,transientCommit,persistentHash,persistentCommit,degradeToTransientandupgradeFromTransientSmart contract security: official guidance on hash functions, commitment schemes, hiding and binding, disclosure, randomness reuse, nullifiers, domain separation and access control
Writing a contract: the official worked example of hash-based authority and a round counter
FIPS PUB 180-4, Secure Hash Standard: the U.S. National Institute of Standards and Technology specification of SHA-256
Compact compiler release compactc-v0.35.0: the compiler version used by the author to test the example and the disclosure behavior
The example and the disclosure tests were checked with the compiler's syntax and disclosure analysis, without generating proving keys. Compiler versions, language versions and library behavior change over time, so compile the example yourself and check the current documentation before relying on it.

Replies
Discussion
Join the discussion
Sign in with Google to reply. You stay on this article while you sign in.
No replies yet
Sign in with Google to start the discussion.