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
- web3 — sealed submission and settlement
- Introduction — Shuffle
src/receipt.rs— the sealing primitive.src/zipnet_io.rs— the shuffle channel (namedzipnet_ioin the crate because zipnet is the specific shuffle implementation this example bonds against).