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 is5// executed, so a naive sealed-bid auction or simultaneous-move game lets the6// last player read everyone else's move and win for free. Commit-reveal splits7// the action in two — first publish H(value || salt), later publish the value8// 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: a12// rock-paper-scissors move has three possible hashes, so hashing all three13// breaks the scheme entirely. MinSaltLen is enforced at commit time rather than14// left as advice in a comment.15//16// Verification is CONSTANT-TIME over the digest. A short-circuiting comparison17// leaks, through timing, how many leading bytes of a guess were right, which is18// enough to reconstruct a commitment byte by byte.19//20// This package computes and checks commitments; it stores nothing and knows21// nothing about phases or deadlines. The realm owns that.22//23// A live demo of this package is at24// [r/moul/x/daily/commitrevealdemo](/r/moul/x/daily/commitrevealdemo/v0).25package commitreveal2627import (28 "crypto/sha256"29 "encoding/hex"30 "errors"31)3233// MinSaltLen is the shortest salt accepted. Short salts make a small-domain34// commitment brute-forceable, which defeats the whole scheme.35const MinSaltLen = 163637// MaxValueLen bounds the committed value so gas stays predictable.38const MaxValueLen = 40963940var (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)4647// DigestLen is the length in bytes of a commitment digest (SHA-256).48const DigestLen = 324950// Commit returns the hex-encoded commitment for value and salt.51//52// The salt is length-prefixed rather than simply concatenated: with plain53// concatenation, ("ab","cd") and ("a","bcd") hash identically, so one54// commitment could be opened two different ways.55func Commit(value, salt string) (string, error) {56 if len(salt) < MinSaltLen {57 return "", ErrShortSalt58 }59 if len(value) > MaxValueLen {60 return "", ErrLongValue61 }62 return hex.EncodeToString(digest(value, salt)), nil63}6465// MustCommit is Commit, panicking on invalid input. For tests and for callers66// 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 c73}7475// Verify reports whether value and salt open the given commitment. The digest76// comparison is constant-time.77func Verify(commitment, value, salt string) bool {78 if len(salt) < MinSaltLen || len(value) > MaxValueLen {79 return false80 }81 want, err := hex.DecodeString(commitment)82 if err != nil || len(want) != DigestLen {83 return false84 }85 return equalConstantTime(want, digest(value, salt))86}8788// Open verifies and reports why it failed, for callers wanting a reason rather89// than a bool.90func Open(commitment, value, salt string) error {91 if len(salt) < MinSaltLen {92 return ErrShortSalt93 }94 if len(value) > MaxValueLen {95 return ErrLongValue96 }97 want, err := hex.DecodeString(commitment)98 if err != nil || len(want) != DigestLen {99 return ErrBadHexDigest100 }101 if !equalConstantTime(want, digest(value, salt)) {102 return ErrMismatch103 }104 return nil105}106107// ValidCommitment reports whether s is well-formed as a commitment: hex, and108// 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) == DigestLen112}113114// 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 first116// 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}126127// 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}133134// equalConstantTime compares two byte slices without short-circuiting, so the135// time taken does not reveal how many leading bytes matched.136func equalConstantTime(a, b []byte) bool {137 if len(a) != len(b) {138 return false139 }140 var diff byte141 for i := range a {142 diff |= a[i] ^ b[i]143 }144 return diff == 0145}146Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.