Skip to content
ENC Protocol

zkEnc — validity proofs

zkEnc gives ENC a succinct proof that an enclave's RBAC state was reached by valid transitions — so a node acting as prover cannot forge authorization, and a client verifies the whole history in one O(1) check: no replay, no trust in the operator for validity.

This is the implemented form of the validity story in the litepaper; the normative protocol surface is /spec/node/zk.

It ships three things:

  • a prover — a bounded Groth16 circuit plus an unbounded Nova-IVC folding prover;
  • a prover service (prover-service) that folds a node's transitions and serves proofs;
  • an out-of-circuit verifier (verifier) — the client that checks a proof against the node's signed log.

Quick start (≈2 min, no node required)

Verify a real, captured validity proof end-to-end, offline:

git clone https://github.com/enc-protocol/enc
cd enc/zk/verifier
cargo run --release --bin verify_anchor -- ../e2e/anchor_vector.json

First build is ~1 min; then it recomputes the verifying key (~8 s) and prints:

  ✓ ACCEPT — r_n is a valid RBAC successor of the log-anchored r_0, verified in O(1)

That fixture (e2e/anchor_vector.json) was captured from a live node: the verifier checks the node's STH (Schnorr) + CT inclusion proofs and the folding proof — exactly what a real client does.

Install

  • Rust (stable) — the only hard dependency for building and testing.
  • Node.js (18+) — only for the end-to-end drivers (they import the node SDK).

No system libraries and no trusted-setup ceremony are needed — the folding path uses transparent IPA commitments.

Run locally

Build & test each component

# bounded Groth16 circuit (root crate)
cargo test --release            # circuit + live-node real-root validity proof + cross-impl parity
cargo run  --release            # the bounded prove → verify demo
 
# folding (Nova IVC) prover library
cd folding && cargo test --release
cd folding && cargo run --release --example fold_chain   # fold a chain → compress → verify
 
# out-of-circuit client verifier
cd verifier && cargo test --release

The prover service + a verifier

The service folds a node's finalized transitions into one IVC chain per enclave and serves proofs; the verifier consumes one and anchors it to the node's signed log.

# 1. boot the service (precomputes PublicParams once, then listens on :8799)
cd prover-service && PROVER_ADDR=127.0.0.1:8799 cargo run --release
 
# 2. verify a captured proof offline (no node needed)
cd verifier && cargo run --release --bin verify_anchor -- ../e2e/anchor_vector.json
 
# 3. one service, many enclaves (folds one captured transition under N enclave ids)
PROVER_URL=http://127.0.0.1:8799 node e2e/multi_enclave.mjs   # → /health enclaves=N

Against a live node

The e2e/ drivers exercise a real node by pointing the driver's PROVER_URL at a running prover and node.

Deploy

The prover is a native long-running service — folding proofs needs native assembly, so it runs outside the node's WASM runtime. Run one prover container per node: it holds that node's shared PublicParams (~900 MB) plus a hot LRU of per-enclave proof chains, and follows the node's CT/STH stream to fold new transitions.

