Skip to content

Research: P2P staging-window and database-cache optimization headroom #51

Description

@gosunuts

Problem

The local-file processing path is already competitive with Bitcoin Core, but the loopback P2P path still trails Core by approximately 1.37x in the historical matched 0→150k measurement. The current implementation also exposes storage-cache configuration that does not appear to reach the selected backend.

This issue tracks two concrete sources of remaining headroom:

  1. P2P download/staging depth and memory representation.
  2. Effective database-cache configuration and measured storage behavior.

This is a scoped investigation under #33 and #39. The controlled P2P fixture in #35 should be used for acceptance measurements.

Current observations

Fixed 128-block P2P window

crates/node/src/sync.rs currently couples:

  • PENDING_BUDGET = 128
  • RECEIVED_BLOCK_BUDGET = 128
  • a 2 MiB estimated block size
  • an approximately 256 MiB pending/staging byte budget
  • 16 in-flight blocks per peer while fan-out is active

The count bound is reasonable for modern 1–2 MiB blocks, but it binds far before the byte budget for small historical blocks. This limits both request depth and the script-verification windows formed by P2P sync. The README already identifies the 128-block staging depth as the reason peer sync does not reach local-replay throughput.

A constant-only increase is not acceptable. The count is coupled to staller arming, pending/staged parity, inline capacities, channel drain sizing, peer fan-out, and memory assertions.

Staging retains decoded and serialized bodies

crates/node/src/sync/stage.rs retains both:

  • a decoded bitcoin::Block
  • the original bytes::Bytes P2P payload

The byte budget accounts for the serialized payload, while the decoded block adds substantial unaccounted memory. A fully out-of-order window can therefore retain roughly twice the nominal staged bytes.

dbcache_mb is not plumbed into backend construction

Config::dbcache_mb is parsed and defaults to 450 MiB, but NodeStorage::open calls backend constructors without passing it.

Current backend behavior appears to be:

  • Fjall: builder defaults
  • RocksDB: fixed 256 MiB block cache
  • redb/MDBX: backend defaults

The configured cache target therefore does not currently define a process-level database cache posture. This also makes cache-parity claims difficult to interpret.

Investigation plan

1. Add attribution before tuning

Measure at least:

  • time apply is idle waiting for blocks
  • time download/staging is blocked by apply
  • per-peer block RTT and bytes/sec EWMA
  • inbound queue wait and block decode time
  • pending/staged block and byte high-water marks
  • actual retained memory for decoded plus serialized bodies
  • retry count, duplicate delivery, staller disconnects, and pending timeouts
  • backend write, flush, compaction, and cache behavior

Do not select a mechanism until the controlled P2P baseline attributes the gap.

2. Evaluate a dynamic count-and-byte window

Evaluate a larger count ceiling while preserving the existing byte ceiling:

allowed blocks = min(count ceiling, byte headroom / observed block-size EWMA)

The intended behavior is:

  • small historical blocks: hundreds or thousands of blocks may be in flight/staged
  • modern large blocks: the byte ceiling keeps the effective depth near the current range
  • one oversized valid block remains processable
  • no eviction/re-download churn is introduced
  • staller detection continues to blame only a peer-caused blocked frontier

All count-coupled invariants and tests must be made explicit rather than mechanically changing 128.

3. Evaluate raw-first staging

Evaluate retaining only the checksum-validated wire payload plus the minimum identity metadata needed by the scheduler, deferring full bitcoin::Block decode until the block enters an apply window.

This candidate should be judged primarily as a way to:

  • make the byte budget reflect actual retained block data more closely
  • permit a deeper small-block window under the same RSS limit
  • remove full decode work from peer reader threads

It must not add another parse or serialization pass to the apply path.

4. Evaluate measured peer-quality scheduling

Only if the baseline shows peer heterogeneity is material, evaluate request allocation using recent per-peer throughput, completion latency, and frontier-blocking history.

Any policy must retain peer diversity and bounded exploration rather than assigning all work permanently to the currently fastest peer. Existing staller disconnect, cooldown, single-peer fallback, and no-blame backpressure behavior must remain intact.

5. Make dbcache_mb effective

Introduce backend constructors that accept a cache budget and define how one process-level budget is divided among chainstate and optional indexes.

Requirements:

  • Fjall cache capacity is configured explicitly.
  • RocksDB no longer ignores the configured value in favor of an unconditional 256 MiB cache.
  • Enabling txindex and blockfilterindex does not multiply the total configured budget once per database.
  • Effective capacities are observable in logs or metrics.
  • Zero, minimum, overflow, and unsupported-backend behavior is defined.

Measure cold start, warm replay, modern-corpus apply, and indexed workloads. The RAM-resident UTXO path means an early-chain replay alone is insufficient evidence for a cache change.

6. Keep lower-ceiling storage work secondary

Window-level block-file/index batching and incremental checkpoints may be investigated only after attribution shows they are material.

Historical 0→150k decomposition bounded block-body persistence at approximately 4.66 seconds total. Do not weaken body availability, reorg readiness, pruning ordering, or crash durability to remove that bounded cost.

Acceptance criteria

  • Baselines use the controlled comparator from Build controlled loopback P2P comparator #35 and the measurement rules from Attribute every measured product hot path exhaustively #39/Capture fresh 36-cell baseline matrix #43.
  • Wall time, CPU time, peak RSS, actual staged memory, retry/churn counters, and correctness commitments are captured together.
  • Candidate and control runs are interleaved; isolated microbenchmarks are diagnostic only.
  • A dynamic-window candidate preserves all staller, backpressure, single-peer fallback, and memory-bound invariants.
  • A raw-first candidate demonstrates that no additional full parse or serialization pass was introduced.
  • dbcache_mb produces observable backend capacities without multiplying the process budget across optional indexes.
  • Reopen, interrupted-write, branch-switch, pruning, and checkpoint tests preserve the existing durability and body-availability contract.
  • Every candidate receives an evidence-backed disposition: land, reject, or defer.
  • No result is generalized to public-WAN full-tip IBD without a separate live high-height validation.

Non-goals

  • Weakening consensus or script verification.
  • Trust shortcuts or broader assume-valid use.
  • Increasing constants without updating their coupled safety invariants.
  • Moving synchronous storage I/O onto the event loop.
  • Claiming public-network IBD leadership from a loopback result.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions