Skip to content

Wallets that keep their own history

Most “light” wallets of the last decade worked the same way. The wallet holds a seed, derives its Addresses, and asks an indexer which Transactions touched those Addresses. The wallet itself holds very little: its balance, its history and its coins are all a view of somebody else’s index.

In this section we will look at a different kind of wallet, one that keeps the full history of every Transaction it is party to, together with the merkle proof that shows each one was mined. This is how libspiffy wallets work, and it is the foundation of the index-free applications described in this section.

A full-history wallet holds, for every coin it owns:

  • the Transaction that created it,
  • the merkle proof (BUMP) that places that Transaction in a block, or, while it is not yet mined, the unmined ancestors that lead back to Transactions that are,
  • whatever the wallet needs to spend it: the key derivation, and any data a Script needs.

Every coin needs its proof. This is non-negotiable for an SPV wallet. A coin whose ancestry can’t be proven can’t be spent, because the wallet has no way of handing the next payee a BEEF for it.

Holding this much might sound expensive, but it is a lot less than it seems. A wallet only keeps its own Transactions, and a merkle proof is a few hundred bytes. In exchange, the wallet can do everything a payment needs without asking anyone:

  • It knows its balance, because it knows every coin it holds and that each one is proven.
  • It can pay, because it can assemble the BEEF for any of its coins from its own store: the Transaction, the unmined ancestors, and their proofs.
  • It can check a payment it receives against its own header chain, as described in Simplified Payment Verification.

A wallet like this considers a Transaction confirmed when it holds a merkle proof for it that matches a block in its own header chain, and for no other reason. There is no “six confirmations” rule. Waiting for more blocks on top is a policy choice an application may make, but a proof at depth one is exactly as good as a proof at depth six.

A full-history wallet does not watch Addresses, and it does not scan blocks. It doesn’t need to. Every Transaction it holds was either built by the wallet itself, or handed to it by the counterparty who paid it, as a BEEF.

This turns the old model on its head. In the old model, Alice pays Bob by broadcasting a Transaction, and Bob’s wallet finds out about it by asking an indexer about his Addresses. In the new model Alice hands the payment to Bob directly, and Bob checks it, broadcasts it and keeps it. We cover this in detail in Paying peer to peer.

Do note that a wallet that follows Addresses it isn’t party to, or Transactions that have no bearing on its own payments, has gone back to depending on an index. libspiffy deliberately does neither.

With the old model, losing a phone was not a disaster. Restore the seed on a new phone, ask the indexer about the Addresses the seed derives, and the coins reappear.

That no longer holds for wallets that use BRC-42 keys (BRC-42). A BRC-42 payment is locked to a key that is derived from three things:

  • our own identity (or anchor) key, which the seed does give us,
  • the payer’s public key,
  • an invoice number the payer chose for this payment.

The last two come from the payer. Nothing in our seed tells us what they were, so no amount of scanning can find these coins again. The wallet’s history, including the derivation data for each payment, is the record of what we own.

This changes what a backup is. A backup of a full-history wallet has to carry its history, not just its seed. The seed recovers the keys; the history recovers the coins.

There is one situation where a full-history wallet does need an index: importing a wallet that was not built this way, e.g. a seed phrase from another wallet application. Its history exists only on the blockchain, so we have to go and find it.

libspiffy does this through a blockchain data source (WhatsOnChain at the time of writing). The import:

  1. discovers which of the wallet’s Addresses have been used, scanning each chain of keys until it reaches a run of unused Addresses (the “gap limit”),
  2. fetches each Transaction together with its merkle proof,
  3. checks every proof against the wallet’s own header chain before recording anything.

Do note step 3. Even when we have to ask an index, we don’t take its word for anything we can check ourselves. Once the import is done, the wallet holds its own history and the index is no longer needed. We cover indexers in Indexers.