Waterfall Node v2 for Docker: Stake Delegation, Key Portability and Built-in Snapshots
Disclosure
- This article text was prepared with AI assistance.
- All commands and outputs shown here were produced on a real Mainnet node.
- What it is: a second-generation Docker image,
registry.waterfall.network/waterfall/protocol/docker:2-mainnet, with stake delegation, key import and export, validator labels, built-in snapshot seeding and configurable ports. - Who it is for: anyone running a Waterfall Mainnet node in Docker, whether starting fresh or already on the first-generation image.
- For existing operators: migration is an image swap. Your validators and your synchronized database carry over — no re-sync, no exit and re-activation.
Stake delegation
A validator can now be created with delegation rules attached, so the stake, the rewards, the right to withdraw and the right to exit no longer have to belong to a single address.
Rules are written as a JSON file and passed to add.sh or import.sh when the validator is
created. They are part of the deposit data, which means the chain enforces them rather than
the node. status.sh shows both what the node has on file and what the chain actually stored:
Can Withdraw: 0x804b943d…,0xf4293794…
Receives rewards: 0x804b943d…=70%, 0xf4293794…=30%
Can Exit: 0x804b943d…,0xf4293794…
Receives stake: 0x804b943d…=100%
Compare with state: true
Compare with state: true is the line that matters — it confirms the rules the node shows are
the rules the chain is applying.
Import and export keys
export.sh packs a validator's coordinator keystore together with its verifier key and
password into one directory; import.sh unpacks that directory onto another node. The export
side selects by label, by index range, or both, so a subset of validators can be moved without
touching the rest; the import side can attach a label and delegation rules as it goes.
Importing the same directory twice is safe: validators already present on the node are reported and skipped rather than duplicated.
Update, 29 September 2026: check your verifier key passwords
The verifier unlocks its keys by position, not by address: key number i — key files
sorted by creation time — gets line number i of gwat/password.txt. Keys imported
before this update, or keys from several nodes merged into one, could end up with another
key's password. Such keys cannot be unlocked, and their validators skip block creation
while still attesting, so nothing looks wrong at first sight. On one Mainnet node 768 of
3040 keys were affected.
Pull the current :2-mainnet image and run
docker exec -it wf /opt/wf/sh/verifier_passwords.sh check
If it reports a problem, verifier_passwords.sh fix repairs the file without a restart.
import.sh now keeps the order right by itself, and status.sh warns when it is off.
Details: Check verifier key passwords.
Validator labels
label.sh assigns a name to a validator or to a range of them. Labels are then accepted
wherever a validator selector is: status.sh, export.sh and import.sh all take a label in
place of an index, which keeps operations readable on a node running dozens of validators.
Labels live on the node, not on the chain.
Built-in snapshot seeding
Syncing Mainnet from genesis takes a long time. v2 ships snapshot.sh, which fetches a recent
chain snapshot and seeds the database from it:
docker exec -it wf /opt/wf/sh/snapshot.sh status
docker exec -it wf /opt/wf/sh/snapshot.sh download
docker exec -it wf /opt/wf/sh/snapshot.sh apply
docker exec -it wf /opt/wf/sh/snapshot.sh commit
The download resumes where it left off if it is interrupted, and apply refuses to run a
second time over a database that has already moved on, so a mistyped command cannot silently
roll the node back.
Keeping the validator database small
Update, 1 October 2026: reset the slashing protection history
The validator keeps a record of everything it has signed in
coordinator/wallet/direct/validator.db, and nothing trims it while the validator runs.
On our Mainnet nodes it had grown to 50–100 GB. The :2-mainnet image now ships
reset_slashing_protection.sh to start it afresh.
Check the size first:
docker exec wf du -h /opt/wf/data/coordinator/wallet/direct/validator.db
Then pull the current :2-mainnet image and run the script in the background:
docker exec -d wf sh -c '/opt/wf/sh/reset_slashing_protection.sh --yes > /opt/wf/data/logs/reset_slashing_protection.log 2>&1'
It stops the validator, moves the database aside, waits three epochs (about 10 minutes), starts the validator with an empty history and deletes the old database once the validator attests again. After the pause every new attestation targets a later epoch than anything signed before, so the old history is no longer needed. On our nodes the validator was offline for under 10 minutes and the database went from 51–64 GB to 17 MB.
Never run it while the same keys are running — or may recently have run — on another machine: an empty history would not protect against signing something that conflicts with what the other machine signed. Details: Keep the validator database small.
Configurable ports
Ports are set through GWAT_COMMANDS and COORDINATOR_COMMANDS:
-e GWAT_COMMANDS="--port 30304 --http.port 9445 --ws.port 9446" \
-e COORDINATOR_COMMANDS="--rpc-port 4001 --p2p-tcp-port 13001 --p2p-udp-port 12001"
In v1 the helper scripts still assumed the default ports and broke once they were changed. In
v2 the scripts read the ports back out of these variables, so status.sh and everything else
keeps working on a node with a custom port layout — which is what makes running several nodes
on one host practical.
One volume, and a container that survives a reboot
v1 mounted three separate volumes (logs, gwat, coordinator) and ran with --rm. v2
mounts the whole data directory as one volume and runs with --restart unless-stopped:
-v $PWD/.wf:/opt/wf/data
This is not only cosmetic. v2 keeps the validator list, labels and delegation rules in
config/, alongside gwat/ and coordinator/ in the data directory. Start v2 with the old
three-volume layout and config/ lands on an anonymous Docker volume instead — one that
--rm throws away on every stop, taking those three with it. The node detects that layout and
warns about it.
The image also exposes two health endpoints on port 8080: / for readiness, which reports
success only once the node is fully synchronized, and /live for liveness, which reports
success while the clients are alive and answering. A node that has been syncing for hours is
alive but not ready, and the two endpoints keep that distinction.
Migrating from v1
The database format has not changed, which is why the upgrade is an image swap rather than a re-sync. The short version:
- Stop and remove the old container — the data stays on disk.
- Pull
:2-mainnet. - Start the new container with the single-volume layout.
- Check
status.sh.
Your validators, their keys and their chain state carry over. Two changes are worth knowing
before you start: the validator list moves from coordinator/validator_keys/deposit_data.json
to config/config.json, and restore.sh now takes the number of validators to restore as a
second argument.
Full procedure, including how to tell which version you are on: Migrate a Docker node from v1 to v2.
Getting started
- New node: Run Waterfall Node Mainnet in Docker (v2)
- Existing node: Migrate a Docker node from v1 to v2