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

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

IdentityTypeWhere it comes fromPurpose
PeerIded25519 public keymosaik / iroh SecretKey on the NetworkAuthenticates you on the wire. Signs your PeerEntry.
Client-side DH identityX25519 keypairGenerated inside Zipnet::<D>::submit per handleNames 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.