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

Run an ENC Node

An ENC node hosts enclaves: it sequences signed events, folds them into verifiable state (a Sparse Merkle Tree) and an append-only history (a Certificate Transparency log), and serves both to clients over HTTP + WebSocket. The node never has to be trusted — every byte it emits is independently verifiable against the protocol's signed events. See the Node API spec for the full wire surface.

The node logic is a single kernel compiled from the formally-verified Lean specification. That kernel ships in four interchangeable runtimes, all of which answer the same wire request byte-identically.

The four runtimes

RuntimeWhat it isBest for
cf-workerTypeScript host loading the WASM kernel on Cloudflare Workers + Durable ObjectsProduction at scale — billions of per-user enclaves, geo-routed, zero-ops
rs-server (encd)Pure-Rust axum + wasmtime host; one static binarySelf-hosted / air-gapped / sovereign VM + bare-metal deploys
pure-JS WorkerThe kernel as pure JavaScript, emitted from the Lean specThe cross-implementation parity reference
lean-hostRuns the Lean program directly via a small Node byte-pumpZero translation gap — the wire bytes are interpreted by the program that was proven correct; also the correctness referee

All three WASM-based runtimes load a byte-identical enc-core.wasm. The Lean runtime doesn't go through WASM at all — it is the Lean program. Cross-runtime tests assert all four produce identical output for the same input.

Which should I run? Pick cf-worker for anything that needs to scale past one machine or be geo-distributed — it's the default for new deploys. Pick rs-server for self-hosted, compliance, or air-gapped deploys where one VM is enough. lean-host and pure-JS are correctness references rather than production targets.

Install and build

The runtimes live in the impl-node repository. The SDK packages, the JS node handlers, and most of the cf-worker host are generated from the Lean specificationyarn build:js runs that codegen.

git clone https://github.com/enc-protocol/impl-node
cd impl-node
yarn install
yarn build:js      # regenerate SDK + node + worker from the Lean spec
yarn test:pure     # smoke test — 62 assertions, ~800ms, no env needed

For the Rust + WASM artifacts:

yarn build:rs      # build the Rust VM library (rs/)
yarn build:wasm    # wasm-pack build of the kernel
yarn build         # build:js + build:rs + build:wasm
 
# The standalone Rust HTTP server is an independent crate:
cd rs-server && cargo build --release

