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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}