---
title: "Verification"
description: "verify() derives the address from the one secret a record exposes and reruns the recipe behind it. Expected failures are values."
canonical_url: "https://puzzles.agntn.dev/guide/verification"
---
# Verification

> verify() derives the address from the one secret a record exposes and reruns the recipe behind it. Expected failures are values.

## The call

```ts
import { verify, requirePuzzle } from "@agntn/puzzles";

const result = await verify(await requirePuzzle("bits/1"));

result.verified; // true
result.derivedAddress; // "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"
result.expectedAddress; // the same
result.privateKey; // "00…01", the hex the derivation started from
```

`verify(puzzle)` returns a promise. It reads `secretOf(puzzle.keyData())`, turns the secret into a raw key, derives the address through the `@agntn/keys` wallet for the chain, and compares. `addressesEqual` lowercases Ethereum and compares everything else byte for byte.

The first call imports keys, and every later call reuses it. That keeps the curves out of `import "@agntn/puzzles"` and out of the CLI's start: `puzzles collections` doesn't need them, so it doesn't pay for them. Collections wrap it in their own lookups: `bits.verify(1)` and `bits.verifyById("bits/1")`.

## Failures are values

```ts
const result = await verify(await requirePuzzle("bits/71"));

result.verified; // false
result.unavailable; // true
result.error; // "Puzzle has no private key"
```

Three answers, one shape. `verified: true` with the derived address. `verified: false, unavailable: true` when there was nothing to derive: no key, an encrypted key without its passphrase, a mini key. And `verified: false, unavailable: false` with an error when a key exists and derives the wrong address. That last one is what the data gate exists to catch. Nothing throws for a bad record. Why not? Because `puzzles verify --all` would crash on the first unsolved puzzle.

<table>
<thead>
  <tr>
    <th>
      Secret
    </th>
    
    <th>
      Verified how
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        hex
      </code>
    </td>
    
    <td>
      The key as written. Format comes from the declared public key, then a matching decrypted WIF, then defaults to compressed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wif
      </code>
    </td>
    
    <td>
      Decoded first. The WIF says whether the key is compressed, so the record doesn't have to
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        seed
      </code>
      
       with a phrase
    </td>
    
    <td>
      BIP39 to BIP32 along the record's path, at its <code>
        .rounds()
      </code>
      
       when it has them, or Electrum's own salt after <code>
        .electrum()
      </code>
      
      . No path, or a <code>
        Required
      </code>
      
       passphrase, means unavailable
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cosigners
      </code>
    </td>
    
    <td>
      Every cosigner's key, then the multisig rebuilt and checked against the P2WSH address. Bitcoin only. No witness script means unavailable
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        encrypted
      </code>
    </td>
    
    <td>
      Unavailable here. The data gate decrypts BIP38 payloads with the passphrase when the record has one, so an encrypted key with a known passphrase is still checked, just not at runtime
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keyfile
      </code>
    </td>
    
    <td>
      Opened with the recorded answer, like its Puzzle Weave page does. No answer, unavailable
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        mini
      </code>
    </td>
    
    <td>
      Unavailable. Nobody has asked yet
    </td>
  </tr>
</tbody>
</table>

A WIF contributes its format only if it decodes on the record's chain to the same hex key. An invalid WIF or one for a different key doesn't override hex verification.

Bitcoin and Litecoin derive P2PKH and P2WPKH. Dogecoin never took SegWit, so it's P2PKH only. Bitcoin Cash and eCash derive their P2PKH in CashAddr, Ethereum and Base the same Keccak address, Decred its BLAKE-256 P2PKH. Monero has no key to address derivation here. Arweave gets its address out of a keyfile, nothing else. Any other key there stays `unavailable` too, and the data gate treats "nothing to contradict" as a pass, not a failure.

An Arweave keyfile is RSA, so no curve helps. So `verify` reruns the page's own `decodewallet`. SHA-512 over the answer 11,513 times. CryptoJS AES with a 1024-bit key. Then SHA-256 of the modulus. About 60 ms a puzzle. `privateKey` is then the keyfile as JSON, not hex. Every wallet that opens this way was claimed years ago.

P2SH, P2WSH and `standard` addresses on UTXO chains need more than a private key. With otherwise valid key material, these return `unavailable: true`, not a mismatch. Missing derivation support isn't evidence that the key is wrong.

Unless the record brings the rest. A Bitcoin P2WSH with its witness script and a key for every cosigner gets its multisig rebuilt.

The threshold comes from the script. The keys go in sorted, or in the script's order. Then the script hashes to an address like any other. `privateKey` is every cosigner's key, joined by spaces. Genesis was the first record to need it.

A seed phrase derives as written. The BIP39 checksum isn't enforced, because the seed is PBKDF2 over the words and a puzzle can publish a phrase whose checksum fails: `movie-enigma` did, and its phrase spent the prize. A word outside the list or a wrong word count still fails, and a wrong phrase of listed words fails on the address comparison.

