Composition: how organisms wire together
audience: contributors
Architecture maps the six organisms onto a lattice. Organisms specifies each organism’s public surface. This chapter shows the wiring: which stream and collection subscription drives which organism’s state machine, and where the happy path splits when something upstream fails.
The wiring is intentionally weak. No cross-Group atomicity, no
shared state, no global scheduler. Each organism reacts to its
upstream’s public commits via mosaik’s when() DSL and Collection
/ Stream subscriptions. That is the whole composition model.
The slot as foreign key
Every commit in every organism is keyed by a slot — the chain’s slot number (on L1, the proposer slot; on L2, the sequencer’s block or sub-block index). The slot is the only shared identifier across organisms’ state machines. This is deliberate:
- It is a small, dense, monotonic integer. Cheap to carry in every commit.
- It is picked by the chain, not by any lattice organism. No organism can unilaterally fabricate or reorder slots.
- It is a natural synchronization point: every organism has a well-defined answer to “what slot are we working on?”
Contributors writing new organisms should reuse the slot as the composition key. Composing on anything else — transaction hashes, bundle IDs, block numbers — breaks when chains reorg, re-propose, or split.
The subscription graph
Each arrow is a subscription: the downstream organism watches the upstream organism’s public collection or stream and reacts in its own state machine. No arrow represents atomic cross-Group commit.
integrators
(wallets, ┌─► zipnet:Broadcasts ────► unseal:UnsealedPool ──┐
searchers) │ │
│ │ ├─► atelier:Candidates ──► relay:AcceptedHeaders ──► tally:Refunds
├──► zipnet:Submit │ │
│ │ │
└──► offer:Bid ────────► offer:AuctionOutcome ─────────────┘ │
│
on-chain inclusion watcher ──────────────────────────────────────────────┘
│
tally:Attestations ──► integrators
Read top to bottom, left to right:
- Wallets submit to
zipnet:Submit; searchers submit tooffer:Bid. Both are ticket-gated writes onto the lattice’s public surface. zipnetfinalizes a round and appends toBroadcasts.unsealwatcheszipnet:Broadcasts, combines threshold shares, and writes cleartext intoUnsealedPool.offerwatchesUnsealedPoolfor slot boundaries, runs its sealed-bid auction over bundles that reference the pool, and commitsAuctionOutcome.atelierwatches bothUnsealedPoolandAuctionOutcome, assembles a candidate block in the TDX committee, and commitsCandidates.relaywatchesCandidates, ships headers to the proposer, and commitsAcceptedHeaderswhen the proposer acknowledges.- An on-chain inclusion watcher watches the chain; when the
winning block lands,
tallycommitsRefundsand publishesAttestations. - Integrators read
RefundsandAttestationsto claim their share.
Subscription code shape
A contributor implementing one organism writes a role driver in
that organism’s crate. The driver is just a mosaik event loop that
watches an upstream primitive and calls group.execute(...) on
its own Group. The pattern is identical across organisms; the
unseal driver is the simplest and serves as the template:
// Inside unseal::node::roles::server
loop {
// Watch zipnet's public Broadcasts collection for the next
// finalized round.
let next = broadcasts.when().appended().await;
let round = broadcasts.get(next).expect("appended implies present");
// Compute this node's threshold share for the round.
let share = compute_share(&self.share_secret, &round);
// Commit into unseal's own Group.
self.unseal_group
.execute(UnsealCommand::SubmitShare { slot: round.slot, share })
.await?;
}
atelier’s driver is the same shape over UnsealedPool + AuctionOutcome.
tally’s is the same shape over AcceptedHeaders + an on-chain
watcher. In every case the driver ends at group.execute(...) on
its own state machine; the upstream is read-only.
Apply order across organisms
Within one organism, mosaik’s Raft variant guarantees that every committee member applies commands in the same order. Across organisms, no such guarantee exists — each organism’s state machine runs independently. The lattice relies on two properties instead:
- Monotonicity by slot. Within any organism, commits for slot
S+1are never applied before commits for slotS. The organism’s own state machine enforces this in its apply handler by rejecting out-of-order slots. - Eventual consistency by subscription. A downstream organism’s driver will observe every upstream commit eventually, because mosaik collections are append-only and readers converge. It may observe them out of wall-clock order across organisms, but each organism’s state machine processes them in slot order regardless.
The two properties together are enough to reconstruct a globally consistent view per slot without global atomicity. Integrators wanting “the canonical decision for slot S” read each organism’s per-slot commit independently and join on the slot number.
What happens when an upstream organism fails
Each row below covers one organism failing or stalling. “Fails” means its committee cannot commit within a slot’s deadline. The columns are the downstream organisms’ observable behaviour.
| Upstream fails | zipnet | unseal | offer | atelier | relay | tally |
|---|---|---|---|---|---|---|
zipnet | - | no UnsealedPool[S] | AuctionOutcome[S] still commits (bids still valid) | Candidates[S] degrades (no order flow, searcher bids only) | AcceptedHeaders[S] still possible | Refunds[S] attribution set may be empty for slot |
unseal | unaffected | - | AuctionOutcome[S] still commits | Candidates[S] degrades (bids only) | AcceptedHeaders[S] still possible | Refunds[S] attribution set missing wallet contributions |
offer | unaffected | unaffected | - | Candidates[S] degrades (no bid included) | AcceptedHeaders[S] still possible | Refunds[S] missing searcher attributions |
atelier | unaffected | unaffected | unaffected | - | no Candidates[S] to ship; proposer falls back to another builder | no Refunds[S] committed |
relay | unaffected | unaffected | unaffected | Candidates[S] commits fine | - | no Refunds[S] if block never reaches chain |
tally | unaffected | unaffected | unaffected | unaffected | unaffected | - |
Two patterns fall out:
- Upstream failures degrade downstream outputs; they do not
corrupt them. A missing
unsealfor slotSproduces aCandidates[S]built from searcher bids alone, which is still a well-formed block, just with less order-flow content. The chain still progresses via the proposer’s fallback builder. - The pipeline is drainable. Failures during slot
Sdo not block slotS+1— every organism’s state machine accepts new slots without waiting for earlier ones to finalise.
What the composition contract guarantees
Given that every organism commits its own decision for slot S,
the lattice guarantees:
- Deterministic replay. Given the full commit logs of all six
organisms for slot
S, anyone can recompute every per-organism decision and cross-checktally’s attribution. - Independent auditability. A consumer that trusts the chain
can verify
tally’s attestations against on-chain inclusion without trusting any other organism’s commit log directly — the attestation carries the evidence. - No silent corruption across organisms. The derived id
discipline means mis-configured organisms in a lattice produce
disjoint IDs and cannot cross-subscribe at all. A half-upgraded
lattice is always a
ConnectTimeout, never a subtle mis-attribution.
What the contract does not guarantee
- Atomic all-or-nothing inclusion across organisms. Already discussed. A partial path through the pipeline is a valid state.
- Bounded end-to-end latency in the presence of failures. If
relaystalls indefinitely,tally’s commit for that slot never happens; no organism-level timeout triggers it. Operators who need bounded end-to-end latency add per-slot deadlines at thetallylevel (commit an emptyRefunds[S]after a timeout rather than blocking). - Cross-lattice coordination. Out of scope for this chapter; see Cross-lattice coordination.
Contributors implementing a new organism
If you are adding a seventh organism to the lattice, your wiring checklist is:
- Identify the upstream organism(s) you subscribe to. If none —
you are a root organism like
zipnet, triggered only by integrator input. - Identify the downstream organism(s) that will subscribe to you.
If none — you are a leaf like
tally, triggered only by external observers. - Key every commit by slot. If your organism has a natural sub-slot cadence (per-round inside a slot, per-bundle), commit at the sub-slot cadence but always stamp the owning slot.
- Write one role driver per
Groupmember role. Keep it as a singletokio::select!over the upstream subscriptions and your local timers. - Write unit tests against in-memory collections (mosaik ships test helpers); integration tests against an in-process lattice of two or three organism Groups.
- Document the subscription contract in your organism’s
contributors/composition-hooks.md. Update organisms.md and this page’s subscription graph to include the new organism.
Cross-references
- Architecture — the lattice shape these subscriptions run on.
- The six organisms — each organism’s own public surface that this page’s arrows point at.
- Threat model — how trust assumptions compose across the subscription graph.
- Cross-lattice coordination — what happens when the subscription graph crosses a lattice boundary.