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.
Commitments are the binding 'nail down a value' operation at the heart of every ZK scheme. Pedersen commitments live in a group (good for linear arithmetic: Com(a) + Com(b) = Com(a+b)), Merkle commitments use hashing (good for set membership and variable-length data), and KZG commitments are polynomial commitments with constant-size openings (the engine of every modern SNARK). Knowing which commitment to use in which context is a prerequisite for reading any zk paper.
A Pedersen commitment: Com(m, r) = m·G + r·H where G and H are two generators in an elliptic curve group (with unknown discrete log between them). Binding: can't open to a different m. Hiding: r makes Com statistically indistinguishable from random. Homomorphic: Com(a) + Com(b) = Com(a+b). These three properties are why Pedersen is in virtually every range proof, Bulletproof, and Halo2 witness encoding.
// main.go — Pedersen commitment: binding + hiding + additively homomorphic
// go mod init pedersen && go get github.com/decred/dcrd/dcrec/secp256k1/v4
package main
import (
"crypto/rand"
"crypto/sha256"
"fmt"
"math/big"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
var curve = secp256k1.S256()
// fieldOrder is the group order n of secp256k1
var n = curve.N
// secondGenerator derives a second base point H by hashing a seed
func secondGenerator(seed []byte) (hx, hy *big.Int) {
hash := sha256.Sum256(seed)
h := new(big.Int).SetBytes(hash[:])
h.Mod(h, n)
hx, hy = curve.ScalarBaseMult(h.Bytes())
return
}
// pointAdd adds two curve points
func pointAdd(ax, ay, bx, by *big.Int) (*big.Int, *big.Int) {
return curve.Add(ax, ay, bx, by)
}
// scalarMult multiplies a point by a scalar
func scalarMult(px, py, scalar *big.Int) (*big.Int, *big.Int) {
return curve.ScalarMult(px, py, scalar.Bytes())
}
// commit computes C = m*G + r*H; generates random r if nil
func commit(m *big.Int, r *big.Int, Hx, Hy *big.Int) (cx, cy, rOut *big.Int) {
if r == nil {
rb := make([]byte, 32)
rand.Read(rb)
r = new(big.Int).SetBytes(rb)
r.Mod(r, n)
}
// m*G
mgx, mgy := curve.ScalarBaseMult(m.Bytes())
// r*H
rhx, rhy := scalarMult(Hx, Hy, r)
cx, cy = pointAdd(mgx, mgy, rhx, rhy)
return cx, cy, r
}
func main() {
Hx, Hy := secondGenerator([]byte("pedersen-H"))
// 1) Binding + Hiding
c1x, c1y, r1 := commit(big.NewInt(42), nil, Hx, Hy)
c2x, _, _ := commit(big.NewInt(42), nil, Hx, Hy)
fmt.Println("same value, different commits:", c1x.Cmp(c2x) != 0) // hiding
// 2) Additively homomorphic
cax, cay, ra := commit(big.NewInt(3), nil, Hx, Hy)
cbx, cby, rb := commit(big.NewInt(5), nil, Hx, Hy)
csumx, csumy := pointAdd(cax, cay, cbx, cby)
rSum := new(big.Int).Add(ra, rb)
rSum.Mod(rSum, n)
cexpx, _, _ := commit(big.NewInt(8), rSum, Hx, Hy)
_ = csumy
fmt.Println("homomorphic:", csumx.Cmp(cexpx) == 0) // True
// 3) Verify an opening
verifyOpen := func(commitX, commitY, m, r *big.Int) bool {
vx, vy := commit(m, r, Hx, Hy)
return vx.Cmp(commitX) == 0 && vy.Cmp(commitY) == 0
}
fmt.Println("valid opening:", verifyOpen(c1x, c1y, big.NewInt(42), r1)) // True
fmt.Println("fake opening:", verifyOpen(c1x, c1y, big.NewInt(43), r1)) // False (binding)
}
go run main.goUse these three in order. Each builds on the one before.
In one paragraph, explain what a commitment scheme is and the three concrete flavours (Pedersen, Merkle, KZG) — what each is best at, and what 'binding' and 'hiding' mean.
Walk me through the Pedersen commitment scheme step by step: why two generators with unknown discrete log are necessary, how the randomness gives hiding, and how group-operation commutativity gives the homomorphism.
Given I'm designing a ZK rollup where I need to commit to a vector of 2^20 balances and prove individual balance updates, which commitment scheme do I choose (Pedersen, Merkle, KZG, Verkle) and what's the proof size / update cost trade-off for each?