How I Run My Ethereum Validators

ethereum staking validator vero web3signer mev-boost self-hosting

A validator has two jobs: attest to what it thinks the head of the chain is, and once in a blue moon propose a block. Both are just signatures. The one way to really screw it up is signing two things that contradict each other. That’s a slashing, and it costs you part of your stake and force-exits your validator off the network.

So my whole setup comes down to two rules: the signing key lives in exactly one process, and nothing can trick that process into signing something slashable. Everything else exists to defend one of those two rules.

This is the generic shape of what I run, not a copy-paste guide. Pick your own hardware and clients.

The layout

Validator host (an Intel NUC behind a firewall):

  Vero ──sign :9000──> Web3Signer ──jdbc :5432──> PostgreSQL
  (decides what         (holds the keys,          (slashing-
   to sign)              signs the bytes)          protection memory)

     │ duties · attest · propose

  eth01    eth02    eth03    eth04      four full EL+CL nodes,
  EL+CL    EL+CL    EL+CL    EL+CL      diverse clients


  mev-boost
  • The validator host is a small Intel NUC. It does zero chain work: syncs nothing, stores nothing. It runs three containers and asks the beacon nodes what’s going on. If it dies I lose some uptime, not keys.
  • eth01-eth04 are four full nodes, each an execution client (the EVM, mempool, state) paired with a consensus client (the beacon node). Diverse clients on purpose. These are the validator’s eyes.
  • mev-boost is its own deployment.

The important bit: the keys aren’t on the nodes. They’re on the NUC, in one process.

Three containers on the NUC

Vero (validator client), Web3Signer (remote signer), and PostgreSQL. The split is the whole design:

  • Vero decides what to sign. It tracks duties, builds the attestation and block contents, and never sees a private key. When it needs a signature it asks Web3Signer over HTTP.
  • Web3Signer is the only thing that touches keys. It loads EIP-2335 keystores, and before it signs anything it checks the request against the recorded history and refuses if it would be slashable.
  • PostgreSQL is where that history lives: the highest source/target epochs and block slot Web3Signer has ever signed, per key. The signing API and DB are bound to 127.0.0.1 so nothing off-box can ask them for anything.

Vero and Web3Signer are separate processes, and Web3Signer has the final say on whether a signature goes out. It won’t sign anything that contradicts its own recorded history, no matter what Vero asks.

Four beacon nodes that have to agree

Web3Signer’s check has a blind spot: it only knows your own history. It’ll stop a double-vote or a surround-vote, but it has no idea whether the chain you’re attesting to is the real one.

The nightmare: a client bug splits the network, your beacon node is on the wrong side of it, and it tells you the head is somewhere it isn’t. You dutifully attest to the minority fork. That attestation doesn’t contradict anything you’ve signed before, so it looks perfectly legal and Web3Signer signs it happily, and you can still get slashed once the fork resolves. This isn’t hypothetical: a version of it played out on Holesky, and it was big drama at the time.

Fix: don’t trust any single node. Vero talks to all four and only attests when enough of them agree on the data.

VERO_BEACON_NODE_URLS=http://eth01.lan:5052,http://eth02.lan:5052,http://eth03.lan:5052,http://eth04.lan:5052
VERO_THRESHOLD=3
  • VERO_THRESHOLD = how many nodes must agree before Vero signs. With four nodes I run 3. One can be down or wrong and I’m fine, but no single node, and no single client bug, decides anything on its own.
  • floor(N/2)+1 (a majority) is the sane floor. Vero refuses to start if the threshold is higher than the node count.

This is also why the nodes run different clients. If all four ran the same software, a single bug would give all four the same wrong answer, and the threshold would happily wave it through. Running different clients is what makes agreement worth anything.

And these nodes are separate from the validator. They sync, store hundreds of gigs, get restarted for upgrades, and none of that can touch a key, because the keys aren’t there.

Slashing protection, three ways

I don’t trust any one layer with a stake:

  1. The threshold stops a bad attestation at the source.
  2. Web3Signer’s slashing check refuses to sign anything that contradicts the history it has recorded, no matter what Vero asks. Postgres just keeps that history durable across reboots and restores.
  3. Doppelganger detection: at startup Vero watches the network for a few epochs to see if my keys are already attesting somewhere else. If they are, it refuses to start.

That third one exists because the #1 way to get slashed isn’t fancy. It’s running the same keys in two places at once.

mev-boost

When a validator gets picked to propose, it can build the block itself from its own mempool, or buy a better one from a builder via mev-boost. Builders compete on value (MEV + tips); mev-boost shops the relays and hands the consensus client the best block to sign.

I run it as its own deployment. Two settings matter:

  • --use-external-builder = offer my proposals to the builder market.
  • --builder-boost-factor=90 = prefer the builder’s block, but discount its claimed value to 90% before comparing it to what I’d build locally. If the market isn’t clearly better, or the relays are having a bad day, I build my own and still propose. Local building is the floor, so the chain keeps moving even if every relay vanishes.

That’s it

The pattern repeats: lock down the dangerous part, spread out the rest. The signing key sits in one process behind a database that can say no, and no single beacon node gets to decide anything on its own, so I run four.

← All posts