Odd PBKDF2 rounds don't get a pass. The standard 2048 still runs first, so a phrase keys refuses fails the same way at 181505 rounds.

## Recipes get checked too

A published key proves itself. But a lot of records also say where the key came from. An entropy hash. A WarpWallet passphrase and its salt. A brainwallet phrase. Copy one of those wrong and the key still verifies. Who'd notice?

`verify` would. It reruns the recipe and reports it apart, under `result.recipe`:

```ts
const result = await verify(await requirePuzzle("quizchain/74"));

result.verified; // true, the WIF derives the address
result.recipe; // { recipe: "bip39-entropy", path: "m/44'/0'/0'/0/0", verified: true, … }
```

<table>
<thead>
  <tr>
    <th>
      Recipe
    </th>
    
    <th>
      Runs when
    </th>
    
    <th>
      Rebuilds
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        bip39-entropy
      </code>
    </td>
    
    <td>
      The key holds an entropy hash
    </td>
    
    <td>
      Entropy to mnemonic to the record's path. No path means the BIP39 tool's default, <code>
        m/44'/0'/0'/0/0
      </code>
      
       on Bitcoin
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        warpwallet
      </code>
    </td>
    
    <td>
      A passphrase, tagged <code>
        warpwallet
      </code>
    </td>
    
    <td>
      Nothing here. It's <code>
        unavailable
      </code>
      
      , and the data gate runs it instead
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sha256-brainwallet
      </code>
    </td>
    
    <td>
      A passphrase, tagged <code>
        sha256-brainwallet
      </code>
    </td>
    
    <td>
      SHA-256 of the passphrase
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        triple-sha256-brainwallet
      </code>
    </td>
    
    <td>
      A passphrase, tagged <code>
        triple-sha256-brainwallet
      </code>
    </td>
    
    <td>
      Three rounds of it
    </td>
  </tr>
</tbody>
</table>

The tag picks the recipe, not the passphrase. A BIP38 passphrase looks exactly like a brainwallet one. Only the technique tells them apart. Scrypt brainwallets stay out, because no record holds their costs.

Same three answers as the key. `verified`. `unavailable`, say an entropy seed behind an unknown passphrase. Or a miss with the address it got instead. A miss is a data bug, even when the key verifies.

It works the other way too. `bitimage/kitten` has no printed key, just the SHA-256 of a kitten photo. The key says `unavailable`. The recipe lands on the address. First time that puzzle verifies at all.

Why skip WarpWallet? Its scrypt eats 256 MiB and about a second, which is the whole point of WarpWallet. A Worker gets 128 MB. So it goes the way of BIP38: the data gate rebuilds every WarpWallet key, `verify` says `unavailable`. The rest isn't free either. `verify --all` went from under a second to about two, mostly PBKDF2 behind every entropy seed.

Then Genesis showed up with two seeds and 382,418 rounds between them. That's about 1.4 seconds on its own. One record. Worth it? It's the only way to check that one at all.

## What this isn't

A verified puzzle is one whose published key derives its stored address. It says the record is internally consistent. It doesn't say the address holds anything, that the key was the solver's, or that the puzzle is worth your CPU. `bits/1` verifies and holds nothing. It was solved in 2013.

## CLI and tools

```bash
puzzles verify bits/1          # OK bits/1
puzzles verify quizchain/74     # OK    quizchain/74    recipe bip39-entropy OK
puzzles verify --all --quiet    # exit 1 when any key or recipe fails, nothing printed
puzzles verify --technique md5-to-bip39-entropy   # every MD5 seed, rerun from scratch
```

The `list` filters work here too: `--collection`, `--technique`, `--status`, `--chain`, `--address`, `--with-pubkey`. Why bother? Because a solved puzzle is a free regression test. Changed how `@agntn/keys` turns entropy into a seed? Run the line above. A slip shows up as a `FAIL` with the address it got instead.

`puzzles verify --all` prints one line per puzzle, `OK`, `SKIP` or `FAIL`, and the recipe's own label after it. It exits 1 only on a `FAIL`, the key's or the recipe's. The CI gate runs it with `--quiet`. `puzzles_verify` is the same answer for a model: `bits/1: verified, derives 1BgG…`, or `unverifiable (Puzzle has no private key)`. The recipe gets a second line. `isError` stays unset either way.

The tool takes the filters too. `puzzles_verify` with `technique: "md5-to-bip39-entropy"` replays every MD5 seed in one call, where a model used to make one call per puzzle. A miss comes back whole, key line and recipe line. Everything else folds into ids: the unverifiable ones by reason, then one `Verified:` line. Why fold? Because a wall of identical paragraphs saying "still works" costs a lot of tokens for good news. The answer shrinks to about a tenth. On your machine, that is. The public server at `puzzles.agntn.dev/mcp` takes one `id` per call, so nobody's replay runs on the site's CPU.


## Sitemap

See the full [sitemap](https://puzzles.agntn.dev/sitemap.md) for all pages.
