relay — PBS fanout
audience: contributors
Proposed source: crates/relay/.
The relay organism ships atelier::Candidates to the chain’s
proposer (L1) or sequencer (L2) and records the proposer’s
acknowledgement in AcceptedHeaders. Unlike every other
organism in the lattice, relay’s work is substantially outside
the mosaik universe — it speaks proposer-side protocols like
MEV-Boost — but its commit surface is mosaik-native.
Crate layout
relay::proto— wire types for headers, bid envelopes, send-records, ack-records. No I/O.relay::core— pure deduplication, rate-limiting, and policy evaluation.relay::endpoints— one module per supported policy:l1_mev_boost.rs— MEV-Boost submitter client.l2_sequencer.rs— L2 sequencer endpoint client.l2_leader_rotation.rs— leader-rotation aware client.
relay::node— mosaik integration.RelayMachine, role event loops, policy dispatch.
Public facade:
relay::Relay::<H>::watch(&network, &Config) -> Watch<H>— proposer / sequencer consumers that want to follow the lattice’s view of per-slot acceptance.relay::Config— pins the policy and the proposer endpoint specifiers.
Unlike other organisms, the relay binary is the intended process even for proposers that want the lattice’s view of acceptance — there is no “submit a header” integrator verb (relay is a downstream of atelier, not a submit endpoint).
Public surface
AcceptedHeaders collection
declare::collection! {
pub AcceptedHeaders<H> = Vec<AcceptedHeader<H>>,
derive_id: RELAY_ROOT.derive("accepted"),
consumer require_ticket: LATTICE_READ_TICKET,
writer require_ticket: RelayMember,
}
pub struct AcceptedHeader<H: HeaderDatum> {
pub slot: u64,
pub header: H,
pub bid: u128,
pub proposer: ProposerId,
pub ack_evidence: Vec<u8>, // proposer-signed payload
pub committed_at: UnixSecs,
}
One entry per slot whose header was ack’d by its proposer.
Slots where no proposer acknowledged produce no entry —
operators monitor the gap via
relay_committee_agreement_rate.
Internal plumbing
Derived private network (RELAY_ROOT.derive("private")):
Sends— per-slot records of each committee member’s proposer-side submission (what header, to which proposer, at what time). Used by other members to cross-check and by the state machine to driveRecordSendcommands.
State machine
pub enum Command {
OpenSlot { slot: u64 },
RecordSend(SendRecord),
RecordAck(AckRecord),
RecordTimeout(u64),
CommitAck(u64),
MarkDone(u64),
}
pub enum Query {
OpenSlots,
SendsFor(u64),
AcksFor(u64),
AcceptedFor(u64),
}
Apply semantics
OpenSlot. Leader-issued whenatelier::Candidates[S]appears. Opens per-slot bookkeeping.RecordSend. Committee member registers that it sent the candidate’s header to a specific proposer. Validation: slot matches an open slot;proposeris in the policy’s expected set; member is a distinct role (dedupe).RecordAck. Committee member registers a proposer ack for slotS. Validation: slot matches; ack signature verifies under the proposer’s published key; the proposerSendrecord exists for this member.RecordTimeout. Slot deadline elapsed without ack. Idempotent; prevents stuck slots.CommitAck. When a majority of committee members haveRecordAck’d for slotSwith consistent ack evidence, the leader issuesCommitAck. Apply handler cross-verifies the ack evidence (same proposer, same header hash, similar timestamps) and appendsAcceptedHeadertoAcceptedHeaders.MarkDone. GC post-tally.
Invariants
- One
AcceptedHeaderper slot. Enforced inCommitAck. - Committed ack evidence is consistent across the majority.
CommitAckapply rejects if majorityRecordAcks disagree on header hash or proposer. - Monotonic slots. Appends strictly in slot order.
- Timeouts never override a previously committed ack.
Once
CommitAck(S)has applied,RecordTimeout(S)is a no-op.
Signature versioning
fn signature(&self) -> UniqueId {
let tag = format!(
"relay.v{WIRE_VERSION}.policy={}.endpoints={:x}.committee={}",
self.config.policy.tag(),
blake3(self.config.endpoints.sorted_concat()),
self.config.committee_size,
);
UniqueId::from(tag.as_str())
}
Switching policy (e.g. L1 MEV-Boost to L2 sequencer) changes the fingerprint. Swapping endpoints within the same policy also changes the fingerprint; endpoint rotation is a lattice retirement, not a non-FP change.
Proposer-side specialization
relay::endpoints is the boundary between the lattice and the
chain. Each policy implements one trait:
pub trait Endpoint: Send + Sync {
async fn submit(&self, header: &Header, bid: u128) -> Result<AckFuture>;
async fn verify_ack(&self, ack: &AckEvidence) -> Result<ProposerId>;
fn expected_proposer_set(&self, slot: u64) -> Result<Vec<ProposerId>>;
}
L1MevBoostimplements this over the standard MEV-Boost HTTP API, pinning the validator set from the beacon chain via an embedded beacon-node RPC (configured inConfig).L2Sequencerpairs with one sequencer endpoint; theexpected_proposer_setdegenerates to a singleton.L2LeaderRotationqueries the chain’s leader schedule and targets the rotated leader per slot.
Adding a new policy is a relay::endpoints::newpolicy.rs
module plus a Policy::NewPolicy variant plus a signature
format bump.
ACL composition
impl Config {
pub fn member_validator(&self) -> Validators {
let mut v = Validators::stacked()
.with(JwtIssuer::from(self.operator_jwt_key));
if let Some(mrtd) = self.member_mrtd {
v = v.with(Tdx::new().require_mrtd(mrtd));
}
v
}
}
TDX is optional on relay in v1; the rationale is that the
organism’s integrity claim rides on atelier’s aggregate
signature, not on the relay’s own attestation. The proposer
verifies the atelier sig directly.
Trust shape
Any-trust liveness; majority-honest AcceptedHeaders
integrity. See threat-model.md — relay.
Open questions
- Proposer equivocation. The proposer may ack two
conflicting headers from different builders. Our
AcceptedHeaderscaptures what we saw; but if the proposer equivocates and we ack a header that is later replaced by another builder’s,tallymay misattribute. Detecting equivocation requires cross-builder cooperation; in v1 the lattice trusts the chain’s eventual head and treats ourAcceptedHeadersas a local view. - Flashblocks / pre-confirmations. For lattices targeting
L2s with sub-slot cadence, relay needs to commit multiple
AcceptedHeadersper slot. Same shape, higher rate. Not yet speced. - Relay incentives. Nothing in the current shape compensates
relay members for their connectivity costs. Refund
attribution happens entirely in
tally. A relay member share in the refund is a lattice policy decision, not a protocol one.
Cross-references
- The six organisms
- atelier — upstream.
- tally — downstream.
- threat-model.md — relay
- cross-chain.md — L1 / L2 specialization
- Operator runbook