Provable fairness
Each result is derived from two values: one set by the player, one committed by the operator before the player's is set. Neither party can determine it alone.
The roll is sha256(ourSeed : yourSeed : count), rejection-sampled and reduced mod 1,000,000, where ourSeed is one link of a hash chain generated per address, yourSeed is set by the player before ourSeed is revealed, and count is the player's draw number incremented on-chain. The prize is the first tier whose cumulative weight exceeds the roll.
Commitment
Before an address's first draw the operator publishes root = sha256^N(secret) on-chain for it. Each settlement reveals one link s satisfying sha256(s) == currentTip, then advances the tip.
- The order of every future outcome is fixed at commit time.
- A link is consumed once. Reusing, reordering or inventing one fails the hash check and reverts the settlement.
- The player's seed is chosen after the commitment is published, so the chain cannot have been ground against a known seed.
- Deriving the next link from a published tip requires a SHA-256 preimage.
Rejection sampling
2^256 is not divisible by 1,000,000, so a naive modulo would over-weight the lowest buckets and break the published odds. Values at or above floor(2^256 / 1,000,000) × 1,000,000 are discarded and the hash re-rolled.
On-chain derivation
sha256 is a cheap precompile, so settlement does not accept a claimed result. It verifies the revealed seed chains correctly, computes the roll, and looks up the prize on-chain. A mismatch reverts rather than paying out.
Verification
All inputs are public after settlement. Reference implementation, Node, no dependencies:
const { createHash } = require('crypto')
const sha = b => createHash('sha256').update(b).digest()
const H = h => Buffer.from(h.slice(2), 'hex')
const LIMIT = ((2n ** 256n - 1n) / 1000000n) * 1000000n
function roll(ourSeed, yourSeed, count) {
const n = Buffer.alloc(8); n.writeBigUInt64BE(BigInt(count))
let h = sha(Buffer.concat([H(ourSeed), Buffer.from(':'), H(yourSeed), Buffer.from(':'), n]))
while (BigInt('0x' + h.toString('hex')) >= LIMIT) h = sha(h)
return BigInt('0x' + h.toString('hex')) % 1000000n
}
Two conditions establish a settled draw: sha256(ourSeed) equals the tip published before that draw, and roll matches the settled result. GET /gacha/verify/{spinId} returns both, and the interface performs the same computation client-side.
Chain entropy
There is no usable on-chain entropy on Robinhood Chain. block.prevrandao is the constant 1, blockhash is byte-identical across roughly 160 consecutive blocks, and no VRF is available. Hence commit–reveal.