Skip to content

TxCollection with background coin prefetch - #122

Open
Sjors wants to merge 4 commits into
2026/03/collect-txsfrom
2026/07/collect-txs-prefetch
Open

Sjors wants to merge 4 commits into
2026/03/collect-txsfrom
2026/07/collect-txs-prefetch

Conversation

@Sjors

@Sjors Sjors commented Jul 23, 2026 •

Copy link
Copy Markdown
Owner

Prefetch input coins while a TxCollection's IPC client requests missing transactions from the miner. The constructor queues inputs of transactions found in the mempool, and addMissingTxs() queues inputs of newly supplied transactions. Collections already complete at construction skip speculative allocation, queuing and worker startup. For background on parallel input fetching, see bitcoin/bitcoin#35295 and #35738.

Fetch through PeekCoin() and retain observations in collection-owned storage, reusing existing coins-cache entries without inserting speculative database reads into CoinsTip(). Each prefetching collection has one queue and one background coordinator, which dispatches bounded batches through the node's existing input-fetching thread pool. If the pool is inactive or rejects submission, speculative fetching stops; validation fetches any remaining coins later. Across the node, at most eight collections can prefetch concurrently. Each collection's queue and retained observations share an 8 MiB budget, for at most 64 MiB of that storage in aggregate. Further collections still validate using non-caching reads. Retained observations are invalidated when the chainstate or tip changes. Simulated spends and created outputs stay private to each validation.

A complete makeTemplate() call requests cancellation before taking cs_main and hands remaining work to validation. The preparation coordinator holds cs_main until each dispatched batch finishes, keeping the underlying coins view stable. makeTemplate() never joins the coordinator while holding cs_main. Preparation also stops on shutdown. With bitcoin#35751 applied, validation seeds its input queue from retained observations and starts parallel fetching for the remainder. Collections share the fetching thread pool, with a separate mutable validation overlay for each check.

The stack, in commit order:

  1. bitcoin/bitcoin#35671: the TxCollection interface and external template validation.
  2. Collection-owned input prefetch and validation integration, followed by the three TxCollection benchmarks below. This stage works independently of validation: use parallel input prevout fetching in TestBlockValidity bitcoin/bitcoin#35751.
  3. Both commits from bitcoin/bitcoin#35751: the fuller AssembleBlock benchmark fixture, then parallel input fetching in TestBlockValidity(), adapted to consume retained coins.

The same small-fixture benchmark source is run in four configurations. The baseline receives only the benchmark source; the bitcoin#35751-only control applies both upstream commits to bitcoin#35671 without collection prefetch. Comparing that control with the combination measures what collection prefetch adds if bitcoin#35751 is merged first. All tables show medians in milliseconds on Ubuntu x86_64, Release build, with fixture-only cache eviction and actual NVMe SSD reads.

Small chainstate: one missing transaction, fully prepared before arrival

Configuration Prep before arrival (ms) Validation after arrival (ms)
bitcoin#35671 baseline 0.75 24.09
bitcoin#35671 + bitcoin#35751 0.73 13.34
bitcoin#35671 + collection prefetch 10.15 13.98
bitcoin#35671 + bitcoin#35751 + collection prefetch 9.89 13.78

All three TxCollection benchmarks are introduced by this PR.

  • Prep before arrival — TxCollectionKnownInputPrep: elapsed time from collection creation through unknownTxPos() until the known transactions' input fetches finish. The baseline and validation: use parallel input prevout fetching in TestBlockValidity bitcoin/bitcoin#35751-only configurations only identify the missing transaction; they schedule no input reads before arrival. Fetching after the query can overlap the client's network request.
  • Validation after arrival — TxCollectionOneMissing: time from addMissingTxs() with the one missing transaction through successful makeTemplate(). Known-input prep has finished outside this timed interval. It does not wait for the newly supplied transaction's prefetch; any remaining reads are included.

The prep column is the time needed to finish preparation, not extra time to add to the network round-trip. With a 50 ms round-trip, the combination's 9.89 ms prep would finish before the reply in this small fixture. These after-arrival figures model completed preparation.

Adding collection prefetch to bitcoin#35751 changes the post-arrival median from 13.34 to 13.78 ms. A reverse-order repeat measured 13.62 ms for bitcoin#35751 alone and 14.00 ms combined. These runs do not show a substantial post-arrival latency benefit from collection prefetch in the small fixture.

