The unified Ethereum development toolkit — Ganache-compatible API, powered by Foundry's Forge + Cast + Anvil + Chisel, with LevelDB persistence.
Fork mainnet in one flag:
$ ethsmith node --fork.network mainnet --block-time 12
2026-06-06 [info] Forking mainnet at block 22009000
2026-06-06 [info] RPC proxy ready {"port":8545}
geth attach works out of the box:
$ geth attach http://127.0.0.1:8545
Welcome to the Geth JavaScript console!
instance: anvil/v1.7.1
at block: 0
modules: eth:1.0 net:1.0 personal:1.0 web3:1.0
> eth.accounts
["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", ...]
> eth.blockNumber
0
| Problem | Solution |
|---|---|
| Ganache is dead (archived, no updates) | ethsmith speaks Ganache's CLI and API — drop-in replacement |
| Anvil is fast but uses a different CLI | All your old ganache-cli flags work unchanged |
| Anvil dumps state as giant JSON (100MB+ for forked chains) | ethsmith uses LevelDB — compact, indexed, < 10 MB |
| Four separate Foundry tools (forge, cast, anvil, chisel) | One command: ethsmith compile, ethsmith test, ethsmith call... |
| Ganache's JS EVM was slow | Anvil's Rust EVM is 100× faster |
| No Ganache in CI after v7 | npm install -g ethsmith — works everywhere Node 20+ runs |
npm install -g ethsmithRequires Node.js >= 20.
Foundry binary discovery — what happens automatically:
npm install -g ethsmith
└─ postinstall runs
├─ SKIP_ETHSMITH_POSTINSTALL=1 set? → exit, no download
├─ anvil found in system PATH? → skip, use existing Foundry install
├─ ~/.ethsmith/bin/anvil exists? → skip, already installed
└─ none of the above → download Foundry → ~/.ethsmith/bin/
ethsmith node
└─ looks for anvil in this order:
1. ~/.ethsmith/bin/anvil ← managed copy (installed by postinstall)
2. System PATH ← your existing global Foundry install
3. Downloads on-demand ← last-resort fallback
If postinstall was skipped (corporate proxy, offline, SKIP_ETHSMITH_POSTINSTALL=1) or you want to upgrade Foundry:
ethsmith install # download / re-download Foundry to ~/.ethsmith/bin/
ethsmith update # update Foundry to latest version
ethsmith update --check # check versions without installing
ethsmith doctor # verify Node version, all 4 binaries, paths, DB# Start a local Ethereum node (port 8545, 10 accounts, 1000 ETH each)
ethsmith
# Same but with Ganache options — they all work
ethsmith --port 8545 --accounts 20 --deterministic --chain-id 1337
# Fork mainnet at latest block
ethsmith --fork https://eth.llamarpc.com
# Fork mainnet by name (built-in RPC endpoints)
ethsmith --fork.network mainnet
# Fork at specific block
ethsmith --fork https://eth.llamarpc.com@20000000
# Use LevelDB at custom path (state persists across restarts)
ethsmith --db ./mychain
# Start with block-time 2s (mine every 2 seconds)
ethsmith --block-time 2ethsmith [node] [options]This is the default command — just run ethsmith with any options.
| Flag | Ganache Alias | Description | Default |
|---|---|---|---|
--port <n> |
-p |
HTTP + WebSocket RPC port | 8545 |
--accounts <n> |
-a |
Number of accounts to generate | 10 |
--mnemonic <phrase> |
-m |
BIP39 mnemonic for accounts | random |
--deterministic |
-d |
Use standard test mnemonic (same accounts always) | false |
--chain-id <id> |
--networkId |
Chain ID (Ganache: --networkId) | 1337 |
--gas-limit <n> |
--gasLimit |
Block gas limit | 30000000 |
--gas-price <wei> |
--gasPrice |
Minimum gas price | 1000000000 |
--balance <eth> |
--defaultBalanceEther |
Initial ETH per account | 1000 |
--block-time <s> |
Auto-mine interval (0 = instamine) | 0 |
|
--no-mining |
Disable auto-mining | false | |
--fork <url> |
Fork from URL (url@blockNumber supported) |
— | |
--fork.url <url> |
Fork URL (Ganache compat) | — | |
--fork.blockNumber <n> |
Fork at specific block | latest | |
--fork.network <name> |
Fork by name: mainnet sepolia arbitrum optimism base polygon |
— | |
--hardfork <name> |
EVM hardfork: london paris shanghai cancun prague |
cancun |
|
--unlock <addr> |
--impersonate |
Unlock/impersonate address (repeatable) | — |
--db <path> |
LevelDB directory (state persists) | ~/.ethsmith/db/<chainId> |
|
--state-interval <s> |
Checkpoint to LevelDB every N seconds | 30 |
|
--ipc [path] |
Enable IPC socket | disabled | |
--optimism |
Enable Optimism L2 mode | false | |
--init <file> |
Genesis JSON file | — | |
--fund-accounts <addr:eth> |
Fund address on startup (repeatable) | — | |
--keep-history / --prune-history |
Keep full block history (default: pruned) | pruned | |
--order <type> |
TX ordering: fees or fifo |
fees |
|
--log-level <level> |
debug info warn error |
info |
|
--log-file <path> |
Log file path | ~/.ethsmith/logs/ |
ethsmith compile # forge build
ethsmith compile --optimize # enable optimizer
ethsmith compile --via-ir # use IR pipeline
ethsmith compile --sizes # show contract sizesethsmith test # forge test
ethsmith test --watch # watch for file changes
ethsmith test --gas-report # show gas usage table
ethsmith test --match-test "testFoo" # run specific test
ethsmith test -vvvv # max verbosityethsmith fuzz # forge test --fuzz-runs 10000
ethsmith fuzz --fuzz-runs 100000 # more runs
ethsmith fuzz --match-test "testFuzz_"ethsmith coverage # forge coverage
ethsmith coverage --report lcov # LCOV format for CIethsmith deploy src/Token.sol:Token \
--rpc-url http://localhost:8545 \
--private-key 0xac0974be...
# With verification
ethsmith deploy src/Token.sol:Token \
--rpc-url https://mainnet.infura.io/v3/KEY \
--private-key $PRIVATE_KEY \
--verify \
--etherscan-api-key $ETHERSCAN_KEYethsmith flatten src/Token.sol # forge flatten (for Etherscan)
ethsmith inspect src/Token.sol:Token abi
ethsmith inspect src/Token.sol:Token bytecode
ethsmith fmt # format all Solidity files
ethsmith snapshot # gas snapshot (forge snapshot)ethsmith update # update Foundry binaries to latest
ethsmith update --check # check versions without installing
ethsmith install # (re-)download Foundry to ~/.ethsmith/bin/
ethsmith doctor # verify Node version, binaries, paths, DB
ethsmith clean # remove leftover Anvil tmp dirs from crashes
ethsmith clean --dry-run # preview what would be deleted and disk reclaimedCast Commands (On-chain Interaction) — click to expand
# Read calls
ethsmith call 0xTokenAddr "balanceOf(address)" 0xMyAddr
ethsmith balance 0xMyAddr
ethsmith block latest
ethsmith block 19000000 --rpc-url https://eth.llamarpc.com
ethsmith tx 0xTxHash
ethsmith receipt 0xTxHash
ethsmith logs 0xContractAddr
ethsmith storage 0xContractAddr 0x0 # read storage slot
ethsmith code 0xContractAddr # get bytecode
ethsmith nonce 0xAddr
ethsmith chain-id
ethsmith gas-price
# Write transactions
ethsmith send 0xTokenAddr "transfer(address,uint256)" 0xRecipient 1000000000000000000 \
--private-key 0xac0974be...
# ABI encoding/decoding
ethsmith abi-encode "transfer(address,uint256)" 0xABC 1000
ethsmith abi-decode "transfer(address,uint256)" 0xa9059cbb...
ethsmith decode "transfer(address,uint256)" 0xa9059cbb... # decode calldata
ethsmith sig "transfer(address,uint256)" # → 0xa9059cbb
ethsmith keccak "Transfer(address,address,uint256)"
# Wallet
ethsmith wallet new # generate new wallet
ethsmith wallet import # import from private key
# ERC20 helpers
ethsmith erc20 transfer 0xToken 0xRecipient 1ether
# CREATE2
ethsmith create2 --init-code 0x60... --salt 0x1234...
# Transaction tracing (replay + full EVM trace)
ethsmith trace 0xTxHash --rpc-url https://eth.llamarpc.com
# Utilities
ethsmith to-hex 255 # → 0xff
ethsmith to-dec 0xff # → 255
ethsmith from-utf8 "hello"
ethsmith estimate 0xAddr "foo()" # gas estimate
ethsmith rpc eth_blockNumber # raw JSON-RPCethsmith repl # launch interactive Solidity REPL
# or
ethsmith chiselInside the REPL:
> uint256 x = 42;
> x * 2
Type: uint256
└ Data: 84
> address(0).balance
Type: uint256
└ Data: 0
> !inspect x // show type + value
> !save mysession // save session to disk
> !load mysession // load session
> !quit // exitethsmith automatically saves your blockchain state to LevelDB every 30 seconds and on graceful shutdown. On restart, state is restored exactly where you left off.
# State is stored at:
~/.ethsmith/db/<chainId>/
# Custom location
ethsmith --db ./my-project-chain
# Adjust checkpoint interval (seconds)
ethsmith --state-interval 10
# Multiple independent chains
ethsmith --chain-id 1337 --db ./chainA &
ethsmith --port 8546 --chain-id 1338 --db ./chainB &Why LevelDB instead of Anvil's JSON files?
| Format | Size (forked mainnet) | Compression | Indexed |
|---|---|---|---|
| Anvil JSON | 100 MB+ | None (plain text) | No |
| ethsmith LevelDB | ~8 MB | gzip | Yes |
# Fork mainnet at latest block
ethsmith --fork https://eth.llamarpc.com
# Fork at specific block (reproducible)
ethsmith --fork https://eth.llamarpc.com@20000000
# Fork by network name (built-in endpoints)
ethsmith --fork.network mainnet
ethsmith --fork.network sepolia
ethsmith --fork.network arbitrum
ethsmith --fork.network optimism
ethsmith --fork.network base
ethsmith --fork.network polygon
# Fork + state persistence (fork cache saved in LevelDB)
ethsmith --fork.network mainnet --db ./mainnet-fork
# Reset fork to original state (RPC)
curl -X POST http://localhost:8545 -d '{"jsonrpc":"2.0","method":"anvil_reset","params":[]}'Anvil Special RPC Methods (32 methods) — click to expand
All 32 Anvil special methods work out of the box:
# Storage manipulation
cast rpc anvil_setStorageAt 0xAddr 0x0 0xValue
cast rpc anvil_setCode 0xAddr 0x60... # replace bytecode
cast rpc anvil_setBalance 0xAddr 0xDE0B... # set ETH balance
cast rpc anvil_setNonce 0xAddr 0x5 # override nonce
cast rpc anvil_setChainId 31337
# Account impersonation
cast rpc anvil_impersonateAccount 0xWhale
cast rpc anvil_stopImpersonatingAccount 0xWhale
cast rpc anvil_autoImpersonateAccount true # impersonate any sender
# Block control
cast rpc anvil_mine 100 # mine 100 blocks instantly
cast rpc anvil_setBlockTimestamp 1700000000
cast rpc anvil_setNextBlockBaseFeePerGas 0
# Snapshot / Revert
cast rpc evm_snapshot # → "0x1"
cast rpc evm_revert '["0x1"]' # revert to snapshot
cast rpc anvil_reorg '{"depth":2}' # simulate reorg
cast rpc anvil_rollback '{"blocks":5}' # roll back 5 blocks
# State dump/load (used internally by LevelDB layer)
cast rpc anvil_dumpState # → hex state
cast rpc anvil_loadState '["0x..."]' # restore state
# Mempool
cast rpc anvil_dropTransaction 0xTxHash
cast rpc anvil_dropAllTransactions
# DeFi helpers
cast rpc anvil_dealErc20 '["0xToken","0xAddr","1000000000000000000"]'
cast rpc anvil_setErc20Allowance '["0xToken","0xOwner","0xSpender","1000"]'
cast rpc anvil_addBalance '["0xAddr","0xDE0B..."]'
# Info
cast rpc anvil_nodeInfo
cast rpc anvil_metadataGanache Compatibility Methods (personal_*, miner_*, EIP-712) — click to expand
# Personal namespace (MetaMask compatibility)
cast rpc personal_listAccounts
cast rpc personal_importRawKey '["0xPrivKey","password"]'
cast rpc personal_unlockAccount '["0xAddr","password",0]'
cast rpc personal_lockAccount '["0xAddr"]'
cast rpc personal_sendTransaction '[{"from":"0x...","to":"0x...","value":"0x1"},"password"]'
# Miner control
cast rpc miner_start # start auto-mining
cast rpc miner_stop # stop auto-mining (manual mode)
cast rpc miner_setGasPrice '["0x3B9ACA00"]'
# EIP-712 signing (MetaMask style)
cast rpc eth_signTypedData_v4 '["0xAddr",{"types":{...},"domain":{...},"message":{...}}]'Debug & Trace (Full EVM Tracing) — click to expand
# Step-by-step EVM trace
cast rpc debug_traceTransaction '["0xTxHash"]'
cast rpc debug_traceCall '[{"to":"0x...","data":"0x..."},"latest"]'
cast rpc debug_traceBlockByNumber '["latest"]'
# Parity-style trace
cast rpc trace_transaction '["0xTxHash"]'
cast rpc trace_block '["latest"]'
cast rpc trace_filter '[{"fromBlock":"0x1","toBlock":"0x10","toAddress":["0x..."]}]'
cast rpc trace_replayBlockTransactions '["latest",["trace"]]'
# Mempool inspection
cast rpc txpool_content
cast rpc txpool_inspect
cast rpc txpool_statusDrop-in replacement for the old ganache npm package:
const ethsmith = require('ethsmith')
// ── In-memory provider (for unit tests — no ports) ──────────────────────────
const provider = await ethsmith.provider({
chain: { chainId: 1337 },
mnemonic: 'test test test test test test test test test test test junk',
wallet: { totalAccounts: 5 }
})
// Use with ethers.js
const { ethers } = require('ethers')
const ethersProvider = new ethers.BrowserProvider(provider)
const signer = await ethersProvider.getSigner()
// ── Server with port ────────────────────────────────────────────────────────
const server = ethsmith.server({
port: 8545,
deterministic: true,
'chain-id': '1337',
'block-time': '2'
})
await server.listen(8545)
// server.provider is an EIP-1193 provider
// ── Fork mainnet in tests ───────────────────────────────────────────────────
const forkProvider = await ethsmith.provider({
fork: 'https://eth.llamarpc.com@20000000',
'chain-id': '1'
})
// ── Stop ────────────────────────────────────────────────────────────────────
await server.close()
await provider.disconnect()Hardhat — use as local node:
# Terminal 1
ethsmith --port 8545 --deterministic
# Terminal 2
npx hardhat --network localhost testHardhat config:
module.exports = {
networks: {
localhost: {
url: 'http://127.0.0.1:8545',
chainId: 1337
}
}
}Truffle — works identically to old Ganache:
module.exports = {
networks: {
development: {
host: '127.0.0.1',
port: 8545,
network_id: '*'
}
}
}Foundry — point to ethsmith node:
ethsmith --port 8545 &
forge test --fork-url http://localhost:8545
cast send 0xAddr "mint(address,uint256)" $ADDR 1000 --rpc-url http://localhost:8545 --private-key 0xac0974be...All events are logged to ~/.ethsmith/logs/ in JSON format with daily rotation.
# Set log level
ethsmith --log-level debug
# Custom log file
ethsmith --log-file ./ethsmith.log
# Example log output (JSON):
# {"timestamp":"2026-06-06 03:15:27.000","level":"info","message":"Starting ethsmith node","port":8545,"chainId":1337}
# {"timestamp":"...","level":"info","message":"Checkpoint saved","block":150}
# {"timestamp":"...","level":"info","message":"State restored","blockNumber":150}Log events:
- Node startup (port, chainId, accounts, mnemonic)
- Every checkpoint (block number, state size, compression ratio)
- State restore on startup (block restored to)
- Fork info (URL, block number, latency)
- DB operations (open, close, integrity)
- All errors with full stack traces
Create ~/.ethsmith/config.json for persistent defaults:
{
"port": 8545,
"accounts": 10,
"deterministic": true,
"chainId": 1337,
"gasLimit": 30000000,
"balance": 1000,
"stateInterval": 30,
"logLevel": "info"
}Run multiple independent chains simultaneously:
# Chain A — local dev
ethsmith --port 8545 --chain-id 1337 --db ~/.ethsmith/dev &
# Chain B — mainnet fork
ethsmith --port 8546 --chain-id 1 --fork.network mainnet --db ~/.ethsmith/fork-mainnet &
# Chain C — sepolia fork
ethsmith --port 8547 --chain-id 11155111 --fork.network sepolia --db ~/.ethsmith/fork-sepolia &Each instance has its own LevelDB directory and process — completely independent.
# Ephemeral — fresh chain every restart (quickest start)
docker run -p 8545:8545 lord1egypt/ethsmith:latest
# Detached (background)
docker run -d -p 8545:8545 --name ethsmith lord1egypt/ethsmith:latest
# Stop / restart
docker stop ethsmith
docker start ethsmith# Named volume — state survives container restarts and re-creates
docker run -p 8545:8545 -v ethsmith-data:/root/.ethsmith/db lord1egypt/ethsmith:latest
# Bind-mount — state saved to a local folder you can inspect
docker run -p 8545:8545 -v ./data:/root/.ethsmith/db lord1egypt/ethsmith:latest
docker run -p 8545:8545 -v /absolute/path:/root/.ethsmith/db lord1egypt/ethsmith:latestAlways mount to
/root/.ethsmith/db— not/root/.ethsmith.
Foundry binaries are installed to/root/.ethsmith/bininside the image layer; mounting the parent directory shadows them and breaks startup.
# Standard deterministic accounts (same 10 addresses every time)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic
# Custom number of accounts
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --accounts 20
# Custom mnemonic
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --mnemonic "your twelve word mnemonic phrase goes here trust me bro"
# Custom starting balance (ETH per account)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --balance 5000
# Unlock / impersonate a specific address on startup
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --unlock 0xYourAddress# Custom chain ID
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --chain-id 31337
# Custom port inside container (still map 8545 outside)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --port 8545
# Custom gas limit and gas price
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --gas-limit 60000000 --gas-price 1000000000
# Specific EVM hardfork
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --hardfork cancun
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --hardfork shanghai
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --hardfork london
# Optimism L2 mode
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --optimism# Default: instamine — every transaction mines a block immediately
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic
# Mine a block every 1 second
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --block-time 1
# Mine a block every 5 seconds
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --block-time 5
# Mine a block every 12 seconds (mainnet-like cadence)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --deterministic --block-time 12
# Disable auto-mining entirely (manual mine via RPC only)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --no-mining# Fork mainnet (built-in public RPC)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network mainnet
# Fork sepolia testnet
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network sepolia
# Fork other networks by name
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network arbitrum
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network optimism
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network base
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork.network polygon
# Fork from custom RPC URL
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork https://eth.llamarpc.com
# Fork at a specific block number (reproducible)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --fork https://eth.llamarpc.com@20000000
# Fork with your own RPC via -e (quick, but key appears in shell history and ps aux)
docker run -p 8545:8545 \
-e ETHSMITH_FORK_MAINNET_URL=https://mainnet.infura.io/v3/YOUR_KEY \
lord1egypt/ethsmith:latest node --fork.network mainnet
# Better for secrets: --env-file keeps the key out of shell history, ps aux, and docker inspect args
# 1. Create .env (add to .gitignore — never commit this file)
# ETHSMITH_FORK_MAINNET_URL=https://mainnet.infura.io/v3/YOUR_KEY
# ETHSMITH_FORK_SEPOLIA_URL=https://sepolia.infura.io/v3/YOUR_KEY
# 2. Run with --env-file
docker run -p 8545:8545 \
--env-file .env \
lord1egypt/ethsmith:latest node --fork.network mainnet
# Fork + persistent state (fork cache stays across restarts)
docker run -p 8545:8545 \
-v ethsmith-fork:/root/.ethsmith/db \
lord1egypt/ethsmith:latest node --fork.network mainnet# Set log level (debug / info / warn / error)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --log-level debug
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --log-level warn
# Write logs to a file inside the container (mount volume to retrieve them)
docker run -p 8545:8545 \
-v ./logs:/logs \
lord1egypt/ethsmith:latest node --log-file /logs/ethsmith.log
# Log level via environment variable
docker run -p 8545:8545 \
-e ETHSMITH_LOG_LEVEL=debug \
lord1egypt/ethsmith:latest
# Quiet (errors only) with persistent state
docker run -p 8545:8545 \
-v ethsmith-data:/root/.ethsmith/db \
lord1egypt/ethsmith:latest node --log-level error# Checkpoint every 10 seconds instead of default 30
docker run -p 8545:8545 \
-v ethsmith-data:/root/.ethsmith/db \
lord1egypt/ethsmith:latest node --state-interval 10
# Keep full block history (needed for eth_getLogs across old blocks)
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --keep-history
# TX ordering: fifo (first-in-first-out) instead of default fee-priority
docker run -p 8545:8545 lord1egypt/ethsmith:latest node --order fifo# Full example — deterministic, persistent, fork mainnet, 1s blocks, debug logs
docker run -d \
--name ethsmith-dev \
-p 8545:8545 \
-v ethsmith-data:/root/.ethsmith/db \
-e ETHSMITH_FORK_MAINNET_URL=https://eth.llamarpc.com \
lord1egypt/ethsmith:latest \
node \
--deterministic \
--accounts 20 \
--balance 10000 \
--chain-id 1337 \
--block-time 1 \
--fork.network mainnet \
--gas-limit 60000000 \
--state-interval 10 \
--log-level debug# docker-compose.yml
version: '3.8'
services:
# Local dev node — instamine, deterministic accounts, persistent
node-dev:
image: lord1egypt/ethsmith:latest
ports: ['8545:8545']
volumes: ['./data/dev:/root/.ethsmith/db']
command: node --deterministic --chain-id 1337
restart: unless-stopped
stop_grace_period: 30s # gives checkpoint time to finish before SIGKILL
# Mainnet fork — 12s block time, persistent fork cache
node-mainnet-fork:
image: lord1egypt/ethsmith:latest
ports: ['8546:8545']
volumes: ['./data/fork-mainnet:/root/.ethsmith/db']
environment:
- ETHSMITH_FORK_MAINNET_URL=https://eth.llamarpc.com
command: node --fork.network mainnet --block-time 12 --chain-id 1
restart: unless-stopped
stop_grace_period: 30s
# Sepolia fork — for testnet simulation
node-sepolia-fork:
image: lord1egypt/ethsmith:latest
ports: ['8547:8545']
volumes: ['./data/fork-sepolia:/root/.ethsmith/db']
command: node --fork.network sepolia --deterministic
restart: unless-stopped
stop_grace_period: 30s
# Silent CI node — no auto-mining, errors only
node-ci:
image: lord1egypt/ethsmith:latest
ports: ['8548:8545']
command: node --deterministic --no-mining --log-level error| Flag | Example | Description |
|---|---|---|
--deterministic |
node --deterministic |
Same 10 accounts every run |
--accounts <n> |
node --accounts 20 |
Number of accounts (default 10) |
--mnemonic <phrase> |
node --mnemonic "word1 word2..." |
Custom BIP39 mnemonic |
--balance <eth> |
node --balance 5000 |
Starting ETH per account (default 1000) |
--chain-id <id> |
node --chain-id 31337 |
Chain ID (default 1337) |
--port <n> |
node --port 8545 |
RPC port inside container |
--block-time <s> |
node --block-time 1 |
Seconds between blocks (default: instamine) |
--no-mining |
node --no-mining |
Disable auto-mining |
--fork <url> |
node --fork https://... |
Fork from RPC URL |
--fork <url@block> |
node --fork https://...@20000000 |
Fork at specific block |
--fork.network <name> |
node --fork.network mainnet |
Fork by name |
--hardfork <name> |
node --hardfork cancun |
EVM hardfork |
--gas-limit <n> |
node --gas-limit 60000000 |
Block gas limit |
--gas-price <wei> |
node --gas-price 1000000000 |
Minimum gas price |
--unlock <addr> |
node --unlock 0xABC... |
Impersonate address on startup |
--state-interval <s> |
node --state-interval 10 |
LevelDB checkpoint interval |
--keep-history |
node --keep-history |
Keep full block history |
--order <type> |
node --order fifo |
TX ordering: fees or fifo |
--optimism |
node --optimism |
Enable Optimism L2 mode |
--log-level <level> |
node --log-level debug |
Log verbosity |
--log-file <path> |
node --log-file /logs/node.log |
Write logs to file |
Environment variables:
| Variable | Example | Description |
|---|---|---|
ETHSMITH_LOG_LEVEL |
debug |
Log level (alternative to --log-level) |
ETHSMITH_LOG_FILE |
/logs/ethsmith.log |
Log file path |
ETHSMITH_FORK_MAINNET_URL |
https://... |
Override mainnet RPC endpoint |
ETHSMITH_FORK_SEPOLIA_URL |
https://... |
Override sepolia RPC endpoint |
ETHSMITH_FORK_ARBITRUM_URL |
https://... |
Override arbitrum RPC endpoint |
ETHSMITH_FORK_OPTIMISM_URL |
https://... |
Override optimism RPC endpoint |
ETHSMITH_FORK_BASE_URL |
https://... |
Override base RPC endpoint |
ETHSMITH_FORK_POLYGON_URL |
https://... |
Override polygon RPC endpoint |
CI/CD Pipeline — click to expand
ethsmith uses a fully automated CI/CD pipeline:
| Workflow | Trigger | What it does |
|---|---|---|
CI (ci.yml) |
push / PR to main |
Runs all 6 test suites on Node 20 & 22 with Foundry installed |
Release (release.yml) |
git push --tags with v*.*.* |
Creates a GitHub Release automatically |
Publish (publish.yml) |
GitHub Release created | Runs tests → publishes to npm → builds & pushes Docker image |
# 1. Bump version in package.json
npm version patch # or minor / major
# 2. Push commit + tag
git push && git push --tags
# → GitHub Actions creates the Release automatically
# → Publish workflow fires: npm publish + docker pushGo to Settings → Secrets → Actions and add:
| Secret | Value |
|---|---|
NPM_TOKEN |
npm access token (from npmjs.com → Account → Access Tokens) |
DOCKER_USERNAME |
lord1egypt |
DOCKER_PASSWORD |
Docker Hub password or access token |
Architecture — click to expand
ethsmith/
├── bin/
│ └── ethsmith.js ← CLI entry point (Node >=20 check)
├── src/
│ ├── index.js ← Public API: provider(), server()
│ ├── core/
│ │ ├── binary.js ← Foundry binary detection + auto-download
│ │ ├── flags.js ← Ganache → Anvil flag translation (20+ flags)
│ │ ├── db.js ← LevelDB: state save/load/restore, integrity
│ │ └── logger.js ← Winston + daily log rotation
│ └── cli/
│ ├── index.js ← Commander.js CLI (30+ subcommands)
│ ├── node.js ← EthsmithNode class (spawns Anvil, manages state)
│ ├── forge.js ← Forge wrapper (compile/test/deploy/fuzz/coverage...)
│ ├── cast.js ← Cast wrapper (call/send/balance/trace/abi-*...)
│ ├── chisel.js ← Chisel REPL wrapper
│ └── install.js ← Foundry binary installer
State persistence flow:
startup → LevelDB has state? → anvil_loadState RPC → Anvil restores state
running → every 30s → anvil_dumpState RPC → gzip → LevelDB
shutdown → anvil_dumpState RPC → gzip → LevelDB → kill Anvil
Docker: container exits immediately with "unknown option '--host'"
This was a bug in images older than v1.3.3. The CMD incorrectly passed --host which doesn't exist.
Fix: pull the latest image.
docker pull lord1egypt/ethsmith:latestDocker: state not persisting between container restarts
You need a volume mount. Without -v, every docker run starts a fresh chain.
# Named volume (recommended)
docker run -p 8545:8545 -v ethsmith-data:/root/.ethsmith/db lord1egypt/ethsmith:latest
# Bind-mount to local folder
docker run -p 8545:8545 -v ./data:/root/.ethsmith/db lord1egypt/ethsmith:latestWrong path — mounting to /root/.ethsmith instead of /root/.ethsmith/db shadows the Foundry binaries inside the image. Always use /root/.ethsmith/db.
Docker: "exec: anvil: not found" or Foundry binaries missing
Foundry is installed by postinstall.js when the image is built. This can fail if:
- The image was pulled but not re-built after a base change
- The volume is mounted to
/root/.ethsmith(shadows binaries)
# Always mount to /root/.ethsmith/db — not the parent
docker run -p 8545:8545 -v ethsmith-data:/root/.ethsmith/db lord1egypt/ethsmith:latestgeth attach: "Method not found" for net_peerCount
This was fixed in v1.3.3. The proxy now returns "0x0" for net_peerCount directly instead of forwarding to Anvil.
Upgrade (recommended):
npm install -g ethsmith # npm
docker pull lord1egypt/ethsmith:latest # DockerWorkaround without upgrading — add a small HTTP proxy in front that intercepts net_peerCount and returns {"jsonrpc":"2.0","id":1,"result":"0x0"}, or just upgrade — it's a one-liner.
Port 8545 already in use
# Use a different port
ethsmith --port 8546
docker run -p 8546:8545 lord1egypt/ethsmith:latest node --port 8545Foundry not found / "anvil: command not found"
ethsmith install # downloads Foundry to ~/.ethsmith/bin/
ethsmith doctor # verify all binaries and pathsIf you're on Alpine Linux or Termux (Android), see the musl/Termux limitation in AUDIT.md.
State not restoring on restart
ethsmith restores state automatically if the --db path is the same between runs. Default is ~/.ethsmith/db/<chainId>/.
# Explicit path — safest for scripts
ethsmith --db ./mychain --chain-id 1337
# Make sure chain ID matches — state is keyed by chain ID
ethsmith --chain-id 1337 --db ./mychain # start
ethsmith --chain-id 1337 --db ./mychain # restart — restores correctly
ethsmith --chain-id 9999 --db ./mychain # WRONG chain ID — starts fresh--log-file option ignored
Fixed in v1.3.2. If you're on an older version:
npm install -g ethsmith # upgrade to latest~/.foundry/anvil/tmp/ growing to 50 GB+
Fixed in v1.3.1. ethsmith now cleans Anvil's internal tmp dirs on startup and shutdown.
To clean manually:
ethsmith clean # remove leftover dirs
ethsmith clean --dry-run # preview what would be deleted- Bug reports & feature requests: GitHub Issues
- Full bug history & root causes: AUDIT.md
- All curl RPC examples: RPC_EXAMPLES.md
- Changelog: CHANGELOG.md
| Feature | Ganache (archived) | plain Anvil | ethsmith |
|---|---|---|---|
| Active development | ❌ | ✅ | ✅ |
| EVM speed | JS (slow) | Rust (fast) | Rust (fast) |
| Old Ganache CLI flags | ✅ | ❌ | ✅ |
personal_* RPC |
✅ | ❌ | ✅ |
miner_start/stop |
✅ | ❌ | ✅ |
| LevelDB persistence | ✅ | ❌ | ✅ |
| Compact state storage | ✅ | ❌ (big JSON) | ✅ |
| Anvil special methods | ❌ | ✅ | ✅ |
| Modern hardforks | ❌ | ✅ | ✅ |
debug_trace* |
❌ | ✅ | ✅ |
trace_* (Parity) |
❌ | ✅ | ✅ |
| Forge (compile/test) | ❌ | ❌ | ✅ |
| Cast (call/send/abi) | ❌ | ❌ | ✅ |
| Chisel (Solidity REPL) | ❌ | ❌ | ✅ |
| Fork by network name | ❌ | ❌ | ✅ |
fork.provider (in-memory) |
✅ | ❌ | ✅ |
| Optimism L2 support | ❌ | ✅ | ✅ |
| Node.js >= 20 | ❌ | — | ✅ |
| Programmatic API | ✅ | ❌ | ✅ |
| Package | Purpose |
|---|---|
commander v12 |
CLI framework |
level v8 |
LevelDB bindings (Node.js) |
winston v3 |
Structured logging |
winston-daily-rotate-file v5 |
Log rotation |
axios v1 |
HTTP (RPC calls + binary download) |
tar v7 |
Extract Foundry binaries |
ethers v6 |
Programmatic provider |
| Foundry (system) | forge, cast, anvil, chisel binaries |
MIT — Lord1Egypt
Other open-source projects by the same author (Lord1Egypt):
| Project | Description |
|---|---|
| skillforge-agent | 539 ready-to-use AI agent skills for Claude, Gemini, GPT — npm + PyPI |
| awesome-prompt-forge | 2,592 curated AI system prompts across 14 categories — npm + PyPI |
| awesome-ai-system-prompts | GitHub collection of AI system prompts for developers |
