Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
The simplest concrete commitment scheme is where is a cryptographic hash. It's used everywhere — STARKs, Merkle trees, Fiat–Shamir transcripts, sealed-bid auctions — because it's fast, post-quantum-friendly, and has no trusted setup. But it's only secure under non-trivial assumptions about : collision resistance gives binding, and 'random-oracle-like' behaviour gives hiding. Spelling out exactly which property of buys which property of the commitment is the point of this task. Once you can do that, you can read every Merkle-based ZK paper and immediately see which assumptions are doing the work.
Let be a cryptographic hash. Define where is uniform in . Binding follows from collision resistance of . Hiding follows from being a one-way function on the high-entropy input — formally, modelled as a random oracle, is uniform and independent of .
Use these three in order. Each builds on the one before.
In one paragraph, explain how SHA-256 with a 256-bit random nonce gives a usable commitment scheme. Why does the nonce matter — what attack works without it?
Walk me through the proofs that hash-based commitments are (a) computationally binding under collision resistance and (b) computationally hiding in the random-oracle model. Where exactly does each assumption show up in the reduction?
STARKs use hash-based commitments throughout (Merkle trees of polynomial evaluations) and are therefore plausibly post-quantum, while Pedersen commitments are not. Explain *why* the post-quantum status differs, and what that buys you in practice (transparent setup, conjectured PQ security) at the cost of larger proofs.
// main.go
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func commit(m, r []byte) []byte {
h := sha256.New()
h.Write(m)
h.Write(r)
return h.Sum(nil)
}
func open(c, m, r []byte) bool {
got := commit(m, r)
if len(got) != len(c) {
return false
}
for i := range got {
if got[i] != c[i] {
return false
}
}
return true
}
func main() {
// Honest flow
m := []byte("pay alice 100 sats")
r := make([]byte, 32) // 256-bit randomness
rand.Read(r)
c := commit(m, r)
fmt.Printf("commitment : %s ...\n", hex.EncodeToString(c)[:32])
fmt.Printf("open : %v\n", open(c, m, r))
// Without randomness, the scheme is NOT hiding (low-entropy m is brute-forceable).
mLow := []byte("yes") // only a few likely messages
sum := sha256.Sum256(mLow)
cNoR := sum[:]
// Attacker tries all small messages:
for _, guess := range [][]byte{[]byte("yes"), []byte("no"), []byte("maybe")} {
gs := sha256.Sum256(guess)
if hex.EncodeToString(gs[:]) == hex.EncodeToString(cNoR) {
fmt.Printf("Recovered low-entropy message without randomness: %s\n", guess)
break
}
}
}go run main.go