Transaction
5F6AB255FCF331…5D4ABEC1676E
Block 25,929 · index 0 · indexed
Summary
- Hash
- 5F6AB255FCF331EC82ACA82B8E81A23B5CBE9579D827BD2D4D295D4ABEC1676E
- Block
- 25,929
- Size
- 7764 bytes
- Gas used
- 12,054,046 / 14,464,915
- Fee
- 1000000ugnot
- Memo
- gnopublish
- Status
- success
Messages
Arguments · 9
- #1commitreveal
- #2README.md
- #3# `gno.land/p/moul/x/daily/commitreveal/v0` **Commit–reveal scheme** — `Commit`, `MustCommit`, `Verify`, `Open`, `ValidCommitment`, `MinSaltLen`, `MaxValueLen`, `DigestLen`. ```go import "gno.land/p/moul/x/daily/commitreveal/v0" c, _ := commitreveal.Commit("rock", "alice-secret-salt-1") // phase 1: publish c commitreveal.Open(c, "rock", "alice-secret-salt-1") // phase 2: nil commitreveal.Open(c, "paper", "alice-secret-salt-1") // ErrMismatch ``` A transaction is public before it executes, so a naive sealed-bid auction or simultaneous-move game lets whoever moves last read everyone else's move and win for free. Commit–reveal splits the action: publish `H(value ‖ salt)` first, open it later. **The salt is enforced, not advised.** Rock-paper-scissors has three possible moves, so an unsalted commitment has three possible hashes and is broken by trying all of them. `Commit` refuses a salt shorter than `MinSaltLen` (16), and `Verify` enforces the same floor — a short salt cannot be smuggled past it. Two details that are easy to get wrong, both tested: - **Length-prefixed hashing.** With plain concatenation `("ab","cd…")` and `("abc","d…")` produce identical bytes, so one commitment could be opened two different ways. The lengths are hashed in. - **Constant-time digest comparison.** A short-circuiting check leaks, through timing, how many leading bytes of a guess were right — enough to reconstruct a commitment byte by byte. This package computes and checks commitments. It stores nothing and knows nothing about phases or deadlines; the realm owns that. **Live demo:** [`r/moul/x/daily/commitrevealdemo`](https://github.com/moul/gno-contracts/tree/main/r/moul/x/daily/commitrevealdemo/v0) · render it at [`/r/moul/x/daily/commitrevealdemo/v0`](https://gno.land/r/moul/x/daily/commitrevealdemo/v0). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4commitreveal.gno
- #5// Package commitreveal implements the commit-then-reveal scheme as a pure, // reusable package. // // The problem it solves: anything submitted to a chain is public before it is // executed, so a naive sealed-bid auction or simultaneous-move game lets the // last player read everyone else's move and win for free. Commit-reveal splits // the action in two — first publish H(value || salt), later publish the value // and salt. The commitment binds you to a choice without disclosing it. // // The SALT is not optional and this package refuses to let a caller skip it. // Without one, a commitment over a small domain is trivially brute-forced: a // rock-paper-scissors move has three possible hashes, so hashing all three // breaks the scheme entirely. MinSaltLen is enforced at commit time rather than // left as advice in a comment. // // Verification is CONSTANT-TIME over the digest. A short-circuiting comparison // leaks, through timing, how many leading bytes of a guess were right, which is // enough to reconstruct a commitment byte by byte. // // This package computes and checks commitments; it stores nothing and knows // nothing about phases or deadlines. The realm owns that. // // A live demo of this package is at // [r/moul/x/daily/commitrevealdemo](/r/moul/x/daily/commitrevealdemo/v0). package commitreveal import ( "crypto/sha256" "encoding/hex" "errors" ) // MinSaltLen is the shortest salt accepted. Short salts make a small-domain // commitment brute-forceable, which defeats the whole scheme. const MinSaltLen = 16 // MaxValueLen bounds the committed value so gas stays predictable. const MaxValueLen = 4096 var ( ErrShortSalt = errors.New("commitreveal: salt is shorter than MinSaltLen") ErrLongValue = errors.New("commitreveal: value exceeds MaxValueLen") ErrMismatch = errors.New("commitreveal: reveal does not match the commitment") ErrBadHexDigest = errors.New("commitreveal: commitment is not a valid hex digest") ) // DigestLen is the length in bytes of a commitment digest (SHA-256). const DigestLen = 32 // Commit returns the hex-encoded commitment for value and salt. // // The salt is length-prefixed rather than simply concatenated: with plain // concatenation, ("ab","cd") and ("a","bcd") hash identically, so one // commitment could be opened two different ways. func Commit(value, salt string) (string, error) { if len(salt) < MinSaltLen { return "", ErrShortSalt } if len(value) > MaxValueLen { return "", ErrLongValue } return hex.EncodeToString(digest(value, salt)), nil } // MustCommit is Commit, panicking on invalid input. For tests and for callers // that have already validated. func MustCommit(value, salt string) string { c, err := Commit(value, salt) if err != nil { panic(err.Error()) } return c } // Verify reports whether value and salt open the given commitment. The digest // comparison is constant-time. func Verify(commitment, value, salt string) bool { if len(salt) < MinSaltLen || len(value) > MaxValueLen { return false } want, err := hex.DecodeString(commitment) if err != nil || len(want) != DigestLen { return false } return equalConstantTime(want, digest(value, salt)) } // Open verifies and reports why it failed, for callers wanting a reason rather // than a bool. func Open(commitment, value, salt string) error { if len(salt) < MinSaltLen { return ErrShortSalt } if len(value) > MaxValueLen { return ErrLongValue } want, err := hex.DecodeString(commitment) if err != nil || len(want) != DigestLen { return ErrBadHexDigest } if !equalConstantTime(want, digest(value, salt)) { return ErrMismatch } return nil } // ValidCommitment reports whether s is well-formed as a commitment: hex, and // exactly DigestLen bytes. It says nothing about what it commits to. func ValidCommitment(s string) bool { b, err := hex.DecodeString(s) return err == nil && len(b) == DigestLen } // digest computes SHA-256 over a length-prefixed encoding of value and salt. // gno's crypto/sha256 exposes only Sum256, so the message is assembled first // rather than streamed through a hash.Hash. func digest(value, salt string) []byte { buf := make([]byte, 0, 8+len(value)+len(salt)) buf = append(buf, lengthPrefix(len(value))...) buf = append(buf, value...) buf = append(buf, lengthPrefix(len(salt))...) buf = append(buf, salt...) sum := sha256.Sum256(buf) return sum[:] } // lengthPrefix encodes n as 4 big-endian bytes. func lengthPrefix(n int) []byte { return []byte{ byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n), } } // equalConstantTime compares two byte slices without short-circuiting, so the // time taken does not reveal how many leading bytes matched. func equalConstantTime(a, b []byte) bool { if len(a) != len(b) { return false } var diff byte for i := range a { diff |= a[i] ^ b[i] } return diff == 0 }
- #6gnomod.toml
- #7module = "gno.land/p/moul/x/daily/commitreveal/v0" gno = "0.9"
- #8/gno.MemPackageType
- #9 MPUserProd
Result log
msg:0,success:true,log:,events:[]