World Chain Specs
About World Chain
World Chain is a blockchain designed for humans. Prioritizing scalability and accessibility for real users, World Chain provides the rails for a frictionless onchain UX.
Site Navigation
Navigate this site using the sidebar on the left, the search icon found at the top of this page, or the left/right navigation buttons found to the sides of each page.
Flashblocks P2P Extension
This document is an extension to the original Flashblocks specification, modifying the flashblock propagation mechanism to use a peer-to-peer (P2P) network instead of WebSockets. It highlights the new P2P protocol and the changes in Rollup-Boost and builder interactions, aimed at simplifying distribution and improving fault tolerance in High Availability (HA) sequencer setups.
Table of Contents
Abstract
This document introduces an enhancement to Flashblocks where the propagation of partial blocks (“flashblocks”) is done over an Ethereum P2P subprotocol instead of a WebSocket broadcast. By integrating flashblock distribution into the peer-to-peer network, we eliminate the need for a dedicated WebSocket proxy and enable more robust, decentralized propagation of flashblock data. Crucially, this P2P approach uses cryptographic authorization to ensure that only an authorized block builder (and its designated successors in an HA setup) can publish flashblocks, improving fault tolerance during sequencer failovers. The end result is a simpler and more resilient system for delivering rapid preconfirmation data to users, without altering the core OP Stack protocol.
Motivation
The original Flashblocks design relied on a centralized broadcast (via Rollup-Boost and a WebSocket proxy) to propagate flashblocks to RPC providers. While effective, that design introduced operational complexity and potential single points of failure:
- Operational Complexity: Sequencer operators had to manage a WebSocket broadcasting service (e.g. Rollup-Boost’s WebSocket proxy) to fan-out flashblocks to providers. In multi-sequencer (HA) configurations, handing off this connection or migrating subscribers was cumbersome.
- Failover Challenges: In a High Availability sequencer setup, if the active sequencer failed the act of switching to a new sequencer/rollup-boost/builder combo would mean that already published flashblocks would not make it in the new block produced by the new builder. This breaks the promise that flashblocks makes to its consumers.
- Scalability and Decentralization: Relying on a single hub (the sequencer’s Rollup-Boost) to redistribute flashblocks could become a bottleneck. A P2P approach can naturally scale out to many peers and align with Ethereum’s existing propagation model for blocks and transactions.
P2P Propagation addresses these issues by leveraging a gossip network for flashblocks. In this model, any number of RPC provider nodes (or other interested parties) can connect to the flashblock P2P network to receive preconfirmation updates. Failover is handled gracefully through the RLPx protocol: if a new sequencer takes over, its builder is already aware of previously published flashblocks, and so it can build on top of what has already been promised to the network.
Specification
Terminology
We inherit all terminology from the original Flashblocks spec (Sequencer, Block Builder, Rollup-Boost, etc.), with a few new terms introduced:
- Authorizer – The entity that vouches for a block builder’s legitimacy to produce flashblocks. In practice, this is rollup-boost who signs an authorization for a given builder each block cycle.
- Builder Public Key – A cryptographic public key identifying a builder on the flashblocks P2P network. This is distinct from an Ethereum address; it’s used for signing/validating flashblock messages.
- Flashblocks P2P Network – The peer-to-peer overlay network (using Ethereum’s devp2p protocols) through which flashblock messages are gossiped. Participants include all builders and one or more subscribing nodes (e.g. RPC providers, possibly other sequencer nodes in standby).
- Publisher – The current active builder that is publishing flashblocks for the ongoing L2 block. In an HA setup, the role of publisher can transfer to a new builder if the sequencer fails over.
Flashblocks Sequence Diagram
%%{init: {'sequence': {'noteMargin': 15}} }%%
sequenceDiagram
participant ON as op-node<br/>(Sequencer)
participant RB as rollup-boost<br/>(Authorizer)
participant B as world-chain reth<br/>(Builder)
participant P2P as P2P Peers<br/>(Receiving Nodes)
Note over ON,P2P: ── Slot N (T=0s) ── FCU + Authorization
ON->>RB: engine_forkchoiceUpdatedV3(fcs, attributes)
Note over RB: Derive payload_id deterministically:<br/>payload_id = hash(parent_block_hash, attributes)<br/>(same algorithm as the builder)
Note over RB: Create Authorization token:<br/>sign(payload_id ‖ timestamp ‖ builder_vk)<br/>with authorizer_sk
RB->>B: flashblocks_forkchoiceUpdatedV3(fcs, attributes, authorization)
Note over B: Derive same payload_id from attributes<br/>Match authorization.payload_id to job
B-->>RB: ForkchoiceUpdated { payload_id }
RB-->>ON: ForkchoiceUpdated { payload_id }
Note over ON,P2P: Flashblock production (T≈0s–2s)
Note over B: start_publishing(auth)
B->>P2P: P2P: StartPublish (Authorized)
Note over P2P: Verify authorizer_sig<br/>Verify builder_sig<br/>Track active publisher
Note over B: spawn_build_job()<br/>reads txpool, builds payload
Note over B: T≈200ms: flashblock interval fires
Note over B: Flashblock #0 diff:<br/>empty → current best
B->>P2P: P2P: FlashblocksPayloadV1 #0 (Authorized)
Note over P2P: Verify signatures<br/>Apply flashblock #0
Note over B: ~50ms recommits<br/>rebuild with new txs
Note over B: T≈400ms: flashblock interval fires
Note over B: Flashblock #1 diff:<br/>#0 → new best (new txs only)
B->>P2P: P2P: FlashblocksPayloadV1 #1 (Authorized)
Note over B: ~50ms recommits...
Note over B: T≈600ms: flashblock interval fires
B->>P2P: P2P: FlashblocksPayloadV1 #2 (Authorized)
Note over B: ... repeats every ~200ms until deadline
Note over ON,P2P: Seal the block (T≈2s)
ON->>RB: engine_getPayloadV3(payload_id)
RB->>B: engine_getPayloadV3(payload_id)
Note over B: Return committed payload<br/>(state at last flashblock)
B-->>RB: ExecutionPayloadEnvelopeV3
RB-->>ON: ExecutionPayloadEnvelopeV3
Note over B: stop_publishing()
B->>P2P: P2P: StopPublish (Authorized)
Note over P2P: Verify signatures<br/>Remove active publisher
Note over ON,P2P: ── Slot N+1 (T≈2s) ──
Data Structures
The fundamental flashblock data structures (FlashblocksPayloadV1, ExecutionPayloadFlashblockResultV1, ExecutionPayloadStaticV1, and the various Metadata containers) remain unchanged. Flashblocks are still represented as a sequence of incremental payloads culminating in a full block.
To support P2P propagation and authorization, we introduce several new structures:
Authorization
Represents a sequencer’s cryptographic authorization for a specific builder to produce a block with a given payload context. This is essentially a signed token from the sequencer (authorizer) that the builder includes with its flashblocks.
#![allow(unused)]
fn main() {
pub struct Authorization {
pub payload_id: PayloadId,
pub timestamp: u64,
pub builder_vk: VerifyingKey,
pub authorizer_sig: Signature,
}
}
payload_id: The unique ID for this block’s payload (as provided byengine_forkchoiceUpdatedin the OP Stack Engine API). All flashblocks for the block share this ID.timestamp: The timestamp associated with this payloadbuilder_vk: The verifying key identifying the builder authorized to publish this block’s flashblocks. Peers will use this to verify the builder’s signatures on messages.authorizer_sig: A signature produced by the sequencer (authorizer) over the concatenation ofpayload_id,timestamp, andbuilder_vk. This proves that the sequencer has approved the given builder (and key) to act for this block. Only one authorizer key (controlled by the rollup-boost operator) is recognized by the network, and all peers are configured with its public key for verification.
Authorized Message
Container for any flashblocks P2P message that requires authorization. It bundles a payload (one of the message types defined below) with the authorization and a builder’s signature.
#![allow(unused)]
fn main() {
pub struct Authorized {
pub msg: AuthorizedMsg,
pub authorization: Authorization,
pub actor_sig: Signature,
}
}
#![allow(unused)]
fn main() {
pub enum AuthorizedMsg {
FlashblocksPayloadV1(FlashblocksPayloadV1) = 0x00,
StartPublish(StartPublish) = 0x01,
StopPublish(StopPublish) = 0x02,
}
}
-
authorization: The Authorization object, as described above. -
msg: The message content. This is a tagged union that can be one of:- A Flashblock Payload – Contains a
FlashblocksPayloadV1(partial block delta), see below. - A StartPublish signal – Indicates the builder is starting to publish a new block (detailed in StartPublish).
- A StopPublish signal – Indicates the builder is stopping publication (detailed in StopPublish).
- A Flashblock Payload – Contains a
-
actor_sig: The builder’s signature over the combination of themsgand theauthorization. This attests that the message indeed comes from the holder of thebuilder_skin the Authorization, and that it hasn’t been tampered with in transit.
Every P2P message in the Flashblocks protocol is sent as an AuthorizedMessage. This double-signature scheme (authorizer + builder) provides two layers of security:
- Only a builder with a valid Authorization (signed by the sequencer) can get its messages accepted by peers.
- Only the genuine builder (holding the private key corresponding to
builder_sk) can produce a validbuilder_signatureon the message content.
StartPublish
A small message indicating the intention to begin publishing flashblocks for a new L2 block.
#![allow(unused)]
fn main() {
pub struct StartPublish;
}
The StartPublish message is always sent wrapped in an AuthorizedMessage (with the appropriate authorization and signatures). It serves as an announcement to the network that “Builder X is about to start publishing”
StopPublish
An authorized message indicating that the builder will no longer publish any flashblocks
#![allow(unused)]
fn main() {
pub struct StopPublish;
}
Note: A builder will typically send a StopPublish when it receives a ForkChoiceUpdated without an accompanying Authorization from rollup-boost or upon handing off flashblock production to a new builder.
Flashblocks P2P Protocol
See also: Flashblocks P2P Protocol v2 amends this section with bounded, latency-optimized peer selection.
Protocol Overview
Flashblocks P2P communication is implemented as a custom Ethereum subprotocol. Specifically, it defines a new devp2p capability:
- Protocol Name:
flblk(flashblocks) - Version:
1
Nodes that support flashblocks will advertise this capability when establishing devp2p connections. Once connected, they can exchange flashblock messages as defined in this spec.
All flashblock messages are encoded in a compact binary format (analogous to Ethereum block gossip). Each message begins with a one-byte type discriminator, followed by the serialized content. The primary message type is an AuthorizedMessage (discriminator 0x00), which, as described, contains a nested payload type.
Key design features of the P2P protocol:
- Multipeer Gossip: A builder’s flashblock is forwarded to all connected peers, who in turn may forward it to their peers, etc., ensuring the payload reaches all participants without needing a single central broadcaster. The protocol includes basic duplicate suppression so that flashblocks aren’t endlessly propagated in loops.
- Real-time Coordination: Using
StartPublishandStopPublishsignals, multiple potential publishers (builders) can coordinate access to the network. This prevents conflicts where two builders might try to publish simultaneously, and allows a smooth handoff in failover scenarios (detailed below).
Message Types
Within the AuthorizedMsg union, we define the following variants and their semantics:
- Flashblock Payload Message: Carries a
FlashblocksPayloadV1(as defined in the original spec) for a specific partial block. This includes the incremental transactions, updated state root, receipts root, logs bloom, etc., up through that flashblock. Peers receiving this message will apply the included state updates to their preconfirmation cache. Each Flashblock message has anindex(the flashblock sequence number) and may include thebasesection if it’s the first flashblock (index 0) for that block. - StartPublish Message: Announces the start of a new publishers flashblock sequence. Peers use this to note which builder is now active for a given L2 block number, possibly resetting any previous state or halting their own publishing.
- StopPublish Message: Indicates the end of the flashblock sequence for the current publisher. After this message, no further flashblocks from that publisher should arrive. Inactive or waiting publishers use this as a cue that they may now take over for subsequent flashblocks.
All these are encapsulated in AuthorizedMsg with the requisite signatures.
Authorization and Security
The P2P protocol introduces a trust model wherein peers accept flashblocks only from an authorized builder. The security measures include:
-
Authorizer Signature Verification: Upon receiving any
AuthorizedMessage, a peer will first verify theauthorizer_sigin theAuthorizationagainst the known authorizer public key. This confirms that rollup-boost has indeed permitted the stated builder to produce the block with the givenpayload_idand timestamp. If this signature is missing or invalid, the message is discarded as untrusted. -
Builder Signature Verification: Next, the peer verifies the
builder_signatureon the message content using thebuilder_vkprovided in the Authorization. This ensures the message was genuinely produced by the authorized builder and not altered. If this check fails, the message is rejected. -
Payload Consistency Checks: Peers also check that the fields in the message are self-consistent and match expectations:
- The
payload_idin the Authorization must match theFlashblocksPayloadV1.payload_id(for flashblock messages). Each builder’s flashblock messages carry the same payload_id that was authorized, ensuring they all belong to the same block-building session. - Freshness: The
timestampin Authorization helps guard against replay of old messages. If a flashblock or StartPublish arrives with a significantly older timestamp (or for an already completed block), peers will ignore it and decrement the sender’s reputation.
- The
These measures ensure that only the rollup-boost sanctioned builder’s data is propagated and that it’s cryptographically sound. Unauthorized parties cannot inject false flashblocks or tamper with content without detection. This design also allows dynamic builder changes: as long as the sequencer signs a new Authorization, the peers will accept the new builder’s messages even if they have never seen that builder before, because trust is transitive from the authorizers’s key.
Multi-Builder Coordination
A major benefit of the P2P approach is the ability to coordinate multiple builders in an HA (High Availability) setting. The StartPublish and StopPublish messages, in conjunction with a small amount of logic in Rollup-Boost and the network, handle the arbitration:
- Single Publisher Rule: The network expects at most one builder to be actively publishing flashblocks for a given L2 block number at any time. If two different builders both attempt to publish for the same block, the conflict must be resolved to maintain a consistent preconfirmation state.
- Announcing Intent –
StartPublish: When Rollup-Boost (sequencer) initiates a new block with an external builder, it immediately broadcasts aStartPublishmessage (as an AuthorizedMessage) from that builder. This tells all peers: “Builder X is about to start publishing” If any other builder was thinking of building block N (perhaps there was a recent failover), it will see this and stand down. - Graceful Yield – reacting to
StartPublish: If a builder is currently publishing and receives aStartPublishfrom a different builder for the same or next block, it means a failover or override is happening. The expected behavior is that the current publisher will cease publishing (and issue aStopPublish). The protocol is designed such that the honest builder who is not supposed to publish will yield to the authorized one. The reference implementation will automatically send aStopPublishif it is publishing and learns that another builder has taken over authority for the block. The new builder will wait until it receives theStopPublishbefore continuing. - Completion –
StopPublish: When a builder receives the next FCU without an accompanyingAuthorization, it will send out aStopPublish. This removes the builder from the “active publisher” role in the eyes of the network. If there was another builder in waiting (perhaps one that had attempted to start earlier but was told to wait), that waiting builder will now see that the coast is clear. - Timeouts and Fallback: There is an implicit timeout in the coordination. If a builder is in a waiting state after announcing
StartPublishbut for some reason the previous publisher fails to produce aStopPublish(for example, if it crashed mid-block), other participants will not wait indefinitely. In our design, if a new block number is reached and the previous publisher hasn’t stopped we assume the previous builder is incapacitated and proceed with the new publisher.
This coordination ensures that in an HA setup with multiple sequencer instances and multiple builders, preconfirmation data remains consistent: only one set of flashblocks is ever in flight for a given block. If a sequencer failover occurs, the worst-case scenario (which occurs only during a very rare race condition) is a single block publication gap or discontinuity at a block boundary. In the far more likely case, there will be exactly no flashblock disruption. The next publisher will simply start where the last publisher left off, even if that is mid block.
Rollup-Boost and Builder Communication
In the P2P-enhanced design, Rollup-Boost’s interaction with the external block builder is slightly adjusted:
- Authorization Delivery: When the sequencer (op-node) triggers a new block proposal via
engine_forkchoiceUpdated(with payload attributes), Rollup-Boost creates anAuthorizationfor the chosen builder. This requires that Rollup-Boost knows the builder’s public key in advance. In practice, the builder can be configured or registered with Rollup-Boost, providing its long-term public key. Rollup-Boost uses its authorizer private key (associated with the L2 chain or sequencer) to sign the authorization (covering payload_id, timestamp, builder’s key). - Forkchoice Updated Forwarding: Rollup-Boost forwards the fork choice update to the builder as usual (so the builder can start building the block). In this modified protocol, the fork choice update (or a parallel communication) includes the newly created
Authorization. For example, a custom field or side-channel could convey the authorizer’s signature to the builder. (Implementation-wise, this might be an extension of the Engine API or an internal call – the key point is the builder receives the Authorization token before it begins sending flashblocks.) - StartPublish Broadcast: If the builder was not previously publishing, then immediately after receiving the authorization it will emit a
StartPublishmessage over the P2P network. This tells all listening nodes that the authorized builder will begin flashblock publication. - Streaming Flashblocks: The builder executes transactions and produces flashblocks incrementally just as described in the original spec’s Flashblock Construction Process. However, instead of returning these payloads to Rollup-Boost, the builder now signs each flashblock with its key and directly broadcasts an Authorized Flashblock message to the P2P network.
- No Inline Validation by Sequencer: In the original design, Rollup-Boost would validate each flashblock against the local execution engine before propagating it. In the P2P model, this is not done synchronously for each flashblock (it would negate some latency benefits). Instead, trust is managed via the Authorization. The sequencer trusts its chosen builder to only send valid blocks (and will ultimately verify the final block when
engine_getPayloadis called). Peers trust the flashblocks because they trust the Rollup-Boost’s signature.
In summary, Rollup-Boost’s role shifts from being a middleman for data to being a controller and coordinator. It authorizes the builder and informs the network about which builder is active, but it doesn’t need to ferry every flashblock through itself. This streamlines the path from builder to RPC providers.
Flashblocks P2P Protocol v2
This document specifies changes to the flashblocks P2P protocol (flblk) to replace full-broadcast fanout with bounded, latency-optimized peer selection. It is an amendment to the existing Flashblocks P2P Extension.
Problem
The current flashblocks P2P protocol broadcasts every FlashblocksPayloadV1 to all connected peers (handler.rs:585, connection.rs:97-129). A node with N peers sends N copies of every flashblock. For a node connected to 50 peers, that is 50x outgoing bandwidth per flashblock. As the network grows, this becomes unsustainable.
Design Goals
- Reduce bandwidth — Each node sends flashblocks to a bounded number of peers instead of all peers.
- Optimize latency — Nodes periodically rotate out their slowest receive peers in favor of random alternatives.
- Reliability — Multiple receive peers provide redundancy against individual peer failures.
- Trusted peer priority — Trusted peers are always served when they request flashblocks, regardless of limits.
- Uniform roles — Builders and consumers follow the same protocol. No special-casing by role.
Overview
Each node maintains two bounded peer sets:
- Send Set (max
max_send_peers, default 10): Peers this node actively forwards flashblocks to. These are peers that have sent aRequestFlashblocksand been accepted. Trusted peers bypass the limit. - Receive Set (max
max_receive_peers, default 3): Peers this node actively receives flashblocks from. These are peers this node has selected as active feed sources.
Flashblocks propagate through the network as a directed acyclic graph: the builder sends to its send set, those nodes relay to their send sets, and so on. With a fanout of 10, a network of N nodes requires approximately log₁₀(N) hops from builder to the most distant node.
Periodically, each node evaluates the latency of its receive peers and may rotate out the highest-latency peer in favor of a randomly-selected alternative, one peer at a time.
Protocol Version
This change adds new message types to the flblk protocol. The protocol version is bumped from 1 to 2. Nodes advertising flblk/2 support the fanout control messages described below. Nodes running flblk/1 will not understand these messages and are incompatible.
New Message Types
Four unsigned control messages are added to FlashblocksP2PMsg:
| Discriminator | Message | Direction | Description |
|---|---|---|---|
0x01 | RequestFlashblocks | Receiver → Sender | “I want to receive flashblocks from you” |
0x02 | AcceptFlashblocks | Sender → Receiver | “Accepted. I will send you flashblocks” |
0x03 | RejectFlashblocks | Sender → Receiver | “Rejected. I am at capacity” |
0x04 | CancelFlashblocks | Receiver → Sender | “Stop sending me flashblocks” |
These messages carry no payload. The connection context (peer ID) provides all necessary information.
#![allow(unused)]
fn main() {
pub enum FlashblocksP2PMsg {
/// Existing authorized message wrapper (flashblock payloads, StartPublish, StopPublish).
Authorized(Authorized) = 0x00,
/// New fanout control messages (unsigned).
RequestFlashblocks = 0x01,
AcceptFlashblocks = 0x02,
RejectFlashblocks = 0x03,
CancelFlashblocks = 0x04,
}
}
Message Semantics
RequestFlashblocks — Sent by a node that wants to receive flashblocks from the connected peer. The recipient evaluates:
- Is the requester a trusted peer? → Always accept (trusted peers bypass
max_send_peers). - Is the number of non-trusted peers in the send set below
max_send_peers? → Accept. - Otherwise → Reject.
AcceptFlashblocks — Response to RequestFlashblocks. After this, the sender begins forwarding all Authorized messages to the receiver and adds the receiver to its send set.
RejectFlashblocks — Response to RequestFlashblocks when the sender cannot accommodate more peers. The requester should try another peer.
CancelFlashblocks — Sent only by a receiver to the sender it no longer wants to receive flashblocks from (e.g., during peer rotation).
After receiving CancelFlashblocks, the sender immediately stops forwarding flashblocks to that peer and removes it from its send set.
Peer Management
Per-Node State
#![allow(unused)]
fn main() {
struct FanoutState {
/// Peers we are actively sending flashblocks to.
send_set: HashSet<PeerId>,
/// Peers we are actively receiving flashblocks from.
receive_set: HashSet<PeerId>,
/// Peers we have sent RequestFlashblocks to but not yet received a response.
pending_requests: HashSet<PeerId>,
/// Sliding-window latency stats per receive peer.
peer_latency: HashMap<PeerId, LatencyTracker>,
/// Whether a rotation is currently in progress.
rotation_in_progress: bool,
}
}
Bootstrapping (Node Startup)
When a node starts and connects to peers via devp2p:
- As peers connect and complete the
flblk/2handshake, discover whether they are trusted or untrusted. - Only request peers whose trust classification is known, so trusted peers are always considered first.
- Continue sending requests as new peers connect until
receive_set.len() >= max_receive_peers. - Once the receive set is full, stop sending unsolicited requests (further changes happen via rotation).
Handling Incoming RequestFlashblocks
receive RequestFlashblocks from peer P:
if P is trusted:
add P to send_set
send AcceptFlashblocks to P
else if non_trusted_send_count < max_send_peers:
add P to send_set
send AcceptFlashblocks to P
else:
send RejectFlashblocks to P
Handling Disconnections
When a peer disconnects unexpectedly (connection drops):
- If peer was in send set → remove it. The slot is now available for future requests.
- If peer was in receive set → remove it. Immediately attempt to fill the slot by sending
RequestFlashblocksto a random connected peer not already inreceive_setorpending_requests. - If peer was in pending requests → remove it. If receive set is not full, try another peer.
Forwarding Rules
When a node receives an Authorized message from a peer in its receive set:
FlashblocksPayloadV1: Verify signatures, process the flashblock (update state, emit to flashblock stream). Then forward the serialized bytes to all peers in the send set except the peer that sent it.StartPublish: Verify signatures and process locally. Do not relay it beyond the direct neighbor that sent it.StopPublish: Same asStartPublish— process locally, do not relay.
If a node receives an Authorized(FlashblocksPayloadV1) from a peer not in its receive set, or from a peer whose RequestFlashblocks is still pending, the message should be ignored and the peer should be penalized. This prevents unsolicited data delivery.
Duplicate Handling
Since a node receives from up to max_receive_peers peers, it will receive multiple copies of each flashblock. This is expected behavior in the new protocol.
- First copy: Process normally (update state, emit to stream, forward to send set). Record latency for the delivering peer.
- Subsequent copies (same flashblock from different peers): Record the latency for the delivering peer. Discard the flashblock data.
- Same flashblock from the same peer twice: This is NOT expected. Apply reputation penalty as before (
ReputationChangeKind::AlreadySeenTransaction).
This is a behavioral change from the current protocol, where any duplicate from any peer triggers a reputation penalty (connection.rs:288-290).
Latency-Based Peer Rotation
Latency Measurement
Each FlashblocksPayloadV1 includes a flashblock_timestamp in its metadata, set by the builder at creation time. When a node receives a flashblock from a receive peer, it computes:
one_way_latency = now() - flashblock_timestamp
This measurement is attributed to the specific peer that delivered the flashblock. Nodes maintain a sliding window of the last latency_window (default 1000) measurements per receive peer and compute a moving average.
Since all receive peers deliver the same flashblock (with the same flashblock_timestamp), the relative ordering of peers by latency is accurate even with clock skew between the builder and receiver.
Rotation Algorithm
The receive set must never exceed max_receive_peers.
When rotating:
- Select the worst-scoring peer in the current receive set.
- Remove that peer from the receive set immediately and send
CancelFlashblocks. - Pick a replacement candidate, prioritizing trusted peers.
- Add the replacement peer to the receive set in a provisional state and send
RequestFlashblocks.
The provisional peer occupies a receive slot immediately, so the node still never exceeds max_receive_peers. While provisional, the peer is scored for missed flashblocks the same as any other receive peer. If it fails to respond or fails to deliver flashblocks, its score will deteriorate and it can be rotated out on a later interval.
Configuration Parameters
| Parameter | Default | Description |
|---|---|---|
max_send_peers | 10 | Maximum non-trusted peers to send flashblocks to |
max_receive_peers | 3 | Maximum peers to receive flashblocks from |
rotation_interval | 30s | How often to evaluate and potentially rotate receive peers |
latency_window | 1000 | Number of flashblocks to track for per-peer latency averaging |
Trusted peers are always served on request and do not count toward max_send_peers.
Interaction with Existing Protocol
Unchanged Components
The existing Authorized message types (FlashblocksPayloadV1, StartPublish, StopPublish) remain unchanged. They continue to use the Authorized wrapper with sequencer + builder signatures. The multi-builder coordination state machine (Publishing, WaitingToPublish, NotPublishing) is unaffected. StartPublish and StopPublish remain direct-neighbor messages and are not relayed.
Required Changes to Existing Code
-
Duplicate handling must change — The current per-peer duplicate check at
connection.rs:278-291penalizes any duplicate flashblock withReputationChangeKind::AlreadySeenTransaction. In the new protocol, receiving the same flashblock from different receive peers is expected. Only same-peer duplicates (same flashblock index from the same peer twice) should trigger a penalty. -
Flashblock forwarding must be scoped to send set — The current broadcast channel (
peer_tx) sends to all connections. This must be replaced with targeted sends to only peers in the send set. ThePeerMsg::FlashblocksPayloadV1variant currently uses a broadcast channel subscribed by all connections; this must be changed so each connection checks whether the destination peer is in the send set before forwarding. -
Receive-peer selection must respect trust discovery — Nodes should not request unknown peers before their trust classification is available, otherwise untrusted peers can fill the bounded receive set before trusted peers are considered.
-
Protocol version bump —
Capability::new_static("flblk", 1)athandler.rs:239must be updated to version2.
Priority Blockspace for Humans
Priority Blockspace for Humans introduces a new transaction ordering policy on World Chain that grants verified World ID holders top-of-block priority, reducing friction and making transactions fairer for real users.
Where bots create congestion, PBH is a highway for humans.
PBH Architecture
World Chain is an OP Stack chain that enables Priority Blockspace for Humans (PBH) through the World Chain Builder. World Chain leverages rollup-boost to support external block production, allowing the builder to propose PBH blocks to the sequencer while remaining fully compatible with the OP Stack.
Block Production on the OP Stack
The Engine API defines the communication protocol between the Consensus Layer (CL) and the Execution Layer (EL) and is responsible for orchestrating block production on the OP Stack. Periodically, the sequencer’s consensus client will send a fork choice update (FCU) to its execution client, signaling for a new block to be built. After a series of API calls between the CL and EL, the EL will return a new ExecutionPayload containing a newly constructed block. The CL will then advance the unsafe head of the chain and peer the new block to other nodes in the network.
sequenceDiagram
box OP Stack Sequencer
participant sequencer-cl as Sequencer CL
participant sequencer-el as Sequencer EL
end
box Network
participant peers-cl as Peers
end
Note over sequencer-cl: FCU with Attributes
sequencer-cl->>sequencer-el: engine_forkChoiceUpdatedV3(ForkChoiceState, Attrs)
sequencer-el-->>sequencer-cl: {payloadStatus: {status: VALID, ...}, payloadId: PayloadId}
sequencer-el->>sequencer-el: Build execution payload
sequencer-cl->>sequencer-el: engine_getPayloadV3(PayloadId)
sequencer-el-->>sequencer-cl: {executionPayload, blockValue}
sequencer-cl->>peers-cl: Propagate new block
For a detailed look at how block production works on the OP Stack, see the OP Stack specs.
Rollup Boost
rollup-boost is a block building sidecar for OP Stack chains, enabling external block production while remaining fully compatible with the OP Stack. rollup-boost acts as an intermediary between the sequencer’s consensus and execution client. When sequencer-cl sends a new FCU to rollup-boost, the request will be multiplexed to both the sequencer’s execution client and external block builders signaling that a new block should be built.
When the sequencer is ready to propose a new block, op-node will send an engine_getPayload request to rollup-boost which is forwarded to the default execution client and external block builders.
Once rollup-boost receives the built block from external builder, it will then validate the block by sending it to the sequencer’s execution client via engine_newPayload. If the external block is valid, it is returned to the sequencer’s op-node, otherwise rollup-boost will return the fallback block. Note that rollup-boost will always fallback to the default execution client’s block in the case that the external builder does not respond in time or returns an invalid block.
sequenceDiagram
box Sequencer
participant sequencer-cl as Sequencer CL
participant rollup-boost
participant sequencer-el as Sequencer EL
end
box Builder
participant builder-el as Builder EL
end
Note over sequencer-cl: FCU with Attributes
sequencer-cl->>rollup-boost: engine_forkChoiceUpdatedV3(..., Attrs)
Note over rollup-boost: Forward FCU
rollup-boost->>builder-el: engine_forkChoiceUpdatedV3(..., Attrs)
rollup-boost->>sequencer-el: engine_forkChoiceUpdatedV3(..., Attrs)
sequencer-el-->>rollup-boost: {payloadId: PayloadId}
rollup-boost-->>sequencer-cl: {payloadId: PayloadId}
Note over sequencer-cl: Get Payload
sequencer-cl->>rollup-boost: engine_getPayloadV3(PayloadId)
Note over rollup-boost: Forward Get Payload
rollup-boost->>sequencer-el: engine_getPayloadV3(PayloadId)
rollup-boost->>builder-el: engine_getPayloadV3(PayloadId)
builder-el-->>rollup-boost: {executionPayload, blockValue}
sequencer-el-->>rollup-boost: {executionPayload, blockValue}
Note over rollup-boost, sequencer-el: Validate builder block
rollup-boost->>sequencer-el: engine_newPayloadV3(ExecutionPayload)
sequencer-el->>rollup-boost: {status: VALID, ...}
Note over rollup-boost: Propose execution payload
rollup-boost->>sequencer-cl: {executionPayload, blockValue}
Note over sequencer-cl: Propagate new block
In addition to Engine API requests, rollup-boost will proxy all RPC calls from the sequencer op-node to its local execution client. The following RPC calls will also be forwarded to external builders:
miner_*- The Miner API is used to notify execution clients of changes in effective gas price, extra data, and DA throttling requests from the batcher.
eth_sendRawTransaction*- Forwards transactions the sequencer receives to the builder for block building.
Block Production on World Chain
World Chain leverages rollup-boost to enable external block production and integrates the World Chain Builder as a block builder in the network. The World Chain Builder implements a custom block ordering policy (ie. PBH) to provide priority inclusion for transactions with a valid World ID proof. Note that the custom ordering policy adheres to the OP Stack spec.
Each block has a “PBH blockspace capacity”, which determines how many PBH transactions will be included in the block. Blocks on World Chain will always reserve a percentage of blockspace for non-PBH transactions to ensure inclusion for automated systems and non-verified users. If there are not enough pending PBH transactions to fill the entirety of PBH blockspace, standard transactions will be used to fill the remainder of the block.
| Tx Hash | Fee |
|---|---|
| 0xaaaa | $0.04 |
| 0xbbbb | $0.04 |
| 0xcccc | $0.03 |
| 0xdddd | $0.03 |
| 0xeeee | $0.03 |
| 0x2222 | $0.02 |
| 0x3333 | $0.02 |
| 0x4444 | $0.02 |
| 0x5555 | $0.01 |
| 0x6666 | $0.01 |
| Tx Hash | Fee |
|---|---|
| 0x3333 | $0.02 |
| 0x4444 | $0.02 |
| 0x5555 | $0.01 |
| 0x6666 | $0.01 |
| 0xaaaa | $0.04 |
| 0xbbbb | $0.04 |
| 0xcccc | $0.03 |
| 0xdddd | $0.03 |
| 0xeeee | $0.03 |
| 0x2222 | $0.02 |
If the amount of pending PBH transactions exceed the PBH blockspace capacity, the remaining PBH transactions will carry over to the next block. PBH transactions aim to provide verified users with faster, cheaper transaction inclusion, especially during network congestion. Note that transactions within PBH blockspace are ordered by priority fee.
In the event that the block builder is offline, rollup-boost will fallback to the block built by the default execution client with standard OP Stack ordering rules.
PBH Transactions
The World Chain Builder introduces the concept of PBH transactions, which are standard OP transactions that target the PBHEntryPoint and includes a PBHPayload encoded in the tx calldata.
PBH 4337 UserOps
The PBHEntryPoint contract also provides priority inclusion for 4337 UserOps through PBH bundles. A PBH bundle is a standard 4337 bundle where the aggregated signature field is consists of an array of PBHPayload. A valid PBH bundle should include a n PBHPayloads, with each item corresponding to a UserOp in the bundle.
When creating a PBH UserOp, users will append the PBHPayload to the signature field and specify the PBHSignatureAggregator as the sigAuthorizer. The UserOp can then be sent to a 4337 bundler that supports PBH and maintains an alt-mempool for PBH UserOps.
The bundler will validate the PBHPayload, strip the payload from the userOp.signature field and add it to the aggregated signature.
/**
* Aggregate multiple signatures into a single value.
* This method is called off-chain to calculate the signature to pass with handleOps()
* @param userOps - Array of UserOperations to collect the signatures from.
* @return aggregatedSignature - The aggregated signature.
*/
function aggregateSignatures(PackedUserOperation[] calldata userOps)
external
view
returns (bytes memory aggregatedSignature)
{
IPBHEntryPoint.PBHPayload[] memory pbhPayloads = new IPBHEntryPoint.PBHPayload[](userOps.length);
for (uint256 i = 0; i < userOps.length; ++i) {
(, bytes memory proofData) = SafeModuleSignatures.extractProof(
userOps[i].signature, ISafe(payable(userOps[i].sender)).getThreshold()
);
pbhPayloads[i] = abi.decode(proofData, (IPBHEntryPoint.PBHPayload));
}
aggregatedSignature = abi.encode(pbhPayloads);
}
Upon submitting a PBH bundle to the network, the World Chain builder will ensure that all PBH bundles have valid proofs and mark the bundle for priority inclusion.
Visit the validation section of the docs to see how to encode the signalHash for a PBH UserOps work, check out the handleAggregatedOps() function and PBH4337Module.
PBH Payload
PBH Payload
A PBHPayload consists of the following RLP encoded fields:
| Field | Type | Description |
|---|---|---|
external_nullifier | bytes | A unique identifier derived from the proof version, date marker, and nonce. |
nullifier_hash | Field | A cryptographic nullifier ensuring uniqueness and preventing double-signaling. |
root | Field | The Merkle root proving inclusion in the World ID set. |
proof | Proof | Semaphore proof verifying membership in the World ID set. |
External Nullifier
The external_nullifier is a unique identifier used when verifying PBH transactions to prevent double-signaling and enforce PBH transaction rate limiting. For a given external_nullifier, the resulting nullifier_hash will be the same, meaning it can be used a unique marker for a given World ID user while maintaining all privacy preserving guarantees provided by World ID. Note that since the external_nullifier is different for every PBH transaction a user sends, no third party can know if two PBH transactions are from the same World ID.
The external_nullifier is used to enforce that a World ID user can only submit n PBH transactions per month, with the Builder ensuring that all proofs and external_nullifiers are valid before a transaction is included.
The external_nullifier is encoded as a 32-bit packed unsigned integer, with the following structure:
- Version (
uint8): Defines the schema version. - Year (
uint16): The current year expressed asyyyy. - Month (
uint8): The current month expressed as (1-12). - PBH Nonce (
uint8): The PBH nonce, where0 <= n < pbhNonceLimit.
Below is an example of how to encode and decode the external_nullifier for a given PBH transaction.
uint8 public constant V1 = 1;
/// @notice Encodes a PBH external nullifier using the provided year, month, and nonce.
/// @param version An 8-bit version number (0-255) used to identify the encoding format.
/// @param pbhNonce An 8-bit nonce value (0-255) used to uniquely identify the nullifier within a month.
/// @param month An 8-bit 1-indexed value representing the month (1-12).
/// @param year A 16-bit value representing the year (e.g., 2024).
/// @return The encoded PBHExternalNullifier.
function encode(uint8 version, uint8 pbhNonce, uint8 month, uint16 year) internal pure returns (uint256) {
require(month > 0 && month < 13, InvalidExternalNullifierMonth());
return (uint256(year) << 24) | (uint256(month) << 16) | (uint256(pbhNonce) << 8) | uint256(version);
}
/// @notice Decodes an encoded PBHExternalNullifier into its constituent components.
/// @param externalNullifier The encoded external nullifier to decode.
/// @return version The 8-bit version extracted from the external nullifier.
/// @return pbhNonce The 8-bit nonce extracted from the external nullifier.
/// @return month The 8-bit month extracted from the external nullifier.
/// @return year The 16-bit year extracted from the external nullifier.
function decode(uint256 externalNullifier)
internal
pure
returns (uint8 version, uint8 pbhNonce, uint8 month, uint16 year)
{
year = uint16(externalNullifier >> 24);
month = uint8((externalNullifier >> 16) & 0xFF);
pbhNonce = uint8((externalNullifier >> 8) & 0xFF);
version = uint8(externalNullifier & 0xFF);
}
The World Chain Builder enforces:
- The
external_nullifieris correctly formatted and specifies the current month, year and a valid nonce. - The
proofis valid and specifies theexternal_nullifieras a public input to the proof. - The
external_nullifierhas not been used before, ensuring that thepbh_nonceis unique for the given month and year.
Signal Hash
One of the required inputs when generating a World ID proof is the signal_hash.
As the name suggests, the signal_hash is the hash of the signal which is an arbitrary message provided by the prover, allowing the verifier to hash the signal when verifying the proof, ensuring that the proof was generated with the expected inputs.
Within the context of PBH, the signal_hash is used to ensure the proof was created for the transaction being submitted. Depending on the type of the PBH transaction, this value could be a hash of the tx calldata, a 4337 UserOp hash or the tx hash itself.
PBH Validation
Upon receiving new transactions, the World Chain Builder will first ensure that the payload is a valid OP Stack tranasaction. In addition to the default checks, the builder will also evaluate transactions for PBH conditions.
Any transaction that calls the pbhMulticall() or handleAggregatedOps() function on the PBHEntyrPoint will be considered a PBH transaction and must clear PBH Validation. PBH transactions must contain a valid PBHPayload or PBHPayload[] in the case of PBH 4337 bundles.
struct PBHPayload {
uint256 root;
uint256 pbhExternalNullifier;
uint256 nullifierHash;
uint256[8] proof;
}
Signal Hash
Transactions that target the pbhMulticall() function must provide a valid PBHPayload where included proof is generated with a signalHash specified as:
uint256 signalHash = abi.encode(msg.sender, calls).hashToField();
Transactions that target the handleAggregatedOps()function (ie. PBH 4337 Bundles) must contain an aggregated signature consisting of an array of PBHPayload where there is a PBHPayload for each UserOp in the bundle. The included proof must be generated with a signalHash specified as:
uint256 signalHash = abi.encodePacked(sender, userOp.nonce, userOp.callData).hashToField();
External Nullifier
PBH transactions must contain a valid external nullifier where:
- The
monthis the current month - The
yearis the current year (specified asyyyy) - The
pbhNonceis <pbhNonceLimit. PBH nonces are0indexed, meaning if thepbhNoncelimit is29, a user is allotted30PBH transactions per month.
Root
The root provided must be a valid World ID Root with a timestamp less than 7 days old.
Proof
The proof must be a valid semaphore proof, proving inclusion in the World ID set associated with the specified root.
CLI Reference
Auto-generated — run
cargo xtask docsto regenerate.
world-chain
The World Chain node binary. All flags below are passed to world-chain.
World Chain Node
Usage: world-chain [OPTIONS]
Options:
-h, --help
Print help (see a summary with '-h')
Rollup:
--rollup.sequencer <SEQUENCER>
Endpoint for the sequencer mempool (can be both HTTP and WS)
[aliases: --rollup.sequencer-http, --rollup.sequencer-ws]
--rollup.disable-tx-pool-gossip
Disable transaction pool gossip
--rollup.compute-pending-block
By default the pending block equals the latest block to save resources and not leak txs from the tx-pool, this flag enables computing of the pending block from the tx-pool instead.
If `compute_pending_block` is not enabled, the payload builder will use the payload attributes from the latest block. Note that this flag is not yet functional.
--rollup.discovery.v4
enables discovery v4 if provided
--rollup.enable-tx-conditional
Enable transaction conditional support on sequencer
--rollup.supervisor-http <SUPERVISOR_HTTP_URL>
HTTP endpoint for the supervisor
[default: http://localhost:1337/]
--rollup.supervisor-safety-level <SUPERVISOR_SAFETY_LEVEL>
Safety level for the supervisor
[default: CrossUnsafe]
--rollup.sequencer-headers <SEQUENCER_HEADERS>
Optional headers to use when connecting to the sequencer
--rollup.historicalrpc <HISTORICAL_HTTP_URL>
RPC endpoint for historical data
--min-suggested-priority-fee <MIN_SUGGESTED_PRIORITY_FEE>
Minimum suggested priority fee (tip) in wei, default `1_000_000`
[default: 1000000]
--flashblocks-url <FLASHBLOCKS_URL>
A URL pointing to a secure websocket subscription that streams out flashblocks.
If given, the flashblocks are received to build pending block. All request with "pending" block tag will use the pending state based on flashblocks.
--flashblock-consensus
Enable flashblock consensus client to drive the chain forward
When enabled, the flashblock consensus client will process flashblock sequences and submit them to the engine API to advance the chain. Requires `flashblocks_url` to be set.
--proofs-history
If true, initialize external-proofs exex to save and serve trie nodes to provide proofs faster
--proofs-history.storage-path <PROOFS_HISTORY_STORAGE_PATH>
The path to the storage DB for proofs history
--proofs-history.window <PROOFS_HISTORY_WINDOW>
The window to span blocks for proofs history. Value is the number of blocks. Default is 1 month of blocks based on 2 seconds block time. 30 * 24 * 60 * 60 / 2 = `1_296_000`
[default: 1296000]
--proofs-history.prune-interval <PROOFS_HISTORY_PRUNE_INTERVAL>
Interval between proof-storage prune runs. Accepts human-friendly durations like "100s", "5m", "1h". Defaults to 15s.
- Shorter intervals prune smaller batches more often, so each prune run tends to be faster and the blocking pause for writes is shorter, at the cost of more frequent pauses. - Longer intervals prune larger batches less often, which reduces how often pruning runs, but each run can take longer and block writes for longer.
A shorter interval is preferred so that prune runs stay small and don’t stall writes for too long.
CLI: `--proofs-history.prune-interval 10m`
[default: 15s]
--proofs-history.verification-interval <PROOFS_HISTORY_VERIFICATION_INTERVAL>
Verification interval: perform full block execution every N blocks for data integrity. - 0: Disabled (Default) (always use fast path with pre-computed data from notifications) - 1: Always verify (always execute blocks, slowest) - N: Verify every Nth block (e.g., 100 = every 100 blocks)
Periodic verification helps catch data corruption or consensus bugs while maintaining good performance.
CLI: `--proofs-history.verification-interval 100`
[default: 0]
Priority Blockspace for Humans:
--pbh.verified_blockspace_capacity <VERIFIED_BLOCKSPACE_CAPACITY>
Sets the max blockspace reserved for verified transactions. If there are not enough verified transactions to fill the capacity, the remaining blockspace will be filled with unverified transactions. This arg is a percentage of the total blockspace with the default set to 70 (ie 70%)
[default: 70]
--pbh.entrypoint <ENTRYPOINT>
Sets the ERC-4337 EntryPoint Proxy contract address This contract is used to validate 4337 PBH bundles
[default: 0x0000000000000000000000000000000000000000]
--pbh.world_id <WORLD_ID>
Sets the WorldID contract address. This contract is used to provide the latest merkle root on chain
[default: 0x0000000000000000000000000000000000000000]
--pbh.signature_aggregator <SIGNATURE_AGGREGATOR>
Sets the ERC0-7766 Signature Aggregator contract address This contract signifies that a given bundle should receive priority inclusion if it passes validation
[default: 0x0000000000000000000000000000000000000000]
Block Builder:
--builder.enabled
--builder.private_key <PRIVATE_KEY>
Private key for the builder used to update PBH nullifiers
[env: BUILDER_PRIVATE_KEY=]
[default: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef]
--builder.block-uncompressed-size-limit <BLOCK_UNCOMPRESSED_SIZE_LIMIT>
Maximum cumulative uncompressed (EIP-2718 encoded) block size in bytes
[env: BUILDER_BLOCK_UNCOMPRESSED_SIZE_LIMIT=]
Flashblocks:
--flashblocks.enabled
--flashblocks.authorizer_vk <AUTHORIZER_VK>
Authorizer verifying key used to verify flashblock authenticity
[env: FLASHBLOCKS_AUTHORIZER_VK=]
--flashblocks.builder_sk <BUILDER_SK>
Flashblocks signing key used to sign authorized flashblocks payloads
[env: FLASHBLOCKS_BUILDER_SK=]
--flashblocks.override_authorizer_sk <OVERRIDE_AUTHORIZER_SK>
Override incoming authorizations from rollup boost
[env: FLASHBLOCKS_OVERRIDE_AUTHORIZER_SK=]
--flashblocks.force_publish
Publish flashblocks payloads even when an authorization has not been received from rollup boost.
This should only be used for testing and development purposes.
[env: FLASHBLOCKS_FORCE_PUBLISH=]
--flashblocks.interval <FLASHBLOCKS_INTERVAL>
The interval to publish pre-confirmations when building a payload in milliseconds
[env: FLASHBLOCKS_INTERVAL=]
[default: 200]
--flashblocks.recommit_interval <RECOMMIT_INTERVAL>
Interval at which the block builder should re-commit to the transaction pool when building a payload.
In milliseconds.
[env: FLASHBLOCKS_RECOMMIT_INTERVAL=]
[default: 200]
--flashblocks.access_list
Enables flashblocks access list support.
Will create access lists when building flashblocks payloads. and will use access lists for parallel transaction execution when verifying flashblocks payloads.
[env: FLASHBLOCKS_ACCESS_LIST=]
--flashblocks.max_send_peers <MAX_SEND_PEERS>
Override the flashblocks send-set size
[env: FLASHBLOCKS_MAX_SEND_PEERS=]
[default: 10]
--flashblocks.max_receive_peers <MAX_RECEIVE_PEERS>
Override the number of receive peers maintained for flashblocks fanout
[env: FLASHBLOCKS_MAX_RECEIVE_PEERS=]
[default: 3]
--flashblocks.rotation_interval <ROTATION_INTERVAL>
Override the flashblocks rotation interval in seconds
[env: FLASHBLOCKS_ROTATION_INTERVAL=]
[default: 30]
--flashblocks.score_samples <SCORE_SAMPLES>
Override the number of latency samples retained for receive-peer scoring
[env: FLASHBLOCKS_SCORE_SAMPLES=]
[default: 1000]
--flashblocks.force_receive_peers <PEER_ID>
Peers to always receive flashblocks from regardless of their score.
These peers will be requested as soon as they connect and will never be evicted by rotation. They count toward `max_receive_peers`.
[env: FLASHBLOCKS_FORCE_RECEIVE_PEERS=]
--tx-peers <PEER_ID>
Comma-separated list of peer IDs to which transactions should be propagated
--worldchain.disable-bootnodes
Disable the default World Chain bootnodes