Skip to content

Instantly share code, notes, and snippets.

@WietseWind
Last active August 2, 2026 23:09
Show Gist options
  • Select an option

  • Save WietseWind/6d0e000293af4c56b41ca94a0566bd07 to your computer and use it in GitHub Desktop.

Select an option

Save WietseWind/6d0e000293af4c56b41ca94a0566bd07 to your computer and use it in GitHub Desktop.
6 sided (regular) dice ED25519 account generation
import accountlib from 'xrpl-accountlib'
import assert from 'assert'
// Modified for 6 sided dice, from the 10 sided version:
// https://gist.github.com/WietseWind/78c90534b866bb59d5d78cacf48400f9
// based on @RichardAH's work: https://github.com/RichardAH/validator-keys-from-dice/blob/main/dice.js
const requiredBits = 256
const dieFaces = 6
// Why 104 throws and not the naive 100:
// 256 / log2(6) = 99.03, so 100 throws is the smallest count that clears 256 bits.
// But 6^N is never a power of two, and the .slice(-requiredBits) below folds the
// result into 256 bits (i.e. mod 2^256). That fold is a biased truncation: the
// lowest (6^N mod 2^256) residues are reachable one more way than all the others.
// At N=100, 6^100 / 2^256 ~= 5.64, so 64% of the keyspace is reachable 6 ways and
// the rest only 5 -- a 6:5 skew that drags P(top bit = 1) down to 0.468 and costs
// ~0.09 bits of min-entropy. Not exploitable, but free to remove: a few extra
// throws make 6^N / 2^256 big enough that the skew disappears into the noise.
// Requiring `biasMargin` spare bits over 256 yields N=104, where 6^104 / 2^256 is
// ~7312, the residue skew is 7313:7312 (1.0001:1) and min-entropy is 256.000 bits.
const biasMargin = 12
const requiredThrows = Math.ceil((requiredBits + biasMargin) / Math.log2(dieFaces)) // = 104
// YOU NEED A REGULAR 6-SIDED DICE FOR THIS!
// Roll it `requiredThrows` times and record the pip count exactly as thrown: 1-6.
// Do NOT subtract anything, the script converts to base-6 digits (0-5) for you.
// Do NOT photograph, screenshot, or upload the real rolls anywhere.
// Type them directly into this array on a trusted, offline-if-possible machine.
// DUMMY DATA — replace entirely with your own real dice rolls before use.
// Deliberately a repeating pattern, NOT random: it only shows the shape/format.
// Anyone can derive the key below from this file, so never fund what it prints.
const dicerolls = [
5, 1, 3, 6, 2, 4, 4, 1, 6, 3, // 10
2, 5, 1, 4, 6, 3, 2, 1, 5, 4, // 20
6, 3, 1, 2, 4, 5, 6, 1, 3, 2, // 30
4, 6, 5, 1, 2, 3, 4, 6, 1, 5, // 40
3, 2, 6, 4, 1, 5, 3, 2, 6, 4, // 50
1, 5, 3, 2, 6, 4, 1, 5, 3, 2, // 60
6, 4, 1, 5, 3, 2, 6, 4, 1, 5, // 70
3, 2, 6, 4, 1, 5, 3, 2, 6, 4, // 80
1, 5, 3, 2, 6, 4, 1, 5, 3, 2, // 90
6, 4, 1, 5, 3, 2, 6, 4, 1, 5, // 100
3, 2, 6, 4 // 104
]
assert(dicerolls.length >= requiredThrows, 'Invalid # of dice rolls, min. throws: ' + requiredThrows)
// Checked before the range assert so this more specific message wins: a 0 almost
// certainly means the rolls were written down 0-5 style, which would silently
// shift every single digit and derive a completely different key.
assert(!dicerolls.includes(0),
`Found a 0: record the pip count you threw (1-${dieFaces}), not a 0-${dieFaces - 1} index`)
assert(dicerolls.every(d => Number.isInteger(d) && d >= 1 && d <= dieFaces),
`Every roll must be an integer 1-${dieFaces}`)
// Over this many throws, never seeing a given face has probability (5/6)^104 ~= 6e-9,
// so a missing 1 or 6 means mis-recorded, clipped or made-up input, not bad luck.
assert(dicerolls.includes(1),
`No 1 in ${dicerolls.length} throws — that is ~6e-9 unlikely, check your recorded rolls`)
assert(dicerolls.includes(dieFaces),
`No ${dieFaces} in ${dicerolls.length} throws — that is ~6e-9 unlikely, check your recorded rolls`)
// Convert the recorded pip counts (1-6) to base-6 digits (0-5) and sum d_i * 6^i
const bignum = dicerolls.reduce((acc, d) => acc * BigInt(dieFaces) + BigInt(d - 1), 0n)
const bnumber = bignum.toString(2 /* binary */).padStart(requiredBits, '0').slice(-requiredBits)
const bnumhex = BigInt(`0b${bnumber}`).toString(16 /* hex */).padStart(requiredBits / 4, '0').slice(-(requiredBits / 4))
const account = accountlib.derive.privatekey(`ED${bnumhex.toUpperCase()}`)
console.log(account.keypair.privateKey)
console.log(account.address)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment