Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 drive RecordSend commands.

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 when atelier::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; proposer is in the policy’s expected set; member is a distinct role (dedupe).
  • RecordAck. Committee member registers a proposer ack for slot S. Validation: slot matches; ack signature verifies under the proposer’s published key; the proposer Send record exists for this member.
  • RecordTimeout. Slot deadline elapsed without ack. Idempotent; prevents stuck slots.
  • CommitAck. When a majority of committee members have RecordAck’d for slot S with consistent ack evidence, the leader issues CommitAck. Apply handler cross-verifies the ack evidence (same proposer, same header hash, similar timestamps) and appends AcceptedHeader to AcceptedHeaders.
  • MarkDone. GC post-tally.

Invariants

  1. One AcceptedHeader per slot. Enforced in CommitAck.
  2. Committed ack evidence is consistent across the majority. CommitAck apply rejects if majority RecordAcks disagree on header hash or proposer.
  3. Monotonic slots. Appends strictly in slot order.
  4. 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>>;
}
  • L1MevBoost implements this over the standard MEV-Boost HTTP API, pinning the validator set from the beacon chain via an embedded beacon-node RPC (configured in Config).
  • L2Sequencer pairs with one sequencer endpoint; the expected_proposer_set degenerates to a singleton.
  • L2LeaderRotation queries 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 AcceptedHeaders captures what we saw; but if the proposer equivocates and we ack a header that is later replaced by another builder’s, tally may misattribute. Detecting equivocation requires cross-builder cooperation; in v1 the lattice trusts the chain’s eventual head and treats our AcceptedHeaders as a local view.
  • Flashblocks / pre-confirmations. For lattices targeting L2s with sub-slot cadence, relay needs to commit multiple AcceptedHeaders per 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