Last active
August 2, 2026 23:09
-
-
Save WietseWind/6d0e000293af4c56b41ca94a0566bd07 to your computer and use it in GitHub Desktop.
6 sided (regular) dice ED25519 account generation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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