How it works.
A commit-reveal scheme has two moves: bind now, reveal later. The library adds an optional third — prove you can reveal, without revealing. Here is the construction for each, grounded in the source.
Figure 1 — the two-phase flow
COMMIT PHASE REVEAL PHASE
┌──────────────────────────┐ ┌──────────────────────────┐
│ value │ │ value, salt │
│ │ │ │ │ │
│ ▼ salt ← CSPRNG(32B) │ │ ▼ │
│ H(value ‖ salt) │ │ H(value ‖ salt) │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ commitment ──── publish ──────────▶ compare_digest(·, commitment)
│ (salt stays hidden) │ │ │
└──────────────────────────┘ │ ▼ True / False │
└──────────────────────────┘
OPTIONAL: prove knowledge without revealing (Schnorr, non-interactive)
secret ← f(value, salt) P = secret·G k ← CSPRNG R = k·G
e = SHA256(P ‖ R ‖ commitment) s = k + e·secret → (P, R, e, s)
verify: s·G == R + e·P ‖ is byte concatenation. G is the
secp256k1 base point. The exact transcript the library returns is
(public_key, R_compressed, challenge, response).
1 · The commitment
A commitment is H(value ‖ salt). The salt defaults to
secrets.token_bytes(32) — 32 bytes from the OS CSPRNG. The hash is chosen per scheme
instance from an eight-algorithm allowlist; md5 and sha1 are rejected at
construction with SecurityError, and anything off the list raises
ValidationError. Two properties matter:
- Hiding. The commitment reveals nothing about the value — a good hash over a 256-bit salt is infeasible to invert or dictionary-attack.
- Binding. You cannot later find a different (value′, salt′) that hashes to the same commitment. That is exactly the collision resistance of the underlying hash.
2 · The reveal
To reveal, you re-supply value and salt;
the library recomputes the hash and compares it against the stored commitment using
hmac.compare_digest. That comparison is constant-time: it does not short-circuit on the
first differing byte, so it does not leak how much of a guess was correct via timing. Using plain
== here would be a real, exploitable bug — the library does not.
3 · The zero-knowledge proof
When you construct the scheme with use_zkp=True, you can produce a Schnorr proof of
knowledge. The interactive Schnorr protocol proves "I know the discrete log of
P" in three messages; the library collapses it into a single
non-interactive proof using the Fiat–Shamir heuristic, deriving the verifier's
challenge as SHA-256 over the transcript instead of asking a live verifier.
The curve is secp256k1 — the same curve Bitcoin uses — implemented by
hand in zkp.py with the canonical parameters (p,
a = 0, b = 7,
G, n), affine point addition and
scalar multiplication, and 0x02 / 0x03 point compression. The nonce
k comes from secrets, never random.
This implementation is affine and not constant-time at the scalar-multiply level. It is built for protocol prototyping and low-throughput use, not HFT-scale proving. For pairing curves, BLS, or SNARK arithmetic, see the py_ecc comparison.
Table 1 — module map
What lives where
Seven small files under commit_reveal/. The cryptographic
core and the ZKP are cleanly separated from validation, audit, and the CLIs.
| File | Responsibility |
|---|---|
| commit_reveal/core.py | CommitRevealScheme: commit, reveal/verify, hash selection, salt generation. |
| commit_reveal/zkp.py | Hand-rolled secp256k1 + non-interactive Schnorr proof via Fiat–Shamir. |
| commit_reveal/validation.py | Input validation and the hash-algorithm allowlist; ValidationError / SecurityError. |
| commit_reveal/audit.py | Operation audit trail, separate from the cryptographic core. |
| commit_reveal/secure_cli.py | No-plaintext-at-rest CLI: getpass entry, 0600 files, sanitized names. |
| commit_reveal/cli.py | Deprecated legacy CLI, kept for backward compatibility. |
| commit_reveal/migrate.py | Migrates commitment files written in older formats. |
Read on
The threat model is explicit.
See exactly what is in and out of scope, or jump to the quickstart to run it.