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

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.

ModuleRole
wireClientEnvelope, AggregateEnvelope, PartialUnblind, BroadcastRecord, ClientId, ServerId, RoundId
cryptoHKDF-SHA256 salt composition, AES-128-CTR pad generator, blake3 falsification tag
keysDhSecret (X25519 StaticSecret), ClientKeyPair, ServerKeyPair, public ClientBundle / ServerBundle
paramsRoundParams (broadcast shape)
xorxor_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.

ModuleRole
client::sealAlgorithm 1 — TEE-side sealing of one envelope
aggregator::RoundFoldAlgorithm 2 — stateful XOR fold of envelopes for one round
server::partial_unblindAlgorithm 3 — per-server partial computation
server::finalizeCommittee combine — aggregate + partials → broadcast
slotDeterministic 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.

ModuleRole
protocoldeclare::stream! + declare::collection! items, tag constants, ticket class constants
committeeCommitteeMachine: StateMachine, Command, Query, QueryResult, LiveRound, CommitteeConfig
ticketsBundleValidator<K>: TicketValidator for client / server bundle tickets
roles::commonNetworkBoot helper that wraps iroh secret, tags, tickets, and mDNS setup
roles::clientclient event loop
roles::aggregatoraggregator event loop
roles::servercommittee 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.

ModuleRole
environmentsUNIVERSE constant
configConfig, ShuffleWindow (deployment fingerprint)
datumShuffleDatum trait, DecodeError
clientZipnet::<D>::{submit, receipts, read, deployment_id}, Submitter<D>, Receipts<D>, Reader<D>
driverinternal — the per-handle driver task powering submit / read
errorError { WrongUniverse, ConnectTimeout, Attestation, Shutdown, Protocol }
typesReceipt, 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.

SubcommandRole-specific flags
zipnet clientZIPNET_MESSAGE, ZIPNET_CADENCE
zipnet aggregatorZIPNET_FOLD_DEADLINE
zipnet serverZIPNET_COMMITTEE_SECRET, ZIPNET_MIN_PARTICIPANTS, ZIPNET_ROUND_PERIOD, ZIPNET_ROUND_DEADLINE
zipnet ingestZIPNET_INGEST_LISTEN (feature ingest)

Full flag and env-var reference in Environment variables.

Feature flags

  • tee-tdx (off by default) — folds mosaik::tickets::Tdx::new().require_own_mrtd()? into the committee’s admission validators. Requires mosaik’s tdx feature (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 of cargo build. Emits artifacts under target/<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-dalek 2.0 pins rand_core 0.6 (not workspace rand 0.9). zipnet::proto uses rand_core = "0.6" explicitly for OsRng compatibility with StaticSecret::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; the declare::stream! / declare::collection! macros are stable-ish, the ticket and group APIs have shifted across minor versions.
  • axum is declared as an optional dependency pulled in only by the ingest feature. tower and http-body-util are dev-dependencies for the ingest router unit tests.