Verifying randomness
DICE's 32-byte output is a pure function of public chain data. You don't have to trust the coordinator — you can reproduce it yourself.
One-line recipe
The aggregation rule is:
Where reveal_i is the 32-byte entropy each selected device published in submit_reveal, concatenated in ascending device_id order. Run that SHA-256 locally, compare to the value stored on the DiceChannel. If they match, the round is authentic.
ℹ Why this is enough
Every reveal is (a) pre-committed, (b) signed by hardware, and (c) on chain. There is no secret input to the function. Anyone with a Solana RPC connection can reconstruct the output from first principles.
Gather the inputs
- Find the round's
submit_revealtransaction. The DICE explorer links it directly from the channel's round history. - Decode the TX's instruction data — each share is a
(device_id, entropy, signature)triple. - Fetch the
DiceChannelaccount to get the storedrandomness(your comparison target) andround_id.
Reproduce the output
Plain TypeScript, no DICE SDK required. This is the canonical check:
// Reproduce a DICE round's 32-byte randomness from chain data.
// All inputs are public — anyone with a Solana RPC connection can run this.
import { createHash } from "node:crypto"
type RevealShare = {
deviceId: string // hex-encoded sha256(device_pubkey)
entropy: Uint8Array // 32 bytes
signature: Uint8Array // secp256k1 DER — optional for this check
}
export function reproduceRandomness(shares: RevealShare[]): Uint8Array {
// DICE orders shares by device_id ascending. This is enforced on-chain
// so a malicious reorder would have been rejected at submit time.
const sorted = [...shares].sort((a, b) =>
a.deviceId.localeCompare(b.deviceId),
)
const h = createHash("sha256")
for (const s of sorted) {
if (s.entropy.length !== 32) throw new Error("share must be 32 bytes")
h.update(s.entropy)
}
return h.digest()
}
// Usage:
const computed = reproduceRandomness(sharesFromChain)
const onChain = channelAccount.randomness
if (!bytesEq(computed, onChain)) {
throw new Error("randomness does not match — investigate")
}Sort order matters. The program enforces device_id-ascending concatenation inside submit_reveal. A verifier that concatenates in a different order will compute a different hash and (correctly) conclude the round looks wrong. Always sort by device_id as hex before hashing.
Verifying signatures
The SHA-256 check only proves the bytes on chain are internally consistent. To also prove they came from real hardware, verify each reveal signature against the device's registered public key. The signed message is SHA-256("dice-reveal-v1" ‖ round_id ‖ entropy).
// Optional: verify every reveal signature against its registered pubkey.
import { ecdsaVerify } from "secp256k1"
import { createHash } from "node:crypto"
function verifyShare(
share: RevealShare,
devicePubkey: Uint8Array,
roundId: bigint,
): boolean {
// DICE signs `sha256("dice-reveal-v1" || round_id_le || entropy)`.
const preimage = Buffer.concat([
Buffer.from("dice-reveal-v1"),
Buffer.from(toLeBytes(roundId, 8)),
Buffer.from(share.entropy),
])
const msgHash = createHash("sha256").update(preimage).digest()
return ecdsaVerify(share.signature, msgHash, devicePubkey)
}Device pubkeys are stored on NodeRegistry accounts and keyed by device_id = SHA-256(device_pubkey). The program enforces this identity at registration time, so a matchingdevice_id guarantees you're checking against the correct key.
What this proves
- Integrity — the 32-byte result is exactly the SHA-256 of the revealed shares, not some other value injected later.
- Authenticity — every share was signed by a device whose pubkey is registered on chain. No software-only impersonation.
- Unbiasability — commits landed before reveals (you can check this from the TX timestamps), so no node could pick its share after seeing the others.
Taken together, reproducing randomness yourself is the "don't trust — verify" story for DICE. It replaces the question "do I trust this oracle?" with "do I trust Solana's consensus?"