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 receipts and settlement

audience: ai

Reads feed observation (market-reads); backends wrap the cloud (wrapping); the provider loop fulfils grants (provisioning); this chapter is the write side. The bridge turns a provisioned instance into an encrypted SSH access receipt, seals it to the requester’s x25519 public key, and returns it via the shuffle. The coordination market then clears settlement on the cleared rate, the bridge’s revenue folds into the dashboard, and on-chain payment (if that is the coalition’s settlement policy) lands.

web3’s matching chapter is sealed submission and settlement. The asymmetry: the searcher submits a sealed payload to the lattice’s unseal committee; the bridge returns a sealed payload to the requester. Both ride a shuffle; neither party sees the other’s coalition identity.

Shape of a receipt

A receipt is one object: a serialised SshAccessReceipt sealed to the requester’s x25519 public key and pushed through the shuffle reply channel. From src/receipt.rs:

pub struct SshAccessReceipt {
    pub instance_host:   String,   // public DNS or IP
    pub instance_port:   u16,      // usually 22
    pub user:            String,   // ec2-user, root, …
    pub ssh_key_private: Vec<u8>,  // per-grant key, PEM
    pub ssh_host_key:    Vec<u8>,  // for known_hosts
    pub grant_id:        UniqueId,
    pub valid_to:        AlmanacTick,
}

Seven fields. Enough for the requester to SSH in, verify the host key matches what the backend reported, and know when the grant expires.

Two fields are deliberately absent. The bridge operator’s identity does not appear — the receipt binds the requester to a host, not to the bridge. (A requester who wants to know which bridge served the grant looks up grant_id in the ComputeLog stream; the provider field points back.) Out-of-band settlement evidence does not appear either — settlement flows through whichever off-module rail the coalition publishes, not the receipt. Keeping payment out of the receipt means the receipt can be delivered before settlement confirms, and the workload can start before the on-chain transaction clears.

Sealing

The scheme is standard X25519-ChaCha20-Poly1305:

pub fn seal_to(
    &self, peer_x25519_public: &[u8; 32],
) -> anyhow::Result<Vec<u8>> {
    let ephemeral = EphemeralSecret::random_from_rng(OsRng);
    let ephemeral_public = PublicKey::from(&ephemeral);
    let peer = PublicKey::from(*peer_x25519_public);
    let shared = ephemeral.diffie_hellman(&peer);

    let cipher = ChaCha20Poly1305::new_from_slice(shared.as_bytes())?;
    let receipt_bytes = serde_json::to_vec(self)?;

    // Zero nonce is safe because the shared secret is fresh.
    let nonce = Nonce::from_slice(&[0u8; 12]);
    let ct = cipher.encrypt(nonce, receipt_bytes.as_ref())
        .map_err(|e| anyhow::anyhow!("encrypt failed: {e}"))?;

    let mut out = Vec::with_capacity(32 + ct.len());
    out.extend_from_slice(ephemeral_public.as_bytes());
    out.extend_from_slice(&ct);
    Ok(out)
}

Output layout:

  [0..32]   ephemeral_x25519_public
  [32..]    ChaCha20-Poly1305(shared_secret, receipt_bytes)

The ephemeral public key is generated per receipt, so two receipts to the same requester are not linkable by their ephemeral keys. The bridge sees only the requester’s static x25519 public key inside the shuffle envelope. Zero nonce is safe because the shared secret is fresh per call (ephemeral times static, ChaCha20-Poly1305 does not reuse keys). Only the requester’s static x25519 private key can decrypt.

Routing through the shuffle

The bridge does not push the sealed receipt across an open network:

pub async fn reply(
    &self,
    request_id: &RequestId,
    sealed_receipt: Vec<u8>,
) -> anyhow::Result<()> {
    // Publishes on the shuffle reply stream
    // addressed to the peer the request came from.
    // The shuffle carries the sealed blob; the blob
    // is opaque to the shuffle.
    self.reply_sender.publish(
        request_id, sealed_receipt,
    ).await
}

The reply is addressed to the rotating peer_id the request came from, not to the requester’s coalition identity. An observer learns that the bridge replied to a peer, not to which coalition agent. Zipnet’s reply channel does not guarantee in-order delivery relative to the request; consumers key replies by request_id. Replies ride the same shuffle the requests ride; a bridge that tried to reply out of band would leak its own identity, which the shuffle’s privacy contract is designed to prevent.

