Client identity
audience: users
A zipnet client has two distinct identities that work together. The SDK
manages one of them for you; the other you control through the mosaik
Network you hand to Zipnet::<D>::submit.
Two identities
| Identity | Type | Where it comes from | Purpose |
|---|---|---|---|
PeerId | ed25519 public key | mosaik / iroh SecretKey on the Network | Authenticates you on the wire. Signs your PeerEntry. |
| Client-side DH identity | X25519 keypair | Generated inside Zipnet::<D>::submit per handle | Names your slot in the anonymous-broadcast rounds. Binds your pads. |
The DH identity is internal to zipnet and not exposed across the SDK
surface — you never see a ClientId or DhSecret type in user code.
Every call to Zipnet::<D>::submit generates a fresh DH keypair,
installs the matching bundle ticket through mosaik’s discovery layer,
and waits until the committee admits the binding into a live round.
When you drop the Submitter<D> handle, that keypair and its ticket
go with it.
Your PeerId is the only identity you materially choose.
Choose your PeerId lifetime
Fully ephemeral (default)
Build the Network without calling with_secret_key. Mosaik picks a
random iroh identity per run. Combined with the per-handle DH
identity, this means every process run is an unlinkable
(PeerId, client-DH) pair.
use std::sync::Arc;
use mosaik::Network;
use zipnet::{Config, ShuffleWindow, UNIVERSE, Zipnet};
use zipnet::{DecodeError, ShuffleDatum, UniqueId, 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]);
async fn run() -> anyhow::Result<()> {
let network = Arc::new(Network::new(UNIVERSE).await?);
let tx = Zipnet::<Note>::submit(&network, &ACME_MAINNET).await?;
let _ = tx; Ok(()) }
This is the right default for anonymous use cases. An observer
correlating PeerIds across rounds learns only “this peer was online
during this interval” — which is what mosaik’s transport layer exposes
anyway, independent of zipnet.
Stable PeerId, ephemeral client DH identity
Useful when you want a predictable bootstrap target (your agent’s
PeerId stays the same across restarts) but you don’t want to be
correlatable inside zipnet rounds. Each opened submitter gets a fresh
DH keypair regardless of the PeerId.
use std::sync::Arc;
use mosaik::{Network, SecretKey};
use zipnet::{Config, ShuffleWindow, UNIVERSE, Zipnet};
use zipnet::{DecodeError, ShuffleDatum, UniqueId, 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]);
async fn run(my_seed_bytes: [u8; 32]) -> anyhow::Result<()> {
let sk = SecretKey::from_bytes(&my_seed_bytes);
let network = Arc::new(
Network::builder(UNIVERSE)
.with_secret_key(sk)
.build()
.await?,
);
let tx = Zipnet::<Note>::submit(&network, &ACME_MAINNET).await?;
let _ = tx; Ok(()) }
Re-opening the submitter produces a new client DH identity even with
the same PeerId, so rounds stay unlinkable at the zipnet layer. If
you hold one Submitter<D> for a long time and publish many messages,
those messages share one client DH identity and are linkable to each
other. To rotate, drop the handle and call submit again.
Stable everything (rare)
The current SDK does not expose a way to persist the per-handle DH identity across restarts. If you need stable client identity for a reputation or allowlist use case, talk to the operator about attested-client TDX features — see TEE-gated deployments. Stable anonymous-publish identity at the application layer is an anti-pattern: it trivially breaks unlinkability across rounds.
Multiple submitters per process
The Zipnet::<D>::* constructors only borrow the Arc<Network>, so
one network can host many submitters — the same deployment many times,
different deployments side by side, or zipnet alongside other mosaik
services:
use std::sync::Arc;
use mosaik::Network;
use zipnet::{Config, ShuffleWindow, UNIVERSE, Zipnet};
use zipnet::{DecodeError, ShuffleDatum, UniqueId, 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]);
const PREVIEW_ALPHA: Config = Config::new("preview.alpha")
.with_window(ShuffleWindow::interactive())
.with_init([0u8; 32]);
async fn run() -> anyhow::Result<()> {
let network = Arc::new(Network::new(UNIVERSE).await?);
let prod = Zipnet::<Note>::submit(&network, &ACME_MAINNET).await?;
let testnet = Zipnet::<Note>::submit(&network, &PREVIEW_ALPHA).await?;
let prod_bis = Zipnet::<Note>::submit(&network, &ACME_MAINNET).await?;
let _ = (prod, testnet, prod_bis); Ok(()) }
prod and prod_bis have the same PeerId but independent
client DH identities; the committee treats them as two distinct
publishers. This is occasionally useful for widening your own
anonymity set in test deployments, but it does not buy you extra
anonymity in production against a global observer watching your
network interface.
Rotating
Drop the Submitter<D> handle and call submit again:
drop(prod);
let prod = Zipnet::<Note>::submit(&network, &ACME_MAINNET).await?;
drop tears down the driver task, removes the bundle ticket from
discovery, and lets the committee’s roster forget the old DH
identity at the next gossip cycle. The next submit starts clean.
In-flight send calls that haven’t been picked up when you drop the
last clone surface as Error::Shutdown. Watch the Receipts<D>
stream if you need to know which pending submissions landed before
the rotation.
What about the peer catalog?
The mosaik peer catalog — network.discovery().catalog() — lists
every peer zipnet and anything else on the shared universe sees. It
is not zipnet-specific, and the SDK does not ask you to interact with
it. If you need to inspect it for debugging, see the
mosaik book on discovery.