Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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.

ENC's SMT proof + CT inclusion + STH chain authenticates what the node says the state is — not that the node computed it honestly. A malicious node could write Mallory = admin, commit it, sign it, and every membership proof still verifies. zkEnc closes that gap.

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 deployable service (prover-service) that folds a node's transitions and serves proofs, running as a Cloudflare Container;
  • 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/impl-zk zkEnc && cd zkEnc
cd 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 e2e/ drivers (they import the node SDK from a sibling impl-node checkout at ../impl-node).
  • Docker + buildx — only to build/run the container or deploy to Cloudflare.

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 — a sibling impl-node checkout run via wrangler dev:

# terminal 1 — the node
cd ../impl-node && CI=1 npx wrangler dev --local --port 8798 --config test/wrangler.toml
 
# terminal 2 — drive it (drivers honor NODE_URL / PROVER_URL)
node e2e/export_transition.mjs   # capture one real transition (roots + wire proofs)
node e2e/capture_anchor.mjs      # full anchor vector (STH + CT inclusion + folded proof)

Deploy to Cloudflare

The service runs as a Cloudflare Container:

# build/run the image locally first (needs Docker + buildx)
docker build -f prover-service/Dockerfile -t enc-prover .
docker run -p 8799:8799 enc-prover
 
# then deploy
cd prover-service/cf && npm install
wrangler secret put PROVER_AUTH_TOKEN          # + PROVER_S3_ACCESS_KEY / _SECRET_KEY for R2
wrangler deploy

The full production runbook — R2 checkpoints, auth, verifying-key publishing, the node → prover follower, instance sizing — lives in prover-service/DEPLOY.md.

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.