Commitment Structure
A commitment in the Specter protocol is a single Poseidon hash that binds a user to a piece of data without revealing what that data is. The commitment hash is stored on-chain (in a Merkle tree); the inputs to the hash — the phantom key — are kept privately by the user. Later, the user can prove knowledge of the inputs via a ZK proof without ever disclosing them.
Specter defines two commitment variants: a generic data commitment for arbitrary data (credentials, documents, API keys, encrypted secrets) and a token commitment for GHOST token amounts. Both variants use the same cryptographic structure — they differ only in the number and semantics of their inputs.
Generic Data Commitment (4-Input)
The data commitment is the protocol's most general-purpose primitive. It accepts any data hash and makes no assumptions about what the data represents.
commitment = Poseidon4(secret, nullifierSecret, dataHash, blinding)
Field Descriptions
| Field | Size | Purpose |
|---|---|---|
secret | 254-bit random | Ownership proof. Only the holder of this value can generate a valid ZK proof for this commitment. Acts as the "private key" for the commitment. |
nullifierSecret | 254-bit random | Nullifier derivation. Used to compute the deterministic nullifier that prevents double-reveal. Separated from secret so that nullifier computation does not leak ownership information. |
dataHash | 254-bit | Data binding. The Poseidon or Keccak hash of the actual data being committed. Intentionally generic — this can be the hash of an encrypted credential, an image, an API key, a message, or any other data. The protocol imposes no schema. |
blinding | 254-bit random | Brute-force protection. Prevents an attacker who knows (or can guess) the dataHash from testing all possible commitments to find a match. Even if the data is known, the random blinding factor makes the commitment unpredictable. |
Why Four Inputs?
Each input serves a distinct security purpose that cannot be combined:
secretandnullifierSecretmust be separate because the nullifier computation is public (the nullifier is published on-chain during reveal), while the secret must remain permanently private. If the same value were used for both, publishing the nullifier would leak information about the ownership secret.dataHashis the payload — without it, the commitment would not be bound to any data.blindingis necessary becausedataHashalone may have low entropy. If the committed data is one of a small set of known values (e.g., a credential type), an attacker could hash each candidate and compare against on-chain commitments. The random blinding factor makes this infeasible.
Use Cases
The 4-input commitment is used by:
- OpenGhostVault — for one-time data reveals (publish a secret, prove a credential, share an API key)
- PersistentKeyVault — for reusable data access (encrypted storage keys that can be accessed repeatedly without spending the nullifier)
Token Commitment (7-Input)
The token commitment extends the data commitment with three additional fields specific to token transfer: an identifier for which token, an amount, and policy binding parameters.
commitment = Poseidon7(secret, nullifierSecret, tokenId, amount, blinding, policyId, policyParamsHash)
Field Descriptions
| Field | Size | Purpose |
|---|---|---|
secret | 254-bit random | Same as data commitment — ownership proof. |
nullifierSecret | 254-bit random | Same as data commitment — nullifier derivation. |
tokenId | 254-bit | Token identification. Computed as Poseidon2(tokenAddress, 0) where tokenAddress is the ERC-20 contract address (or a sentinel value for native GHOST). Hashing the address ensures it fits in the BN254 field. |
amount | Up to 252-bit | Token quantity. The number of tokens (in the token's smallest unit, e.g., aghost for GHOST) being committed. Must be > 0 at commit time. Used for amount conservation checks during reveal. |
blinding | 254-bit random | Same as data commitment — brute-force protection. |
policyId | 254-bit | Policy contract address. The Ethereum address of the policy contract that must validate this commitment's reveal. Set to 0 if no policy is applied. Because it is an input to the commitment hash, the policy is cryptographically bound — it cannot be changed or removed after commit. |
policyParamsHash | 254-bit | Policy parameters. Computed as keccak256(policyParams) % BN254_FIELD_PRIME. The policy parameters (timelock windows, recipient allowlists, etc.) are also cryptographically bound to the commitment. |
Token ID Computation
Token IDs are computed deterministically from the token's contract address:
tokenId = Poseidon2(tokenAddress, 0)
The second input is always 0 (reserved for future use, such as chain identifiers for cross-chain tokens). This hash serves two purposes:
- Field compatibility. Ethereum addresses are 160-bit values, which fit in the BN254 field, but hashing them produces a uniform distribution over the field — preventing any special-case behavior based on address structure.
- Circuit efficiency. The circuit can verify token binding with a single Poseidon hash rather than address parsing.
Policy Binding
The policyId and policyParamsHash fields are baked into the commitment hash at commit time. This means:
- The policy contract address is fixed when the commitment is created
- The policy parameters (encoded as an ABI-encoded byte array, then hashed) are fixed when the commitment is created
- During reveal, the ZK circuit enforces that the
policyIdandpolicyParamsHashin the proof match what was committed - The on-chain verifier independently confirms
keccak256(policyParams) % p == policyParamsHash - The policy contract is called with the decoded parameters and must return
truefor the reveal to succeed
This ensures that policies cannot be stripped, modified, or bypassed after commitment. If you commit tokens with a timelock policy, that timelock is permanent — there is no way to reveal them outside the specified time window, even by the original committer.
Use Cases
The 7-input commitment is used by:
- CommitRevealVault / BatchCommitRevealVault — for private GHOST token transfers with optional policy constraints
Comparing the Two Variants
| Property | Data Commitment (4-input) | Token Commitment (7-input) |
|---|---|---|
| Hash function | PoseidonT5 | PoseidonT8 |
| Input count | 4 | 7 |
| Data binding | dataHash (any data) | tokenId + amount (specific token and quantity) |
| Policy support | No | Yes (policyId + policyParamsHash) |
| Token operations | None (no mint/burn) | Burn on commit, mint on reveal |
| Vault | OpenGhostVault, PersistentKeyVault | CommitRevealVault |
| Reveal type | One-time (OpenGhostVault) or repeatable (PersistentKeyVault) | One-time (nullifier spent) |
| Partial reveal | No | Yes (change commitment for remainder) |
BN254 Field Constraint
All commitment inputs must be valid BN254 scalar field elements. This means every value must be a non-negative integer strictly less than the field prime:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
This is a ~254-bit prime. In practice:
- Random secrets, nullifier secrets, and blinding factors are generated as random 254-bit integers and checked to be less than
p - Token amounts are naturally small enough (max 252-bit to allow non-negative range checks in the circuit)
- Ethereum addresses (160-bit) fit easily within the field
- Keccak-256 outputs (256-bit) must be reduced modulo
pbefore use as circuit inputs — this is howpolicyParamsHashis derived
The smart contracts validate this constraint at commit time, rejecting any commitment that is not a valid field element. The ZK circuits enforce field membership implicitly through their arithmetic — any out-of-field value would cause the proof to fail.
Commitment Lifecycle
The phantom key — the set of input values to the commitment hash — is the user's private credential. Whoever possesses the phantom key controls the commitment. The key is never transmitted to the chain; only the hash output appears on-chain. Proving knowledge of the key (via a ZK proof) is the only way to reveal or access the committed data.