Architecture
The engineering companion to the whitepaper — how the reference node realises the design, and the decisions behind it. Explanatory, not normative.
The protocol is the parameter files and the golden vectors, plus the named rules wherever they are defined; the wire specification carries the peer layer's requirements. This page explains that surface and records the decisions behind it. Where it disagrees with the normative surface, the normative surface wins and this text is corrected. The full engineering companion is docs/ARCHITECTURE.md.
Two predicates, two engines#
The life of a certificate, end to end:
wallet or sequencer network every full node
+-------------+ gossip +--------------+ +---------------------+
| build cert | ---------> | cert topic | ------------> | STATELESS PIPELINE |
| (reads, | | (TLS gossip) | | V-rules, strict |
| writes, | +--------------+ | Ed25519, program |
| sigs, fee, | | re-execution |
| deposit) | | -> mark VALID |
+-------------+ +----------+----------+
| mempool
v
miner (any node) +---------------------+
+----------------------+ block | FOLD (sequential) |
| order the hash list, | ---------> | per certificate: |
| append forced certs, | gossip | APPLIED / SKIPPED / |
| RandomX solve | | FORCED_FAILED / |
+----------------------+ | DROPPED -> state |
+---------------------+
Validity is stateless and parallel: it runs once per certificate per node, before and independent of blocks. Applicability is stateful and sequential: it runs inside the fold at the certificate's committed position. The miner executes nothing it is given: it orders hashes it has already seen validated and solves proof of work, or, after the switch to proof of stake in era 2, signs the slot it was drawn for. Its one piece of execution is the forced path's, from era 1: it may execute open intents against the state at the end of its block and append the results, at most 16 a block, which every node then checks like any other certificate. The fold is a tight loop over an in-memory working set — compare, add, write — and executes nothing at all.
Engineering principles#
| Principle | |
|---|---|
| P1 | The fold is sacred. The state-transition function lives in one pure package with no I/O, no clocks, no goroutines, no map iteration and no floating point. It is the only code whose bugs are unfixable after the fact. Everything else in the node is replaceable plumbing. |
| P2 | Determinism beats performance. Any optimisation that risks nondeterminism is rejected in consensus code. Performance belongs in the stateless pipeline, where it is safe. |
| P3 | No admin keys, no privileged RPC. There is no code path by which any key can pause, upgrade, mint or reorganise. If it is not in the fold rules, it does not exist. |
| P4 | Small consensus surface. core/ imports nothing outside the standard library. The rest of the node may use ecosystem libraries; the core may not. |
| P5 | Spec-first. The golden vectors are the protocol. The Go code is a reference implementation; an independent implementation that passes the vectors is a peer, not a fork. This is what allows the maintainer to eventually be nobody in particular. |
| P6 | Reproducible from v0.1. Pinned Go toolchain, -trimpath, pinned dependencies. Trust moves from the binary to the code, which is the only trust an anonymous author can offer. |
| P7 | One binary, gated by height. The machine, the sequencer machinery and the forced path are compiled into every release and refused below their activation height, h1_vm (80,640 on mainnet), which Validate requires to fall on an epoch boundary. Era 1 arrives because the chain reached a number, never because operators were asked to upgrade. The later eras are not in the binary: each ships as its own release, by hard fork. |
P3 is the one a sceptical reader should press hardest, and the treasury is where to press it: genesis contains no key and no spend path at all, so there is no privilege to hold, delegate or steal. The treasury cannot be spent before height 259,200, and whatever rule opens it arrives as a consensus change — the same social mechanism as any other, subject to the same refusal, and able to move exactly one cell even then. The founder share is a credit, not a key: 3% of each subsidy to one address fixed in the parameter file, until height 3,153,600, which era 2 reads as epoch 1,095. That address holds coins and reaches nothing else. An admin key is one that exists before anyone consented and reaches everything.
Cryptographic primitives#
| Role | Choice | Why |
|---|---|---|
| Signatures | Ed25519 | Batch verification (the GPU on-ramp), no malleability, tiny keys. Strict rules fixed at genesis: canonical encodings required for the public key and R, the public key torsion-free and not of low order, and verification cofactorless. |
| Hashing | BLAKE3 | Certificate and block ids, addresses, state root. Fast enough to hash at relay line rate; parallel-friendly for the epoch state root. |
| Proof of work | RandomX | CPU-optimised. The only cgo in the tree, behind a build tag, and absent from a build without it. pow_engine is in the consensus root, so a binary holding the wrong engine refuses to start rather than accepting the wrong proof. |
| Domain separation | mandatory | Every hash is blake3(tag ‖ payload). Signatures sign over the chain id and the consensus root, which kills replay both between networks and across two incarnations of one network. |
Torsion rejection is what makes the batch path safe. A mixed-order key is not small-order, so no blocklist reaches it, and it is exactly where a cofactored batch verifier and a cofactorless single verifier disagree. With the key and R in the prime-order subgroup the two are provably equivalent — so a batch verifier may be cofactored provided it applies the same encoding and torsion rules before batching. That obligation is the price of the choice, and the batch verifier does not exist yet.
Canonical encoding and identifiers#
All consensus objects are SSZ containers: a single canonical byte encoding, no map ordering, no optional-field ambiguity, fixed offsets for cheap partial parsing, and native merkleization.
cert_id = blake3("zcd/certid/v1" || ssz(certificate with an empty signature list))
cert_exemplar = blake3("zcd/cert/v1" || ssz(certificate))
block_id = blake3("zcd/block/v1" || ssz(header))
The first two are different digests answering different questions, and an implementation that uses one where the other belongs is monetarily broken. The id answers has this authorization been billed; the exemplar hash answers do these bytes prove it. The two never share a key.
Signatures are outside the id's preimage because a signature is a randomized demonstration: the signer picks the nonce, every nonce yields another valid and perfectly canonical signature over the same body, and no verifier can check which was used. Were they inside, one authorization would have unboundedly many ids, each billable, each producible by any one of its required signers out of the others' authority.
A bid outside the id would be a bid anyone in transit could rewrite — inflated to burn the signer's balance through the base fee, or zeroed to keep the certificate out of every block. What a certificate pays is part of what its signer authorized, so it is hashed and it is signed. A later encoding change that "moves the fee out of the signed body for relay flexibility" would look like an optimisation and would be a theft vector.
The rule that follows: parse, don't validate twice. Decoding enforces canonical form, so a decoded object is structurally valid by construction and the rule engines never re-check shape.
The cell model#
A cell is the value at a slot. Cell values are unsigned 256-bit integers stored big-endian in 32 bytes. Absent cells read as zero — zero is absence, which is a consensus requirement rather than an implementation convenience: it keeps the state root a function of the state rather than of the history that produced it.
Separately, the protocol keeps a spent-address registry: a permanent consensus set of one-shot addresses whose signing authority has been burned.
Addr = version || blake3("zcd/addr/v1" || version || payload)[:31]
| Version | Kind | Debit authorization |
|---|---|---|
0x01 | one-shot user | Owner signature; any debiting certificate must also carry an explicit MARK_SPENT. After it applies, every read and write under the address fails forever. |
0x02 | persistent user | Owner signature; reusable forever. Can never enter the spent registry. |
0x03 | asset | Governed by the asset's immutable authority cells. |
0x00 | protocol | Fold-only: epoch beacon, base-fee cells, coinbase maturity ring, treasury cell, sequencer counters. |
0x05 | contract | Nobody by key: no key hashes to one. The contract's code and the fold rules decide. |
0x04 | not defined | No cell under it exists; a certificate naming such an address is invalid. |
Typed access is where the parallelism comes from. A read declares how it
constrains its cell — EXACT, or one of the guards GUARD_GE,
GUARD_LE, GUARD_EQ — and a write declares what it does:
SET, DELTA_ADD, DELTA_SUB or MARK_SPENT. Only
EXACT feeds the computation, and only EXACT goes stale when the cell moves;
a guard returns nothing and a delta reads nothing, so re-execution stays a pure function of the
exact reads and any number of guarded debits and credits to one slot commute. One discipline per
slot per certificate: a set and a delta never share a slot. Writing zero deletes the cell, so a
slot never written and a slot written to zero are the same slot.
One-shot versus persistent is a wallet policy surfaced as an address bit, not two ledgers. Wallets default to a fresh one-shot address per incoming payment, the zero-contention path; whoever wants an account uses the persistent version. Mining rewards belong in one: consensus accepts either kind as a payout, but a one-shot address burns every reward it holds the moment it is spent, so the node refuses one as its payout address.
The registry entry is never compacted. Cell values under a spent address may be pruned after the undo horizon, but the entry that records the address as spent is what stops it being resurrected. It is append-only consensus state, about 33 bytes per address, forever — the protocol's honest open problem, shared structurally with every nullifier-set design.
Stateless validity, and the billing law#
The V-rules run on every certificate, in parallel, before mempool admission and during block verification, and require zero state: canonical form and chain id, with every list within its limit; every signature verifying under strict Ed25519 over the signing root, with the signature set minimal as well as sufficient; the program's re-execution against the declared reads producing exactly the declared writes; authorization for every write that needs it, and a signature on the deposit cell unconditionally; a deposit that is the native balance of a user address and covers the certificate's worst case; self-consistency, so that no write contradicts a read of the same slot and no debited one-shot address escapes being marked spent; no write to a protocol cell; a defined version byte on every address named; and, in a contract whose sequencer key is non-zero, that key's signature on the exact path.
The system's billing law is one sentence, and it is the thing to hold on to:
One signature, at most one bill, never at a position its signer could not avoid.
The fold orders a block in three classes and leaves the producer no discretion: first the
fills, in the order of the sequencer pipelines they belong to; then every other
certificate by (deposit address, Seq, id); last the forced
certificates, by id. Each certificate then has exactly one of four outcomes, and the
golden vectors name them:
| Outcome | When | Who pays |
|---|---|---|
APPLIED | Every declared read holds. | The depositor, at actual cost: base fee burned, priority fee to the producer. |
SKIPPED_STALE, SKIPPED_OVERFLOW | A read no longer holds, or a delta would leave [0, 2²⁵⁶). Nothing is written. | The depositor: a fixed skip fee, burned. For a fill, a sequencer — never the user. |
FORCED_FAILED | A forced certificate whose call reverted or broke its bounds. | Its payers; nothing else is written. |
DROPPED | The deposit cannot be reserved: the cell is unfunded or spent. | Nobody. Not marked seen, so it may be resubmitted against a funded deposit. |
Applied, skipped and forced-failed certificates are all marked seen, and including a seen, expired or under-bid certificate makes the block invalid, so no producer can re-include or delay someone else's certificate to harvest its deposit. Failures that have nobody to charge — a fill whose pipeline counter is out of order or whose fence counter has moved, a forced certificate whose reads do not hold — make the block invalid instead: the producer, who built the block from state it holds, is the party who could have known.
Era gates#
The activation heights live in spec/params.json. h1_vm = 80,640 opens
era 1: the cEVM and the pre-deployed library, contract records with a sequencer key, an admin and a
fence counter, intents and fills, the directory, and the forced path. The era's own constants
— the 8-block intent window, 16 forced certificates per block at 4,000 derivation units each,
a directory entry's lifetime of 259,200 blocks past its announcing certificate's TTL — are compiled rather than carried by the parameter
file, because adding a parameter would move the genesis id of every live network; the gate block
writes them into protocol cells, so the state root commits to them from then on.
h1_bond equals h1_vm on mainnet and on the public testnet, so the
bonding operations never open there: there is no stake bonding in era 1. The tree still carries the
v1.0 underwriting, lease and forced-queue rules, and they are valid only on the development
network, in a 256-block window above its h1_vm and below its own v1 gate, which exists
so that the vectors recording that machinery keep a network on which it is valid. h2_pos = 1,576,800 is reserved for
proof of stake, frozen at genesis and read by no rule. Era 2, proof of stake, ships as its own
release by hard fork, with its constants compiled like era 1's: it opens bonding at epoch 182 by
height, starts its ramp when active stake holds 10% of supply for 30 epochs or at epoch 273 at the
latest, and switches to 5-second slots when the ramp completes. From the switch it reads every
genesis constant that is really a duration in the unit it measures, so the parameter file does not
change.
Repository layout#
zycord/
spec/ parameter sets, golden vectors, library images <- THE PROTOCOL
core/ consensus-critical; standard library only (P4)
types/ crypto/ ssz/ u256/ state/ validity/ fold/ params/ genesis/
cevm/ the certificate-adapted EVM; vendored interpreter, pure Go
stdlib/ the pre-deployed library, its addresses and code hashes
pow/randomx/ the mainnet engine: vendored C++, cgo, behind a build tag
node/ storage/ chain/ verify/ mempool/ miner/ p2p/ sync/ rpc/ stratum/
wallet/ key management, certificate builders (reference; not consensus)
contracts/ the reference contracts, in Solidity
sim/ simulator, fuzz harnesses, differential refold, chaos soak
cmd/ zycordd, zcd
desktop/ the wallet in a native window — a separate Go module
docs/ architecture, protocol, operating guide, whitepaper
Dependency arrows point inward only — node → core,
wallet → core, never the reverse — and nothing inside core/
reaches outside core/ and the standard library. One exception, named and enforced:
core/pow/randomx is the only cgo in the tree, and it compiles only under the build
tag, so every build without the tag is still standard-library-only and needs no C toolchain. CI
runs the check that enforces it, because the third-party check greps module paths and cgo has
none — which left the rule enforced by nobody until it was added.
How it is tested#
- Golden vectors. Every fold, block and validity rule has positive and negative cases as
(pre-state, block) → (post-state | invalid, outcomes, fees). The suite is the compatibility contract for independent implementations. - The griefing suite. Re-inclusion of applied and skipped certificates, expired inclusion, under-bid inclusion, dependent chains under proposer shuffling, burn-and-refund cycles, third-party credit storms, mint-cap boundary races.
- Property-based. Fold determinism under proposer-order permutation, delta commutativity, ABA tolerance, and conservation — including deposits in flight, the maturity ring and the treasury cell, since a conservation check that omits the treasury reports every block as creating value.
- Differential. A deliberately naive second fold implementation, written for obviousness rather than speed, fuzzed against the real one. Divergence is a release blocker.
- Adversarial simulation. Skip storms, deposit-drain races, drop-stuffing miners, reorg torture, timestamp manipulation, eclipse-lite relay scenarios. Scenario configs are committed; runs are reproducible by seed.
- Concurrency, deliberately. Every component more than one goroutine touches has a test that is concurrent, in the shape the process actually uses.
-raceon a single-goroutine suite measures nothing and reports success. - The chaos soak. Real node processes over real sockets behind a proxy injecting latency, jitter, loss and partitions, with nodes killed by
SIGKILLat random. This is the surface that found the data race the entire-racesuite missed.
Genesis is an artifact, not a ceremony#
zcd genesis emits the genesis block — empty state, beacon cells initialised,
empty spent registry, no allocations of any kind — and its id. The launch
committed in advance to the code tag, the parameter hash, the genesis id, and the launch time, and
the parameter set carried a commitment to a launch message whose digest block 1 had to reveal, so
nobody could mine the chain before its announced start. Anyone can rebuild the first four in
milliseconds, from source, on any machine. There is nothing else to trust.