Running a client
audience: operators
The typical zipnet publisher is an external user running their own
TDX-attested agent — you don’t operate those. This page is about the
reference zipnet-client binary you ship to publishers (or run
yourself for a bundled wallet, a cover-traffic filler, or a
smoke-test participant).
A client generates an X25519 keypair, publishes its public bundle via gossip, and seals one envelope per round. In production every client runs inside a TDX guest whose MR_TD matches the value your committee pinned; see Quickstart TDX section.
One-shot command
ZIPNET_INSTANCE="acme.mainnet" \
ZIPNET_MESSAGE="payload-to-broadcast" \
./zipnet-client --bootstrap <peer_id_of_aggregator_or_server>
Omit ZIPNET_MESSAGE to run a cover-traffic client that participates
in every round with a zero payload. Cover traffic is the operator’s
tool for raising the effective anonymity set size when real
publishers are sparse.
Environment variables
| Variable | Meaning | Notes |
|---|---|---|
ZIPNET_INSTANCE | Instance name to bind to | Required. Same string the committee uses; typos show up as ConnectTimeout. |
ZIPNET_UNIVERSE | Universe override | Optional; leave unset to use the shared universe. |
ZIPNET_BOOTSTRAP | Peer IDs to dial on startup | Aggregator’s PeerId or any committee server’s. Needed only on cold networks. |
ZIPNET_MESSAGE | UTF-8 message to seal per round | Truncate yourself to fit slot_bytes − tag_len. Default slot width is 240 bytes of user payload. |
ZIPNET_CADENCE | Talk every Nth round | Default 1. Useful for dialing your own talk/cover ratio. |
ZIPNET_METRICS | Prometheus bind address | Optional. |
Building the TDX image you ship to publishers
Publishers to a TDX-gated instance need to run your client image (not their own ad-hoc build), because the committee will reject any client whose quote doesn’t match the pinned MR_TD. Build it the same way you build the server image — mosaik ships the builder:
// crates/zipnet-client/build.rs
fn main() {
mosaik::tee::tdx::build::alpine()
.with_default_memory_size("512M")
.build();
}
# crates/zipnet-client/Cargo.toml
[dependencies]
mosaik = { version = "0.3", features = ["tdx"] }
[build-dependencies]
mosaik = { version = "0.3", features = ["tdx-builder-alpine"] }
Alpine is the usual choice for clients — ~5 MB versus Ubuntu’s
~25 MB — unless your agent has a specific glibc dependency. After
cargo build --release the artifacts land under
target/release/tdx-artifacts/zipnet-client/alpine/:
| Artifact | What it’s for |
|---|---|
zipnet-client-run-qemu.sh | Self-extracting launcher publishers invoke on a TDX host. |
zipnet-client-mrtd.hex | The 48-byte measurement. You pin this in the committee and publish it to readers. |
zipnet-client-vmlinuz | Raw kernel, for repackaging. |
zipnet-client-initramfs.cpio.gz | Raw initramfs. |
zipnet-client-ovmf.fd | Raw OVMF firmware. |
Publish zipnet-client-mrtd.hex alongside your release notes. It
goes into the committee’s Tdx::require_mrtd(...) configuration and
into readers’ verification code. See
Rotations and upgrades
for rolling a new MR_TD without downtime.
What a healthy client log looks like
INFO zipnet_client: spawning zipnet client client=550fda1ffa
INFO zipnet_node::roles::common: zipnet up: network=<universe> instance=acme.mainnet peer=c2e9aeee0e... role=a8b7ed5911...
INFO zipnet_node::roles::client: client booting; waiting for rosters
After boot, every sealed envelope is a DEBUG event. Raise
RUST_LOG to debug,zipnet_node=debug to see them.
Why a client’s envelope might get dropped
- The client bundle hasn’t replicated yet. The first few rounds
after a client connects may not include it in
ClientRegistry. Wait forzipnet_client_registeredto flip to 1 before relying on anonymity guarantees. - Slot collision with another client. v1’s slot assignment is a deterministic hash — two clients occasionally pick the same slot and XOR their messages into garbage. Neither falsification tag verifies, the committee still publishes the broadcast, the messages are lost, the clients retry next round. A 4x-oversized scheduling vector in v2 makes this rare.
- Message is longer than
slot_bytes − tag_len. The client exits withMessageTooLong. Shorten, or raiseslot_bytesat the instance level (which retires the instance — see Rotations and upgrades).
Identity lifetime
In the mock path (TDX disabled), each process run generates a fresh X25519 identity — run-to-run unlinkability is free. In the TDX path, the identity lives in sealed storage inside the enclave so a restart preserves it; useful for reputation systems, but means the same enclave is recognizable across runs. Design accordingly when you pick a cover-traffic cadence.
See also
- Running a committee server
- Rotations and upgrades — rebuilding the client image and rolling a new MR_TD.
- Security posture checklist — client-host hygiene, TDX expectations.