Already complete: no missing-transaction round-trip

Configuration Complete template (ms)
bitcoin#35671 baseline 25.13
bitcoin#35671 + bitcoin#35751 14.99
bitcoin#35671 + collection prefetch 22.80
bitcoin#35671 + bitcoin#35751 + collection prefetch 16.27

TxCollectionComplete measures collection creation, unknownTxPos() and successful makeTemplate(). Complete collections skip the speculative worker. The combination still uses a fresh collection validation overlay and a zero-budget collection coin view; bitcoin#35751 alone uses the reusable chainstate overlay. Their allocation and locking costs differ even without speculative preparation. In the reverse-order repeat, complete templates took 14.68 ms with bitcoin#35751 alone and 15.08 ms combined.

The separate AssembleBlock benchmark from bitcoin#35751 measures block assembly, while these three cases measure the TxCollection flow.

The fixture has 5,900 spending transactions and 11,800 confirmed input coins. Including the generated coinbase, each template weighs 3,989,208 weight units (99.73% of the 4,000,000 limit). Complete collections have all transactions in the mempool; the missing-transaction cases have 5,899. The input coins are outputs of one confirmed funding transaction.

Before each sample, input coins are evicted from CoinsTip(), LevelDB is reopened to discard its block/table caches, and Linux posix_fadvise(POSIX_FADV_DONTNEED) requests eviction of only the fixture's files from the OS page cache. The regular cache's allocated memory pool is preserved. Startup metadata can remain warm. These resets, fixture generation and mempool population are untimed. Other platforms reopen LevelDB without OS-cache eviction; Linux diagnostics report process storage-read bytes for both phases. Use TMPDIR on the storage device being measured, since a RAM-backed temporary directory will not exercise SSD reads.

In the final small-fixture samples, the baseline read 540 KiB from storage after arrival, and bitcoin#35751 alone read 500 KiB. The combination read 528 KiB during prep and 0 KiB afterward. The missing transaction's inputs share LevelDB blocks warmed by known-input reads here; zero post-arrival reads is not a general guarantee.

Small-fixture timings use 101 samples after five warm-ups. CPU frequency scaling is enabled. Collection preparation uses the existing two-thread fetching pool, as does bitcoin#35751's validation. Cache diagnostics show 11,800 input entries retained in CoinsTip() after baseline validation, versus zero in all three other configurations. An incomplete collection retains 11,798 observations after prep and 17,702 after validation, including cached absence observations, using about 2.48 MiB and 3.20 MiB respectively. A collection already complete at construction allocates no speculative cache.

Large chainstate: one missing transaction, fully prepared before arrival

A separate local experiment scatters the input coins across a 9.98 GB chainstate database on the NVMe SSD. Both configurations start from bitcoin#35671:

Configuration Prep before arrival (ms) Validation after arrival (ms)
bitcoin#35751 0.78 1148.06
bitcoin#35751 + collection prefetch 1331.52 37.96

These are variants of TxCollectionKnownInputPrep and TxCollectionOneMissing with the larger database and the same timing boundaries defined above. Prep waits for all 11,798 known input coins when collection prefetch is enabled. The bitcoin#35751-only control schedules no reads before arrival. The table assumes preparation has finished before the missing transaction arrives; it does not assume preparation fits within an ordinary network round-trip.

The template still contains 5,900 transactions, 11,800 inputs and 3,989,208 weight units. The database contains 130 million synthetic filler coins plus the target coins, using standard coin serialization. Target keys are deterministic SHA-256 outputs distributed throughout the keyspace; compaction interleaves them with filler records. Its 290 SST files total 9,982,393,433 bytes, and target positions span 4,333 distinct 2 MiB regions. This is a synthetic UTXO database, not a mainnet snapshot.

The experiment uses Core's 8 MiB LevelDB cache budget and the same cache eviction procedure as above. Timings are medians of 31 samples after three warm-ups. Both collection preparation and bitcoin#35751 validation use the existing two-thread fetching pool. Generation, cache resets and signature-cache warm-ups are outside the measured intervals.

Fully completed preparation reduces post-arrival validation from 1148.06 to 37.96 ms, about 30 times faster. In the final sample, it moved 1.68 GB of physical reads into preparation, leaving 5.8 MB afterward, versus 1.69 GB afterward without collection prefetch. Database metadata and page reads make these byte counts much larger than the coin records themselves. Unlike the small fixture, the missing transaction can require additional storage reads. The small-fixture result therefore does not establish that coin fetching is no longer a validation bottleneck.

