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
| # | Item | What it is | Where it goes in your code |
|---|---|---|---|
| 1 | Deployment Config | The 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(...); |
| 2 | Datum 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 { … } |
| 3 | Bootstrap PeerId | At 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. |
| 4 | Committee 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 iszipnet::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/GroupIdvalues. The SDK derives all of them from theConfig+ 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
SecretKeyif you want a stablePeerIdacross 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
Ddirectly;D::encodeproduces exactlyWIRE_SIZEbytes 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.