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

atelier — TDX co-building

audience: contributors

Proposed source: crates/atelier/.

The block-assembly organism. Every atelier committee member runs a reproducible TDX image whose MR_TD is pinned in the lattice’s atelier::Config. Members subscribe to unseal::UnsealedPool and offer::AuctionOutcome, simulate bundles inside the enclave, and commit a single Candidates entry per slot signed under a BLS aggregate signature across the committee.

Crate layout

  • atelier::proto — wire types for block templates, hints, candidate-block payload, aggregate signature. No I/O.
  • atelier::core — pure simulation + ordering. Inputs: unsealed transactions, auction winner, parent state root. Output: deterministic canonical tx list + gas estimate.
  • atelier::node — mosaik integration. AtelierMachine, role event loops, TDX ticket validator, chain-RPC bridge.

The chain-RPC bridge is the one place atelier reaches outside the lattice universe: simulation needs parent state. The bridge is a sealed in-enclave HTTP client; the RPC endpoint is pinned in atelier::Config so that the bridge’s target is part of the fingerprint (and so that a rogue operator cannot swap it out of an attested image).

Public facade:

  • atelier::Atelier::<Block>::read(&network, &Config) -> Reader<Block> — read-side for proposers, sequencers, analytics agents.
  • atelier::Atelier::<Block>::verify(&Block, &Config) -> bool — pure verification of the BLS aggregate signature against the pinned committee roster.
  • atelier::Config.

Public surface

Candidates collection

declare::collection! {
    pub Candidates<B> = Vec<Candidate<B>>,
    derive_id: ATELIER_ROOT.derive("candidates"),
    consumer require_ticket: LATTICE_READ_TICKET,
    writer   require_ticket: AtelierMember,
}

pub struct Candidate<B: BlockDatum> {
    pub slot:              u64,
    pub parent_hash:       [u8; 32],
    pub header:            B::Header,
    pub body:              Vec<B::Tx>,
    pub hints_applied:     Vec<HintId>,
    pub builder_sig:       BlsAggSig,
    pub committee_roster:  Vec<BlsPub>,
}

B: BlockDatum is the lattice’s chain-specific block schema (e.g. L1Post4844, OpStackBedrock).

Hint<Template> stream

declare::stream! {
    pub Hint<T: HintDatum> = HintBundle<T>,
    derive_id: ATELIER_ROOT.derive("hint"),
    producer require_ticket: CoBuilder,
    consumer require_ticket: AtelierMember,
}

pub struct HintBundle<T: HintDatum> {
    pub slot:       u64,
    pub co_builder: CoBuilderId,
    pub template:   T,
    pub signature:  Secp256k1Sig,
}

Hints are how co-builder operators (Phase 2) contribute partial block templates. In a Phase 1 single-operator lattice the Hint stream has no external writers; the committee’s own members may still emit hints internally for intra-member proposal exchange.

Internal plumbing

Derived private network keyed off ATELIER_ROOT.derive("private"). Two streams:

  • Simulations — pairwise exchange of simulation outputs for cross-member agreement. Not on the public universe because it carries pre-commit tentative block bodies at high frequency.
  • RosterGossip — BLS roster synchronization used during committee churn.

State machine

pub enum Command {
    OpenSlot { slot: u64, parent: [u8; 32] },
    AcceptHint(HintAccept),
    SubmitSimulation(SimOutput),
    SealCandidate(SealCommit),
    MarkDone(u64),
}

pub enum Query {
    OpenSlots,
    HintsFor(u64),
    SimulationAgreement(u64),
    CandidateFor(u64),
}

