Skip to the content
nullroute
Browse the docs

5. Recovering without nullroute

This is the most important check in this document. If it fails, your funds depend on this project continuing to exist.

INV-INTEROP-1, stated in the threat model and not declared as an invariant in any spec: every wallet nullroute creates is fully recoverable from the BIP-39 mnemonic (plus passphrase, if set) and a standard output descriptor, with third-party software and no nullroute code in the path.

To confirm it for your own wallet against a Bitcoin Core node:

  1. Export both descriptors from nullroute, Receiving and Change. Each is a canonical BIP-380 descriptor with a checksum.

  2. In Bitcoin Core, create a fresh wallet and import both, the change one as internal:

    $ bitcoin-cli createwallet "recovery-check" true true "" false true
    $ bitcoin-cli -rpcwallet=recovery-check importdescriptors '[{"desc":"<receive descriptor>","timestamp":0,"active":true,"internal":false},{"desc":"<change descriptor>","timestamp":0,"active":true,"internal":true}]'
    
  3. Confirm Core derives, from the receive descriptor, the same addresses nullroute shows you:

    $ bitcoin-cli -rpcwallet=recovery-check deriveaddresses "<your descriptor>" "[0,5]"
    
  4. Confirm the balance matches, and that Core can construct a spend. Core 31 needs the change descriptor for this: without it, it fails with "Transaction needs a change address, but we can't generate it". Unless the wallet is p2wpkh, pass a matching change_type (legacy, p2sh-segwit or bech32m), as the drill does.

Try a testnet or signet wallet first, with the same script type as your real one.

CI runs this drill on every push to main and every pull request, with a Bitcoin Core from bitcoincore.org checked against a pinned SHA-256. For all four single-signature address types, taproot included, and for a 2-of-3 quorum as wsh and as tr, it:

  • derives each wallet with nullroute's core package from test mnemonics with no passphrase. The descriptors are written by the drill, or by nullroute's assembleQuorum for the wsh quorum, not taken from the device's export
  • has Core derive receive addresses 0 to 4 and asserts they match nullroute's; change addresses are not compared
  • imports the receive and change descriptors, funds one address, has Core build the spend (Core fills in the taproot PSBT fields for a tr() descriptor), signs it with nullroute, and has Core finalise and broadcast it

nullroute contributes only a signature to the recovery path. With no regtest node reachable the drill exits non-zero rather than skipping: a vacuous pass would put a green tick beside the claim that someone's funds are recoverable.

Run it yourself with make test-recovery-drill against a local regtest node. If you cannot reproduce this by hand, that is a security report, not a support question: see SECURITY.md.


Rendered from docs/VERIFICATION.md, the same bytes that ship with the code. SHA-256 15ae2ba12417b4691f63d9a4777eba2c328ff694961efea68f5e7dec4ae2e0bd