fix(txpool): expire validity transactions after published flashblocks - #5477
fahimahmedx wants to merge 6 commits into
Conversation
🟡 Heimdall Review Status
|
|
✅ All benchmarks green — 14 within ±2% (deterministic instruction counts). View run Benchmark details (14)
|
| let _admission_guard = self.protocol_admission_lock.lock(); | ||
| let result = publish()?; | ||
| let expired = self.expire_published_locked(block_number, flashblock_index); | ||
| Ok((result, expired)) |
There was a problem hiding this comment.
The protocol_admission_lock is now held for the entire duration of the publish closure (websocket serialization + send), whereas before this PR publish() ran without it. During this window, all protocol and sidecar pool admission (add_validated_protocol_transaction, add_validated_sidecar_transaction, add_transaction_and_subscribe) blocks on the same mutex.
If the websocket publish experiences backpressure or latency (e.g., slow consumer, large flashblock serialization), pool admission stalls for all submitters until it completes. This is the intended tradeoff for atomicity, but worth confirming that the publish path cannot block long enough to cause transaction submission timeouts or RPC deadline violations under production load.
There was a problem hiding this comment.
The lock intentionally serializes admission with publication and expiry so an observed deadline frees the same-nonce slot before admission proceeds. WebSocketPublisher::publish (crates/builder/publish/src/publisher.rs) performs synchronous JSON serialization, a ring-buffer update, and tokio broadcast::Sender::send, not a socket send. Per-client network sends run separately in BroadcastLoop; replay snapshots release the ring-buffer read lock before awaiting sends. Slow consumers therefore do not backpressure this admission lock. Serialization still contributes to lock hold time; this inspection does not establish production latency bounds, but it does not identify a timeout regression requiring a code change.
🤖 Reply generated with Toshi
Acceptance Tests: ✅ Passed3/3 checks passed · Checks & reports Acceptance report
|
| }) { | ||
| return 0; | ||
| } | ||
| *published = Some(position); |
There was a problem hiding this comment.
published_flashblock is set to Some(position) here but is never reset to None when a new block starts (e.g., in on_canonical_state_change). This works today because expire_published_locked uses a monotonic >= guard at line 443 and ensure_not_published_expired uses is_batch_expired which correctly handles cross-block semantics (a stale published position from block N won't falsely reject transactions targeting block N+1).
However, the stale published_flashblock from a previous block means ensure_not_published_expired performs a non-trivial is_batch_expired evaluation on every admission for the rest of the node's lifetime, even when no flashblocks are being built (e.g., sequencer downtime). More importantly, the implicit invariant that the stale position is always safe relies on is_batch_expired's block-vs-flashblock interaction. An explicit reset in on_canonical_state_change (e.g., *self.published_flashblock.write() = None; under the admission lock) would make this simpler to reason about and remove the coupling.
There was a problem hiding this comment.
The retained position is intentional across ordinary block transitions: canonical maintenance can arrive after flashblocks for the next block have already been published. An unconditional reset would erase that newer deadline protection and admit already-expired submissions.
I did reproduce a correctness issue on reorgs: a position from the old branch rejects otherwise-valid rebuilt-block deadlines and suppresses expiry at lower block/index positions. The follow-up resets the watermark only for PoolUpdateKind::Reorg, while holding the admission lock. Regression tests cover protocol and sidecar admission/expiry after both a lower-height rebuild and a same-height flashblock-index restart, plus preservation of newer publication state during an ordinary commit. The reorg regression fails before the fix.
No performance bottleneck was demonstrated for retaining the position during ordinary commits, so I have not added a broader reset or a performance refactor.
🤖 Reply generated with Toshi
Resolve the overlap with #5333 by keeping inclusive expiry in BlockExpiryIndex and the canonical-maintenance boundary regression. Refresh the stale tx-forwarding README digest inherited from main after verifying its existing summary.
Keep main’s updated tx-forwarding docs-index summary and matching generated indexes. Preserve the flashblock expiry and reorg handling unchanged.
Review SummaryThis PR correctly coordinates flashblock publication with pool admission and deadline eviction, addressing the race where an expired same-nonce transaction could block its unbumped replacement. The design is sound: Findings from prior review passes (already commented inline):
Additional observations (not warranting inline comments):
No critical or high-severity findings. The existing inline comments cover the substantive review points. |
Summary
Review follow-ups
Addresses all three comments from Brian Bland on #5436:
(u64, u64)expiry keys with a documentedExpiryPositionstruct, ordered by block number and then flashblock index (comment).GitHub review follow-up: reset the orphaned publication watermark on canonical reorgs, while preserving it during ordinary commits that may lag newer flashblock publication. Regression tests cover protocol/sidecar admission and expiry after lower-height or lower-index rebuilds, and preserve active deadline protection across delayed ordinary commits (comment).
Verification
cargo test -p base-execution-txpool --lib(298 passed, 3 ignored)cargo test -p base-builder-core --test transaction_events -- --test-threads=1(3 passed)cargo clippy -p base-execution-txpool -p base-builder-core --lib --tests -- -D warningscargo fmt --all -- --checkgit diff --checkThe end-to-end test waits until flashblock 1 is published, submits an unbumped same-nonce transaction, and verifies it is included in the current L2 block before that block seals. It does not assert a particular later flashblock index.
Replaces #5436 with the head branch hosted in
base/baseatfahim/fix-flashblock-tx-expiryrather than in the fork.Generated with Toshi