Skip to content

Commit 3380505

Browse files
committed
fix(node): keep checkpoint-only blocks leader-neutral
1 parent 6d47c69 commit 3380505

2 files changed

Lines changed: 76 additions & 37 deletions

File tree

crates/node/src/engine.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ use zone_l1::{DepositQueue, EncryptionKeyRing, L1BlockDeposits, L1BlockTracker};
5959
use zone_p2p::{LeadershipSchedule, P2pPeerId};
6060
use zone_payload::{TempoImport, ZonePayloadAttributes, ZonePayloadTypes};
6161

62-
/// Per-anchor production permit backed by the effective leadership schedule.
62+
/// Full-block production permit backed by the effective leadership schedule.
6363
///
64-
/// The permit is a single schedule lookup: produce anchor `N` only if the portal schedule or a
65-
/// forced-recovery override assigns `N` to this node. An optimistic override is open-ended until
66-
/// the next finalized portal transition supplies the ordinary-authority boundary.
64+
/// Full blocks require the leader assigned to their imported Tempo header. Checkpoint-only blocks
65+
/// are leader-neutral and bypass this permit. An optimistic override is open-ended until the next
66+
/// finalized portal transition supplies the ordinary-authority boundary.
6767
#[derive(Debug, Clone)]
6868
pub struct ProductionPermit {
6969
schedule: LeadershipSchedule,
@@ -79,7 +79,7 @@ impl ProductionPermit {
7979
}
8080
}
8181

82-
/// Decide whether this node may produce the zone block embedding `tempo_anchor`.
82+
/// Decide whether this node may produce the full zone block embedding `tempo_anchor`.
8383
///
8484
/// `None` authorizes production; `Some(exit)` is the reason the engine must stop.
8585
pub fn check(&self, tempo_anchor: u64) -> Option<EngineExit> {
@@ -483,6 +483,19 @@ impl AvailableBlockDrain for ZoneEngine {
483483
}
484484

485485
fn permit(&self, block: &Self::Block) -> Option<EngineExit> {
486+
let queued_headers = self
487+
.deposit_queue
488+
.peek_headers(zone_primitives::constants::MAX_TEMPO_HEADERS_PER_ZONE_BLOCK + 1);
489+
if checkpoint_header_count(
490+
&self.chain_spec,
491+
&queued_headers,
492+
self.l1_block_tracker.finalized_target(),
493+
) > 0
494+
{
495+
// TIP-1096 assigns no leader to checkpoint-only blocks. Leader authority resumes at
496+
// the final full block, whose single imported Tempo header is checked below.
497+
return None;
498+
}
486499
self.production_permit
487500
.as_ref()
488501
.and_then(|permit| permit.check(block.header.number()))

crates/node/src/replication.rs

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -702,9 +702,9 @@ pub(crate) async fn collect_follower_settlement_signatures<P>(
702702

703703
/// Import live/backfilled blocks in canonical order on a follower.
704704
///
705-
/// Live blocks are only imported from the leader when the sender equals
706-
/// `schedule.leader_for(the block's embedded anchor)`. We do this because if there are accidentally
707-
/// two leaders (split brain) for a block, we need to decide to import the correct one.
705+
/// Live full blocks are only imported when the sender equals the leader for their imported Tempo
706+
/// header. Checkpoint-only blocks have no designated leader and may be imported from any authorized
707+
/// P2P peer. The full-block check resolves accidental split brain using the finalized schedule.
708708
///
709709
/// Backfilled blocks carry no producer claim and are judged by
710710
/// parent/anchor/execution/conflict validation alone. The loop exits when `stop` fires.
@@ -1161,22 +1161,21 @@ where
11611161
let headers = tempo_import.headers();
11621162
validate_l1_checkpoint_range(headers, local.number, local.hash, block_number)?;
11631163
let anchor = headers.last().expect("validated nonempty range").num_hash();
1164-
let production_anchor = tempo_import.production_anchor();
1164+
let leader_anchor = tempo_import.leader_anchor();
11651165

1166-
// Anchor-aware fence for live blocks: a full block has one imported header, while a
1167-
// checkpoint-only block may cross leadership boundaries and is assigned to the leader of its
1168-
// first imported header. This gives every live block one producer without making a later
1169-
// transition inside a checkpoint range invalidate the block.
1166+
// Anchor-aware fence for live full blocks. Checkpoint-only blocks are leader-neutral and may
1167+
// cross leadership boundaries; leader authority resumes at the final full block and is checked
1168+
// against that block's single imported Tempo header.
11701169
// Backfilled blocks carry no producer claim and are judged by parent/anchor/execution/conflict
11711170
// validation only.
11721171
//
11731172
// Check once before waiting to reject an already-known invalid sender without blocking the
1174-
// import loop, then again after observing every imported header. The first header itself may
1175-
// finalize a leadership transition that changes its assigned producer.
1176-
validate_live_block_sender(
1173+
// import loop, then again after observing every imported header. The full block's imported
1174+
// header may itself finalize a leadership transition that changes its assigned producer.
1175+
validate_live_import_sender(
11771176
schedule,
11781177
peer_block.live_sender.as_ref(),
1179-
production_anchor,
1178+
leader_anchor,
11801179
block_number,
11811180
)?;
11821181
let mut observed_headers = Vec::with_capacity(headers.len());
@@ -1207,10 +1206,10 @@ where
12071206
};
12081207
observed_headers.push(observed);
12091208
}
1210-
validate_live_block_sender(
1209+
validate_live_import_sender(
12111210
schedule,
12121211
peer_block.live_sender.as_ref(),
1213-
production_anchor,
1212+
leader_anchor,
12141213
block_number,
12151214
)?;
12161215

@@ -1277,7 +1276,7 @@ where
12771276
}
12781277
schedule.record_applied_anchor(anchor.number);
12791278

1280-
info!(target: "zone::p2p", block_number, ?hash, "Imported canonical leader block");
1279+
info!(target: "zone::p2p", block_number, ?hash, "Imported canonical peer block");
12811280
Ok(PeerBlockImportOutcome::Imported)
12821281
}
12831282

@@ -1305,6 +1304,18 @@ fn validate_live_block_sender(
13051304
}
13061305
}
13071306