Operational shape:

  • Compute — ~4 GiB RAM + native CPU per container, sized to PublicParams plus the hot-chain cap. Keep it warm; a cold boot re-derives params (~8 s).
  • Auth — require a strong bearer token on every /enclave/* call.
  • Checkpoints — persist per-enclave chains to durable, S3-compatible object storage so a restart resumes instead of re-folding from genesis.
  • Endpoints — clients poll GET /root (cheap freshness) and fetch GET /proof (compressed, ~28 KB) only when the root advances; GET /health reports the verifying-key fingerprint.

Pin the verifying key. A proof is only valid against the prover's vk — publish it once and have clients pin its fingerprint:

curl https://<prover-host>/health | jq -r .vk_fingerprint

Cost, performance & scaling

One container = one node's enclaves (a shared PublicParams plus a per-enclave IVC chain). Measured for the UniversalStep (~152k constraints/step) on a full CPU core:

OperationCostNotes
Boot (params + pp + compress keys)~8 sone-time per cold start — keep the instance warm
Fold one transition (prove + verify)~0.45 sonly state-changing transitions are folded
Compress + serve a proof (Spartan)~11–16 scache it; recompute on a cadence, not per query
GET /root (freshness)~0.2 mscheap; poll this, fetch /proof only when needed
Compressed proof size~28 KBserved to clients (O(log n))

Memory is the sizing driver: ~900 MB (shared pp + commitment keys) + PROVER_MAX_HOT × ~10 MB per hot chain. The defaults (max_hot=256) fit a 4 GiB instance. Cold chains live in the store, not RAM, so the number of enclaves is bounded by storage, not memory.

The economics hinge on one fact: proving touches only state-changing transitions, and RBAC events are rare — so steady state is a warm, mostly-idle instance (paying memory) with occasional CPU bursts, not continuous proving. Two levers matter most: cache proofs (the ~11–16 s compress dwarfs everything; serve cached and recompress on a checkpoint cadence), and choose keep-warm (pay memory, skip the 8 s cold-start) vs scale-to-zero (pay cold starts) per your transition cadence. To scale past one instance, shard enclaves by id across containers — throughput scales linearly, with the ~900 MB pp as the fixed per-shard overhead.

How it works

RoleWhoTrust
SequencerNodeOrders events; untrusted for validity
ProverNode (or a delegated service)Holds the witness; produces the proof
VerifierClientPublic inputs + proof only, O(1)

The validity relation old_root → new_root is proven in two complementary forms:

  • Bounded — a Groth16 proof of one transition (or a fixed batch). On-chain-friendly, O(1) verify.
  • Unbounded (folding) — a Nova IVC proof folding the enclave's entire transition history (O(1) prover work per step), then Spartan-compressed to one succinct proof. Needed because the SMT also holds arbitrary app state (KV namespace 0x02), so transitions are unbounded over an enclave's lifetime.

The SMT hashes with Poseidon2 over BN254 (t=3, x^5, R_F=8, R_P=56) so membership is proven natively in-circuit, byte-identical across the JS node, the encvm Rust logic, and every circuit here. Signatures, CBOR/event hashing, CT inclusion, and STH verification stay out of circuit (ordinary crypto), linked to the proof only by equality of the public roots — so the client does ~3 cheap checks plus the O(1) proof.

Why this stack

Every choice is forced by the shape of ENC's problem: a node is an untrusted sequencer, each enclave's state is a Sparse Merkle Tree, transitions are RBAC events, and the client must verify in O(1). The hard part is proving SMT membership inside the circuit; everything else is comparatively cheap.

  • Folding (Nova IVC), not just a bounded SNARK. The SMT holds arbitrary application state, so transitions per enclave are unbounded — a bounded circuit always has a ceiling. Nova folds each transition into one running instance (O(1) work/step, constant memory), then Spartan-compresses to one O(1)-verifiable proof of the whole history.
  • Poseidon2 over BN254 — match the hash to the field. SMT membership dominates the circuit. A byte-oriented hash (SHA-256) costs tens of thousands of constraints per call; an algebraic x^5 hash costs a few hundred. BN254 is both the Groth16 pairing curve and the base of Nova's BN254/Grumpkin cycle, so one field serves the SMT, the bounded circuit, and the folding cycle.
  • Transparent IPA commitments — no ceremony. ENC is permissionless, so a per-circuit trusted setup is a trust liability. Nova on IPA/Pedersen is transparent: no setup, deterministic verifying key. The tradeoff is an O(log n) (not O(1)) compressed proof, acceptable for a client already doing CT/STH checks.
  • Witness the op and the schema — one PublicParams for the whole node. Nova requires identical constraint matrices across folded steps. zkEnc makes the op a one-hot input and the schema a witnessed input bound to a manifest commitment, so one ~900 MB PublicParams folds every enclave's transitions under any schema — which is what lets a single prover-service serve a whole node.
  • Prove only what needs proving; bind the rest by equality. Putting SHA-256, CBOR, and Schnorr in R1CS would cost millions of constraints per event for zero soundness gain. zkEnc keeps them out of circuit and links them by equality of the public field elements — reusing ENC's existing STH/CT/Schnorr infrastructure instead of re-deriving it in a SNARK.

The net: unbounded history → folding; membership-dominated cost → Poseidon2/BN254; permissionless prover → transparent IPA; many enclaves/schemas → witnessed op+schema, one shared key; existing signature/log infra → out-of-circuit binding. Each is the cheapest sound choice for this relation.