Skip to content
ENC Protocol

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 kernelenc-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

RuntimeWhat it isBest for
rs-server (encd)Pure-Rust axum + wasmtime host loading the canonical enc-core.wasm; one static binaryThe production node — self-hosted, air-gapped, bare-metal, or on-premise
lean-hostRuns the Lean program directly via a small Node byte-pump — the one runtime that doesn't go through WASM, because it is the proven artifactThe 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):

RuntimeGET / throughputper request
rs-server (encd)~14,500 req/s0.07 ms
lean-host~9,500 req/s0.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 specificationyarn 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 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/ + 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:8080

Loads 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 tests

The 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.target
sudo systemctl enable --now encd

Terminate 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/encd

state.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 passed

Verify 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 passed

A 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:

  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 — for example, 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 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.