Skip to content

feat: Let followers import zone blocks from leader sequencers#668

Merged
adityapk00 merged 10 commits into
mainfrom
aditya/p2p_block_import
Jul 16, 2026
Merged

feat: Let followers import zone blocks from leader sequencers#668
adityapk00 merged 10 commits into
mainfrom
aditya/p2p_block_import

Conversation

@adityapk00

@adityapk00 adityapk00 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator
  • follower sequencers can import blocks from leader sequencers.
    • leader broadcasts canonical block
    • follower recieves it over commonware p2p
    • Validate block + re-execute it + ingest into DB

Note:

@tempoxyz-bot tempoxyz-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️ Cyclops Review

This PR adds leader-to-follower block replication, but the current implementation has verified safety and liveness issues around delivery guarantees, oversized block handling, Zone-specific validation, and deterministic replay of L1-derived state. I left the actionable findings inline.

Reviewer Callouts
  • Follower role vs sequencer config: crates/node/src/node.rs:629 still starts ZoneEngine whenever sequencer_config exists, while follower behavior is keyed off the P2P role. A node configured as both follower and sequencer should hard-fail rather than both importing leader blocks and producing local blocks.
  • Block-channel memory/backpressure budget: MAX_MESSAGE_SIZE = 10 MiB, BLOCK_BACKLOG = 128, and receive-side role filtering after delivery mean authenticated peers can impose non-trivial memory/bandwidth pressure. Review Commonware queueing/backpressure before multi-operator exposure.
  • Broader replay determinism: The policy and portal-storage findings are concrete examples of the same class: follower replay currently depends on node-local L1-derived providers. When fixing them, audit every EVM-visible L1 read, especially “latest” fallback paths, and ensure imported block bytes or follower derivation fully determine execution inputs.

Comment thread crates/p2p/src/runtime.rs
})
.await;
let sent = match sent {
Ok(sent) => sent?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] Oversized canonical blocks can permanently stop leader-to-follower replication

sender.send(...) errors are re-raised here. Since the P2P channel is capped at 10 MiB and the leader broadcasts full RLP blocks without a byte-size cap, a canonical block over the Commonware limit returns MessageTooLarge, terminates run_commands, stops the P2P runtime, and leaves followers permanently behind with no backfill.

Recommended Fix:
Handle MessageTooLarge/send errors non-fatally, enforce a block byte cap below the P2P limit or chunk large blocks, and make unexpected P2P/broadcast termination restart or fail the node.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At 300m gas, the theorotical max is 7.5Mb (claude estimate) so i bumped it to 20MB by default for now. However, this point is moot because if the block size exceeds the max, then the chain will stall anyway since there won't be enough quorum in the ZonePortal to advance the batch

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, this number is 8MB on tempo L1

Comment thread crates/node/src/node.rs Outdated
}

// 3. All txns in the block execute properly
let payload = ZonePayloadTypes::block_to_payload(block, None);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] Follower import relies on NoopConsensus instead of Zone validation

This wraps authenticated leader bytes directly into an execution payload. The Zone node is wired with NoopConsensus, so Reth's normal structural/header/receipt/gas validation hooks and Zone-specific derivation checks are not enforced; a compromised configured leader can canonicalize executable but non-Zone-valid blocks on followers.

Recommended Fix:
Use a Zone-specific consensus/derivation validator for imported blocks and verify the expected advanceTempo grammar, withdrawal rules, absence of extra privileged system calls, and L1-derived inputs before forkchoice.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this is ultimately the provers job. The NoopConsensus is intentional, since we don't want any additional concensus at the zone layer.

Comment thread crates/node/src/node.rs
Comment thread crates/p2p/src/runtime.rs
.send(Recipients::Some(followers.clone()), block.clone(), true)
.await
.map_err(|err| eyre::eyre!("failed broadcasting zone block: {err}"))?;
if !sent.is_empty() || followers.is_empty() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ISSUE] Best-effort broadcast plus no backfill can permanently desync followers

Returning once sent is non-empty treats delivery to any one follower as success. A temporarily disconnected configured follower gets no retry for this block, and if it later receives N+1, import_leader_block rejects the gap because backfill is not implemented. That follower remains stale indefinitely.

Recommended Fix:
Track per-follower delivery/ACK high-water marks and add backfill or staged-sync recovery. At minimum, warn/metric partial delivery and do not treat it as sufficient for HA/failover assumptions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backfill is fixed in another PR

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread crates/node/src/node.rs Outdated
…k_import

# Conflicts:
#	crates/node/src/node.rs
#	crates/node/tests/it/utils.rs
@adityapk00
adityapk00 marked this pull request as ready for review July 16, 2026 02:02
Comment thread crates/node/src/node.rs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move these functions somewhere else and keep this file to ZoneNode

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to its own file

Comment thread crates/node/src/node.rs Outdated
Comment on lines +126 to +127
while let Some(persisted_tip) = persisted.next().await {
if persisted_tip.number < last_broadcast {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there's a race condition here, inverse to what's mentioned above:

   // Subscribe before reading the database height so persistence cannot race task startup.
    let mut persisted = provider.persisted_block_stream();
    let mut last_broadcast = match provider.last_block_number() {

last_block_number can already include the newest message in the stream I believe

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good. Catch. Fixed + added test

Comment thread crates/p2p/src/network.rs
Comment on lines +16 to +17
// At 30M gas, calldata is bounded below 7.5 MiB; leave headroom for block overhead.
pub(crate) const MAX_MESSAGE_SIZE: u32 = 20 * 1024 * 1024;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be increased, we dont expect malicious messages anyway

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this in another PR, but there's a thread on slack about limiting this in the block builder (like we do in Tempo L1) so we don't end up with an unbroadcastalbe block

@tempo-voight-kampff

Copy link
Copy Markdown

Hi @mattsse — your changes-requested review was detected by Voight-Kampff but no live Voight-Kampff agent connection received it, so Voight-Kampff sent a push fallback. The push was not approved before the approval window closed. Your review did not count toward this PR.

You can also try requesting changes directly via CLI with

vk reject pr https://github.com/tempoxyz/zones/pull/668

# Conflicts:
#	crates/node/tests/it/e2e.rs
#	crates/node/tests/it/utils.rs
@adityapk00
adityapk00 added this pull request to the merge queue Jul 16, 2026
Merged via the queue into main with commit 8765c14 Jul 16, 2026
16 checks passed
@adityapk00
adityapk00 deleted the aditya/p2p_block_import branch July 16, 2026 15:21
Comment on lines +172 to +175
if let Err(err) = import_leader_block(&provider, &engine, &block).await {
tracing::error!(target: "zone::p2p", %err, "Rejected leader block");
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logs an import failure and continues, but the local head has not advanced. Every subsequent leader block will then be rejected as a gap, so the follower remains running and serving a permanently stale chain.

We should either terminate the task or try to backfill when a leader block cant be imported. Silently continuing makes the failurelook recoverable but IIUC it isnt.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is fixed in #679

if the imported block is really invalid (eg. leader is compromised), the follower should stall
if this is a transient issue, #679 will retry (with a backfill that will fill the missed block(s)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants