Simplified Payment Verification
Section 8 of the Bitcoin whitepaper describes how one can check a payment without keeping a copy of the blockchain:
“It is possible to verify payments without running a full network node. A user only needs to keep a copy of the block headers of the longest proof-of-work chain, which he can get by querying network nodes until he’s convinced he has the longest chain, and obtain the Merkle branch linking the transaction to the block it’s timestamped in.”
This is Simplified Payment Verification (SPV), and it is the foundation of every application in this section. In this section we will look at the three things SPV needs: the Block Header chain, merkle proofs, and a way of handing a Transaction to someone together with its proofs, which is BEEF.
The Block Header chain
Section titled “The Block Header chain”Every block starts with an 80-byte Block Header. It holds, among other things, the hash of the previous block’s header and the merkle root of all the Transactions in the block. The headers link back to the genesis block to form a chain, and the proof-of-work in each header tells us how much work went into it.
At 80 bytes per header, the whole chain of headers fits in well under 100 megabytes, which is something a phone can comfortably hold. A wallet that holds the header chain can check for itself that a block exists, without asking anyone to vouch for it.
There are two ways for a wallet to get its headers:
- From Bitcoin peers, over the Bitcoin wire protocol. A wallet connects to a few peers and asks for headers, 2,000 at a time.
- From a CDN. On a fresh install, fetching every header from peers can take tens of minutes on testnet. Instead, a wallet can download the headers as pre-built binary files from a CDN, check them, and then ask peers only for the blocks the CDN does not have yet.
Do note that Teranode only accepts a peer whose user agent contains
BSVorBitcoin SV, and it bans the IP address of any other peer for 24 hours. If you set your own user agent, keepBSVin it, or every client behind the same address will be locked out.
Merkle proofs
Section titled “Merkle proofs”The merkle root in a Block Header is the top of a tree of hashes built from every Transaction in the block. To show that a Transaction is in a block, one only needs the hashes along the path from that Transaction up to the root: a few hundred bytes, even for a block with millions of Transactions. This path is a merkle proof.
Checking a merkle proof is rather straightforward. We hash our Transaction, combine it with each hash on the path in turn, and compare the result with the merkle root in the Block Header. If they match, the Transaction is in that block.
The format we use for merkle proofs is the BSV Unified Merkle Path, or BUMP (BRC-74). A single BUMP can hold the proofs of several Transactions in the same block.
BEEF: a Transaction with its proofs
Section titled “BEEF: a Transaction with its proofs”Suppose Alice pays Bob. Bob wants to know that the coins Alice is spending are real, i.e. that the Transactions that created them were mined, or are themselves the children of mined Transactions. Alice can’t simply point Bob at an indexer; we are trying to do without one. Instead, Alice hands Bob everything he needs to check the payment himself.
That bundle is BEEF (Background Evaluation Extended Format, BRC-62). A BEEF holds:
- the new Transaction,
- each of its ancestors that is not yet mined, going back until every branch reaches a mined Transaction,
- a BUMP for each of those mined Transactions.
Referring to Figure 1, we notice a few interesting things:
- The payment (Tx c) spends Tx b, which has not been mined yet, so Tx b travels in the BEEF too.
- Tx b spends Tx a, which has been mined. The ancestry stops there, because the BUMP proves Tx a is in block 812,401.
- Bob checks the BUMP against block 812,401 in his own header chain. Nobody else is involved.
Where a BEEF is meant to deliver exactly one Transaction, we use Atomic BEEF (BRC-95). It names the one Transaction the BEEF is about, so the receiver knows which Transaction to check, and refuses a BEEF that carries anything unrelated.
Checking a BEEF
Section titled “Checking a BEEF”When Bob’s wallet receives a BEEF, it works through it in order:
- It checks each BUMP against the Block Header at that height in its own header chain. A BUMP whose merkle root doesn’t match is refused.
- It checks that each Transaction’s inputs are provided for, i.e. that each one spends an output of a Transaction that is in the BEEF, and that every branch ends in a proven Transaction.
- It checks that the payment pays what Bob asked for, to the addresses Bob gave, and that it pays a fee the network will accept.
Only then does Bob’s wallet broadcast the payment and record it. In libspiffy this is the job of the SPVActor.
When blocks are reorganised
Section titled “When blocks are reorganised”Every now and then the network reorganises its most recent blocks: a competing chain with more proof-of-work replaces the last block or two. When this happens, a merkle proof can point to a block that is no longer in our header chain. The Transaction is almost always mined again in the new chain, but its proof has changed.
A wallet that keeps its own history has to deal with this itself, and it turns out there are exactly two ways to recover, neither of which involves a lookup service:
- the orphaned block comes back, because the network reorganises again, or
- the counterparty who paid us hands over a fresh BEEF with a fresh BUMP.
libspiffy keeps track of which received payments have lost their proof, and asks the counterparty for a new one. For Transactions we broadcast ourselves, the broadcaster we submitted them through follows them again until they are mined in the new chain, as described in Broadcasting and merkle proofs.
Do note that we don’t ask Arcade about somebody else’s Transaction. A broadcaster answers for what was submitted through it and nothing else, so it has no standing to prove a counterparty’s Transaction.

