Managing validator keys
This page explains where Grandine gets validator keys from, how it finds keystores and their passwords, and how to add, remove and move keys safely. Everything here applies both to the integrated validator client (grandine) and to the separate one (grandine vc).
Key sources
Grandine loads keys from these sources, all of which can be combined:
| Source | How keys get there | Removable through the Keymanager API |
|---|---|---|
| Keystore directory | --keystore-dir with --keystore-password-file or --keystore-password-dir | No, read-only |
| Keystores imported through the API | POST /eth/v1/keystores on the Keymanager API | Yes |
Legacy key storage (keystores.json) | API imports made by older Grandine versions | Yes, with --keystore-storage-password-file |
| Web3Signer | --web3signer-urls, web3signer entries in validators.yml, or POST /eth/v1/remotekeys | Remote keys only, through DELETE /eth/v1/remotekeys |
Every key Grandine manages locally, and every remote key imported through the API, gets an entry in validators.yml. That file also holds per-validator settings. Keys that only a --web3signer-urls signer serves get no entry. See Web3Signer.
Where key files live
With the default data directory, the files for a network are under ~/.grandine/<network>/:
~/.grandine/hoodi/
├── validator/
│ ├── validators.yml validator definitions and per-validator settings
│ ├── slashing_protection.sqlite slashing protection database
│ ├── api-token.txt Keymanager API token (with --enable-validator-api)
│ ├── 0x<pubkey>/keystore.json keystores imported through the Keymanager API
│ ├── validator_key_cache_0x<hash>.json key cache (with --use-validator-key-cache)
│ └── keystores.json legacy key storage, from older versions only
└── secrets/
└── 0x<pubkey> passwords of keystores imported through the API
Pass --data-dir to use another parent directory. The layout below it stays the same. Keystores loaded with --keystore-dir stay where they are and are never copied.
Loading keystores from a directory
--keystore-dir points to a directory of EIP-2335 keystores, such as the validator_keys directory that ethstaker-deposit-cli writes. It must be combined with exactly one of --keystore-password-file and --keystore-password-dir.
On every start, Grandine scans the directory:
- Only the top level is scanned. Subdirectories are not searched.
- Files ending in
.jsonand files without an extension are read as keystores. Files with any other extension are ignored. - Files whose names start with
deposit_dataare skipped with a warning, so the deposit data file can stay in the directory. - Any other file that is not a valid keystore stops Grandine with
Failed to load a keystore from file .... Keep only keystores in the directory, and no subdirectories without an extension. - A keystore without a
pubkeyfield is skipped with a warning. - A keystore whose public key already has an entry in
validators.ymlis not added again. The existing entry, including its settings, wins.
New keystores are added to validators.yml as local_keystore entries with the keystore path and the password file path. Pass an absolute path to --keystore-dir, because the paths are recorded as given.
One password for all keystores
When all keystores share a password, as they do when they were generated in one run of ethstaker-deposit-cli, put the password in a file:
printf '%s' 'YOUR-KEYSTORE-PASSWORD' > /path/to/password.txt
chmod 600 /path/to/password.txt
grandine \
--network hoodi \
--eth1-rpc-urls http://EL-HOST:8551 \
--jwt-secret /path/to/jwt.hex \
--keystore-dir /path/to/validator_keys \
--keystore-password-file /path/to/password.txt
One password per keystore
When the keystores have different passwords, put each password in its own file in a directory and pass --keystore-password-dir. Each password file is named after its keystore:
- for a keystore named
NAME.json, the password file isNAME.txt; - for a keystore without an extension, the password file has exactly the same name.
/path/to/validator_keys/keystore-m_12381_3600_0_0_0-1700000000.json
/path/to/validator_keys/keystore-m_12381_3600_1_0_0-1700000000.json
/path/to/passwords/keystore-m_12381_3600_0_0_0-1700000000.txt
/path/to/passwords/keystore-m_12381_3600_1_0_0-1700000000.txt
grandine vc \
--network hoodi \
--beacon-node-urls http://BN-HOST:5052 \
--keystore-dir /path/to/validator_keys \
--keystore-password-dir /path/to/passwords
Note: Grandine decides between the two modes by checking whether the password path is a directory. Passing a directory to
--keystore-password-filetherefore also works as a password directory. Passing both options at once is refused.
Password format
Grandine reads the whole password file and, as EIP-2335 requires, normalizes it to NFKD and removes control characters. A trailing newline is therefore ignored, as are tabs and other control characters anywhere in the password. Passwords in validators.yml (keystore_password_path or an inline keystore_password) are treated the same way.
Validator key cache
Keystores are encrypted with a deliberately slow key derivation function, so decrypting hundreds of keystores on every start takes time. --use-validator-key-cache makes Grandine save the decrypted keys in an encrypted cache file in the validator directory. On the next start, keys found in the cache are not decrypted again.
grandine \
--network hoodi \
--eth1-rpc-urls http://EL-HOST:8551 \
--jwt-secret /path/to/jwt.hex \
--keystore-dir /path/to/validator_keys \
--keystore-password-file /path/to/password.txt \
--use-validator-key-cache
How the cache works:
- It covers keystore files: the ones from
--keystore-dirand the ones imported through the API. Web3Signer keys and the legacy storage are not cached. - It is encrypted with the passwords of all cached keystores together, and its file name (
validator_key_cache_0x<hash>.json) is derived from the keystores' UUIDs. A different set of keystores uses a different cache file. An old cache file left behind can be deleted. - If the cache cannot be read, Grandine logs
Unable to load validator key cacheand decrypts the keystores as usual.
Note: The cache holds the same secret keys as the keystores, protected by the same passwords. Protect it like the keystores and include it in the same access controls.
Adding keys
There are three ways to add a validator:
- Without a restart, through the Keymanager API.
POST /eth/v1/keystoresimports a keystore with its password and, ideally, its slashing protection history. The key starts signing right away and is kept across restarts. See Keymanager API. - Without a restart, through a Web3Signer. Import a remote key with
POST /eth/v1/remotekeys, or add the key to a signer listed in--web3signer-urlsand run with--web3signer-refresh-keys-every-epoch. See Web3Signer. - With a restart, through the keystore directory. Copy the keystore (and, with
--keystore-password-dir, its password file) into--keystore-dirand restart Grandine. The directory is only scanned at startup.
Whichever way you choose, import the key's slashing protection history first if it ever signed anywhere else. See Slashing protection.
Removing and disabling keys
- Keys imported through the API are removed with
DELETE /eth/v1/keystores. Grandine stops signing with them right away, deletes the keystore file, its password file and thevalidators.ymlentry, and returns the keys' slashing protection data. Save that data if the key will run elsewhere. - Remote keys are removed with
DELETE /eth/v1/remotekeys. - Keystores from
--keystore-dirare read-only: the API reports them asreadonly: trueand refuses to delete them. To remove one, stop Grandine, delete the keystore file, and remove its entry fromvalidators.ymlor setenabled: falseon it. Do both: an entry whose keystore file is gone stops Grandine at startup, and a keystore file left in the directory is discovered again if its entry is removed.
To keep a key on disk but stop using it, set enabled: false on its entry in validators.yml and restart. Grandine logs validator 0x... is disabled in validators.yml and will not be loaded. Discovery never re-enables a disabled entry. See Validator definitions.
Legacy key storage
Before validators.yml existed, Grandine kept keystores imported through the Keymanager API in a single encrypted file, keystores.json, in the validator directory, unlocked with the password in --keystore-storage-password-file.
New imports now go to separate keystore files, and existing installations migrate on their own:
- Keys in
keystores.jsongetkeystore_storageentries invalidators.ymland keep loading. They still need--keystore-storage-password-file. - Importing such a keystore through the API again moves the key out of
keystores.jsoninto its own keystore file. - Deleting such a key through the API also needs
--keystore-storage-password-file. Without it the request fails withpassword for decrypting keystores is missing, run Grandine with --keystore-storage-password-file to provide it. - When the last key leaves
keystores.json, Grandine deletes the file, and the password option is no longer needed.
Moving validators between machines
Moving a validator is the riskiest routine operation for a staker, because a short overlap between the old and the new signer is enough to get slashed. Follow this order:
-
Make sure the new machine is ready: the beacon node is synced, Grandine is configured, and it starts cleanly without the keys.
-
Stop the old validator client and make sure it cannot restart: disable its service, or remove the keys from it. If the old client is not Grandine, follow its documentation.
-
Export the slashing protection data from the old client. On Grandine:
grandine --network mainnet interchange export /path/to/slashing-protection.json -
Delete the keys from the old machine, or at least make sure nothing there loads them.
-
Import the slashing protection data on the new machine before its first start with the keys:
grandine --network mainnet interchange import /path/to/slashing-protection.jsonThrough the Keymanager API, you can pass the interchange data in the same request that imports the keystores instead.
-
Load the keys on the new machine and start it. Enable
--detect-doppelgangersfor extra safety. See Doppelganger protection. -
Watch the logs and the validators' attestations for the next few epochs.
If you are not sure that the old machine is stopped for good, wait at least two epochs (about 13 minutes) after it last signed before you start the new one. A missed epoch costs a little. A slashing costs much more.
For moving keys from the integrated validator client to a separate one on the same machine, see Validator client.
Security
- Generate keys offline, keep the mnemonic offline, and never copy it to the staking machine.
- Make password files, the
secretsdirectory andvalidators.ymlreadable only by the user that runs Grandine. Grandine createsvalidators.ymland API-imported password files with mode600itself. - For large or valuable validator sets, keep keys off the staking machine with a Web3Signer.
- See Security best practices.
Command line options
--keystore-dir(no default): directory with EIP-2335 keystore files to load at startup. Requires a password option.--keystore-password-file(no default): file with the password for all keystores in--keystore-dir. Conflicts with--keystore-password-dir.--keystore-password-dir(no default): directory with a password file per keystore. Conflicts with--keystore-password-file.--keystore-storage-password-file(no default): password for the legacykeystores.jsonstorage of keys imported by older versions.--use-validator-key-cache(disabled): cache decrypted keys for faster startup.--data-dir(default~/.grandine): parent directory of<network>/validatorand<network>/secrets.
See Command line reference for the full list.