Skip to content
nklave GitHub

sign_ok · refuse_double_vote

Policy-enforcing trust boundary
for PoS validators.

nklave sits between your validator client and your signing keys. It enforces slashing-prevention rules and configurable policies before any signature is produced — so slashable signing is impossible by construction, even if the host is compromised.

View on GitHub Read the docs

Untrusted

Validator client

Lighthouse, Teku, Prysm, Lodestar. Speaks the Web3Signer HTTP signing protocol.

POST /api/v1/eth2/sign/:pubkey

Trust boundary

nklave

Policy engine evaluates the request. Slashing-protection DB is consulted. Decision is logged. Only then is the key touched.

policy.evaluate(ctx) → Allow | Refuse

Custody

Signing keys

Local keystore, YubiHSM 2, AWS CloudHSM, GCP KMS. Refused requests never reach the HSM.

BLS (ETH2) · Ed25519 (Cosmos)

What the trust boundary enforces

Four built-in policies. All evaluate before the key is consulted.

Each refusal is deterministic, has a reason code, and is committed to the append-only log atomically with the slashing-protection DB update.

slashing-protection-attestation

Double-vote & surround-vote

Refuses any attestation whose target_epoch matches a previously-signed one, or whose (source, target) would surround / be surrounded by a prior signature.

tracked: min/max source · min/max target epoch · per pubkey

slashing-protection-block

Double block proposal

Refuses a beacon-block proposal if a block has already been signed for the same slot. For Cosmos / CometBFT validators, the equivalent rule applies on (height, round).

tracked: min/max proposal slot · last height/round

fork-allowlist

Allowlisted fork versions

Refuses any signing root whose fork version is not in the operator's allowlist — a defense against accidentally signing on testnet or an unintended fork.

allowed_forks = ["0x05000000", "0x06000000"]

rate-limit

Per-validator rate cap

Refuses signing requests when the validator's signing rate exceeds max_signs_per_hour. A backstop against runaway validator clients.

max_signs_per_hour = 240 # default ~4/min

Custom policies are first-class: implement the Policy trait, register at startup, and your rule runs in the chain ahead of the key. Policies evaluate in registration order; the first refusal short-circuits.

Integrations

Keep your validator client. Keep your HSM. Drop nklave in between.

Validator clients

nklave speaks the Web3Signer HTTP signing protocol. Anything that supports a remote signer URL works.

  • · Lighthouse — --use-remote-signer
  • · Teku — validators-external-signer-url
  • · Prysm — --web3signer-url
  • · Lodestar

Key custody backends

Signing keys live in dedicated custody. nklave only requests signatures — and only for requests the policy chain allowed.

  • · Local keystore — dev / single-host
  • · YubiHSM 2
  • · AWS CloudHSM
  • · GCP KMS

Slashing-protection DB

Embedded or shared. The HA pattern is two nklave instances sharing a Postgres DB serialized by row-level locks.

  • · RocksDB — default, single host
  • · Postgres — leader/follower HA
  • · EIP-3076 import/export

Threat model — v1

What nklave defends against. And what it doesn't.

We publish the threat model in full because the security properties of a signer are only as good as the assumptions behind them. Summary below; the authoritative version is in the docs.

v1 security goals

  • guaranteed Signing keys are not readable by the host.
  • guaranteed Slashable requests are refused, even from compromised software.
  • guaranteed Safety state is monotonic — cannot be rolled back by host tampering.
  • guaranteed Every decision is logged with a deterministic reason code.

Out of scope (v1)

  • not_covered Physical access attacks (cold boot, DMA, evil maid).
  • not_covered Advanced side-channel attacks on enclave execution.
  • not_covered Compromise of the enclave implementation itself.
  • not_covered Global anti-slash across multiple independent enclaves without explicit coordination.

Residual risks we name explicitly: a compromised host can still cause denial of service by blocking requests; strict enforcement can reduce availability if state is corrupted; supply-chain vulnerabilities in the build pipeline matter.

Append-only log

Every decision. Allow or refuse. Forever.

Every evaluation lands in a newline-delimited JSON log. Entries are sealed into Merkle-root checkpoints every 60 seconds and signed by a separate operator key. Tampering is detectable by re-walking the chain.

{"ts": 1742054400, "validator": "0xabc…", "type": "ATTESTATION", "decision": "allow", "signing_root": "0xdef…"}
{"ts": 1742054401, "validator": "0xabc…", "type": "ATTESTATION", "decision": "refuse",
 "policy": "slashing-protection-attestation",
 "reason": "double vote at target_epoch=12345"}

verify chain

nklave log verify --from 0 --to latest re-walks every checkpoint, recomputes each Merkle root, and confirms the operator-key signatures. Non-zero exit means the log was edited.

operator key custody

Checkpoint-signing key is separate from any validator key. Lives in its own keystore, a YubiHSM slot, or an AWS KMS key — operator's choice.

retention

Logs rotate at 1 GB / 365 days by default. Older logs seal; the checkpoint chain remains verifiable across the boundary into cold storage.

Getting started

Five minutes from zero to validator signing through nklave.

1. Run nklave

docker run -d --name nklave \
  -p 9000:9000 \
  -v nklave-data:/var/lib/nklave \
  -v $(pwd)/keystores:/keystores:ro \
  ghcr.io/cryptuon/nklave:latest \
  --keystore-dir /keystores \
  --data-dir /var/lib/nklave

Or cargo install nklave-server if you build from source.

2. Point your validator at nklave

# Lighthouse
lighthouse vc \
  --beacon-nodes http://localhost:5052 \
  --validators-dir /opt/lighthouse/validators \
  --use-remote-signer http://localhost:9000

# Teku
validators-external-signer-url: http://localhost:9000

Prysm: --web3signer-url=http://localhost:9000

3. Import your slashing history

nklave import \
  --interchange-file ./slashing-protection.json \
  --keystore-dir ./keystores

Standard EIP-3076 interchange. nklave will refuse anything that would duplicate what your previous client recorded.

4. Verify

curl http://localhost:9000/livez
curl http://localhost:9000/readyz
curl http://localhost:9000/health

Then watch <data-dir>/log/ for signing decisions as they happen.

Slashing should be a class of bug, not a class of incident.

nklave is MIT-licensed, written in Rust, and built to be the simplest correct thing between your validator client and your keys. Read the threat model. Run the import. Switch your signer URL.