FairRandom
Two-phase commit-reveal protocol for deriving a shared random Long seed.
All peers (including the local peer identified by seam's Seam.selfId) participate. Once every peer has committed and then revealed, every participant derives the same seed:
seed = first 8 bytes of SHA-256(H(id₁ ‖ secret₁) ‖ … ‖ H(idₙ ‖ secretₙ))Per-contributor inputs are hashed as SHA-256(peerId.encodeToByteArray() ‖ secret) before combination, making the seed framing self-describing: the 32-byte hash domain-separates each contributor by identity, preventing length-extension and ambiguous-split attacks. Contributors are sorted by PeerId value (lexicographic) before concatenation so the result is deterministic regardless of reveal-arrival order.
Abort resistance
A last-mover peer can observe all other peers' reveals before deciding whether to reveal, allowing it to abort when the outcome is unfavourable. This is a known limitation of the basic commit-reveal protocol. Applications enforce forfeit semantics for abort at the game layer. Full abort-resistance requires threshold signatures or a VRF — out of scope here.
A peer that never sends a reveal (or never sends a commit) will cause roll to stall until the coroutine is cancelled. Applications should apply an outer timeout. Similarly, a seam that becomes us.tractat.kuilt.core.FabricState.Torn during either phase will never deliver the missing message; callers should observe seam state and cancel accordingly.
Usage
val fairRandom = FairRandom(seam, setOf(aliceId, bobId))
val seed: Long = fairRandom.roll() // suspends through both phasesFairRandom is single-use: each roll is a fresh protocol run. Create a new instance for each roll if repeated rounds are needed.
Parameters
the woven seam shared with all peers.
all participant PeerIds, including seam's own Seam.selfId.