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

What you need from the operator

audience: users

Before you can write a line of code against a running zipnet deployment, collect three (or four, if it is TDX-gated) items from whoever runs it. That is the whole handshake — zipnet does not gossip a deployment registry, so everything you need to reach the deployment has to arrive out of band.

The handshake

#ItemWhat it isWhere it goes in your code
1Deployment ConfigThe operator’s chosen instance name, shuffle window, and 32-byte init salt. The fingerprint that determines the on-wire identity.const ACME_MAINNET: Config = Config::new("acme.mainnet").with_window(...).with_init(...);
2Datum schema (D::TYPE_TAG, D::WIRE_SIZE)The exact bytes-on-the-wire contract for one payload. Usually shipped as a small Rust crate the operator publishes.impl ShuffleDatum for YourDatum { … }
3Bootstrap PeerIdAt least one reachable peer on the shared universe — typically the operator’s aggregator or a committee server. Without one, cold-start discovery falls back to the Mainline DHT and takes minutes instead of seconds.discovery::Config::builder().with_bootstrap(peer_id) on the Network builder.
4Committee MR_TD (TDX-gated deployments only)48-byte hex measurement of the operator’s committee image. Pin this if your agent verifies inbound committee attestation, or match it if you are building a client image.See TEE-gated deployments for which applies to your setup.

Every byte of the Config plus the datum’s TYPE_TAG and WIRE_SIZE folds into the deployment’s on-wire identity via a single content-addressed hash. If your Config disagrees with the operator’s by one field — a typo in the name, a different ShuffleWindow preset, a stale init salt — your code derives an id nobody is serving, and the Zipnet::<D>::* constructors return Error::ConnectTimeout after the bond window elapses.

The bootstrap peer is universe-level, not zipnet-specific. Any reachable peer on the shared universe is a valid starting point; once you are bonded, mosaik’s discovery finds the specific instance’s committee and aggregator through the shared peer catalog.

The MR_TD is relevant only if the operator has turned on TDX gating. Most development deployments do not; production often does.

What you do not need to ask for

  • The universe NetworkId. It is zipnet::UNIVERSE — a shared constant baked into the SDK. Every operator and every user on zipnet uses the same value. You only need an operator-supplied override in the rare case they run an isolated federation on a different universe; assume they will tell you explicitly if so.
  • Per-deployment StreamId / StoreId / GroupId values. The SDK derives all of them from the Config + datum schema. Operators never hand these out, and the facade does not accept them.
  • Committee server secrets or any committee member’s X25519 secret. You are a consumer, not a committee member.
  • A seat on the committee’s Raft group. The SDK reads the broadcast log through a replicated collection; it does not vote.

How the handshake travels

Out of band. Release notes, a README in the operator’s repo, a Slack message, a secret-manager entry. Zipnet deliberately does not carry an on-network registry — the shared-universe model assumes consumers compile-time reference the Config they trust, rather than discovering “what deployments exist” at runtime. See Designing coexisting systems on mosaik for the rationale.

Pinning the Config at compile time

A typo in any Config field silently produces a different deployment id and surfaces as ConnectTimeout. For production code, bake the whole fingerprint in as a const:

use zipnet::{Config, ShuffleWindow, Zipnet};

const ACME_MAINNET: Config = Config::new("acme.mainnet")
    .with_window(ShuffleWindow::interactive())
    .with_init([
        // 32 operator-published bytes.
        0x7f, 0x3a, 0x9b, 0x1c, /* … */ 0x00,
    ]);

async fn run(network: std::sync::Arc<mosaik::Network>) -> anyhow::Result<()> {
struct Note; impl zipnet::ShuffleDatum for Note {
    const TYPE_TAG: zipnet::UniqueId = zipnet::unique_id!("demo");
    const WIRE_SIZE: usize = 240;
    fn encode(&self) -> Vec<u8> { vec![0u8; 240] }
    fn decode(_: &[u8]) -> Result<Self, zipnet::DecodeError> { Ok(Note) }
}
let tx = Zipnet::<Note>::submit(&network, &ACME_MAINNET).await?;
let _ = tx; Ok(()) }

Zipnet::<D>::deployment_id(&ACME_MAINNET) is a pure function — print it on both sides of the handshake to confirm you and the operator computed the same fingerprint without any wire traffic.

What you bring yourself

  • Your mosaik SecretKey if you want a stable PeerId across restarts. Leave it unset to get a random identity per run, which is the usual choice for anonymous-use-case clients. See Identity.
  • Your datum values. The SDK takes the typed D directly; D::encode produces exactly WIRE_SIZE bytes on the wire.

Minimal smoke test before writing anything substantial

Once you have the items above (four if TDX-gated), this program publishes to the deployment and prints its outcome within a few round periods:

use std::sync::Arc;
use futures::StreamExt;
use mosaik::{Network, discovery};
use zipnet::{Config, DecodeError, ShuffleDatum, ShuffleWindow,
              UNIVERSE, UniqueId, Zipnet, unique_id};

pub struct Note(pub [u8; 240]);
impl ShuffleDatum for Note {
    const TYPE_TAG: UniqueId = unique_id!("acme.note-v1");
    const WIRE_SIZE: usize = 240;
    fn encode(&self) -> Vec<u8> { self.0.to_vec() }
    fn decode(b: &[u8]) -> Result<Self, DecodeError> {
        <[u8; 240]>::try_from(b).map(Self).map_err(|e| DecodeError(e.to_string()))
    }
}

const ACME_MAINNET: Config = Config::new("acme.mainnet")
    .with_window(ShuffleWindow::interactive())
    .with_init([0u8; 32]); // replace with the operator's bytes

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let bootstrap = "<paste-the-operator's-peer-id>".parse()?;

    let network = Arc::new(
        Network::builder(UNIVERSE)
            .with_discovery(discovery::Config::builder().with_bootstrap(bootstrap))
            .build()
            .await?,
    );

    let tx         = Zipnet::<Note>::submit  (&network, &ACME_MAINNET).await?;
    let mut rx     = Zipnet::<Note>::receipts(&network, &ACME_MAINNET).await?;

    let id = tx.send(Note([0u8; 240])).await?;
    while let Some(receipt) = rx.next().await {
        if receipt.submission_id == id {
            println!(
                "landed in round {} slot {} -> {:?}",
                receipt.round, receipt.slot, receipt.outcome,
            );
            break;
        }
    }
    Ok(())
}

If the submitter returns ConnectTimeout, the Config or the bootstrap peer is the first suspect — see Troubleshooting.

Trust

The operator is trusted for liveness — they can stall or kill rounds at will. They are not trusted for anonymity, provided the any-trust assumption holds across their committee. See Threat model if you are auditing before integrating.