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 canonical kernel — enc-core.wasm — generated from the
formally-verified Lean specification by CodeGen and hash-pinned on every
build. Any host that can run WebAssembly runs the exact same bytes, so every deployment answers
the wire byte-identically.
The runtimes
| Runtime | What it is | Best for |
|---|---|---|
rs-server (encd) | Pure-Rust axum + wasmtime host loading the canonical enc-core.wasm; one static binary | The production node — self-hosted, air-gapped, bare-metal, or on-premise |
| lean-host | Runs the Lean program directly via a small Node byte-pump — the one runtime that doesn't go through WASM, because it is the proven artifact | The correctness referee / proof ground-truth, not a production target |
rs-server is the production node: it loads enc-core.wasm via wasmtime. lean-host is the
exception — the Lean program the wasm kernel is proven against, run directly as the referee.
Both answer identical bytes for the same input.
Benchmark
Request-handling throughput on a single box (local loopback, GET / — HTTP parse + enclave
dispatch):
| Runtime | GET / throughput | per request |
|---|---|---|
rs-server (encd) | ~14,500 req/s | 0.07 ms |
| lean-host | ~9,500 req/s | 0.11 ms |
encd — the Rust kernel via wasmtime — is the throughput target; lean-host trades raw speed
for being the literal proven program.
Install and build
The runtimes are in the ENC source repository.
The SDK packages and the enc-core.wasm kernel are generated from the Lean specification —
yarn build:js regenerates the SDK and yarn build:wasm the kernel.
git clone https://github.com/enc-protocol/enc
cd enc/node
yarn install
yarn build:js # regenerate the SDK from the Lean spec
yarn test:pure # smoke test — 62 assertions, ~800ms, no env neededFor 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 --releaseThe generated files (sdk/ + the enc-core.wasm kernel) are byte-pinned to the spec — don't
hand-edit them; changes are overwritten on the next build, and the codegen idempotence gate will
flag the drift.
Run locally
rs-server (encd):
cd rs-server
cargo build --release
./target/release/encd --port 8080 # → http://0.0.0.0:8080Loads the embedded WASM kernel once at startup; in-memory state by default.
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 testsThe JS suites check the codegen'd JS kernel against the Rust and Lean runtimes for byte-parity; see the node/ folder's README for environment setup.
Deploy
Deploy rs-server as a single static binary under systemd. Build with the musl target so there is no glibc dependency:
# Build a static binary
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
# Copy it to the VM
scp target/x86_64-unknown-linux-musl/release/encd user@vm:/usr/local/bin/Run it as a systemd service:
# /etc/systemd/system/encd.service
[Unit]
Description=encd — ENC protocol node
After=network.target
[Service]
ExecStart=/usr/local/bin/encd --port 8080
Restart=on-failure
User=enc
DynamicUser=yes
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now encdTerminate TLS with nginx (or any reverse proxy) in front of the node, or run behind a managed host such as fly.io or railway.app.
Persistent storage. encd keeps enclave state in memory by default. Pass --state-dir <path> for durability: each enclave is written through to <path>/<enclave_id>/ as
state.bin (the snapshot), open_bundle, and meta.json (sequencer key + dataview
registrations), loaded on boot and persisted on every commit, close-bundle, and set-manifest.
encd --port 8080 --state-dir /var/lib/encdstate.bin is the same .enc snapshot the enc CLI writes (enc_export_storage
format, ENC\x01 magic) — so a snapshot is portable between them: enc restore an encd
state file, or drop an enc-produced .enc/state.bin (with a meta.json carrying its
sequencer key) into the node's state dir. To scale horizontally, sticky-route by enclave_id
so each enclave lives on exactly one node, and partition the state dir by enclave_id.
Verify a deploy
The node repo ships an end-to-end test that drives a full signed workflow and asserts every
step — init → a genesis Manifest (seq 0) → owner posts (seq 1…N) → close-bundle → a
sequencer-signed STH. The commits are built with the same SDK a client uses
(mkManifestCommit / mkCommit / signCommit), so a green run confirms the node validates
real Schnorr signatures, RBAC, content staging, the expiry window, and monotonic timestamps —
the full host-sign-seq round-trip.
Verify the build — spawns a freshly-built local encd:
cd rs-server
cargo build
node tests/e2e-commit.mjs
# Expected: 16/16 passedVerify a running deploy — drives the node at ENCD_URL, no spawn:
ENCD_URL=https://your-node.example.com node rs-server/tests/e2e-commit.mjs
# Expected: 16/16 passedA green run confirms the full host-sign-seq round-trip works against your binary.
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:
- The kernel computes the 32-byte message hash and calls
host_sign_seq. - The host retrieves the private key from off-WASM storage — for example, the Rust process heap on rs-server.
- The host signs with BIP-340 Schnorr (
@noble/curvesin the JS hosts, thek256crate in rs-server) and writes the 64-byte signature back into WASM memory. - 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 JWT before any state-changing action:
const authJwtHook: AdmissionHook = async (request, action, env) => {
if (PUBLIC_ACTIONS.has(`${request.method} ${action}`)) return { pass: true };
const jwt = request.headers.get('Authorization')?.split(' ')[1];
if (!jwt) return { pass: false, reject: { code: 'unauthorized', message: 'missing JWT', status: 401 } };
const claims = verifyJwt(jwt, env.JWT_SECRET);
return claims
? { pass: true }
: { pass: false, reject: { code: 'unauthorized', message: 'invalid JWT', status: 401 } };
};
admissionChain.push(authJwtHook);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
At scale, what matters is hosting billions of enclaves with per-user isolation while
maintaining commit history integrity. This is safe because distinct enclaves have disjoint
commit histories — proved in the Lean spec as the theorem enclave_isolation. Each
enclave's storage holds only that enclave's commits, so no cross-enclave coordination is
needed.
rs-server achieves per-enclave isolation by sticky-routing by enclave_id and
replicating state within your infrastructure. For a busy enclave (a single active chat at
25–50K msgs/sec), one logical enclave can be split across N physical instances via consistent
hashing, with every routing decision anchored by Lean theorems — and every instance loads the
same kernel and produces byte-identical events, so the choice is operational, not
protocol-level.