package core import ( "chain" "chain/runtime" "gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/safe/v1" "gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/types/v1" ) // Commit–reveal v2 (design §4.2–4.3, closes RA H-3, M-5, M-6). // // The map is fixed by three inputs: the creator's secret sc, the beacon's // secret sb and the public mint-entropy accumulator acc. Neither committer // knows the other's secret while minting is open, so neither can time the // last mint for a good map. Order of reveals: // // 1. nothing is accepted before Closed (an early reveal informs the other // side); // 2. the creator may reveal until revealDeadline - BeaconGraceBlocks; the // beacon follows at any time before the deadline; // 3. inside the last BeaconGraceBlocks only the beacon may reveal. If the // creator stayed silent, the beacon's reveal finalizes at once // ("beacon-only"), so the creator never sees sb while it can still // choose (SA-M1; this also closes the mempool race of a creator // front-running the beacon's grace reveal); // 4. after revealDeadline anyone finalizes with whatever was revealed. // // What stays open, by design: the beacon, as last revealer after the // creator, can still choose between revealing and not (one bit). It runs // public code and the badge shows the outcome. The transfer lock // (tokens.gno) keeps anyone from trading on early knowledge until Final. // // A collection launched without the beacon (only possible while no beacon // is configured, SA-M2) gives the creator the whole choice of the map; it // can have no creator reserve and is never reported as a fair launch // (FairLaunch, Provenance "fair"). type provenance struct { creatorCommit, beaconCommit string acc string creatorSecret, beaconSecret string creatorRevealed bool beaconRevealed bool salt, badge string finalHeight int64 } const ( roleCreator = "creator" roleBeacon = "beacon" ) // BeaconCommit is the beacon's commitment, before any token exists. func BeaconCommit(cur realm, cid, commitHex string) { assertNoDeposit(cur) c := mustCollection(cid) if beacon == "" || caller(cur) != beacon { panic("only the configured beacon can commit") } if c.state == types.StateDraft || c.prov == nil || !c.useBeacon { panic("this collection is not waiting for a beacon") } if c.prov.beaconCommit != "" { panic("the beacon already committed") } if c.minted() > 0 { panic("too late: tokens were minted before the beacon committed") } commit := safe.AssertHex("commit", commitHex, 32) if isBurned(c, commit) { panic("this commitment's secret was already revealed for this collection") } c.prov.beaconCommit = commit chain.Emit("BeaconCommitted", "cid", cid, "commit", commit) } func RevealCreatorSecret(cur realm, cid, secret string) { assertNoDeposit(cur) c := mustCollection(cid) assertCreator(cur, c) h := height() assertRevealOpen(c, h) if c.prov.creatorRevealed { panic("the creator secret is already revealed") } if c.useBeacon { // SA-M1: once the grace period starts only the beacon may reveal, so // the creator can never reveal after seeing sb. if c.prov.beaconRevealed { panic("the beacon revealed first; the outcome is fixed") } if h >= revealDeadline(c)-BeaconGraceBlocks { panic("the creator's reveal window ended at height " + itoa(revealDeadline(c)-BeaconGraceBlocks-1) + "; only the beacon may reveal now") } } assertSecret(c, roleCreator, secret, c.prov.creatorCommit) c.prov.creatorSecret = secret c.prov.creatorRevealed = true chain.Emit("CreatorRevealed", "cid", cid, "secret", secret) if revealsComplete(c) { finalize(c, h) } } func RevealBeaconSecret(cur realm, cid, secret string) { assertNoDeposit(cur) c := mustCollection(cid) if beacon == "" || caller(cur) != beacon { panic("only the configured beacon can reveal") } h := height() assertRevealOpen(c, h) if !c.useBeacon || c.prov.beaconCommit == "" { panic("this collection has no beacon commitment") } if c.prov.beaconRevealed { panic("the beacon secret is already revealed") } if !c.prov.creatorRevealed && h < revealDeadline(c)-BeaconGraceBlocks { panic("the beacon reveals after the creator, or in the window's last " + itoa(BeaconGraceBlocks) + " blocks") } assertSecret(c, roleBeacon, secret, c.prov.beaconCommit) c.prov.beaconSecret = secret c.prov.beaconRevealed = true chain.Emit("BeaconRevealed", "cid", cid, "secret", secret) // After the creator: both secrets are in (2/2). First, in the grace // period: the creator can no longer reveal, so the map is fixed now // (beacon-only) — nobody is left holding a choice (SA-M1). finalize(c, h) } // Finalize is permissionless and never paused (I-2): immediately once every // expected secret is revealed, otherwise after revealDeadline (fallback). func Finalize(cur realm, cid string) { assertNoDeposit(cur) c := mustCollection(cid) h := height() refresh(c, h) if c.state != types.StateClosed { panic("only a closed collection can be finalized (state " + c.state.String() + ")") } if !revealsComplete(c) && h <= revealDeadline(c) { panic("the reveal window is open until height " + itoa(revealDeadline(c))) } // SA-I9: a sale that minted nothing stays Closed, so its creator can // still abort and relaunch; nobody else can lock it into Final. if c.minted() == 0 { panic("nothing was minted; the creator may abort and relaunch instead") } finalize(c, h) } func assertRevealOpen(c *collection, h int64) { refresh(c, h) if c.state != types.StateClosed { panic("secrets are accepted only after the sale closed (state " + c.state.String() + ")") } if h > revealDeadline(c) { panic("the reveal window closed; call Finalize") } } func assertSecret(c *collection, role, secret, commit string) { if len(secret) < MinSecretLen || len(secret) > MaxSecretLen { panic("secret must be " + itoa(MinSecretLen) + ".." + itoa(MaxSecretLen) + " bytes") } safe.AssertText("secret", secret, MaxSecretLen) if safe.Commit(runtime.ChainID(), c.id, role, secret) != commit { panic("secret does not match the " + role + " commitment") } } func revealsComplete(c *collection) bool { return c.prov.creatorRevealed && (!c.useBeacon || c.prov.beaconRevealed) } // finalize runs at most once per collection (I-10): every caller checks // state Closed, and this sets Final. func finalize(c *collection, h int64) { p := c.prov p.salt = safe.Salt(c.id, p.creatorSecret, p.beaconSecret, p.acc) switch { case p.creatorRevealed && p.beaconRevealed: p.badge = BadgeBoth case p.creatorRevealed: p.badge = BadgeCreatorOnly case p.beaconRevealed: p.badge = BadgeBeaconOnly default: p.badge = BadgeFallback } p.finalHeight = h c.state = types.StateFinal c.st.final = true chain.Emit("CollectionFinal", "cid", c.id, "salt", p.salt, "badge", p.badge) } // VerifyProvenance recomputes every commitment and the salt from public // data. True only for a Final collection whose record is consistent. func VerifyProvenance(cid string) bool { c := mustCollection(cid) if c.state != types.StateFinal { return false } p := c.prov chainID := runtime.ChainID() if p.creatorRevealed && safe.Commit(chainID, c.id, roleCreator, p.creatorSecret) != p.creatorCommit { return false } if p.beaconRevealed && safe.Commit(chainID, c.id, roleBeacon, p.beaconSecret) != p.beaconCommit { return false } return p.salt == safe.Salt(c.id, p.creatorSecret, p.beaconSecret, p.acc) } // FairLaunch is true only for a Final, consistent collection whose map no // single party could choose: launched with the beacon and finalized with // both secrets or with the beacon's alone (after the creator stayed // silent). Creator-only collections (no beacon: the creator alone knew the // only secret and could steer the last mint, SA-M2) and fallbacks are not // fair launches, even when VerifyProvenance is true. func FairLaunch(cid string) bool { c := mustCollection(cid) if !VerifyProvenance(cid) || !c.useBeacon { return false } return c.prov.badge == BadgeBoth || c.prov.badge == BadgeBeaconOnly } func isBurned(c *collection, commit string) bool { for _, b := range c.burnedCommits { if b == commit { return true } } return false } // Provenance is the whole record as JSON; secrets and salt stay "" until // revealed / final. func Provenance(cid string) string { c := mustCollection(cid) w := &safe.JSONWriter{} w.BeginObject() w.StrField("cid", c.id) w.StrField("chainId", runtime.ChainID()) w.StrField("state", stateAt(c, height()).String()) w.IntField("minted", c.minted()) w.IntField("reserved", c.reserved) if p := c.prov; p != nil { w.BoolField("useBeacon", c.useBeacon) w.StrField("creatorCommit", p.creatorCommit) w.StrField("beaconCommit", p.beaconCommit) w.StrField("entropy", p.acc) w.StrField("creatorSecret", p.creatorSecret) w.StrField("beaconSecret", p.beaconSecret) w.StrField("salt", p.salt) w.StrField("badge", p.badge) w.IntField("revealDeadline", revealDeadline(c)) w.IntField("finalHeight", p.finalHeight) w.BoolField("verified", VerifyProvenance(cid)) w.BoolField("fair", FairLaunch(cid)) } w.EndObject() return w.String() }