Guide

Puzzle records

One PuzzleSpec literal per puzzle handed to a factory for its chain. Builders for addresses and keys and transactions. Absent means absent and never null.

One file, one literal

src/collections/b1000/71.ts
import { bitcoinPuzzle, bits, funding, increase, p2pkh } from "@agntn/puzzles";

export const b1000Puzzle71 = bitcoinPuzzle({
  id: "b1000/71",
  address: p2pkh("1PWo3JeB9jrGwfHDNpdGK54CRas7fsVzXU", "f6f5431d25bbf7b12e8add9af5e3475c44a0a5b8"),
  sourceUrl: "https://bitcointalk.org/index.php?topic=5218972",
  startedAt: "2015-01-15 18:07:14",
  prize: 7.100226,
  key: bits(71),
  transactions: [
    funding(
      "08389f34c98c606322740c0be6a7125d9860bb8d5cb182c02f98461e5fa6cd15",
      "2015-01-15 18:07:14",
      0.071,
    ),
    increase(
      "5d45587cfd1d5b0fb826805541da7d94c61fe432259e68ee26f4a04544384164",
      "2017-07-11 05:00:53",
      0.639,
    ),
  ],
});

That's the whole record. No status, because unsolved is the default. No solver, no assets, no null anywhere. The factory picks the chain. p2pkh picks the address kind. bits(71) says the key is somewhere in [2^70, 2^71 - 1] and nothing more. Typo in a field name? The type checker rejects it before any test runs.

Six factories

bitcoinPuzzle, ethereumPuzzle, litecoinPuzzle, decredPuzzle, arweavePuzzle and moneroPuzzle take the same PuzzleSpec and answer chain() for it. Monero has a factory and no puzzles yet. Behind each one sits an abstract base, BitcoinPuzzle and so on. A puzzle that needs behavior of its own can extend a base directly. None of the 332 records do.

FieldRequiredBuilt with
id, sourceUrl, startedAtyesstrings, YYYY-MM-DD HH:MM:SS in UTC
addressyesp2pkh, p2sh, p2wpkh or standard, each with the optional HASH160
statusno, unsolvedStatus.Solved, Status.Claimed, Status.Swept, Status.Expired
prize, currencynoa number in the chain's native token, or currency: "DAI" when it isn't
pubkeynocompressed("02…") or uncompressed("04…")
keynothe Key builder below
transactionsnofunding, increase, decrease, pubkeyReveal, claim, sweep, each (txid, date, amount)
solvedAt, solveTime, solvernowhen, seconds, and party(name, { addresses, profiles })
assetsnoassets({ puzzle, solver, hints, sourceUrl }), file names under assets/<collection>/
preGenesisnothe address predates the puzzle thread, so the funding date is the thread's

Keys are a chain of calls

Key material comes in more shapes than any other field. So it's a builder, not a literal:

import { bits, derivation, encryptedWif, hex, seed, share } from "@agntn/puzzles";

hex("00…01", 1).wif("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"); // b1000/1
hex("af9a…4a0d").wif("5K9d…U7g").passphrase("5784623964023 578462396402"); // a brainwallet
encryptedWif("6PnW…4cH", { passphrase: "335Y-K745-C8WT-4D2W-80WP" }); // a BIP38 Ballet wallet
derivation("m/84'/0'/0'/0/0")
  .xpub("zpub6qd…JCqL")
  .shares(3, 5, [share(1, "session cigar …")]); // Bitaps
derivation("m/84'/0'/0'/0/0").entropy(
  "1808…32ed",
  source("https://twitter.com/aantonop/status/603701870482300928"),
); // Bitimage
seed("abandon abandon … about", "m/44'/0'/0'/0/0"); // a full BIP39 record, none in the dataset yet
bits(71); // a search width and nothing else

key() hands back the builder. keyData() gives its serialized record. secretOf(keyData) picks the one representation verification will use, in this order: a hex key, a decrypted WIF, an encrypted WIF, a seed phrase, a mini key. A record with only a derivation path or an xpub has no secret. hasPrivateKey() says false for it, and verification agrees.

Status is explicit

A claim transaction plus a published key still means solved. A sweep by a stranger after the public key leaked means swept. A prize the author took back means expired. Those are judgments, and the record writes them down. The library refuses to guess them from the transaction list. It would guess wrong on exactly the puzzles people care about.

No nulls

toJSON() omits what a puzzle never claimed to have. No solver, no assets, no prize? Then no keys for them either. The data gate fails the build if the string null shows up anywhere in the dataset. Consumers check === undefined once and move on.

{
  "id": "b1000/71",
  "chain": "bitcoin",
  "address": { "value": "1PWo3JeB9jrGwfHDNpdGK54CRas7fsVzXU", "kind": "p2pkh", "hash160": "f6f5…a5b8" },
  "status": "unsolved",
  "key": { "bits": 71 },
  "prize": 7.100226,
  "start_date": "2015-01-15 18:07:14",
  "source_url": "https://bitcointalk.org/index.php?topic=5218972",
  "transactions": [ ... ]
}

The serialized names are snake case, start_date and source_url. That's what the JSON dataset looked like before this was a TypeScript package. Nothing downstream should have to care about the rewrite.

The data gate

pnpm test re-checks every record. Identifiers are unique and owned by their collection. Every address fits its chain's format. A published private key derives the stored address. A decrypted WIF matches its hex. A BIP38 payload decrypts to the declared key when the passphrase is known. A claimed or swept puzzle carries a public key. Every asset path exists. Nothing serializes as null. A wrong hex digit doesn't reach main.

@agntn/puzzles·MIT license· Public data about public puzzles. Balances come from the chains' explorers through the worker, cached for five minutes. Every key here was public before it landed in a record.