Crate map
audience: contributors
Workspace at /Users/karim/dev/flashbots/zipnet/. Edition 2024, MSRV
1.93. Mosaik pinned to =0.3.17 (see CLAUDE.md
for rationale).
One crate, layered. Everything ships inside crates/zipnet/.
Inside, modules form a strict layering where each layer can import
the one above but not the reverse:
zipnet::proto (pure: no mosaik, no tokio, no I/O)
▲
│
zipnet::shuffler (pure: no mosaik, no tokio, no I/O)
▲
│
zipnet::node ── mosaik 0.3.17 ── iroh 0.97 (QUIC)
▲
│
zipnet (crate root)
├── src/lib.rs — SDK facade consumed by other mosaik apps
├── src/main.rs — `zipnet {server|aggregator|client}` operator binary
└── src/ingest.rs — optional REST gateway (feature-gated)
The purity boundary between proto/shuffler and the rest is
load-bearing, not cosmetic. Anything that touches tokio,
mosaik, or I/O must live in node (or higher). Anything that
could be reused by an alternative transport lives in proto /
shuffler. If you find yourself reaching for tokio::spawn or
mosaik:: inside proto or shuffler, you are in the wrong
module.
zipnet::proto
Pure wire types and crypto primitives. No mosaik, no async.
| Module | Role |
|---|---|
wire | ClientEnvelope, AggregateEnvelope, PartialUnblind, BroadcastRecord, ClientId, ServerId, RoundId |
crypto | HKDF-SHA256 salt composition, AES-128-CTR pad generator, blake3 falsification tag |
keys | DhSecret (X25519 StaticSecret), ClientKeyPair, ServerKeyPair, public ClientBundle / ServerBundle |
params | RoundParams (broadcast shape) |
xor | xor_into, xor_many_into over equal-length buffers |
WIRE_VERSION is bumped any time a wire or params shape changes.
CommitteeMachine::signature() in zipnet::node mixes this in so
nodes with different wire versions will never form a group.
zipnet::shuffler
Paper’s algorithms as pure functions over zipnet::proto types. No
async.
| Module | Role |
|---|---|
client::seal | Algorithm 1 — TEE-side sealing of one envelope |
aggregator::RoundFold | Algorithm 2 — stateful XOR fold of envelopes for one round |
server::partial_unblind | Algorithm 3 — per-server partial computation |
server::finalize | Committee combine — aggregate + partials → broadcast |
slot | Deterministic slot assignment + slot layout helpers |
The full round trip is exercised by
shuffler::server::tests::e2e_two_servers_three_clients, which
constructs a 3-server / 4-client setup (2 talkers + 2 cover) and
asserts that the final BroadcastRecord contains each talker’s
plaintext at the expected slot with a valid falsification tag. No
transport is involved — this is the pure-algebra proof.
zipnet::node
The mosaik integration layer. Hosts the declare! items, the
committee state machine, and the role event loops.
| Module | Role |
|---|---|
protocol | declare::stream! + declare::collection! items, tag constants, ticket class constants |
committee | CommitteeMachine: StateMachine, Command, Query, QueryResult, LiveRound, CommitteeConfig |
tickets | BundleValidator<K>: TicketValidator for client / server bundle tickets |
roles::common | NetworkBoot helper that wraps iroh secret, tags, tickets, and mDNS setup |
roles::client | client event loop |
roles::aggregator | aggregator event loop |
roles::server | committee server event loop (single tokio::select! over three event sources) |
The role modules are reusable as a library — the
operator binary is a thin CLI
wrapper around them. Test code in crates/zipnet/tests/e2e.rs reuses the
same primitives but inlines the server loop so it can inject a
pre-built Arc<Network> and cross-sync_with all peers before
anything starts (same pattern as mosaik’s examples/orderbook).
protocol.rs today vs target
protocol.rs currently declares its StreamId / StoreId literals
as flat strings ("zipnet.stream.client-to-aggregator", etc.). The
target per design-intro
is DEPLOYMENT.derive("submit") / .derive("broadcasts") / …
chained off the per-deployment content + intent addressed root so
multiple deployments can coexist on one mosaik universe without
colliding. The migration removes the ZIPNET_SALT.derive(shard)
NetworkId scoping in favour of the shared zipnet::UNIVERSE
constant.
SDK facade (lib, src/lib.rs)
Public surface consumers interact with. Re-exports hide the layered
proto / shuffler / node modules unless the caller explicitly
reaches for them.
| Module | Role |
|---|---|
environments | UNIVERSE constant |
config | Config, ShuffleWindow (deployment fingerprint) |
datum | ShuffleDatum trait, DecodeError |
client | Zipnet::<D>::{submit, receipts, read, deployment_id}, Submitter<D>, Receipts<D>, Reader<D> |
driver | internal — the per-handle driver task powering submit / read |
error | Error { WrongUniverse, ConnectTimeout, Attestation, Shutdown, Protocol } |
types | Receipt, SubmissionId, Outcome |
Re-exports from mosaik that the SDK intentionally surfaces:
UniqueId, NetworkId, Tag, unique_id!. Nothing else is
re-exported — callers that need raw mosaik types drop to
zipnet::node::* directly.
Zipnet::<D>::deployment_id(&Config) is the pure-function derivation
that produces the on-wire identity — use it on both sides of the
handshake for diagnostic parity. The canonical form is
blake3("zipnet|" || name || "|type=" || TYPE_TAG || "|size=" || WIRE_SIZE || "|window=" || window || "|init=" || init); any change
to that encoding is a wire-breaking change and must be versioned.
Operator binary (bin, src/main.rs)
Single executable the operator deploys; role is chosen at launch:
zipnet server [--committee-secret …] [--min-participants …] …
zipnet aggregator [--fold-deadline …]
zipnet client [--message …] [--cadence …]
zipnet ingest [--listen …] (requires --features ingest)
All subcommands share the common flags — --shard, --bootstrap,
--metrics, --secret — and dispatch into
zipnet::node::roles::{server, aggregator, client}::run (or
zipnet::ingest::run for the optional gateway). In v1 the binary
still takes ZIPNET_SHARD and scopes to ZIPNET_SALT.derive(shard);
this predates the UNIVERSE + deployment-fingerprint design and is
tracked for retirement in the roadmap.
| Subcommand | Role-specific flags |
|---|---|
zipnet client | ZIPNET_MESSAGE, ZIPNET_CADENCE |
zipnet aggregator | ZIPNET_FOLD_DEADLINE |
zipnet server | ZIPNET_COMMITTEE_SECRET, ZIPNET_MIN_PARTICIPANTS, ZIPNET_ROUND_PERIOD, ZIPNET_ROUND_DEADLINE |
zipnet ingest | ZIPNET_INGEST_LISTEN (feature ingest) |
Full flag and env-var reference in Environment variables.
Feature flags
tee-tdx(off by default) — foldsmosaik::tickets::Tdx::new().require_own_mrtd()?into the committee’s admission validators. Requires mosaik’stdxfeature (on by default) and TDX hardware. Mock TEE is the default path (// SIMPLIFICATION:in source); critical-path enforcement lands in v2 (see Roadmap).tdx-builder-ubuntu/tdx-builder-alpine— opt-in: run mosaik’s TDX image builder as part ofcargo build. Emits artifacts undertarget/<profile>/tdx-artifacts/zipnet/<distro>/.ingest— compile the optional REST gateway subcommand and its axum dependency. Off by default so library consumers pay nothing for HTTP if they aren’t running the gateway.
Dependency choices worth knowing
x25519-dalek2.0 pinsrand_core0.6 (not workspace rand 0.9).zipnet::protousesrand_core = "0.6"explicitly forOsRngcompatibility withStaticSecret::random_from_rng; the rest of the crate uses workspace rand 0.9.mosaik = "=0.3.17"— the API we developed against. Upgrades are expected to break compile; thedeclare::stream!/declare::collection!macros are stable-ish, the ticket and group APIs have shifted across minor versions.axumis declared as an optional dependency pulled in only by theingestfeature.towerandhttp-body-utilare dev-dependencies for theingestrouter unit tests.