Sealed submission and settlement
audience: ai
Reads feed the policy (market-reads,
wrapping); the policy runs on Compute
(inference); this chapter is the
write-side. The searcher converts a policy decision
into a sealed bundle, routes it through
testnet-1a’s zipnet → unseal → offer pipeline,
watches for the landing block, and folds the refund
attribution back into its outcome stream.
This is where the searcher’s agreement on shared
policy — ai/emergent-coordination.md — shared
policy — bites hardest. Every organism in the
submission pipeline (zipnet, unseal, offer,
tally) carries a Config.content the searcher’s
TicketValidator has already admitted
(binding). The sealed payload flows
across four organisms without any of them having to
trust the searcher beyond what the shared policy
declares.
The shape of a submission
A submission is two objects: a public intent the searcher commits on its own surface, and a sealed payload the searcher pushes through a shuffle. The two are linked by a blake3 fingerprint only.
use searcher_alpha::public::{
SealedSubmissionIntent, BundleDigest,
};
pub struct SealedSubmissionIntent<'a> {
pub tick: AlmanacTick,
pub bundle_digest: BundleDigest, // blake3 of sealed payload
pub target_block: u64,
pub evidence: &'a [EvidencePointer<'a>],
// The policy does not publish bundle content
// here. Consumers see the commitment, the block
// target, and pointers into the observations
// that drove the decision.
}
The public intent commits what and when without leaking the bundle’s content. A late auditor can recompute the bundle digest from the unsealed payload (once the block lands and the content is public), verifying that the intent matches the submission.
Sealing via zipnet::seal
use coalition::zipnet::{SealClient, SealError};
use builder::testnet_1a::zipnet;
async fn submit_bundle(
network: &Arc<Network>,
searcher: &Searcher<Submission>,
bundle: Bundle,
tick: AlmanacTick,
) -> Result<BundleDigest, SealError> {
// 1. Compute the bundle digest. The digest is the
// content hash, not the intent hash — it will
// match the payload the unseal committee
// releases post-block.
let payload = bundle.serialize_sealed();
let digest = BundleDigest::from(
blake3::hash(&payload).into(),
);
// 2. Push the sealed payload through a shuffle.
// The seal client uses the Flashbots-declared
// zipnet::seal endpoint on testnet-1a; the
// searcher's TicketValidator has admitted its
// Measurements since chapter 2.
let seal_client = SealClient::new(
network, &zipnet::SEAL_CFG,
);
seal_client.submit_sealed(&payload).await?;
// 3. Commit the public intent. The intent stream
// is the searcher's own public surface — no
// bundle content leaks, only the digest and
// the evidence pointers.
searcher.submissions.publish(
SealedSubmissionIntent {
tick,
bundle_digest: digest,
target_block: bundle.target_block,
evidence: &bundle.evidence(),
},
).await?;
Ok(digest)
}
Three properties of the sealed path:
- Anonymisation. The searcher’s shuffle client rotates a per-round peer id, so the unseal committee cannot link the submission to a coalition-facing identity. See zipnet book for the full decryption protocol.
- Attested recipients. The unseal committee is
TDX-attested with Measurements the searcher
verifies via
verify_peer(binding). An unseal committee running a different binary cannot decrypt the searcher’s payload without failing the verification. - Evidence pointers in the intent, not the payload. The public intent is small and immediate; the sealed payload is a blob the unseal committee releases only after the target block lands.
Routing through unseal → offer
The searcher does not push the payload through
unseal or offer directly. The builder lattice’s
internal wiring routes the payload from zipnet to
unseal (the decryption committee) to offer (the
order-matching organism) under the lattice’s own
trust composition. The searcher’s role ends when the
zipnet::seal commit succeeds.
What the searcher observes, on the public read side:
use builder::testnet_1a::offer::{Orders, Order};
use builder::testnet_1a::atelier::{Blocks, BlockCandidate};
// An Order commit carrying the searcher's
// bundle digest confirms the bundle cleared the
// unseal committee and reached the offer organism.
let mut orders = network
.subscribe::<Orders>(TESTNET_1A.stable_id())
.await?;
while let Some(order) = orders.next().await {
if order.bundle_digest == Some(digest) {
tracing::info!(
digest = %digest,
order = %order.order_id,
"bundle cleared unseal and reached offer"
);
}
}
If the searcher’s bundle digest never appears in an
Orders commit, one of three things happened:
- The shuffle sealing layer dropped the payload (encryption mismatch, rate limit, malformed payload).
- The unseal committee rejected it (validator policy, digest re-use, expired nonce).
- The offer organism discarded it (clearing policy, duplicate, stale target block).
All three are observable as either the absence of
the expected Order commit or a diagnostic commit
the offer organism publishes for operator-visible
errors. The searcher’s policy retries with a fresh
digest or gives up; the substrate does not retry on
the searcher’s behalf.
Settlement via tally::Refunds
Once the target block lands, the tally organism
commits a RefundEntry per bundle that contributed:
use builder::testnet_1a::tally::{Refunds, RefundEntry};
let mut refunds = network
.subscribe::<Refunds>(TESTNET_1A.stable_id())
.await?;
while let Some(entry) = refunds.next().await {
if entry.bundle_digest == digest {
outcome_store.update(CycleId(entry.block_number),
|o| o.realised_refund_wei = entry.refund_wei,
);
break;
}
}
Three structural notes:
RefundEntryis the ground truth. Not the searcher’s own claim, not the provider’s usage log, not an off-chain receipt. Thetallycommittee’s commit is what the searcher’s reputation organism and the Flashbots operator both treat as authoritative. See builder book — refunds for the full attribution algorithm.- Evidence folds cleanly. The
RefundEntrycarries a pointer to the winning block’s candidate inatelier::Blocksand a pointer to theoffer::Ordersentry the bundle cleared through. A replayer can trace the entire causal chain without trusting the searcher or the operator. - Off-chain settlement is out of scope here. The
refund is an accounting commit, not a payment. The
actual wei movement, if any, flows through whatever
on-chain or off-protocol contract the Flashbots
operator has published;
tally::Refundspoints at the evidence, not at the transfer.
Settlement evidence into Compute
The RefundEntrys the searcher accumulates feed back
into inference — the top-up loop.
The searcher’s settlement_evidence_hash() function
returns a blake3 pointing into the recent
RefundEntrys. The Compute module stores the hash
opaquely in the ComputeRequest; verification of
whatever the hash points to lives with whichever
settlement rail the coalition uses — on-chain
payment contract, reputation card, tally-refund
pointer. The module is neutral to the format.
A provider serving the grant can independently
verify the refund entry exists on testnet-1a’s
tally before committing to the workload.
Settlement becomes a loop closed entirely inside
mosaik.
Multi-block submissions and retries
A searcher that wants to target several upcoming blocks emits multiple intents and multiple sealed payloads, each with a distinct bundle digest:
for block in current..current + WINDOW {
let bundle = policy.propose_bundle_for(block);
submit_bundle(
&network, &searcher, bundle, tick,
).await?;
}
Each intent is independent; none depend on the others clearing. The substrate commits what the searcher submits and attributes what lands. Strategy-level dependencies between bundles are the searcher’s problem.
Market-maker variant — differences at submission
- Quote surface, not sealed-bundle surface.
Market-makers typically submit quotes on
offer’s write-side quote endpoint directly (when the operator permits), bypassing shuffle’s seal. The Measurements Flashbots pins for the quote endpoint are distinct fromZIPNET_SEAL_MEASUREMENTS. - Finer-grained retries. Quote updates fire multiple times per second; the “give up” path is different — an unmatched quote expires, not rejected.
- Fill attribution, not refund attribution.
Market-makers read
tally::Fills(a distinct collection fromtally::Refunds) that commits per-fill settlement. Their outcome stream is filled-quantity-versus-quoted-quantity, not refund- wei-per-bundle. - Inventory reconciliation. The market-maker
maintains inventory state outside mosaik; each
FillEntryforces a reconciliation. The reconciliation itself is not a mosaik commit, but its reconciled rollups appear on the market-maker’s publicfillscollection.
Cross-references
- Part I — emergent coordination — shared policy — the mechanism the submission pipeline exhibits.
- Part II — binding — where the
TicketValidatorclauses admittedzipnet::seal,unseal,offer, andtallyMeasurements. - zipnet book — the sealing and decryption protocol the submission rides.
- builder book — submitting
- builder book — refunds