What the requester does

The requester decrypts, verifies the host key against the provided ssh_host_key, and SSHes in:

// requester side (not in the bridge's crate)
let receipt = SshAccessReceipt::unseal_with(
    &sealed_from_shuffle,
    &my_x25519_static_private,
)?;

let mut known_hosts = String::new();
known_hosts.push_str(&format!(
    "[{}]:{} {}",
    receipt.instance_host,
    receipt.instance_port,
    hex::encode(&receipt.ssh_host_key),
));
// … standard ssh with the per-grant private key

One function call to decrypt, then a standard ssh client. No bridge-specific client library.

Settlement

The receipt is delivered; the workload runs; the usage log is emitted. The coordination market then closes the loop.

The Compute module does not define a settlement format. Each ComputeRequest carries a settlement: UniqueId — a blake3 hash the requester chose. The module stores the hash opaquely. Whichever off-module rail the coalition uses (on-chain payment contract, reputation organism, credit token) is what dereferences the hash and effects the transfer.

For bridge-β, the coalition publishes the rails it supports on its handshake page; the bridge’s dashboard records revenue as each rail commits a transfer to the bridge’s settlement address. For each cleared grant:

On-chain payment: the requester’s bid carried a settlement hash pointing to an already-submitted on-chain payment to a contract the coalition operator published. The scheduler committee verified the payment evidence at clearing. After the ComputeLog lands, the committee releases the payment to the bridge’s settlement address.

Reputation card: the requester’s bid carried a reputation card entry from an organism the bridge trusts; the bridge’s revenue is a reputation commit, not cash. Used for in-coalition credit systems where cash settlement is deferred.

The bridge does not directly manage either settlement path. It commits the ComputeLog and watches the dashboard. From src/dashboard.rs:

pub enum DashboardEvent {
    SubscriberJoined,
    SubscriberLeft,
    GrantAccepted    { backend: String },
    GrantCompleted   { backend: String, usage: UsageDelta },
    RevenueSettled   { usd: f64 },
    ProviderCardRefreshed,
}

RevenueSettled is what the dashboard records when the module’s settlement side (on-chain or reputation) commits the bridge’s share.

Revenue on the dashboard

The dashboard folds every RevenueSettled into a rolling window total:

DashboardEvent::RevenueSettled { usd } => {
    s.window_revenue_usd += usd;
}

The operator can now answer three questions live. Are declared rates being accepted? Revenue arrives after a grant clears and settles; rates that never clear produce zero regardless of capacity. Is the cost-to-revenue gap favourable? The dashboard folds per-backend cost estimates from the operator-supplied rate table; the difference is the bridge’s margin. Are some backends more profitable than others? Per-backend cost and usage split on the dashboard; bridges often discover one backend is chronically loss-making.

End-to-end privacy

Requester to bridge: shuffle-sealed request envelope; the bridge sees a rotating peer_id and an x25519 public key, not the coalition-facing identity.

Bridge to requester: X25519-ChaCha20-Poly1305 sealed receipt; shuffle reply stream; requester decrypts with their static x25519 private key.

Operator view: aggregate dashboard only. No per-grant identity, no prompt, no per-requester attribution.

Coalition read side: the public ComputeLog stream carries grant id, provider id, and usage. No requester identity. No image contents.

Symmetrical with web3 — submission: both sides of the market are shuffle-anonymised, both sides receipt-sealed, both sides commit evidence pointers.

Multi-receipt flows

A grant with valid_to much larger than workload runtime may terminate early. The bridge emits one ComputeLog at termination; the requester re- SSHing before valid_to on an expired instance gets ConnectRefused — the per-grant private key is dead. No second receipt is issued for the same grant_id; the requester submits a fresh ComputeRequest:

// requester side — continuation pattern
let fresh = ComputeRequest {
    requester:   self.id,
    workload:    same_organism_id,
    pin:         Some(same_content_hash),
    duration_sec: DURATION_WINDOW,
    settlement:  self.new_settlement_evidence_hash()?,
    deadline:    now + deadline_ticks,
};
let grant = compute.submit_and_await(fresh).await?;
// … fresh receipt, fresh per-grant key, possibly a
// fresh backend if fleet routing shifted

Cross-references