Already-complete templates take 1148 ms with bitcoin#35751 and 1151 ms with the combination in this experiment. Those collections skip speculative preparation.

(fully vibe coded proof-of-concept)

@Sjors

Sjors commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner Author

The benchmark with a small disk chainstate suggests this PR adds no benefit over bitcoin#35751. Actual validation is the bottleneck, not fetching coins from disk. However with a 10 GB chainstate and coins randomly distributed over it, this can shave a full second off.

Sjors added 2 commits October 26, 2026 13:51
Fetch collected transactions' input coins with PeekCoin() and retain the
observations in a bounded collection-owned read cache. This avoids growing
the regular coins cache and triggering a flush through speculative reads.
Limit queued inputs to a block's possible input count, reserve at most
eight concurrent prefetch collections, and bound each collection's queue
and cached observations to 8 MiB.

Let TestBlockValidity consume the retained observations through a separate
temporary validation cache. Fallback reads also use PeekCoin(), while
simulated spends and created outputs never modify retained observations
or the active coins cache. Keep retained reads available for repeated
checks, and discard them when the active chainstate, tip, or base changes.

Skip speculative setup when all transactions are found during construction;
validation fetches those inputs directly without filling the regular cache.

When a complete collection starts MakeTemplate, cancel speculative work
and hand remaining reads to validation. Use a cancellable cs_main wait and
check cancellation again after acquiring it; never join the worker while
holding cs_main.

Test retained-read reuse, isolated repeated validation, tip invalidation,
concurrent reads, per-collection and aggregate bounds, budget exhaustion,
oversized queues, and cancellation while cs_main is held by validation.

Stop on node interruption before accessing the chainstate, and check that
coin views still exist after acquiring cs_main. Cover shutdown resetting
the views before IPC collections are disconnected with a regression test.

Dispatch preparation batches through the existing prevout-fetching thread
pool, keeping the base view stable until every reader has finished. Stop
speculative reads when the pool is inactive or rejects submission. Test
concurrent lookups and inactive, single-worker, and interrupted pools.
Use one disk-backed fixture with 5900 transactions spending 11800 confirmed
coins. Fill templates to over 99% of the block weight limit. Keep 5899
transactions in the mempool for the missing-transaction cases, and all
transactions for the complete-template case.

Measure complete collection-to-template work, preparation of known inputs,
and the remaining work after one missing transaction arrives. Use untimed
per-epoch setup so cache eviction and initial preparation are outside
their intended measurement intervals. Do not wait for the newly supplied transaction's
prefetch before measuring the remaining work.

Compact the fixture database, then reopen LevelDB before each sample to
clear its block/table caches. On Linux, evict only the fixture files from
the OS page cache with posix_fadvise and report process storage-read bytes
before and after the missing transaction arrives. Keep these resets and
counter reads outside the timed operations, preserving the regular coins
cache memory pool. Other platforms reset LevelDB without OS eviction.

Sign each spend from just its own input coins to avoid quadratic fixture
setup costs as the transaction count grows. Check and report the resulting
block weight outside the timed intervals.

Report regular coins-cache entries and memory alongside retained storage
after each case. Keep the same source usable as a benchmark-only addition
to the TxCollection baseline so workload changes do not get attributed to
the optimizations being compared.

Use 64-bit process I/O counters on 32-bit builds too.
@Sjors
Sjors force-pushed the 2026/07/collect-txs-prefetch branch from 2960541 to dcb8270 Compare October 26, 2026 14:09
Use CoinsViewOverlay to speed up ConnectBlock in TestBlockValidity.
This also has the benefit of not inserting entries fetched from db
into the main cache.

cs_main must be held for each place where the overlay is being used,
and the reset guard ensures it will be reset for the next caller.

For TxCollection validation, use a separate overlay backed by its bounded
retained observations. Seed the input queue from those observations and
fetch only the remainder, retaining fallback reads for repeated checks.
Share the fetching thread pool, not the mutable validation view.
@Sjors
Sjors force-pushed the 2026/07/collect-txs-prefetch branch from dcb8270 to 5327a96 Compare October 27, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants