Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Compact blocks: send what the peer lacks (Corallo, BIP 152, 2016)#
By the time a block is found, peers have usually already received most of its transactions as they were broadcast. Matt Corallo’s compact blocks (BIP 152) send only the header and a 6-byte short ID per transaction; the receiver rebuilds the block from its mempool and asks for the few it lacks. Short IDs are salted per block, so an attacker cannot craft transactions whose IDs collide in every block.
With a full mempool a 500 kB block shrinks to about 12 kB, in a single round trip. The first missing transaction adds a second round trip, and each one must then be sent in full, so the saving shrinks linearly as the mempool’s coverage falls. Shrinking the short IDs to a single byte backfires: with 2000 transactions, every ID becomes ambiguous.
from random import Random
import matplotlib.pyplot as plt
import numpy as np
import blockchainkit as bk
A 2000-transaction block#
rng = Random(1)
block = [rng.randbytes(250) for _ in range(2000)]
others = [rng.randbytes(250) for _ in range(3000)] # Unrelated mempool entries.
full = bk.network.compact_block_relay(block, block + others)
print(f"full {full.full_bytes} bytes, compact {full.compact_bytes} bytes")
assert full.missing == 0 and full.round_trips == 1
assert full.compact_bytes < full.full_bytes / 40
full 500080 bytes, compact 12088 bytes
As the mempool misses more of the block#
[12, 37, 63, 88, 113, 138, 163, 189, 214, 239, 264] kB
Short-ID length#
ambiguous = {
width: bk.network.compact_block_relay(block, block + others, short_id_size=width).missing
for width in (1, 2, 3, 4, 6)
}
print(ambiguous)
assert ambiguous[1] > 1500 and ambiguous[6] == 0
fig, (left, right) = plt.subplots(1, 2, figsize=(10, 4))
left.plot(coverage * 100, np.array(sizes) / 1000, "o-", label="compact")
left.axhline(full.full_bytes / 1000, color="black", linestyle="--", label="full block")
left.invert_xaxis()
left.set(xlabel="% of block in mempool", ylabel="kB sent", title="Compact block size")
left.legend()
right.bar([str(w) for w in ambiguous], ambiguous.values(), color="#ea580c")
right.set(xlabel="short-ID bytes", ylabel="transactions re-requested", title="Collisions")
fig.tight_layout()
plt.show()

{1: 2000, 2: 144, 3: 1, 4: 0, 6: 0}
Exercise#
With 6-byte short IDs, 2000 block transactions and a 300,000-transaction mempool, estimate the expected number of colliding pairs. Why does BIP 152 still choose 6 bytes rather than 8?
A worked solution is in Exercises: networking.
Total running time of the script: (0 minutes 0.257 seconds)