Skip to content

Fix qbft backlog byte limit - #82

Merged
chrischarlesharrison merged 4 commits into
masterfrom
fix-qbft-backlog-byte-limit
Aug 21, 2026
Merged

Fix qbft backlog byte limit#82
chrischarlesharrison merged 4 commits into
masterfrom
fix-qbft-backlog-byte-limit

Conversation

@chrischarlesharrison

Copy link
Copy Markdown
Collaborator

No description provided.

The future-message backlog only counted messages, not bytes. A PRE-PREPARE carries a full block proposal that can run right up to the 10 MiB transport frame, so the 1024-message per-validator cap (sized for ~1KB messages) let a single validator pin multi-GB of proposals before the block ever reached VerifyBlockProposal.

Reject any future PRE-PREPARE whose encoded size is over 4 MiB before we retain it. A real block is gas-bound to ~1.9 MiB at the 30M gas limit, so 4 MiB leaves comfortable headroom for legit blocks while killing the oversized-proposal vector. The backlog now stores each entry with its encoded size so we can build a proper byte budget on top of this.
The per-message ceiling stops any single oversized PRE-PREPARE, but on its own it doesn't bound how much the whole backlog can hold: 1024 slots times a few MiB each still adds up to gigabytes per sender. This tracks retained bytes per validator and globally and rejects admission once either budget is hit, so aggregate memory is actually bounded rather than just the worst single message.

Budgets are 32 MiB per validator and a dynamic global cap (2x per-validator, floored at 128 MiB, ceilinged at 512 MiB) that mirrors the existing count-cap shape. The byte counters are kept exact across every push, pop, requeue and whole-backlog eviction, since a drift or underflow there would silently wedge the backlog shut and become a self-inflicted liveness bug. Purely local admission policy, so it stays wire- and consensus-compatible with unpatched nodes.
The size ceiling only guarded PRE-PREPARE, but ROUND-CHANGE can embed a full PreparedBlock exactly like a proposal, so it had the same unbounded-payload path into the backlog. PREPARE and COMMIT only carry a fixed-size digest/seal, so they don't need it.

Factor the check behind carriesBlockProposal() and apply it to both block-bearing codes. The aggregate byte budget already covered ROUND-CHANGE, so this just closes the single-oversized-message gap for it as well.
@chrischarlesharrison
chrischarlesharrison merged commit 17c6ffe into master Aug 21, 2026
3 checks passed
The 4 MiB per-message ceiling is sized against the 30M gas limit (max data-carrying block ~1.9 MiB). If someone raises the gas limit later without revisiting the ceiling, legitimate large blocks would start getting dropped from the backlog - a silent liveness bug.

Rather than derive the ceiling from live chain state (which would put a consensus-data read on the admission hot path and still need a margin anyway), pin the assumption in a test. It recomputes the max block from the gas limit and TxDataNonZeroGasEIP2028 and asserts the ceiling keeps ~2x headroom, so the two can't drift apart without a red test forcing a reconcile.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant