refactor: type-guard recently-executed block provenance (leader epoch/round/digest)#20
Conversation
|
Claude finished @procdump's task in 3m 14s —— View job PR Review
SummaryThis is a well-motivated refactoring that fixes a real production bug (proposer throttle wedge) through compile-time enforcement. The type design is sound, the canonical nonce codec is a clear improvement, and the documentation is unusually thorough. The PR introduces no behavioral change. A handful of observations below — one minor correctness concern, a few design gaps, and some test coverage gaps. Observations1.
|
|
Claude finished @procdump's task in 1m 31s —— View job PR Review
The branch now has a follow-up commit ( Addressed ✓Observation #3 — Fixed. This is the right call — the enforcement is now in the type, not the documentation. Observation #4 — Fixed. The doc now mirrors Observation #5 — Missing unit test for the epoch/round extraction chain Fixed with three well-chosen tests:
The Still OpenObservation #1 — Not addressed. The round-trip // recently_executed_blocks.rs:188
pub fn block_capacity(&self) -> u64 {
self.num_blocks as u64
}
// consensus_bus.rs:325–329
let recently_executed_blocks = self.tx_recently_executed_blocks.borrow().block_capacity();
// ...
RecentlyExecutedBlocks::new(recently_executed_blocks as usize)Harmless in practice (a capacity fits in both), but returning Observation #2 — Not addressed. let latest = self.tx_recently_executed_blocks.borrow().latest_block();
let mut recently_executed_blocks =
RecentlyExecutedBlocks::new(recently_executed_blocks as usize);
recently_executed_blocks.push_latest(latest.into_header()); // always pushesThe commit let latest = self.tx_recently_executed_blocks.borrow().latest_block();
let mut recently_executed_blocks =
RecentlyExecutedBlocks::new(recently_executed_blocks as usize);
if !self.tx_recently_executed_blocks.borrow().is_empty() {
recently_executed_blocks.push_latest(latest.into_header());
}AssessmentThe follow-up commit addressed the three highest-value observations: the |
…ssors The consensus bus channel of recently-executed blocks handed out raw SealedHeaders whose packed nonce (epoch<<32 | round) is the committed sub-DAG leader's epoch/round, NOT the execution frontier. A block drained from a parked, out-of-order batch lands as the newest height while carrying an old leader round/epoch, so reading round/epoch off the tip silently regresses the frontier (this previously wedged the proposer throttle). Harden the API so this can't be stumbled into: - Rename RecentBlocks -> RecentlyExecutedBlocks and RecentBlock -> RecentlyExecutedBlock (plus recent_blocks() -> recently_executed_blocks(), the tx field, and the module file) to reorient readers toward execution/arrival recency rather than round recency. - latest_block() and block_at_number() now return the RecentlyExecutedBlock wrapper. Its only epoch/round accessors are subdag_leader_epoch() / subdag_leader_round() (== the committing ConsensusOutput::leader_epoch/ leader_round), never bare epoch()/round(); all other header fields remain available via Deref<SealedHeader>. - Those accessors return distinct SubDagLeaderEpoch / SubDagLeaderRound newtypes instead of bare u32, so the compiler rejects comparing a leader round/epoch against a frontier value or against each other -- the class of mistake that once wedged the proposer throttle. Unwrap via .get() only at a deliberate boundary. - Extract the (epoch<<32 | round) codec into a single canonical home, rayls_infrastructure_types::nonce (pack_nonce/unpack_nonce, with tests), and route every previously hand-inlined encode/decode site through it (Header::nonce, RethEnv::deconstruct_nonce, and the evm decode sites). No behavioral change; renames, one shared codec, and compile-time-typed leader epoch/round.
- SubDagConsensusDigest no longer derives PartialEq/Eq/Hash: a frontier
digest is a bare B256 (that comparison already fails to compile) and
there is no legitimate reason to equality-check two of these, so the
type now enforces "unwrap only to look up, never to compare" instead of
relying on the doc.
- Document latest_block()'s default-on-empty behavior (returns a wrapper
around a default SealedHeader, matching latest_block_num_hash()).
- Add unit tests exercising the full chain (push_latest -> latest_block ->
subdag_leader_{epoch,round}().get()), including the drained-old-batch
round regression and block_at_number.
a08c9ac to
1292b47
Compare
|
Claude finished @procdump's task in 1m 35s —— View job PR Review
The branch has two commits on top of CodeQL Alerts — False PositivesThe security scanner flagged The field called No action needed. If the alerts are noisy, a Addressed ✓ (since previous review)Observation #3 — Fixed.
Enforcement is now in the type system, not the documentation. Observation #4 — Fixed. The doc at
Observation #5 — Missing unit test for the epoch/round extraction chain Fixed with three tests in
Using Still OpenObservation #1 — Not addressed. pub fn block_capacity(&self) -> u64 {
self.num_blocks as u64
}And let recently_executed_blocks = self.tx_recently_executed_blocks.borrow().block_capacity();
// ...
RecentlyExecutedBlocks::new(recently_executed_blocks as usize)The Observation #2 — Not addressed. let latest = self.tx_recently_executed_blocks.borrow().latest_block();
let mut recently_executed_blocks =
RecentlyExecutedBlocks::new(recently_executed_blocks as usize);
recently_executed_blocks.push_latest(latest.into_header()); // always pushesOn a fresh genesis start (empty window), let latest = self.tx_recently_executed_blocks.borrow().latest_block();
let mut recently_executed_blocks =
RecentlyExecutedBlocks::new(recently_executed_blocks as usize);
if !self.tx_recently_executed_blocks.borrow().is_empty() {
recently_executed_blocks.push_latest(latest.into_header());
}Minor — this is pre-existing behavior made more visible by the new API, and the AssessmentThe follow-up commit addressed all three high-value observations from the previous reviews. The CodeQL alerts are false positives with no security impact. The two remaining open items (#1 and #2) are both minor: #1 is a cosmetic type choice, and #2 is a pre-existing behavior that the new API makes more visible but does not change. Neither blocks merge. The PR is in good shape. |
|
Claude finished @procdump's task in 2m 9s —— View job PR Review
Current StateThe previous two review passes left two open items (#1 and #2). Both are still present. One new area to address: the CodeQL alerts on Still OpenObservation #1 —
|
The problem
The consensus bus's recently-executed-blocks channel handed out raw
SealedHeaders. The epoch/round packed in a block's nonce are the committed sub-DAG leader's values (the output that ordered it), not the execution frontier — a block drained from a parked, out-of-order batch lands as the newest height while carrying an old leader round/epoch. Reading round/epoch off the tip silently regresses the frontier (this once wedged the proposer throttle). The old name (RecentBlocks) plus bareu32/B256returns made this easy to stumble into and impossible for the compiler to catch.What's changed
RecentBlocks/RecentBlock→RecentlyExecutedBlocks/RecentlyExecutedBlock(+ accessor, field, module) to signal execution-arrival recency, not round recency.latest_block()/block_at_number()now return theRecentlyExecutedBlockwrapper; noDeref— safe fields (number/hash/num_hash) are delegated explicitly, and the raw header is reachable only viaas_header()/into_header()(a greppable, deliberate exit).SubDagLeaderEpoch,SubDagLeaderRound,SubDagConsensusDigest— so comparing them against a frontier value fails to compile; unwrap via.get()deliberately.(epoch<<32 | round)codec into one canonical home,rayls_infrastructure_types::nonce(pack_nonce/unpack_nonce+ tests), and routed every previously hand-inlined encode/decode site through it.No behavioral change — renames, one shared codec, compile-time-typed provenance.