swiftsync-undo-data-self-contained-blocks

Wed Jun 24 2026

SwiftSync hashes every coin twice: once when it is created as an output, once when it is spent as an input. For the two to cancel in the aggregate sum, they have to be byte-for-byte identical. See swiftsync-protocol for the full picture.

At creation this is easy. You are processing the block that creates the coin, so its full data (script, amount, creation height, coinbase flag) is sitting right there in the block.

At spend time it is not. Block M spends the coin, but a transaction input points at it by outpoint only: a 32-byte txid plus a 4-byte index, 36 bytes total. The script, amount, and height are nowhere in the input. So to recompute the same hash, the spending side has to get those bytes from somewhere. Where?

Where I got stuck

My first instinct was the obvious one: follow the outpoint. The input names the creating transaction by txid, so look up that old block, find the output, read its bytes. A normal node effectively does this through its UTXO set.

Then I saw why that cannot work here. SwiftSync's whole point is processing blocks in any order, in parallel. If spending a coin in block M needs me to read block N where it was born, then block M cannot be processed until block N is downloaded and indexed. That serializes the exact thing we were trying to parallelize. The lookup kills the protocol.

So I guessed the data must live in the hintsfile instead. That fails too, for a different reason. The hintsfile is one global object answering a single yes/no question per output: does this output survive into the UTXO set? It is a membership marker, one bit per output, not a place to stash the script and amount of hundreds of millions of spent coins.

The key move: ask what parallelism requires, not where to store the data

The reframe is to stop hunting for a storage location and state the constraint directly. For any-order parallel processing to work, block M must be self-contained: the moment you download it, you must already hold everything needed to hash the coins it spends, without touching any other block.

Read that constraint forward and the answer is forced. The missing bytes cannot come from another block (a lookup, which breaks parallelism) and cannot come from the global hintsfile (membership only). They have to travel with block M. So alongside each block you download a second blob carrying the full data of every coin that block spends. That blob already has a name: undo data (Bitcoin Core's rev*.dat).

Why undo data already exists

The good part is that this is not a new structure invented for SwiftSync. Every full node already keeps undo data, for a reason that has nothing to do with it: chain reorganizations.

If block M gets disconnected during a reorg, the node has to put back the coins block M spent. The block itself does not contain those coins, it only referenced them by outpoint, so to restore them the node must have remembered their script, amount, height, and coinbase flag. That is exactly the per-spent-coin data SwiftSync needs at spend time. SwiftSync just asks a peer to serve it alongside the block.

One catch to bank: that undo data now arrives from an untrusted peer, and the aggregate equality is the only check standing. A peer could serve valid-looking but wrong coin bytes and make your sum fail. So the same trusted source that gives you the hintsfile also commits to a hash of the undo data. The block stays self-contained, parallelism survives, and you can still tell honest bytes from forged ones.

The thing to remember is the reframe, not the file name. The question is not "where is the spent coin stored", it is "what has to be true for blocks to process in any order". The answer is self-contained blocks, and undo data is just what makes them self-contained.