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

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:

  • RefundEntry is the ground truth. Not the searcher’s own claim, not the provider’s usage log, not an off-chain receipt. The tally committee’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 RefundEntry carries a pointer to the winning block’s candidate in atelier::Blocks and a pointer to the offer::Orders entry 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::Refunds points 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 from ZIPNET_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 from tally::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 FillEntry forces a reconciliation. The reconciliation itself is not a mosaik commit, but its reconciled rollups appear on the market-maker’s public fills collection.

Cross-references