Apply semantics

  • OpenSlot. Leader-issued when the proposer slot boundary is observed on-chain. Initializes per-slot state.
  • AcceptHint. Committee member proposes a hint for inclusion. Validation: hint’s slot matches an open slot; co_builder ACL ticket matches; signature verifies.
  • SubmitSimulation. Each committee member runs the simulation of (UnsealedPool[S] + AuctionOutcome[S] + hints) inside its TDX enclave and commits the resulting output hash. The state machine cross-checks member outputs: if a majority agrees on a single hash, that hash wins; divergence increments a counter the operator watches.
  • SealCandidate. Issued once a majority agrees. Apply handler:
    1. Reconstructs the candidate block body from the deterministic ordering function using the agreed inputs.
    2. Aggregates each member’s BLS signature over the body hash (signatures arrive via the private Simulations stream alongside the simulation output).
    3. Constructs Candidate<B> with builder_sig = BlsAggregate(valid_member_sigs) and committee_roster = members_who_signed.
    4. Appends to Candidates.
  • MarkDone. GC after tally commits Refunds[S].

Invariants

  1. One candidate per slot. SealCandidate rejects if CandidateFor(S) is already populated.
  2. Candidate body is a deterministic function of committed inputs. Two committee members re-applying the same sequence arrive at byte-identical Candidate<B>.body.
  3. Minority divergence cannot commit. A minority of TDX images that simulate differently cannot force their output into a Candidate; apply requires majority agreement on the simulation hash.
  4. Chain RPC is inside the TCB. The RPC endpoint URL is pinned in the fingerprint; a compromised RPC is a compromised atelier.

Signature versioning

fn signature(&self) -> UniqueId {
    let tag = format!(
        "atelier.v{WIRE_VERSION}.schema={}.mrtd={:x}.rpc={}.committee={}",
        self.config.block_schema.tag(),
        blake3(self.config.mrtd_acl.sorted_concat()),
        self.config.chain_rpc_hash(),
        self.config.committee_size,
    );
    UniqueId::from(tag.as_str())
}

Adding an MR_TD to the acl (onboarding a co-builder) changes the signature — this is intentional. See Rotations and upgrades — Co-building.

Committee key ceremony

At bring-up time every member generates a BLS12-381 keypair inside its TDX image. The public keys are collected via the operator’s driver and pinned as the committee_roster_pubkeys in atelier::Config. No DKG is required — atelier’s cryptography is aggregate signature, not threshold secret, so keys are independent.

Rotation is per-member: generate new BLS key, publish, update atelier::Config.committee_roster_pubkeys, retire + replace the lattice. Rotating one key changes the fingerprint.

ACL composition

impl Config {
    pub fn member_validator(&self) -> Validators {
        let mut v = Validators::stacked()
            .with(JwtIssuer::from(self.operator_jwt_key));
        for mrtd in &self.mrtd_acl {
            v = v.with(Tdx::new().require_mrtd(*mrtd));
        }
        v
    }

    pub fn co_builder_validator(&self) -> Validators {
        Validators::stacked().with(JwtIssuer::from(self.co_builder_jwt_key))
    }
}

mrtd_acl is a set (not a single value) to support co-building: two operators contributing atelier members each publish their own MR_TD; both land in the acl; admission succeeds for either. Adding an MR_TD is a fingerprint change; see phase2-spec.

Trust shape

TDX attestation + majority-honest. See threat-model.md — atelier.

Open questions

  • Bundle simulation determinism across TDX images. Simulation at byte-identical output requires a deterministic EVM implementation. The reference atelier ships a vetted revm build inside the image; divergence-in-practice between images is what the simulation_divergence_total metric surfaces. What level of determinism is actually achievable on shared chain RPC is an open research question.
  • Pre-confirmation support. Flashblocks-style pre-confirmations (fast-confirm a partial ordering before the full slot) would need atelier to commit partial Candidates entries per sub-slot. Not in v1; likely a v2 extension that adds a second collection keyed by (slot, sub_slot).
  • Revert protection. Integrator-level revert protection requires atelier to simulate each bundle in a sandbox and exclude any that revert. The logic is in atelier::core; the question is whether to expose revert-filtered bundles in the canonical ordering or only at integrator request. Open.

Cross-references