@firstdeck/shuffle
v0.1.0
Published
Provably fair ordering derived from a Bitcoin block hash — and The First Deck, the first 52-card playing deck inscribed on Bitcoin.
Maintainers
Readme
@firstdeck/shuffle
Provably fair ordering derived from a Bitcoin block hash.
This is the derivation core of The First Deck Shuffle — a recursive ordinal inscription that shuffles The First Deck, the first complete 52-card playing deck ever inscribed on Bitcoin — extracted into a reusable package.
Same seed → same order, in every browser, in every language, forever.
- Zero dependencies. SHA-256 via WebCrypto and nothing else.
- No build step. ESM source is what ships. Node 20+, all modern bundlers, browsers.
- Spec-pinned. Tested against the same frozen vectors the on-chain inscription was tested against before it was inscribed at block 964106.
npm install @firstdeck/shuffleThe generic primitive
Order any list from a block hash. Nothing about this is card-specific.
import { shuffle } from '@firstdeck/shuffle';
const { items, order, seed } = await shuffle(
['alice', 'bob', 'carol', 'dave'],
{ blockHash: '00000000000000000000ba3ace4b409b82faf1e4f4ddc8528588031251a27880' }
);Add a commit when two parties need to agree that neither picked the block after
seeing the outcome:
await shuffle(entrants, { blockHash, commit: myCommitmentHash });permute(count, options) gives you the raw permutation of 0..count-1 if you
would rather apply it yourself.
Verification is the point
Anyone can recompute your result from the block hash alone:
import { verify } from '@firstdeck/shuffle';
const { valid, expected } = await verify(publishedOrder, { blockHash, commit });And trace: true returns every PRNG block, every raw draw, every rejected draw,
and every swap, in order — enough to walk a skeptic through the derivation by
hand:
const { trace } = await permute(52, { blockHash, trace: true });
trace.steps[0]; // { k, n, limit, rejected, draw, r, swapped }The First Deck
import { shuffleDeck, card, contentUrl, FIRST_DECK } from '@firstdeck/shuffle';
const { codes, cards } = await shuffleDeck({ blockHash, commit });
codes[0]; // '5D'
cards[0].inscriptionNumber; // 139473
contentUrl('7D', 'https://ordinals.com');
// https://ordinals.com/content/cb60af7994e16c5a01fcdc599b908e5c9541fea25e4ac496a6fc6e705418d2efi0FIRST_DECK carries all 52 cards in canonical order with their inscription IDs,
so the art is always referenced through ord's recursive endpoints and never
embedded.
Canonical order is Spades A–K (0–12), Hearts (13–25), Diamonds (26–38), Clubs (39–51).
The algorithm
seed_material = block_hash_hex || ":" || commit_hex # ":" omitted if no commit
seed = SHA256( UTF8(seed_material) )
stream(i) = SHA256( seed || UINT32BE(i) ) # consumed as big-endian uint32
# draw r in [0, n) without modulo bias:
limit = floor(2^32 / n) * n
take v; if v >= limit reject and take the next; else r = v mod n
# Fisher-Yates, top down:
for k from count-1 down to 1:
r = draw(k + 1)
swap(deck[k], deck[r])That is the whole thing, and it must never change: this is the derivation already
inscribed on Bitcoin, so the specification is frozen by the chain rather than by
this repository. The frozen vectors in vectors/vectors.json are the normative
statement of it.
The one way to get this wrong
ord's /r/blockhash returns a JSON-quoted string:
"00000000000000000000ba3ace4b409b82faf1e4f4ddc8528588031251a27880"Hashing it with the quotes attached produces a shuffle that looks completely plausible and is permanently wrong. This package refuses a quoted or uppercase hash rather than letting it through:
await shuffle(items, { blockHash: rawFromOrd });
// TypeError: blockHash must be 64 lowercase hex characters — it looks
// JSON-quoted; ord's /r/blockhash returns a quoted string, JSON.parse it firstJSON.parse it first. Always.
Reproducing the canonical shuffle
The inscription's pinned launch link freezes block 964106 and commits to the precursor inscription. You can reproduce exactly what it renders:
node examples/reproduce-launch.mjsblock hash 00000000000000000000ba3ace4b409b82faf1e4f4ddc8528588031251a27880
seed 2635ad436e13243c986ac80491be9f574852d7fe58ec1549dc99b3fa3fc964df
top of deck 5 of DiamondsCompare against the inscription itself. They agree, or one of us is wrong — and the chain does not move.
Testing your own implementation
The frozen vectors are the real asset here. If you reimplement this spec in another language, hold it to this file:
import vectors from '@firstdeck/shuffle/vectors.json' with { type: 'json' };It carries five vectors (seed, order and card codes for each) and four traps —
inputs designed to catch the mistakes that produce plausible-but-wrong output.
A stdlib-only Python reference lives in reference/derive.py; the test suite
runs both implementations against 250 generated inputs and requires that they
never disagree.
If an implementation disagrees with the vectors, fix the implementation. Never the vectors.
On chain
| | inscription | block |
|---|---|---|
| precursor — The First Card | 1df40bc6…i0 (#127,231,957) | 964096 |
| The First Deck Shuffle | 5ba5a97e…i0 (#127,232,859) | 964106 |
Exposed as INSCRIPTIONS from the package.
License
MIT
