Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

commitreveal.gno

4.75 Kb · 145 lines
  1// Package commitreveal implements the commit-then-reveal scheme as a pure,
  2// reusable package.
  3//
  4// The problem it solves: anything submitted to a chain is public before it is
  5// executed, so a naive sealed-bid auction or simultaneous-move game lets the
  6// last player read everyone else's move and win for free. Commit-reveal splits
  7// the action in two — first publish H(value || salt), later publish the value
  8// and salt. The commitment binds you to a choice without disclosing it.
  9//
 10// The SALT is not optional and this package refuses to let a caller skip it.
 11// Without one, a commitment over a small domain is trivially brute-forced: a
 12// rock-paper-scissors move has three possible hashes, so hashing all three
 13// breaks the scheme entirely. MinSaltLen is enforced at commit time rather than
 14// left as advice in a comment.
 15//
 16// Verification is CONSTANT-TIME over the digest. A short-circuiting comparison
 17// leaks, through timing, how many leading bytes of a guess were right, which is
 18// enough to reconstruct a commitment byte by byte.
 19//
 20// This package computes and checks commitments; it stores nothing and knows
 21// nothing about phases or deadlines. The realm owns that.
 22//
 23// A live demo of this package is at
 24// [r/moul/x/daily/commitrevealdemo](/r/moul/x/daily/commitrevealdemo/v0).
 25package commitreveal
 26
 27import (
 28	"crypto/sha256"
 29	"encoding/hex"
 30	"errors"
 31)
 32
 33// MinSaltLen is the shortest salt accepted. Short salts make a small-domain
 34// commitment brute-forceable, which defeats the whole scheme.
 35const MinSaltLen = 16
 36
 37// MaxValueLen bounds the committed value so gas stays predictable.
 38const MaxValueLen = 4096
 39
 40var (
 41	ErrShortSalt   = errors.New("commitreveal: salt is shorter than MinSaltLen")
 42	ErrLongValue   = errors.New("commitreveal: value exceeds MaxValueLen")
 43	ErrMismatch    = errors.New("commitreveal: reveal does not match the commitment")
 44	ErrBadHexDigest = errors.New("commitreveal: commitment is not a valid hex digest")
 45)
 46
 47// DigestLen is the length in bytes of a commitment digest (SHA-256).
 48const DigestLen = 32
 49
 50// Commit returns the hex-encoded commitment for value and salt.
 51//
 52// The salt is length-prefixed rather than simply concatenated: with plain
 53// concatenation, ("ab","cd") and ("a","bcd") hash identically, so one
 54// commitment could be opened two different ways.
 55func Commit(value, salt string) (string, error) {
 56	if len(salt) < MinSaltLen {
 57		return "", ErrShortSalt
 58	}
 59	if len(value) > MaxValueLen {
 60		return "", ErrLongValue
 61	}
 62	return hex.EncodeToString(digest(value, salt)), nil
 63}
 64
 65// MustCommit is Commit, panicking on invalid input. For tests and for callers
 66// that have already validated.
 67func MustCommit(value, salt string) string {
 68	c, err := Commit(value, salt)
 69	if err != nil {
 70		panic(err.Error())
 71	}
 72	return c
 73}
 74
 75// Verify reports whether value and salt open the given commitment. The digest
 76// comparison is constant-time.
 77func Verify(commitment, value, salt string) bool {
 78	if len(salt) < MinSaltLen || len(value) > MaxValueLen {
 79		return false
 80	}
 81	want, err := hex.DecodeString(commitment)
 82	if err != nil || len(want) != DigestLen {
 83		return false
 84	}
 85	return equalConstantTime(want, digest(value, salt))
 86}
 87
 88// Open verifies and reports why it failed, for callers wanting a reason rather
 89// than a bool.
 90func Open(commitment, value, salt string) error {
 91	if len(salt) < MinSaltLen {
 92		return ErrShortSalt
 93	}
 94	if len(value) > MaxValueLen {
 95		return ErrLongValue
 96	}
 97	want, err := hex.DecodeString(commitment)
 98	if err != nil || len(want) != DigestLen {
 99		return ErrBadHexDigest
100	}
101	if !equalConstantTime(want, digest(value, salt)) {
102		return ErrMismatch
103	}
104	return nil
105}
106
107// ValidCommitment reports whether s is well-formed as a commitment: hex, and
108// exactly DigestLen bytes. It says nothing about what it commits to.
109func ValidCommitment(s string) bool {
110	b, err := hex.DecodeString(s)
111	return err == nil && len(b) == DigestLen
112}
113
114// digest computes SHA-256 over a length-prefixed encoding of value and salt.
115// gno's crypto/sha256 exposes only Sum256, so the message is assembled first
116// rather than streamed through a hash.Hash.
117func digest(value, salt string) []byte {
118	buf := make([]byte, 0, 8+len(value)+len(salt))
119	buf = append(buf, lengthPrefix(len(value))...)
120	buf = append(buf, value...)
121	buf = append(buf, lengthPrefix(len(salt))...)
122	buf = append(buf, salt...)
123	sum := sha256.Sum256(buf)
124	return sum[:]
125}
126
127// lengthPrefix encodes n as 4 big-endian bytes.
128func lengthPrefix(n int) []byte {
129	return []byte{
130		byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),
131	}
132}
133
134// equalConstantTime compares two byte slices without short-circuiting, so the
135// time taken does not reveal how many leading bytes matched.
136func equalConstantTime(a, b []byte) bool {
137	if len(a) != len(b) {
138		return false
139	}
140	var diff byte
141	for i := range a {
142		diff |= a[i] ^ b[i]
143	}
144	return diff == 0
145}