New here? Start with the README for the friendly introduction, then come back here. This is the comprehensive reference — every command, flag, exit code, and security detail.
Comprehensive guide for ethernal, the CLI in this repository that takes a
validator all the way from a BIP-39 mnemonic to a broadcast Ethereum deposit
transaction, and (separately) creates Web3 v3 EOA keystores:
ethernal validator new|recover— generates or recovers EIP-2335 BLS validator keystores from a BIP-39 mnemonic (the front of the deposit pipeline).ethernal account new|recover— generates or recovers Web3 Secret Storage v3 secp256k1 EOA keystores (geth / Foundry / MetaMask-importable); not part of the deposit steps.ethernal deposit gen|build— produces Launchpad-compatible deposit data JSON (BLS signatures over the deposit message) and constructs the unsigned deposit transaction.ethernal tx sign|run|send— signs (Ledger or local key), optionally builds+signs in one step, and broadcasts the Ethereum transaction that submits the deposit to the Beacon Chain deposit contract.
Status: unreleased (0.1.0), pending the first tag under the merged name.
ethernal is a Rust workspace that combines the formerly separate
eth-deposit-gen and eth-deposit-tx binaries; see CHANGELOG.md
for the merge, the Go→Rust port, and the documented divergences.
- Concepts and workflow model
- Command structure
- Install
- Quick start (Hoodi testnet)
- Key creation overview
- Create BLS validator keys (
ethernal validator) - Create EOA keystores (
ethernal account) - Step 1 — Generate deposit data (
ethernal deposit gen) - Step 2 — Build the unsigned transaction (
ethernal deposit build) - Step 3 — Sign the transaction (
ethernal tx sign) - Step 4 — Broadcast (optional) (
ethernal tx send) - Convenience:
ethernal tx run(build + sign in one shot) - Air-gapped workflow
- Networks
- Exit codes
- Security
- Recipes
- Troubleshooting
A validator deposit takes three artifacts:
| Artifact | Produced by | Contains |
|---|---|---|
| EIP-2335 keystores | ethernal validator new / validator recover |
Encrypted BLS signing keys (one JSON file per validator index) |
| Deposit data JSON | ethernal deposit gen |
BLS-signed deposit message: validator pubkey, withdrawal credentials, signature, deposit_data_root, amount |
| Signed Ethereum transaction | ethernal deposit build / tx sign / tx run |
EIP-1559 transaction calling the deposit contract's deposit(bytes,bytes,bytes,bytes32) with 32 ETH value, signed by the sender's secp256k1 key |
Separately, ethernal account produces Web3 Secret Storage v3 keystores for ordinary Ethereum (EOA) accounts — the same format geth, Foundry (cast), and MetaMask import. These are not deposit-pipeline inputs; do not pass them to deposit gen.
Two distinct keys are involved in the deposit path:
- BLS validator key (per validator) — held in EIP-2335 keystores created by
ethernal validator(or any compatible tool); used byethernal deposit gento sign the deposit message. Never leaves the keystore decryption boundary. See Create BLS validator keys. - secp256k1 sender key — held in your Ledger (recommended) or a private-key file (testing only); used by
ethernal tx sign/tx runto sign the Ethereum transaction that pays the 32 ETH. Whichever address holds this key needs ≥ 32 ETH + gas. (You can also create a local EOA keystore withaccount new/account recoverfor testing or wallet import; see Create EOA keystores.)
The two-phase split (deposit build then tx sign) supports air-gapped operation: build the unsigned tx on an online machine, transfer the JSON to a signing machine (which may be offline), sign there, transfer the signed JSON back online, broadcast. Prefer generating BLS keys (validator new) on an air-gapped machine as well.
Commands are grouped into four namespaces:
| Namespace | What it groups |
|---|---|
validator |
EIP-2335 BLS keystores by role (new / recover) |
account |
Web3 v3 EOA keystores by role (new / recover) |
deposit |
Launchpad deposit_data (gen) and unsigned deposit-tx construction (build) |
tx |
Sign (sign), build+sign convenience (run), and broadcast (send) |
Typical pipeline:
validator new → deposit gen → deposit build → tx sign → tx send
└──────────── tx run ────────────┘
Non-secret ETHERNAL_TX_* flag fallbacks (ETHERNAL_TX_RPC_URL, _FROM, _GAS_LIMIT) remain available when those flags are omitted. Secrets (passphrases, private keys) come from files or a TTY prompt — never from an env-var-name flag.
- Rust toolchain (stable; install via https://rustup.rs). A C toolchain is needed for the
blstBLS library (Xcode Command Line Tools on macOS,build-essentialon Debian/Ubuntu). - Ledger support is opt-in. The USB/HID transport is behind the
ledgercargo feature; build with--features ledgerto enable it. Without the feature,--signer ledgerfails with exit code 3 and a message pointing at the flag; the rest of the binary still works. - Linux (with
ledgerfeature) — installlibudev-dev(Debian/Ubuntu) orsystemd-devel(Fedora/RHEL) so hidapi can build. For non-root device access, set up udev rules per https://github.com/LedgerHQ/udev-rules. - Windows — not supported.
No prebuilt archives are published for the Rust binary yet — install from source:
git clone https://github.com/rootwarp/ethernal.git
cd ethernal
make build # produces target/release/ethernal
# or, with Ledger hardware support:
cargo build --release --features ledgerPut target/release on your PATH (or copy the binary somewhere on it), then verify:
ethernal --versionEnd-to-end deposit on Hoodi using a Ledger:
# 0. Create validator keystores (interactive TTY ceremony — write down the mnemonic).
# When the ceremony ends, ethernal clears the terminal screen + scrollback
# automatically so the phrase does not linger; if that fails it warns and
# continues. Inside tmux/screen also clear the multiplexer’s own history
# (tmux: `tmux clear-history`; screen: C-a : then `scrollback 0`).
mkdir -p ./keystores ./out
umask 077
printf '%s' 'my-keystore-passphrase' > ./keystore.pw
ethernal validator new --output-dir ./keystores --count 1 --passphrase-file ./keystore.pw
# note the pubkey printed in the summary, then:
# 1. Generate deposit data (withdrawal address must be EIP-55 checksummed)
ethernal deposit gen \
--network hoodi \
--keystore-dir ./keystores/ \
--pubkeys 0x<pubkey-from-validator-new-summary> \
--withdrawal-address 0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1 \
--output-dir ./out \
--passphrase-file ./keystore.pw
# 2. Build unsigned tx (use --nonce explicitly if sender has prior txs)
ethernal deposit build \
--network hoodi \
--input-file ./out/deposit_data-*.json \
--nonce 0 \
--output ./out/unsigned_tx.json
# 3. Sign with Ledger (confirm on device)
ethernal tx sign \
--signer ledger \
--input ./out/unsigned_tx.json \
--output ./out/signed_tx.json
# 4. Broadcast (will prompt to type "hoodi" to confirm)
ethernal tx send \
--input ./out/signed_tx.json \
--rpc-url https://your-hoodi-rpc-url \
--wait-for-receiptIf you already have EIP-2335 keystores from another tool, skip BLS key creation and pass those paths to deposit gen. For a local-key dev flow, see the recipes below.
ethernal can create two kinds of keys from a BIP-39 English mnemonic. They are separate commands, separate keystores, and separate consumers — do not mix directories.
| BLS validator keys | EOA account keys | |
|---|---|---|
| Commands | ethernal validator new / validator recover |
ethernal account new / account recover |
| Curve / use | BLS12-381 validator signing | secp256k1 execution address (EOA) |
| HD path | EIP-2334 m/12381/3600/i/0/0 |
BIP-44 m/44'/60'/0'/0/i |
| Keystore format | EIP-2335 v4 scrypt | Web3 Secret Storage v3 scrypt |
| Filename | keystore-m_12381_3600_<i>_0_0-<unix>.json |
UTC--…--<40-hex-address> |
| Passphrase KDF | EIP-2335 NFKD-normalized | Raw UTF-8 (no NFKD) — geth/MetaMask |
| File mode | 0o600 |
0o600 |
| Summary prints | 96-hex BLS pubkey | EIP-55 address |
| Use with | Validator clients, ethernal deposit gen |
geth, Foundry (cast), MetaMask, wallets |
| Not for | Wallet import / deposit-tx signing | ethernal deposit gen or validator clients |
Same mnemonic, two trees. One BIP-39 seed (plus optional 25th-word mnemonic passphrase) can derive both BLS and EOA keys. The secrets are unrelated; only the seed is shared. See Recipe 6.
Shared interaction model (both namespaces):
new |
recover |
|
|---|---|---|
| Mnemonic source | Fresh 24-word from OS CSPRNG | Existing 12–24-word phrase |
| Terminal | TTY only (stdin and stdout) | TTY prompt or piped stdin |
| Ceremony | Display once + full re-entry | None |
| On abort / mismatch | Exit 4; nothing written | N/A |
Two different passphrases (both commands) — they are never interchangeable:
- Keystore passphrase — encrypts the JSON files (
--passphrase-file PATHor interactive prompt-with-confirm). Minimum 8 bytes. There is no raw-argv form for this secret. - Mnemonic passphrase (optional BIP-39 “25th word”) — mixed into seed derivation only. Empty is valid; no minimum. Four forms: bare
--mnemonic-passphrase(prompt),--mnemonic-passphrase-file PATH, raw--mnemonic-passphrase VALUE(avoid for high-value keys — visible inps/ shell history), or omit → empty.
Both secrets (when not prompted) come from files. A keystore passphrase file and a mnemonic-passphrase file are independent paths; never reuse one file for the other unless you intentionally want the same string for both roles.
The secret is the whole file minus at most one trailing newline; a carriage return anywhere is an error.
Passphrase bytes feed scrypt / PBKDF2 and therefore determine the derived key. Write files carefully:
# printf does NOT append a newline; echo does. Both are fine after the rule
# strips at most one trailing \n — the resulting secret is the same "pw".
printf '%s' 'pw' > ./secret.pw # preferred when you want exact bytes
echo 'pw' > ./secret.pw # also OK: trailing \n is stripped
# A trailing SPACE is significant and is NEVER stripped:
printf '%s' 'pw ' > ./secret.pw # secret is three bytes: p, w, spaceAlways create secret files owner-only — never world-readable:
umask 077
printf '%s' 'my-keystore-passphrase' > ./keystore.pw
# or write first, then: chmod 600 ./keystore.pwNo-disk-file pattern. Prefer process substitution when the secret should not land on disk:
ethernal validator new --output-dir ./keystores \
--passphrase-file <(printf '%s' 'my-keystore-passphrase')/dev/fd/N is the general escape hatch for other fd-based paths. Do not pass - as a path (rejected). Do not rely on /dev/stdin alone when stdin is already claimed (piped mnemonic on recover, or tx sign --input -).
Footgun — path, not passphrase. A passphrase typed where a path is expected lands in argv, ps, shell history, and the not-found error message. There is no shape to key on for a passphrase, so documentation is the mitigation: always pass a real path (or <(...)), never the secret itself.
Deliberately unlike prior art. ethernal’s secret-file rules diverge from geth / OpenSSL / gpg in these cases (reasons matter — these are not accidental omissions):
| Behavior | ethernal | Others | Why |
|---|---|---|---|
| Multi-line file | exit 2 | geth / OpenSSL / gpg: first line wins | Deliberate divergence, not parity — geth accepts multi-line files because --password is a password list indexed per unlocked account, a feature ethernal does not have |
CRLF file (pw\r\n) |
exit 2 | geth: pw; OpenSSL/gpg: pw\r |
The three tools disagree; accepting hard-codes one reading into a derived key. Refusing is the reversible direction |
| non-UTF-8 bytes | exit 2 | geth’s Go strings are byte strings and would accept those bytes | No capability is lost — std::env::var already errored on non-UTF-8 for the replaced flags — and refusing stays the reversible direction |
Hex private-key files (local signer) use a different rule: all leading/trailing ASCII whitespace is trimmed before parsing. Still create them with umask 077 / chmod 600 / <(...).
Use this when you need EIP-2335 keystores for validators and the deposit pipeline (ethernal deposit gen). English BIP-39 only.
| Subcommand | Purpose | I/O |
|---|---|---|
validator new |
Fresh 24-word mnemonic + keystores | TTY only |
validator recover |
Keystores from an existing mnemonic | TTY prompt or piped stdin |
Each run writes one file per index into --output-dir (directory must already exist and be writable):
keystore-m_12381_3600_<i>_0_0-<unix-seconds>.json
Derivation: signing path m/12381/3600/i/0/0 (EIP-2333/2334).
| Flag | Description | Default |
|---|---|---|
--output-dir DIR (required) |
Existing, writable directory for keystore JSON | — |
--count N |
Number of validator keys (≥ 1) | 1 |
--passphrase-file PATH |
File holding the keystore encryption passphrase (min 8 bytes after EIP-2335 normalization). Omit → TTY prompt-with-confirm | TTY prompt |
--mnemonic-passphrase [VALUE] |
Optional BIP-39 25th word. Bare → prompt; with VALUE → raw argv; omit → empty |
empty |
--mnemonic-passphrase-file PATH |
File holding the 25th word (empty file → empty passphrase). Conflicts with --mnemonic-passphrase |
— |
--start-index N |
validator recover only. First HD index; produces [start, start+count) |
0 |
--no-verify |
Skip the post-write keystore decrypt round-trip (C4) only. Derivation self-checks (C1–C3) always run and cannot be skipped. Halves wall-clock at the cost of the strongest correctness check. See What is verified. | off (C4 on) |
validator new always starts at index 0 (no --start-index).
- Raw
--mnemonic-passphrase VALUEis visible inpsand shell history. Prefer--mnemonic-passphrase-fileor bare--mnemonic-passphrase(onvalidator new, bare form is double-entry confirm). Scripting convenience only — not for high-value mnemonics. --passphrase-file/--mnemonic-passphrase-filetake a path, not the secret. A passphrase typed as the flag value lands in argv,ps, shell history, and the not-found error message.- Keystore passphrase is NFKD-normalized for EIP-2335 (different from EOA v3 — see EOA interop note). See Secret files for the byte rule.
Every key from validator new and validator recover is checked before it is treated as done. Three cheap derivation self-checks always run before the file is written; a fourth decrypt round-trip runs after writing by default.
| Check | When | Cost | Skippable |
|---|---|---|---|
| secret → public-key consistency (C1) | before writing | negligible | no |
| public-key point validity (C2) | before writing | negligible | no |
| sign/verify round trip (C3) | before writing | ~2 ms | no |
decrypt the written file and compare secret and pubkey field (C4) |
after writing | ~0.3 s (a second scrypt) | --no-verify |
Wall-clock. Encrypting one EIP-2335 keystore is one scrypt; C4 is a second scrypt at the same cost. Verification therefore roughly doubles the time per key — about 0.6 s instead of 0.3 s on a modern laptop. Measured pure-scrypt cost is ≈ 310 ms per call (ScryptParams::STANDARD, release build) on Apple Silicon; expect proportionally more on older servers or air-gapped boxes (realistically 2–4×). For --count 100 that is roughly one minute instead of thirty seconds.
What --no-verify does not skip. C1–C3 always run. Skipping C4 only means a bad write (corrupt file, wrong ciphertext, mismatched pubkey field) is discovered only when the key is next loaded — possibly after the deposit is already on-chain. Prefer the default path for any mainnet or high-value ceremony; use --no-verify only when you accept that trade-off (e.g. bulk recovery on trusted hardware where wall-clock dominates).
ethernal validator new --output-dir DIR [--count N] [--passphrase-file PATH] \
[--mnemonic-passphrase [VALUE] | --mnemonic-passphrase-file PATH] [--no-verify]Flow
- Non-TTY guard — if stdin or stdout is not a terminal, exit 2 before any entropy is drawn.
- Entropy → mnemonic — 256-bit OS CSPRNG → 24-word English BIP-39 with checksum.
- Mnemonic passphrase — flag / file / prompt-with-confirm / empty.
- Ceremony — mnemonic displayed once on the controlling terminal (
/dev/ttyonly — never stdout/stderr/logs). Write it down offline, then re-enter the full phrase. Mismatch → retry or abort (exit 4); nothing on disk until re-entry succeeds. - Automatic scrollback clear — as soon as the ceremony ends (confirmed or aborted), the screen and scrollback of the controlling terminal are cleared (ANSI
2J/3J/H, written twice) so the mnemonic does not stay readable to anyone scrolling back later — the one leak every deposit-cli audit found. If the clear fails,ethernalcontinues (fail-open) but warns loudly: clear manually (e.g.clear && printf '\x1b[3J', or Cmd+K in Terminal.app) before leaving the machine. tmux/screen caveat: the multiplexer keeps its own scrollback buffer that ANSI sequences cannot reach — clear it there too (tmux:tmux clear-history; screen: C-a : thenscrollback 0). - Keystore passphrase — file (min 8) or interactive confirm.
- Derive → self-check (C1–C3) → encrypt → write → verify (C4) — path
m/12381/3600/i/0/0foriin0..count; EIP-2335 scrypt keystores at0o600. C4 decrypts each written file and re-compares secret andpubkeyunless--no-verifyis set (see What is verified).
Progress output. On a terminal, stderr shows a live phase line per key (deriving / checking / encrypting / writing / verifying) that is erased before each durable keystore i/N: line, so scrollback shape is unchanged. When stderr is piped (non-TTY), the transient line is not drawn; structured log events fire per completed key (including verified=full or verified=derived-only). Scripts parsing stderr therefore see only the existing durable keystore i/N: lines — no \r or CSI escape sequences.
Example
mkdir -p ./keystores
umask 077
printf '%s' 'my-keystore-passphrase' > ./keystore.pw
# One validator
ethernal validator new \
--output-dir ./keystores \
--count 1 \
--passphrase-file ./keystore.pw
# Two validators + optional 25th word (file form preferred)
printf '%s' '...' > ./mnemonic.pw
ethernal validator new \
--output-dir ./keystores \
--count 2 \
--mnemonic-passphrase-file ./mnemonic.pw \
--passphrase-file ./keystore.pw
# no-disk-file alternative:
# ethernal validator new --output-dir ./keystores --count 1 \
# --passphrase-file <(printf '%s' 'my-keystore-passphrase')ethernal validator recover --output-dir DIR [--count N] [--start-index N] \
[--passphrase-file PATH] \
[--mnemonic-passphrase [VALUE] | --mnemonic-passphrase-file PATH] [--no-verify]No display/re-entry ceremony — the mnemonic already exists. Accepts 12 / 15 / 18 / 21 / 24 English words (wordlist + checksum validated first; bad input → exit 2). Interactive prompt when stdin is a TTY; otherwise one line from stdin.
# Interactive
ethernal validator recover \
--output-dir ./keystores \
--count 3 \
--start-index 0 \
--passphrase-file ./keystore.pw
# Piped (automation)
echo "$MNEMONIC" | ethernal validator recover \
--output-dir ./keystores \
--count 1 \
--passphrase-file ./keystore.pw
# Extend an existing set (e.g. next index after 0..2)
ethernal validator recover --output-dir ./keystores --start-index 3 --count 1 \
--passphrase-file ./keystore.pwStderr summary lists each path and its 96-hex-char BLS pubkey. Next steps for a deposit:
- Copy the pubkey(s) from the summary.
- Run
ethernal deposit genwith--keystore-dirand--pubkeys.
Keep the mnemonic offline only. Never paste it into chat, tickets, or cloud notes.
Use this when you need a software EOA encrypted as a standard Web3 v3 keystore (geth / Foundry / MetaMask). This is not the deposit-pipeline keystore format — never pass these files to ethernal deposit gen.
| Subcommand | Purpose | I/O |
|---|---|---|
account new |
Fresh 24-word mnemonic + v3 keystores | TTY only |
account recover |
v3 keystores from an existing mnemonic | TTY prompt or piped stdin |
Each run writes one geth-style file per BIP-44 address index into --output-dir:
UTC--<YYYY-MM-DDTHH-MM-SS.nnnnnnnnnZ>--<40-hex-address-no-0x>
Derivation: m/44'/60'/0'/0/i (Ethereum BIP-44; account' fixed at 0').
| Flag | Description | Default |
|---|---|---|
--output-dir DIR (required) |
Existing, writable directory for keystore JSON | — |
--count N |
Number of EOA keystores (≥ 1) | 1 |
--passphrase-file PATH |
File holding the keystore encryption passphrase (min 8 bytes). Omit → TTY prompt-with-confirm | TTY prompt |
--mnemonic-passphrase [VALUE] |
Optional BIP-39 25th word. Bare → prompt; with VALUE → raw argv; omit → empty |
empty |
--mnemonic-passphrase-file PATH |
File holding the 25th word (empty file → empty passphrase). Conflicts with --mnemonic-passphrase |
— |
--start-index N |
account recover only. First address index; produces [start, start+count) |
0 |
account new always starts at index 0 (no --start-index).
- Raw
--mnemonic-passphrase VALUE— sameps/ shell-history warning asvalidator. Prefer--mnemonic-passphrase-fileor bare prompt. Onaccount new, bare form is double-entry confirm; onaccount recover, bare form is single-entry. --passphrase-file/--mnemonic-passphrase-filetake a path, not the secret — same footgun as validator (argv,ps, shell history, not-found error message).- Interop note — v3 keystore passphrase is raw (no NFKD): scrypt consumes the keystore passphrase as raw UTF-8 bytes (no NFKD, no control-character strip). That matches geth and MetaMask. EIP-2335 (
validator) does normalize — do not assume one passphrase form unlocks both formats for non-ASCII secrets. Prefer ASCII unless you have verified unlock in the target wallet.
ethernal account new --output-dir DIR [--count N] [--passphrase-file PATH] \
[--mnemonic-passphrase [VALUE] | --mnemonic-passphrase-file PATH]Flow (same ceremony shape as validator new):
- Non-TTY guard → exit 2 before entropy.
- Entropy → 24-word BIP-39 mnemonic.
- Mnemonic passphrase → flag / file / confirm / empty.
- Ceremony → display once on
/dev/tty, full re-entry; mismatch → exit 4, nothing on disk. - Automatic scrollback clear → same clear-on-confirm as
validator new(screen + scrollback, on confirm and abort; fail-open with a manual-clear warning if the ANSI write fails). tmux/screen caveat: multiplexers keep their own history that ANSI cannot reach —tmux clear-history; screen: C-a : thenscrollback 0. Details undervalidator newFlow. - Keystore passphrase → file or interactive confirm (min 8, raw bytes to KDF).
- Derive → encrypt → write →
m/44'/60'/0'/0/i, Web3 v3 scrypt,UTC--names, mode0o600. Stderr summary lists path + EIP-55 address.
Example
mkdir -p ./eoa-keys
umask 077
printf '%s' 'my-keystore-passphrase' > ./keystore.pw
ethernal account new \
--output-dir ./eoa-keys \
--count 2 \
--passphrase-file ./keystore.pw
# optional 25th word via file
printf '%s' '...' > ./mnemonic.pw
ethernal account new \
--output-dir ./eoa-keys \
--mnemonic-passphrase-file ./mnemonic.pw \
--passphrase-file ./keystore.pwethernal account recover --output-dir DIR [--count N] [--start-index N] \
[--passphrase-file PATH] \
[--mnemonic-passphrase [VALUE] | --mnemonic-passphrase-file PATH]No ceremony. Same 12–24-word validation as validator recover (bad word reported by 1-based position, never the token). TTY or piped stdin.
# Interactive
ethernal account recover \
--output-dir ./eoa-keys \
--count 3 \
--start-index 0 \
--passphrase-file ./keystore.pw
# Piped
echo "$MNEMONIC" | ethernal account recover \
--output-dir ./eoa-keys \
--count 1 \
--passphrase-file ./keystore.pw
# Next address index (e.g. after 0..2)
ethernal account recover --output-dir ./eoa-keys --start-index 3 --count 1 \
--passphrase-file ./keystore.pwStderr summary lists each path and EIP-55 address. Import with the same keystore passphrase:
| Tool | How |
|---|---|
| Foundry | cast wallet import / cast wallet decrypt-keystore / cast wallet address --keystore … |
| geth | Drop the UTC--… file into <datadir>/keystore/ (standard scrypt n=262144) |
| MetaMask | Import account → JSON File |
Keep the mnemonic offline only.
ethernal deposit gen --keystore-dir DIR --pubkeys HEX[,...] --network NET --output-dir DIR \
--withdrawal-address ADDR [options]
| Flag | Description | Default |
|---|---|---|
--keystore-dir DIR (required) |
Directory containing EIP-2335 JSON keystore files, one per validator | — |
--pubkeys HEX[,...] (required) |
Comma-separated 96-hex-char BLS pubkeys (0x-prefixed or bare) | — |
--network NET (required) |
mainnet or hoodi |
— |
--output-dir DIR (required) |
Existing, writable directory for deposit_data-<ts>.json |
— |
--withdrawal-address ADDR (required) |
EIP-55 checksummed execution address for 0x01 withdrawal credentials (0x01 ‖ 11 zero bytes ‖ addr20). Absent, lowercase, or checksum-mismatched → exit 2 |
— |
--passphrase-file PATH |
File holding the keystore passphrase (omit for TTY prompt) | TTY prompt |
--i-understand-this-is-mainnet |
Required when --network mainnet; acknowledges irreversibility |
false |
--dry-run |
Print JSON to stdout instead of writing a file; sha256 to stderr | false |
--parallel N |
Concurrent signing workers (1 to runtime.NumCPU()×4) | 1 |
--verbose |
Debug-level structured logging to stderr | false |
--json-logs |
Emit logs as JSON objects | false |
--verify-with-deposit-cli |
Cross-check output with staking-deposit-cli >= 2.7.0 |
false |
--deposit-cli-path PATH |
Path to deposit binary for verification |
deposit (PATH) |
--withdrawal-address is strict: the address must be a correctly mixed-case EIP-55 checksum. All-lowercase, all-uppercase, or a mixed-case checksum mismatch is rejected with exit 2. This matches ethstaker/staking-deposit-cli and catches typos before they become irreversible withdrawal credentials.
By contrast, deposit build's --from is lenient: any 0x-prefixed (or bare) 20-byte hex is accepted regardless of case — no checksum check. (tx run has no --from; it derives the sender from its signing key.) Do not expect the two flags to behave the same way.
umask 077
printf '%s' 'my-keystore-passphrase' > ./keystore.pw
ethernal deposit gen \
--network hoodi \
--keystore-dir ./keystores/ \
--pubkeys 0x8420760d0de00ed65f290ab2122e65933e168539ad261b5e444a5094c649272527a1509dd105a801922c359e46e33fb9 \
--withdrawal-address 0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1 \
--output-dir ./out \
--passphrase-file ./keystore.pwethernal deposit gen \
--network hoodi \
--keystore-dir ./keystores/ \
--pubkeys 0xpub1...,0xpub2...,0xpub3...,0xpub4... \
--withdrawal-address 0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1 \
--output-dir ./out \
--passphrase-file ./keystore.pw \
--parallel 4Output JSON is a single array with one entry per pubkey, in the order you supplied.
Mainnet deposits are irreversible. The --i-understand-this-is-mainnet flag is required:
ethernal deposit gen \
--network mainnet \
--i-understand-this-is-mainnet \
--keystore-dir ./keystores/ \
--pubkeys 0xpub1... \
--withdrawal-address 0xYourChecksummedExecutionAddress \
--output-dir ./out \
--passphrase-file ./keystore.pwWithout the flag, --network mainnet exits with code 2. Without --withdrawal-address, deposit gen exits with code 2 (require-choice gate — there is no default BLS-to-execution credential).
ethernal deposit gen ... --dry-run # JSON to stdout, no file[
{
"pubkey": "8420...",
"withdrawal_credentials": "01000...",
"amount": 32000000000,
"signature": "...",
"deposit_message_root": "...",
"deposit_data_root": "...",
"fork_version": "10000910",
"network_name": "hoodi",
"deposit_cli_version": "2.7.0"
}
]ethernal deposit build --input-file FILE --network NET [options]
Produces an EIP-1559 unsigned transaction in JSON. No signing happens — runs fully offline.
| Flag | Description | Default |
|---|---|---|
--input-file PATH / --input PATH / -i PATH (required) |
Path to deposit_data-*.json, or - for stdin |
— |
--network NET / -n NET |
mainnet, hoodi, sepolia, holesky |
hoodi |
--output PATH |
Output file for unsigned tx JSON; omit or - for stdout |
stdout |
--index N |
Which deposit entry to use when the JSON has multiple validators | 0 |
--rpc-url URL |
JSON-RPC endpoint (http/https only; ws:// is rejected). When set, any gas/fee/nonce not passed explicitly is fetched from the node (requires --from); when omitted, the build is fully offline |
— |
--gas-limit N |
EIP-1559 gas limit | 250000 |
--max-fee-per-gas WEI |
EIP-1559 max fee per gas (decimal wei) | 20000000000 (20 gwei) |
--max-priority-fee-per-gas WEI |
EIP-1559 priority fee per gas (decimal wei) | 1000000000 (1 gwei) |
--nonce N |
Sender account nonce. With --rpc-url and omitted, the node's pending nonce is used; offline, omitting defaults to 0 (first-time sender only) |
0 (offline) |
--from ADDR |
Sender address (0x-prefixed, 20-byte hex). Required with --rpc-url when --nonce/--gas-limit is omitted, to fetch the pending nonce and estimate gas |
— |
Wei quantities (fees, value) are held as u128; a value ≥ 2^128 wei is rejected.
Air-gapped build (all values explicit):
ethernal deposit build \
--network hoodi \
--input-file ./out/deposit_data-1716000000.json \
--gas-limit 300000 \
--max-fee-per-gas 30000000000 \
--max-priority-fee-per-gas 2000000000 \
--nonce 17 \
--output unsigned_tx.jsonMultiple validators — produce a tx per validator by varying --index:
for i in 0 1 2 3; do
ethernal deposit build \
--network hoodi \
--input-file deposit_data.json \
--index $i \
--nonce $((BASE_NONCE + i)) \
--output unsigned_tx_${i}.json
done{
"chainId": 560048,
"to": "0x00000000219ab540356cBB839Cbe05303d7705Fa",
"value": "0x1bc16d674ec800000",
"data": "0x22895118...",
"gas": 250000,
"maxFeePerGas": "0x4a817c800",
"maxPriorityFeePerGas": "0x3b9aca00",
"nonce": 0,
"type": "0x2"
}The data field is exactly 420 bytes (0x + 840 hex chars): the 4-byte deposit() selector + 128-byte ABI head + 288-byte tail (pubkey, withdrawal_credentials, signature padded to 32-byte boundaries, deposit_data_root inline).
ethernal tx sign --signer local|ledger --input FILE [options]
| Flag | Description | Default |
|---|---|---|
--signer TYPE (required) |
local or ledger |
— |
--input PATH / -i PATH (required) |
Path to unsigned tx JSON, or - for stdin |
— |
--output PATH / -o PATH |
Output file for signed tx JSON (0o600 perms); omit or - for stdout |
stdout |
--private-key-file PATH |
Path to a file holding the hex private key (required with --signer local) |
— |
LocalSigner is for development, testing, and CI — not for real funds. Use Ledger for any mainnet or non-trivial testnet deposit.
The private key must come from a file named by --private-key-file (required with --signer local; there is no default path). There is no CLI flag that accepts a raw key value. The file holds hex (optional 0x prefix); leading/trailing ASCII whitespace is trimmed before parsing. If the path does not exist and the argument looks like a 64-hex key value, sign refuses with exit code 2 without echoing the argument.
umask 077
# synthetic test key — never real funds
printf '%s' '0x0101010101010101010101010101010101010101010101010101010101010101' > ./local.key
ethernal tx sign \
--signer local \
--private-key-file ./local.key \
--input ./out/unsigned_tx.json \
--output ./out/signed_tx.jsonNo-disk-file alternative:
ethernal tx sign --signer local \
--private-key-file <(printf '%s' '0x...') \
--input unsigned_tx.json --output signed_tx.jsonThe key bytes are zeroized in memory when sign exits (LocalSigner.Close).
Prerequisites:
- Ledger Nano S or Nano X with current firmware
- Ethereum app installed and open on the device
- Binary built with the
ledgercargo feature (cargo build --release --features ledger) - Linux:
libusb-1.0installed and Ledger udev rules in place (see Install)
ethernal tx sign \
--signer ledger \
--input ./out/unsigned_tx.json \
--output ./out/signed_tx.jsonWhat you'll see:
- The CLI prints to stderr:
Please confirm the transaction on your Ledger device... - The Ledger displays the transaction details — read every field carefully:
- Network / Chain ID (e.g.,
Holeskyor chain ID17000) - To address (deposit contract, e.g.,
0x4242424242424242424242424242424242424242on Holesky) - Value (
32 ETH) - Max fee (your gas × maxFeePerGas)
- Network / Chain ID (e.g.,
- Press the right button to confirm. The Ledger returns the signature; sign writes the output file.
If you reject on the device, sign exits with code 4 (user abort). If no Ledger is found or the Ethereum app is not open, exit code 3 with a clear error.
Note on heuristics: the rejection / chain-ID-mismatch / app-not-open detection in crates/ethernal-signer/src/ledger.rs uses pattern matching on the device-side error strings and has NOT yet been validated against real hardware. If you observe unexpected error mappings on a real Ledger, file an issue describing what message you received so the heuristics can be tightened.
{
"unsigned": { ... },
"from": "0xabc...",
"hash": "0xdeadbeef...",
"r": "0x...",
"s": "0x...",
"v": "0",
"rawRLP": "0x02f8c483..."
}v is the EIP-1559 y-parity (decimal "0" or "1").
rawRLP is the EIP-2718 typed envelope (always starts with 0x02 for type-2 transactions) ready for eth_sendRawTransaction.
Output files are created with 0o600 permissions (owner read/write only).
ethernal tx send --input FILE --rpc-url URL [options]
Broadcasts a signed transaction via JSON-RPC with a double-confirmation prompt and optional receipt polling.
| Flag | Description | Default |
|---|---|---|
--input PATH / -i PATH (required) |
Path to signed tx JSON, or - for stdin |
— |
--rpc-url URL (required) |
JSON-RPC endpoint for the target network | — |
--yes |
Skip the typed-confirmation prompt (use for automation only) | false |
--wait-for-receipt |
Poll for the receipt after broadcast | false |
--receipt-timeout DUR |
Receipt poll timeout (duration with ms/s/m/h suffix, e.g. 120s) |
60s |
--receipt-output PATH |
Write receipt JSON to file (0o600 perms) | — |
Unless --yes is set, send prints a summary and waits for you to type the network name:
> You are about to BROADCAST a 32 ETH deposit transaction.
> Network: holesky (chain ID 17000)
> From: 0xabcd...
> To (deposit): 0x4242...4242
> Value: 32.000000 ETH
> Nonce: 17
> MaxFeePerGas: 20.000000 Gwei
> Tx hash: 0xdeadbeef...
> Type the network name to confirm:
holesky
> Broadcasting...
> Tx hash: 0xdeadbeef...
> Explorer: https://holesky.etherscan.io/tx/0xdeadbeef...
Type anything other than the network name (or send EOF) → exit code 4.
send also fetches the chain ID from the RPC endpoint and refuses to broadcast if it doesn't match the signed tx's chain ID — preventing accidental cross-network broadcast (e.g., a Holesky-signed tx sent to a mainnet RPC).
ethernal tx send \
--input ./out/signed_tx.json \
--rpc-url https://holesky.example/rpc \
--wait-for-receipt \
--receipt-timeout 180s \
--receipt-output ./out/receipt.jsonIf you prefer external tools:
RAW=$(jq -r .rawRLP ./out/signed_tx.json)
# Foundry
cast publish --rpc-url https://your-rpc-url "$RAW"
# Or raw curl
curl -X POST -H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_sendRawTransaction\",\"params\":[\"$RAW\"],\"id\":1}" \
https://your-rpc-urlNote: cast send is wrong here — that constructs a new transaction. Use cast publish for pre-signed raw RLP.
When you're signing on the same machine that has the deposit data, tx run collapses deposit build + tx sign into one command:
umask 077
printf '%s' '0x...' > ./local.key # synthetic / test key only
ethernal tx run \
--network hoodi \
--signer local \
--private-key-file ./local.key \
--input-file ./out/deposit_data-1716000000.json \
--nonce 17 \
--output ./out/signed_tx.jsonOutputs:
signed_tx.json(0o600) — SignedTx JSONsigned_tx.raw(0o600) — just therawRLPhex (0x-prefixed), convenient forcast publishor curl. Written only when--outputis a file path; with stdout output no.rawis produced.
Pass --keep-unsigned to also write the intermediate unsigned_tx.json (useful for auditing what was actually signed). Pass --raw-output PATH to override the auto-derived .raw filename.
The same flags work for --signer ledger — tx run calls the Ledger flow internally.
Use the two-step deposit build → tx sign flow when the signing machine is air-gapped; use tx run for the convenience case.
The two-phase design supports air-gapping the signing machine entirely:
[ Online machine #1 ] [ Air-gapped signing machine ]
ethernal deposit gen ... ─USB/QR transfer──> ./signing-machine/in/
ethernal tx sign --signer ledger ...
ethernal deposit build ... ./signing-machine/out/
<─USB/QR transfer── signed_tx.json
[ Online machine #2 ]
ethernal tx send ...
- Air-gapped (recommended for mainnet) — create BLS keystores with
ethernal validator new(TTY ceremony; see Create BLS validator keys), transfer only the encrypted keystores (and later pubkeys) off the machine. Or generate keystores online if you accept the risk. - Online machine — generate deposit data and the unsigned transaction:
ethernal deposit gen ... --withdrawal-address 0x... --output-dir ./out ethernal deposit build --network hoodi --input-file ./out/deposit_data-*.json --nonce N --output unsigned_tx.json - Transfer
unsigned_tx.jsonto the air-gapped machine (USB, QR code, etc.). It contains no secrets. - Air-gapped machine — sign with the Ledger:
ethernal tx sign --signer ledger --input unsigned_tx.json --output signed_tx.json
- Transfer
signed_tx.jsonback to an online machine. - Online machine — broadcast:
ethernal tx send --input signed_tx.json --rpc-url https://...
Neither the unsigned nor the signed deposit-tx artifact contains the BLS private key. The Ledger never exports the secp256k1 key. Note that a merged ethernal binary on the air-gapped machine also carries the deposit gen / deposit build / tx send code paths it isn't using there — see CHANGELOG.md for that tradeoff.
Supported by ethernal (see crates/ethernal-core/src/network.rs):
| Network | Chain ID | Deposit contract | Explorer |
|---|---|---|---|
mainnet |
1 | 0x00000000219ab540356cBB839Cbe05303d7705Fa |
https://etherscan.io |
hoodi |
560048 | 0x00000000219ab540356cBB839Cbe05303d7705Fa |
https://hoodi.etherscan.io |
sepolia |
11155111 | 0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D |
https://sepolia.etherscan.io |
holesky |
17000 | 0x4242424242424242424242424242424242424242 |
https://holesky.etherscan.io |
Notes:
deposit genonly supportsmainnetandhoodi(BLS fork-version material).deposit build/tx sign/tx run/tx sendsupport all four (they just need chain ID + deposit contract address).- For testnet ETH: use the testnet faucets — Hoodi
https://hoodi-faucet.pk910.de/, Sepoliahttps://sepoliafaucet.com/, Holeskyhttps://holesky-faucet.pk910.de/.
All ethernal subcommands use a consistent set of exit codes you can script around:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Unexpected internal error |
| 2 | User / configuration error (bad input, missing/invalid flag, unknown network, missing --withdrawal-address, non-TTY validator new / account new, missing --from/--nonce/--gas-limit for RPC mode, build-side RPC chain-ID mismatch) |
| 3 | Signer / crypto error (Ledger not found, Ethereum app not open, invalid key, signer-side chain-ID mismatch, BLS/SSZ failure) |
| 4 | User abort (SIGINT, or rejected confirmation prompt) |
| 5 | Broadcast / RPC error (RPC dial failure, gas/nonce estimation failure, broadcast-side chain-ID mismatch, node rejection) — deposit build / tx run estimation and tx send broadcast |
Script around these:
if ethernal tx sign ...; then
echo "signed"
else
rc=$?
case $rc in
2) echo "bad input — check flags and files" ;;
3) echo "signer error — Ledger not found or invalid key" ;;
4) echo "user aborted (rejected on device or SIGINT)" ;;
*) echo "unexpected error: $rc" ;;
esac
fiethernal protects:
- Private keys never appear in argv, environment dumps, or shell history when used correctly. Local-signer keys come from a private-key file only; Ledger keys never leave the device. Mnemonics from
validator new/account neware shown only on the controlling terminal and never on stdout/stderr/logs. - Signed artifacts and keystores are written with restricted perms (0o600; receiver can verify a signed tx by recovering the sender and checking the tx hash).
- Broadcast is gated by chain-ID match and operator confirmation. A signed-for-Holesky transaction will not be broadcast to a mainnet RPC endpoint.
- RPC credentials are redacted from error messages by construction. API keys embedded in an
--rpc-urlare stripped before any error is logged or printed.
It does NOT protect:
- A compromised machine. If your build/sign machine is compromised, the unsigned tx data field (which encodes the deposit) could be silently altered. Verify on the Ledger screen before pressing confirm. A compromised keygen machine can capture the mnemonic at generation time — prefer air-gapped
validator new/account newfor high-value keys. - Network-level interception of the broadcast (not a concern for signed transactions — they cannot be modified without invalidating the signature).
- Keystore confidentiality. The keystore passphrase is your responsibility; use a strong one and keep passphrase files mode
0600(or use<(...)) and delete them when done. - A raw
--mnemonic-passphrase VALUEon the command line (visible inpsand shell history) — see Create BLS validator keys and Create EOA keystores.
- BLS mnemonic (
validator new/validator recover) — write it down offline during the ceremony; store it offline only. Never commit it, pipevalidator new(refused), or paste it into tickets/chat. Prefer air-gapped generation for high-value validators. Full guide: Create BLS validator keys. - EOA mnemonic (
account new/account recover) — same ceremony and offline rules as BLS; produces Web3 v3 keystores (not EIP-2335). Prefer air-gapped generation for high-value EOAs. Never pipeaccount new(refused). Full guide: Create EOA keystores. - Mnemonic passphrase (BIP-39 "25th word") — prefer
--mnemonic-passphrase-file PATHor bare--mnemonic-passphrase(prompt). Do not use raw--mnemonic-passphrase VALUEfor high-value mnemonics: the value is visible in the process table (ps) and shell history. A mistyped 25th word yields keys you cannot recover from the mnemonic alone. Applies to bothvalidatorandaccount. A passphrase typed where a path is expected also lands in the not-found error message. - Keystore passphrase (
validator/ EIP-2335) —--passphrase-file PATHor TTY prompt-with-confirm; minimum 8 bytes after EIP-2335 NFKD normalization. There is no raw-argv form (unlike the mnemonic passphrase). Create files withumask 077/chmod 600, or use<(...). See Secret files. - Keystore passphrase (
account/ Web3 v3) — same CLI surface (file or TTY prompt-with-confirm; min 8 bytes; no raw-argv form), but encryption uses the passphrase as raw UTF-8 (no NFKD) for geth/MetaMask interop. - Local private key —
--private-key-file PATHis required with--signer local(no default path). There is NO--private-keyflag that takes a raw key value. Hex files are trimmed of leading/trailing ASCII whitespace; if a missing path looks like 64 hex chars, exit 2 without echoing it. LocalSignerzeroizes the key bytes in memory whenClose()is called (end of everytx sign/tx runinvocation).- For mainnet: use Ledger for the deposit-tx signer. The local signer is explicitly tagged "for development only" in its docs and is not recommended for any real-fund deposit.
- The synthetic test key in
testdata/phase3/holesky/private_key.txtis0x0101010101010101010101010101010101010101010101010101010101010101(obvious pattern). Never use it with real funds; it's for tests only.
Before pressing confirm on your Ledger, always verify on the screen:
- Chain ID matches your intended network (1 = mainnet, 17000 = Holesky, etc.).
- To address is the deposit contract address for your network (see Networks). A different address means something is wrong — abort.
- Value is exactly
32 ETH(32.000000). Any other value is wrong. - From address (recovered by sign and printed in the output) matches the Ledger-derived address you expect to fund the deposit.
Reject on the device if anything is off. The CLI exits with code 4 and no broadcast happens.
The typed exit codes let your automation distinguish between "operator rejected" (code 4 — likely intentional), "signer/crypto problem" (code 3 — investigate), and "RPC / broadcast error" (code 5). Code 5 covers an endpoint dial or gas/nonce estimation failure (deposit build / tx run) and, on tx send, the broadcast safety guard tripping on a chain-ID mismatch — treat a send-side chain-ID mismatch as "wrong network, do not retry blindly".
umask 077
printf '%s' 'test-passphrase' > ./keystore.pw
printf '%s' '0x0101...' > ./local.key # synthetic; never real
mkdir -p ./keystores ./out
# Interactive validator new (or recover from a fixed test mnemonic via stdin)
ethernal validator new --output-dir ./keystores --passphrase-file ./keystore.pw
# copy pubkey from the summary:
ethernal deposit gen \
--network hoodi --keystore-dir ./keystores/ \
--pubkeys 0x... \
--withdrawal-address 0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1 \
--output-dir ./out --passphrase-file ./keystore.pw
ethernal tx run \
--network hoodi --signer local \
--private-key-file ./local.key \
--input-file ./out/deposit_data-*.json \
--nonce 0 --output ./out/signed_tx.jsonumask 077
printf '%s' '...' > ./keystore.pw
ethernal deposit gen ... \
--withdrawal-address 0x... \
--output-dir ./out --passphrase-file ./keystore.pw
ethernal tx run \
--network hoodi --signer ledger \
--input-file ./out/deposit_data-*.json \
--nonce 17 --output ./out/signed_tx.json
# (confirm on Ledger)
ethernal tx send \
--input ./out/signed_tx.json \
--rpc-url https://your-hoodi-rpc \
--wait-for-receipt --receipt-output ./out/receipt.json
# (type "hoodi" to confirm broadcast)
# Air-gapped machine B — generate BLS keys (TTY only)
mkdir -p ./keystores
umask 077
printf '%s' '...' > ./keystore.pw
ethernal validator new --output-dir ./keystores --passphrase-file ./keystore.pw
# transfer keystores (encrypted) + note the pubkeys to online machine A
# keep the mnemonic offline only
# Online machine A
ethernal deposit gen --network mainnet --i-understand-this-is-mainnet \
--keystore-dir ./keystores/ --pubkeys 0x... \
--withdrawal-address 0xYourChecksummedExecutionAddress \
--output-dir ./out --passphrase-file ./keystore.pw
ethernal deposit build --network mainnet \
--input-file ./out/deposit_data-*.json \
--nonce ${NONCE} --output unsigned_tx.json
# transfer unsigned_tx.json via USB/QR to air-gapped machine
# Air-gapped machine B (no network)
ethernal tx sign --signer ledger \
--input unsigned_tx.json --output signed_tx.json
# (confirm on Ledger; verify on-device fields per Security section)
# transfer signed_tx.json back via USB/QR
# Online machine A
ethernal tx send --input signed_tx.json --rpc-url https://your-mainnet-rpc
# (type "mainnet" to confirm)umask 077
printf '%s' '...' > ./keystore.pw
ethernal validator new --output-dir ./keystores --count 3 --passphrase-file ./keystore.pw
ethernal deposit gen --network hoodi --keystore-dir ./keystores/ \
--pubkeys 0xpub1...,0xpub2...,0xpub3... \
--withdrawal-address 0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1 \
--output-dir ./out --passphrase-file ./keystore.pw --parallel 4
# One sign per validator, increment nonce
BASE_NONCE=17
for i in 0 1 2; do
ethernal tx run --network hoodi --signer ledger \
--input-file ./out/deposit_data-*.json --index $i \
--nonce $((BASE_NONCE + i)) \
--output ./out/signed_${i}.json
doneumask 077
printf '%s' '...' > ./keystore.pw
# BLS: you already have indices 0..2; derive the next three
echo "$MNEMONIC" | ethernal validator recover \
--output-dir ./keystores \
--start-index 3 \
--count 3 \
--passphrase-file ./keystore.pw
# EOA: same idea for BIP-44 address indices
echo "$MNEMONIC" | ethernal account recover \
--output-dir ./eoa-keys \
--start-index 3 \
--count 1 \
--passphrase-file ./keystore.pwThe same BIP-39 seed feeds both HD trees. Create (or recover) once, reuse the mnemonic carefully:
# or use different passphrase files per format if you prefer
umask 077
printf '%s' '...' > ./keystore.pw
mkdir -p ./keystores ./eoa-keys
# Option A — fresh mnemonic via BLS ceremony; write the phrase down, then recover EOA
ethernal validator new --output-dir ./keystores --count 1 --passphrase-file ./keystore.pw
# (after writing the mnemonic offline)
echo "$MNEMONIC" | ethernal account recover \
--output-dir ./eoa-keys --count 1 --passphrase-file ./keystore.pw
# Option B — recover both from an existing mnemonic (no ceremony)
echo "$MNEMONIC" | ethernal validator recover \
--output-dir ./keystores --count 1 --passphrase-file ./keystore.pw
echo "$MNEMONIC" | ethernal account recover \
--output-dir ./eoa-keys --count 1 --passphrase-file ./keystore.pwIf you used a BIP-39 mnemonic passphrase (25th word) for one tree, pass the same form to the other. BLS and EOA keystore files still differ (EIP-2335 vs Web3 v3) and must stay in separate directories.
ethernal deposit gen --network hoodi ... --withdrawal-address 0x... --dry-run | \
ethernal deposit build --network hoodi --input-file - --nonce 0 | \
jq '.' # pretty-print the unsigned tx# Decode and inspect with cast
RAW=$(jq -r .rawRLP signed_tx.json)
cast tx --rpc-url https://your-rpc-url --from-rpc "$RAW" # won't work for unbroadcast txs
# Or just decode locally
cast decode-typed-tx "$RAW"| Symptom | Cause / fix |
|---|---|
new requires an interactive terminal ... (exit 2) |
validator new is TTY-only. Run it in a real terminal; do not pipe or redirect stdin/stdout. Use validator recover for scripted mnemonic input. |
--output-dir: directory "..." does not exist / not writable (exit 2) |
Create the directory first (mkdir -p) and ensure write permission. |
--count: value 0 is invalid (exit 2) |
Pass --count ≥ 1. |
Invalid mnemonic / checksum (exit 2, validator recover) |
Check word count (12/15/18/21/24), spelling against the English wordlist, and that the full phrase matches what you wrote down (including any mnemonic passphrase). |
| Ceremony re-entry mismatch → abort (exit 4) | You declined retry after a wrong re-entry, or sent SIGINT. Run validator new again; the previous mnemonic was never written to disk. |
| Keystore passphrase too short (exit 2) | Keystore passphrase must be at least 8 bytes (after EIP-2335 normalization). |
| Symptom | Cause / fix |
|---|---|
| interactive terminal / non-TTY refusal (exit 2) | account new is TTY-only (shares the same gate as validator new). Run it in a real terminal; do not pipe or redirect stdin/stdout. Use account recover for scripted mnemonic input. |
--output-dir: directory "..." does not exist / not writable (exit 2) |
Create the directory first (mkdir -p) and ensure write permission. |
--count: value 0 is invalid (exit 2) |
Pass --count ≥ 1. |
Invalid mnemonic / checksum (exit 2, account recover) |
Check word count (12/15/18/21/24), spelling against the English wordlist, and that the full phrase matches what you wrote down (including any mnemonic passphrase). |
| Ceremony re-entry mismatch → abort (exit 4) | You declined retry after a wrong re-entry, or sent SIGINT. Run account new again; the previous mnemonic was never written to disk. |
| Keystore passphrase too short (exit 2) | Keystore passphrase must be at least 8 bytes. For v3 encryption the bytes are used raw (no NFKD) — see Create EOA keystores. |
| Imported keystore unlocks in neither geth nor MetaMask | Confirm you used the same passphrase string (raw UTF-8) and a v3 file from account, not an EIP-2335 file from validator. See Create EOA keystores. |
| Symptom | Cause / fix |
|---|---|
--withdrawal-address: required flag not set (exit 2) |
Pass --withdrawal-address with an EIP-55 checksummed execution address. There is no default. |
--withdrawal-address: ... EIP-55 checksum mismatch (exit 2) |
Address must be correctly mixed-case EIP-55 (not all-lowercase). Tools like cast to-check-sum-address can re-checksum. Note: deposit build's --from is lenient and does not require EIP-55 — only --withdrawal-address is strict. |
mainnet selected; pass --i-understand-this-is-mainnet to acknowledge (exit 2) |
Add the flag. Mainnet is irreversible. |
pubkey ... not found in keystore directory (exit 2) |
The pubkey listed in --pubkeys has no matching keystore file. Check the keystore directory contents. |
decrypt: invalid passphrase (exit 3) |
Wrong keystore passphrase file (or interactive input). The passphrase decrypts every keystore — all must share it. |
staking-deposit-cli not found in PATH (exit 3, only with --verify-with-deposit-cli) |
Either install staking-deposit-cli >= 2.7.0, set --deposit-cli-path, or drop the verify flag. |
| Symptom | Cause / fix |
|---|---|
--index N: out of bounds (file has M entries) (exit 2) |
Your deposit data JSON has fewer entries than the index you requested. |
deposit entry validation: ... (exit 2) |
The deposit data JSON is malformed (zero pubkey, bad withdrawal credentials prefix, etc.). Regenerate with ethernal deposit gen. |
value mismatch ... (exit 2) |
The entry's amount is not 32 ETH in Gwei. Only 32 ETH first deposits are currently supported. |
| Symptom | Cause / fix |
|---|---|
--private-key-file: required when --signer local (exit 2) |
Pass --private-key-file PATH with a file holding the hex key. There is no default path. |
--private-key-file: ... looks like a key value, not a path (exit 2) |
You passed the hex key as the flag value. Write it to a file (umask 077; printf '%s' '0x...' > key) and pass that path — or use <(printf '%s' '0x...'). |
secret file not found: ... (exit 2) |
Path does not exist. Check the path; if you typed the passphrase/key itself as the path, it also leaks into this error message — fix the invocation. |
invalid private key: expected 32 bytes (exit 3) |
The file contents are not a valid 32-byte hex secp256k1 key. The error never includes the key bytes. |
no Ledger device found (exit 3) |
Plug in the Ledger, unlock it, open the Ethereum app. On Linux, verify udev rules. |
ledger Ethereum app is not open (exit 3) |
Open the Ethereum app on the device, then retry. |
user rejected signing on Ledger (exit 4) |
You pressed the reject button on the device. Retry if intentional was confirm. |
ledger support requires the 'ledger' cargo feature; rebuild with --features ledger (exit 3) |
The binary was built without the Ledger transport. Rebuild with cargo build --release --features ledger. |
| Symptom | Cause / fix |
|---|---|
signed tx chain ID does not match RPC chain ID; refusing to broadcast (exit 5) |
The RPC endpoint reports a different chain ID than the signed tx. You're pointing at the wrong network. Do NOT use --yes to bypass. |
dial RPC: ... (exit 5) |
Bad --rpc-url or network connectivity issue. |
eth_sendRawTransaction: nonce too low (exit 5) |
The sender's actual nonce is higher than what you signed. Rebuild with a correct --nonce, re-sign, retry. |
eth_sendRawTransaction: insufficient funds (exit 5) |
The sender address doesn't have 32 ETH + gas. Fund it. |
eth_sendRawTransaction: known transaction (exit 5) |
This exact tx was already broadcast. Check the explorer for the receipt. |
- If
make e2e-mockpasses but real testnet broadcast fails, the gap is usually nonce or insufficient funds. - For Ledger error-string mismatches (the heuristics aren't real-hardware-validated), file an issue with the exact error text.
- For everything else, run with
--verboseand--json-logs(ethernal deposit gen) to get structured diagnostics.