p2p: Prefill compact blocks #35558

pull davidgumberg wants to merge 14 commits into bitcoin:master from davidgumberg:2025-11-13-0xB10C-prefill-rebase-binary-limit changing 16 files +632 −133
  1. davidgumberg commented at 6:44 AM on June 18, 2026: contributor

    LLM Disclosure: LLMs were used frequently as a research tool while investigating this topic, but no code or text in this PR was generated with an LLM.

    This is an implementation based on 0xB10C's proposal and implementation of prefilling CMPCTBLOCK messages with what we[^1] needed during CMPCTBLOCK reconstruction in the hopes of providing everything our peers will need to reconstruct without having to ask us for transactions.

    Although full support for receiving prefills is implemented in Bitcoin Core, the only prefills sent presently are coinbases. The goal of prefilling is to improve compact block reconstruction rates, which reduces the number of roundtrips needed to propagate blocks per-hop, which reduces the amount of time needed for blocks to propagate on the network, which mitigates selfish mining attacks by lowering the stale rate and the ratio $γ$ of honest miners a selfish miner is able to recruit in a block race.[^2] The tradeoff of prefilling is that it uses a small amount of extra bandwidth and makes propagation slightly worse when the predictions are bad.

    My primary contribution is limiting the prefill based on the connection's TCP window. Exceeding the boundary of the current TCP window will result in a ~roundtrip at the network layer,[^3] and any time a roundtrip is going to be incurred anyways, it would be better to let our peer tell us exactly what they are missing in a roundtrip instead of hazarding a guess, which risks sending redundant information.[^4]

    Prefill do

    In measurements I took from 2026-02-15 to 2026-03-16, a node receiving prefilled CMPCTBLOCKs limited to the TCP window size was able to reconstruct 89.7% of compact blocks received without a GETBLOCKTXN roundtrip, and a node receiving typical CMPCTBLOCKs with only coinbases prefilled was able to reconstruct 56.9% of blocks received without a GETBLOCKTXN roundtrip.[^5]

    The mean prefill sent was 1,694.64 bytes and the median was 897 bytes. If all the bytes of every prefill were redundant [^6], the cost of prefilling 1,694.64 bytes would be 1.39 MiB per node in wasted bandwidth per day (counting both the sending and the receiving bandwidth for all 3 HB peers).

    Prefill why

    There are changes that are likely more effective at improving propagation times than prefilling, for example:

    The advantage of this approach is that it requires no protocol changes and is entirely backwards compatible with existing node software that has implemented the CMPCTBLOCK protocol. Concretely: An old version of Bitcoin Core that knows nothing about this PR can enjoy faster block reconstructions if it connects to a peer that prefills blocks. I think these benefits are worth the tradeoff of violating the layer cake and taking transport matters into our own hands.

    Prefill what

    • The transactions which we were missing during reconstruction and received in a GETBLOCKTXN->BLOCKTXN round trip with our peer.
    • The transactions which were pulled from our extrapool during reconstruction.[^7]
    • Any transactions that were prefilled to us that we didn't already have in our mempool.

    The reasons to like this heuristic are that it's simple, per-block rather than per-peer, makes sense on paper, and seems to be effective in practice.

    Prefill when

    To avoid having to generate unique CMPCTBLOCK messages for each of our peers, which might be costly[^8], we (lazily[^9]) build and cache 2 CMPCTBLOCK messages: one prefilled and one not prefilled.

    At sending time we check the available bytes in our peer's TCP window, and if the total number of TCP windows occupied by the prefilled block is equal to the total number of windows occupied by the nonprefilled block, then we send the prefilled block.


    TCP Windows

    Discussed in greater detail elsewhere, I'll try to summarize here:

    In RFC 793, TCP was specified with receiver advertised window sizes because receivers allocate some buffer size to a given TCP connection, and this receiver advertised window represents the most unacknowledged data a receiver will process before dropping bytes on the ground or something awful like that. So the sender of a message over a TCP connection will only send up to the last acknowledged byte + the receiver's advertised window size in order to avoid filling up their peer's receive buffer.

    It was later discovered that TCP was susceptible to "congestion collapse", which can probably describe any congestion feedback loop but in TCP is where packets being dropped due to congestion results in retransmissions that cause even more congestion. TCP implementers addressed this with "congestion control" algorithms which decide on the sending side to limit the number of bytes to send dynamically, based on how frequently packets are dropped on a TCP connection. For a more concrete account of various congestion control algorithms see RFC 5681. A user's TCP implementation (typically in their OS kernel) will compute a window size dynamically for each TCP connection usually increasing as packets sent are ACKnowledged and decreasing when packets sent are dropped.

    Computed congestion control window sizes vary:

    • per-connection
    • over connection lifetime
    • with the congestion control algorithm used by the system TCP implementation
    • with user configuration of the TCP implementation.

    There is not likely to be any way to guess or predict what the TCP window will be very effectively, and we must query the TCP implementation in order to learn what our connection window sizes are.

    The usable window size is the smaller of the receiver advertised window and the sender computed congestion control window, but in practice the congestion control window is far smaller. In my observation node, the mean Bitcoin P2P congestion window observed was 17,360.52 bytes and the median was 14,480 bytes.

    [^1]: 'We' refers to the Royal Node. [^2]: (2013) Eyal and Sirer Majority is not Enough (pg. 8) https://arxiv.org/pdf/1311.0243 The claim that lowering block propagation times lowers γ probably needs serious analysis, but my hand-waving argument is that the faster public network-wide block propagation is, the more expensive any proportional propagation time advantage over the public network becomes, and γ is a function of propagation time advantage. [^3]: A network layer roundtrip will be faster than an application layer roundtrip, although probably not by much for most connections. Since at the application layer there will be some time your message spends waiting to be processed by your peer, at least for peers that use single-threaded message processing like Bitcoin Core does presently. [^4]: In practice exceeding the TCP window is ~not quite as bad as I've implied here and in the delving post: because the window is sliding, once the first 1.5 round-trips are completed there is a continuous stream as ACKnowledgements for the oldest segments arrive and the newest segments are fired out. The effect of this more precisely is that if a message exceeds a window boundary, the minimum travel time of the message becomes 1.5 round-trip-time (RTT) instead of 0.5 RTT and the throughput limit of the connection becomes $\text{window size} / \text{RTT}$. I am working on a more complete write-up that takes this more precise cost into consideration, but I think approximating it as a ~round trip is reasonable. [^5]: In reality, not all of the prefill is redundant otherwise prefilling would not be very useful. In my observation node that received prefilled compact blocks, the mean redundant prefill bytes was 865.62 bytes/block. 2 HB announcements will always be redundant so: 1.17 MiB per node in wasted bandwidth per day if one CMPCTBLOCK announcement only has 865.62 redundant bytes. [^6]: I will share a write-up describing my full experimental setup and data soon. It was mostly identical to the set up described here: https://delvingbitcoin.org/t/stats-on-compact-block-reconstructions/1052/34 I had one node running a prefilling branch, and another node set up to only receive CMPCTBLOCK messages from the prefilling node. My infrastructure as code observation set up: https://github.com/davidgumberg/prefill-research and the script I used to analyze the results: https://radicle.network/nodes/iris.radicle.network/rad:z37pH1UAxFvazXnfAMS5qbcUjQaP6/tree/leave/scripts/prefill.py [^7]: The reason to pluck from the extrapool is because it is very likely to differ between nodes. [^8]: But maybe there is some cost here that is worth trading off, it's not something I've explored. [^9]: This is based on andrewtoth's #26755.

  2. log: TXID's of all missing cmpctblock tx'es. 83234ba91d
  3. build: Bump minimum Windows to >= Windows 1703
    This is needed for the use of the `SIO_TCP_INFO` API.
    
    https://learn.microsoft.com/en-us/windows/win32/api/mstcpip/ns-mstcpip-tcp_info_v0#requirements
    0b4bdfb0b4
  4. sock: Add TCPInfo wrapper for getting socket info. 242ddd957d
  5. sock: TCPInfo::GetTCPWindowSize() d18e263a75
  6. sock: Add GetOSBytesQueued 9795f39f55
  7. net: Add Transport::GetMessageSize() for serialized msg sizes
    This is unused here, but will be useful for estimating the serialized
    bytes in the application send queue.
    a0242b65ef
  8. net: Add GetSendQueueSize()
    Not used yet, but will be useful for deciding whether or not prefilling
    a CMPCTBLOCK will cause the current message queue to overflow a TCP
    window boundary.
    d6d61c9953
  9. net: Add CNode::WindowBytesTotalAndAvailable() 1f11fec2c0
  10. p2p: refactor: Stuff m_most_recent* into a struct 9078b7a6db
  11. p2p: Cache cmpct_block_msg for low bandwidth relay.
    This commit is drawn from the closed #26755.
    (https://github.com/bitcoin/bitcoin/pull/26755)
    
    This also changes `std::launch::deferred` to `std::launch::async` since
    we are likely going to use this result very soon if someone has
    requested HB blocks from us, and if no one has, then performance here is
    not that critical.
    
    Co-authored-by: Andrew Toth <andrewstoth@gmail.com
    b357f14a96
  12. net: refactor: Move common logic into SendCompactBlock
    Also one non-refactor change which is setting the `pIndexBestHeaderSent`
    on the `SendMessages` CMPCTBLOCK announcement fallback.
    9a6174a2eb
  13. net: Protect PeerHasHeader from nullptr 30203a483f
  14. p2p: keep track of cmpctblock prefill candidates
    Keep track of the block position of transactions that we didn't have in
    our mempool while reconstructing this compact block. We can use these to
    predictively prefill transactions in our compact block annoucements.
    This includes transactions that:
    - were prefilled by the cmpctblock announcer and were not in our mempool
    - transactions we found in our extra_pool (but not mempool)
    - transactions we had to request from the announcer
    1dfa5c469e
  15. p2p: prefill our compact blocks with candidates
    Upon receving a compact block, we keep a set of prefill candidates
    (transactions we didn't have in our mempool) for this block. When
    constructing a compact block, we try to use these candiates to prefill
    our compact block annocement of this block.
    f610cdd2a6
  16. DrahtBot commented at 6:45 AM on June 18, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35558.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK w0xlt, ismaelsadeeq, josibake, edilmedeiros, murchandamus, 0xB10C

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35561 (net: move some CNodeState fields to Peer by Crypt-iQ)
    • #35522 (refactor: Extract per-message helpers from SendMessages() (move-only) by pablomartin4btc)
    • #35502 (refactor: extract per-message helpers from ProcessMessage (move-only) by w0xlt)
    • #35368 (tracing: add block header and compact block tracepoints by w0xlt)
    • #34565 (refactor: extract BlockDownloadManager from PeerManagerImpl by w0xlt)
    • #32606 (p2p: Drop unsolicited CMPCTBLOCK from non-HB peer and when blocksonly by davidgumberg)
    • #29418 (rpc: provide per message stats for global traffic via new RPC 'getnetmsgstats' by vasild)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • "compact block annoucements" -> "compact block announcements" [misspelling]
    • "recontruct" -> "reconstruct" [misspelling]

    <sup>2026-06-18 06:45:20</sup>

  17. davidgumberg renamed this:
    Prefill compact blocks
    p2p: Prefill compact blocks
    on Jun 18, 2026
  18. DrahtBot added the label P2P on Jun 18, 2026
  19. w0xlt commented at 7:55 AM on June 18, 2026: contributor

    Concept ACK

  20. ismaelsadeeq commented at 10:20 AM on June 18, 2026: member

    Concept ACK.

    Curious to see also Prefill Who section.

    The transactions which were pulled from our extrapool during reconstruction.7

    These transactions in the extrapool were sent to us by some peers, no? We can reduce a bit of redundancy by not sending them again to the peers that announced them to us.

  21. josibake commented at 10:28 AM on June 18, 2026: member

    Concept ACK

    Based on prior discussions we've had about this, very excited to see it come to life. I have not looked at the code yet, but after reading the PR description (very well done, btw), I strongly agree that the simplicity and backwards compatibility of the approach make this a very strong proposal.

  22. edilmedeiros commented at 12:50 PM on June 18, 2026: contributor

    Concept ACK

  23. murchandamus commented at 3:49 PM on June 18, 2026: member

    Concept ACK

    Very excited to see this PR.

  24. in src/net_processing.cpp:2273 in f610cdd2a6
    2274 | +                    prefilled_windows,
    2275 | +                    not_prefilled_size,
    2276 | +                    not_prefilled_windows,
    2277 | +                    window_total,
    2278 | +                    window_avail);
    2279 | +            }
    


    0xB10C commented at 6:58 AM on June 19, 2026:

    Running this PR and observing this in the logs should give us information about how often we can prefill.

  25. in src/blockencodings.cpp:253 in f610cdd2a6
     253 | -        if (vtx_missing.size() < 5) {
     254 | -            for (const auto& tx : vtx_missing) {
     255 | -                LogDebug(BCLog::CMPCTBLOCK, "Reconstructed block %s required tx %s\n", hash.ToString(), tx->GetHash().ToString());
     256 | -            }
     257 | +        for (const auto& tx : vtx_missing) {
     258 | +            LogDebug(BCLog::CMPCTBLOCK, "Reconstructed block %s required tx %s\n", hash.ToString(), tx->GetHash().ToString());
    


    0xB10C commented at 7:02 AM on June 19, 2026:

    I'm not too sure about logging this for all required transactions in a non-test setting (which might be a few thousands). Could this cause log rate-limiting? Maybe a good use for the trace log level?

  26. 0xB10C commented at 7:06 AM on June 19, 2026: contributor

    Concept ACK.

    Again, thanks for picking this idea up and moving it closer to the finish line.

    An idea mentioned in the delving post was to log information about wasted prefill bandwidth on the receiving side. How many transactions were received that we didn't use? How many extra bytes did we receive? This doesn't have to happen in this PR, but would probably good to have before this is reaches broader deployment on mainnet.

    Once merged: I've been thinking about how I could test the effect on mainnet once this is deployed. One idea would be to run a node that's patched to specifically NOT use anything prefilled besides the coinbase and compare it's reconstruction performance to the other mainnet nodes. Additionally, keeping track of what share of blocks come with more than a coinbase prefilled and measuring their reconstruction performance would be interesting.

  27. edilmedeiros commented at 1:56 PM on June 19, 2026: contributor

    Once merged: I've been thinking about how I could test the effect on mainnet once this is deployed. One idea would be to run a node that's patched to specifically NOT use anything prefilled besides the coinbase and compare it's reconstruction performance to the other mainnet nodes. Additionally, keeping track of what share of blocks come with more than a coinbase prefilled and measuring their reconstruction performance would be interesting.

    This is probably a good idea even before merging this, maybe there is some fine tuning we can find.

    cc @m4ycon


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-06-20 23:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me