The generated files (sdk/, js/node/, and the codegen'd parts of cf-worker/src/) are byte-pinned to the spec — don't hand-edit them; changes are overwritten on the next yarn build:js, and the codegen idempotence gate will flag the drift.

Run locally

cf-worker (the production variant, run locally):

cd cf-worker
yarn install
yarn wrangler dev          # → http://127.0.0.1:8787

Same kernel as production, backed by WASM + per-enclave SQLite Durable Objects.

rs-server (encd):

cd rs-server
cargo build --release
./target/release/encd --port 8080     # → http://0.0.0.0:8080

Loads the embedded WASM kernel once at startup; in-memory state by default.

pure-JS Worker (the codegen reference):

npx wrangler dev --config test/wrangler.toml --local   # → http://127.0.0.1:8787

Smoke-test a running node — bootstrap an enclave with a fixed throwaway key and read back the derived sequencer public key:

ENC=$(python3 -c "print('ee'*32)")
PRIV=$(python3 -c "print('33'*32)")
curl -X POST http://localhost:8080/enclaves/$ENC/init-with-priv \
  -H "Content-Type: application/json" \
  -d "{\"enclave_id\":\"$ENC\",\"sequencer_priv\":\"$PRIV\"}"
# → {"ok":true,"enclave_id":"ee…","sequencer_pubkey":"3c72addb…"}

The returned sequencer_pubkey is the deterministic BIP-340 derivation from the private key — identical across every runtime for the same input, which is how kernel parity is checked by eye.

Test

yarn test:pure     # 62 assertions, ~800ms, no env
yarn test          # full JS suite (property + node + verifier + fuzz + persistence + …)
yarn test:cross    # cross-implementation parity vectors (JS ↔ Rust ↔ WASM ↔ Schnorr ↔ RBAC)
yarn test:rs       # cargo test for the Rust VM library
yarn test:all      # test + test:cross + test:rs
 
cd rs-server && cargo test --release    # rs-server (encd) unit tests

Most JS suites spawn wrangler dev and exercise the pure-JS Worker; raise WRANGLER_TIMEOUT on slower machines.

Deploy

Cloudflare (cf-worker) — the scale path

Prerequisites: a Cloudflare account with Workers + Durable Objects enabled, an authenticated wrangler (npx wrangler login), and an R2 bucket for closed-bundle archival.

cd cf-worker
yarn install
yarn wrangler deploy --dry-run    # preflight: type-check + bundle size
yarn wrangler deploy

The first deploy runs migrations that create the EnclaveDO and RegistryDO Durable Object classes. Per-enclave isolation is automatic: each enclave_id routes to its own Durable Object — idFromName(enclave_id) — with independent SQLite storage. Cloudflare spawns and evicts isolates globally on demand, so scaling from one enclave to billions is the same code path.

VM / bare-metal (rs-server / encd) — the self-hosted path

Build a static musl binary (no glibc dependency) and run it under systemd:

rustup target add x86_64-unknown-linux-musl
cd rs-server
cargo build --release --target x86_64-unknown-linux-musl
# → target/x86_64-unknown-linux-musl/release/encd   (one static binary)

Copy it to the host and register a systemd unit:

[Unit]
Description=encd — ENC protocol node
After=network.target
 
[Service]
ExecStart=/usr/local/bin/encd --port 8080
Restart=on-failure
DynamicUser=yes
 
[Install]
WantedBy=multi-user.target

The MVP ships with in-memory state; wire an external store (sled / rocksdb / Postgres) for persistence across restarts.

Verify a deploy

The enc CLI ships an end-to-end test harness that drives a full workflow against any node URL — enclave creation, signed commits, push, close-bundle, and signed STH retrieval:

# from a checkout of the enc CLI:
cargo build --release --bin enc
 
# Personal-enclave workflow against any node:
./scripts/enc-test.sh --workflow personal --remote https://your-node.example.com
# Expected: 16/16 ok
 
# Group RBAC workflow:
./scripts/enc-test.sh --workflow group
# Expected: 14/14 ok

A green run confirms the full host-sign-seq round-trip works against your deploy.

How a node protects the sequencer key

A node signs every sequencing event, but the sequencer private key never lives in the kernel's WASM memory. The kernel declares a host_sign_seq import; the host answers it:

  1. The kernel computes the 32-byte message hash and calls host_sign_seq.
  2. The host retrieves the private key from off-WASM storage — Durable Object SQLite on cf-worker, the Rust process heap on rs-server.
  3. The host signs with BIP-340 Schnorr (@noble/curves in the JS hosts, the k256 crate in rs-server) and writes the 64-byte signature back into WASM memory.
  4. The kernel embeds the signature in the event record.

Why it matters: portable snapshots (GET /<id>/snapshot) are keyless. An attacker who dumps a node's memory finds the public key but never the private one — so backups can be archived or shared without ever leaking the signing identity.

Admission hooks — deployment policy, not protocol

A node admits commits through a configurable hook chain. Each hook either passes a request or rejects it — for example, requiring a valid Cloudflare Access JWT before any state-changing action:

const cfAccessJwtHook: AdmissionHook = async (request, action, env) => {
  if (PUBLIC_ACTIONS.has(`${request.method} ${action}`)) return { pass: true };
  const jwt = request.headers.get('Cf-Access-Jwt-Assertion');
  if (!jwt) return { pass: false, reject: { code: 'unauthorized', message: 'missing JWT', status: 401 } };
  const claims = await verifyCfAccessJwt(jwt, env);
  return claims
    ? { pass: true }
    : { pass: false, reject: { code: 'unauthorized', message: 'invalid JWT', status: 401 } };
};
admissionChain.push(cfAccessJwtHook);

Hooks are orthogonal to the protocol: a Lean theorem (replay_invariant_under_hook_swap) proves that adding or removing hooks never changes protocol-layer determinism. They are pure deployment policy — gate access, add auth, throttle — laid over the verifiable event stream without altering a single emitted byte.

Choosing a runtime for scale

A single-VM benchmark says rs-server is several times faster per core — but that's the wrong question for production. At Telegram scale (~900M users, ~50K peak msgs/sec) what matters is hosting billions of enclaves with per-user isolation, geo-routed and crash-surviving:

  • cf-worker gets that for free. Each enclave is its own Durable Object with its own storage, routed by idFromName(enclave_id), spawned and evicted by Cloudflare globally. Scaling 1 → 1B enclaves is the same code path, zero-ops.
  • rs-server can do it too — but you run the VM fleet, sticky-route by enclave_id, replicate each enclave's state, and operate that infrastructure yourself.

This is safe because distinct enclaves have disjoint commit histories — proved in the Lean spec as the theorem enclave_isolation. When a request lands on an enclave's Durable Object, that DO's storage holds only that enclave's commits, so no cross-enclave coordination is ever needed.

For a hot enclave (a single busy chat at 25–50K msgs/sec), cf-worker can split one logical enclave across N physical Durable Objects via jump-hash routing, with every routing decision anchored by Lean theorems.

Both runtimes load the same kernel and produce byte-identical events — the choice is operational, not protocol-level.

Performance

All four runtimes are fast, and the formally-verified ones are not a tax:

RuntimeGET / ops/sec (N=1000, single laptop, loopback)
lean-host (Lean)6,083
encd (Rust)5,497
cf-worker (workerd, local)660

Choosing the Lean runtime — the program that was literally proven correct — costs nothing at the framing layer; at N=1000 it actually crosses ahead of native Rust. (Local wrangler dev understates cf-worker badly; the production Cloudflare deploy measures roughly 1,755 req/s.)