- Never embedded in scenario YAML. The parser uses strict field decoding
(
yaml.Decoder.KnownFields(true)), so an unexpected field like an inlineprivate_key:fails to parse instead of silently being accepted. - Never logged:
wallet.WalletimplementsString()so the private key is redacted in any%v/%s/%+vformat, including accidentalfmt.Println(wallet)calls. - Keystores are plaintext on disk by default. Pass
--password(or, preferably, setWEB3LOAD_KEYSTORE_PASSWORD— the env var never lands in shell history or a process listing the way a flag can) towallets generateto encrypt at rest instead: scrypt (N=32768, r=8, p=1) derives an AES-256-GCM key from the password, following the same parameters geth/ethers keystores use.runandwallets fundboth auto-detect the format (wallet.LoadAny) and accept the same--password/env var. Without a password, the file stays plaintext andwallets generateprints an explicit warning. Either way, treat the file like a secret: don't commit it, and remember a wrong or lost password is unrecoverable — there's no backdoor by design. - RPC URLs/credentials are read from
${ENV_VAR}references, never written into scenario files or logs in resolved form.
evm.Dialfetches the RPC's actualeth_chainIdand refuses to proceed if it doesn't matchtarget.chain_idin the scenario. This is the single most common way a load test accidentally lands on the wrong network — a copy-pasted scenario pointed at a staleRPC_URLenv var.target.environment: productionrequires typing the scenario's exact name back at a confirmation prompt beforerunbroadcasts anything.safety.allowed_chain_idsis an explicit allow-list; if set,runrefuses to start against any chain id not on it.
safety.max_spend_per_wallet_nativebounds any single native-currency transfer;transferrejects amounts above it before signing.safety.max_gas_price_gweiclamps the fee captxenginesigns with (Engine.MaxGasPriceGwei) — not just the tip, and not a rejection: a scenario that would otherwise send at whatever the network suggests gets capped instead, and a warning is logged each time clamping actually changes anything. The tip cap is clamped down with it whenever it would otherwise exceed the (now-lower) fee cap, since a tip above the fee cap is an invalid EIP-1559 transaction — clamping only one of the two could turn "too expensive" into "the RPC rejects this outright."web3load validatenever broadcasts anything — it only parses and checks the schema.run --dry-runbuilds, estimates gas for, and signs every transaction exactly like a real run, but stops beforeeth_sendRawTransaction; the nonce it allocated is released immediately since nothing was actually consumed on-chain.
Controller/worker coordination has no transport encryption or
authentication in v0.3, and Assignment responses carry wallet private
keys over that channel. See docs/distributed.md —
run it only on a trusted network.
internal/rpc is the intended home for a token-bucket limiter around the
adapter's RPC calls, to avoid a runaway scenario getting an API key banned
by a third-party provider (Infura/Alchemy/etc.). v0.1 ships basic
retry-with-backoff (internal/rpc/retry.go); the limiter itself is a v0.2
item — flagged here so it isn't dropped from scope.
- Multi-tenant / untrusted scenario authors (the tool assumes whoever runs
web3load runis trusted with the keys they point it at). - Protection against a malicious
abi_fileor ABI-encoded call itself doing something harmful on-chain — that's a property of the contract and the operator's judgment, not something the load tool can validate.