1307+
fn validate_live_import_sender(
1308+
schedule: &LeadershipSchedule,
1309+
live_sender: Option<&P2pPeerId>,
1310+
leader_anchor: Option<u64>,
1311+
block_number: u64,
1312+
) -> eyre::Result<()> {
1313+
let Some(anchor_number) = leader_anchor else {
1314+
return Ok(());
1315+
};
1316+
validate_live_block_sender(schedule, live_sender, anchor_number, block_number)
1317+
}
1318+
13081319
fn reconcile_canonical_import(
13091320
deposit_queue: &DepositQueue,
13101321
tempo_import: &DecodedTempoImport,
@@ -1511,15 +1522,15 @@ impl DecodedTempoImport {
15111522
}
15121523
}
15131524

1514-
/// Tempo anchor whose leader is the unique live producer for this Zone block.
1525+
/// Tempo anchor whose leader must produce this Zone block.
15151526
///
1516-
/// A checkpoint-only block may cross later leadership boundaries, so its first imported
1517-
/// header selects the producer. A full block contains one header and follows the same rule.
1518-
fn production_anchor(&self) -> u64 {
1519-
self.headers()
1520-
.first()
1521-
.expect("decoded Tempo imports are validated as nonempty")
1522-
.number()
1527+
/// Checkpoint-only blocks have no designated leader. A full block imports exactly one Tempo
1528+
/// header, whose effective leader supplies its production authority.
1529+
fn leader_anchor(&self) -> Option<u64> {
1530+
match self {
1531+
Self::Full { header, .. } => Some(header.number()),
1532+
Self::CheckpointOnly { .. } => None,
1533+
}
15231534
}
15241535
}
15251536

@@ -1993,7 +2004,7 @@ mod tests {
19932004
}
19942005

19952006
#[test]
1996-
fn checkpoint_live_producer_is_leader_of_first_imported_anchor() {
2007+
fn checkpoint_live_producer_is_not_leader_restricted() {
19972008
use commonware_cryptography::{Signer as _, ed25519::PrivateKey};
19982009
use reth_primitives_traits::SealedHeader;
19992010
use tempo_primitives::TempoHeader;
@@ -2018,14 +2029,29 @@ mod tests {
20182029
.to_vec();
20192030
let tempo_import = super::DecodedTempoImport::CheckpointOnly { headers };
20202031

2021-
let production_anchor = tempo_import.production_anchor();
2022-
assert_eq!(production_anchor, 90);
2023-
validate_live_block_sender(&schedule, Some(&outgoing), production_anchor, 7).unwrap();
2024-
let error = validate_live_block_sender(&schedule, Some(&incoming), production_anchor, 7)
2025-
.expect_err(
2026-
"the later epoch leader must not produce a range beginning in the old epoch",
2027-
);
2028-
assert!(error.to_string().contains(&outgoing.to_string()));
2032+
let leader_anchor = tempo_import.leader_anchor();
2033+
assert_eq!(leader_anchor, None);
2034+
super::validate_live_import_sender(&schedule, Some(&outgoing), leader_anchor, 7).unwrap();
2035+
super::validate_live_import_sender(&schedule, Some(&incoming), leader_anchor, 7).unwrap();
2036+
2037+
let full_import = super::DecodedTempoImport::Full {
2038+
header: Box::new(SealedHeader::seal_slow(TempoHeader {
2039+
inner: alloy_consensus::Header {
2040+
number: 110,
2041+
..Default::default()
2042+
},
2043+
..Default::default()
2044+
})),
2045+
deposits: Vec::new(),
2046+
enabled_tokens: Vec::new(),
2047+
};
2048+
let leader_anchor = full_import.leader_anchor();
2049+
assert_eq!(leader_anchor, Some(110));
2050+
super::validate_live_import_sender(&schedule, Some(&incoming), leader_anchor, 8).unwrap();
2051+
let error =
2052+
super::validate_live_import_sender(&schedule, Some(&outgoing), leader_anchor, 8)
2053+
.expect_err("the final full block must be produced by its effective leader");
2054+
assert!(error.to_string().contains(&incoming.to_string()));
20292055
}
20302056

20312057
#[tokio::test]

0 commit comments

Comments
 (0)