From 6f513399a446503ab9f464fc80499b4007f62c43 Mon Sep 17 00:00:00 2001 From: kant Date: Sat, 21 Jun 2025 08:33:51 -0700 Subject: [PATCH 1/3] geth state dump script --- tools/state-dump/README.md | 66 ++++++++++++++ tools/state-dump/initial_genesis.json | 82 ++++++++++++++++++ tools/state-dump/requirements.txt | 40 +++++++++ tools/state-dump/world_state.py | 120 ++++++++++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 tools/state-dump/README.md create mode 100644 tools/state-dump/initial_genesis.json create mode 100644 tools/state-dump/requirements.txt create mode 100644 tools/state-dump/world_state.py diff --git a/tools/state-dump/README.md b/tools/state-dump/README.md new file mode 100644 index 000000000..8ee5e449d --- /dev/null +++ b/tools/state-dump/README.md @@ -0,0 +1,66 @@ +# Geth World State Snapshot + +This repository provides a utility to snapshot the Ethereum world state at a given block and merge it into a new genesis file. The main script, `world_state.py`, connects to an archive node via JSON-RPC, retrieves account and storage data, and produces an updated genesis alloc. + +--- + +## Prerequisites + +- Python 3.8+ +- An Ethereum archive node exposing the JSON-RPC interface (e.g. Geth with `--gcmode=archive`). + +--- + +## Repository Structure + +```bash +/Users/kant/mev-commit/tools/state-dump +├── genesis_test.json +├── initial_genesis.json +├── requirements.txt +└── world_state.py +``` + +--- + +## Setup & Usage + +```bash +# 1. Create and activate a virtual environment +python3 -m venv geth-world-state +source geth-world-state/bin/activate + +# 2. Upgrade pip and install dependencies +pip install --upgrade pip +pip install -r requirements.txt + +# 3. Run the snapshot script and merge into out_genesis.json +python3 world_state.py \ + --rpc http://:8545 \ + --input-genesis initial_genesis.json \ + --output out_genesis.json +``` + +**Example:** + +```bash +python3 world_state.py \ + --rpc http://34.75.194.46:8545 \ + --input-genesis initial_genesis.json \ + --output out_genesis.json +``` + +- `--rpc` + JSON-RPC endpoint of your archive node (e.g. `http://127.0.0.1:8545`). + +- `--input-genesis` + Path to your existing genesis template (e.g. `initial_genesis.json`). + +- `--output` + Path where the merged genesis file will be written (e.g. `out_genesis.json`). + +--- + +## License + +This project is released under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/tools/state-dump/initial_genesis.json b/tools/state-dump/initial_genesis.json new file mode 100644 index 000000000..cc57634bf --- /dev/null +++ b/tools/state-dump/initial_genesis.json @@ -0,0 +1,82 @@ +{ + "config": { + "chainId": 141414, + "homesteadBlock": 0, + "daoForkSupport": true, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "pragueTime": 0, + "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", + "blobSchedule": { + "cancun": { + "target": 3, + "max": 6, + "baseFeeUpdateFraction": 3338477 + }, + "prague": { + "target": 6, + "max": 9, + "baseFeeUpdateFraction": 5007716 + } + }, + "terminalTotalDifficulty": 0 + }, + "nonce": "0x0", + "timestamp": "0x0", + "gasLimit": "0x1c9c380", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "E666c8Fc7be06a4A88145Cd0C626FAe4D40fCDA7": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "3Ba6a3318a7D55C73F743529E2ca69cCF112D538": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "BADc9848e1e87E5017E7790Be7c4b5d35C304FC1": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "c286beF43cEa547545d5b7179AEf6747F63Ac8Aa": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "072F6D4d7A1F7Af547d47D927bEaf38E01Fcb33b": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "00000961Ef480Eb55e80D19ad83579A64c007002": { + "balance": "0x0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd" + }, + "0000bbddc7ce488642fb579f8b00f3a590007251": { + "balance": "0x0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd" + }, + "0x0000F90827F1C53a10cb7A02335B175320002935": { + "balance": "0x0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500" + }, + "0x00000000219ab540356cbb839cbe05303d7705fa": { + "balance": "0x0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220dceca8706b29e917dacf25fceef95acac8d90d765ac926663ce4096195952b6164736f6c634300060b0033" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "withdrawalsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x7", + "excessBlobGas": "0x0", + "blobGasUsed": "0x0" +} diff --git a/tools/state-dump/requirements.txt b/tools/state-dump/requirements.txt new file mode 100644 index 000000000..c3aef21c9 --- /dev/null +++ b/tools/state-dump/requirements.txt @@ -0,0 +1,40 @@ +aiohappyeyeballs==2.6.1 +aiohttp==3.12.13 +aiosignal==1.3.2 +annotated-types==0.7.0 +attrs==25.3.0 +bitarray==3.4.2 +certifi==2025.6.15 +charset-normalizer==3.4.2 +ckzg==2.1.1 +cytoolz==1.0.1 +eth-account==0.13.7 +eth-hash==0.7.1 +eth-keyfile==0.8.1 +eth-keys==0.7.0 +eth-rlp==2.2.0 +eth-typing==5.2.1 +eth-utils==5.3.0 +eth_abi==5.2.0 +frozenlist==1.7.0 +hexbytes==1.3.1 +idna==3.10 +multidict==6.5.0 +parsimonious==0.10.0 +propcache==0.3.2 +pycryptodome==3.23.0 +pydantic==2.11.7 +pydantic_core==2.33.2 +pyunormalize==16.0.0 +regex==2024.11.6 +requests==2.32.4 +rlp==4.1.0 +toolz==1.0.0 +tqdm==4.67.1 +types-requests==2.32.4.20250611 +typing-inspection==0.4.1 +typing_extensions==4.14.0 +urllib3==2.5.0 +web3==7.12.0 +websockets==15.0.1 +yarl==1.20.1 diff --git a/tools/state-dump/world_state.py b/tools/state-dump/world_state.py new file mode 100644 index 000000000..5a6037406 --- /dev/null +++ b/tools/state-dump/world_state.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +import json +import argparse +import requests +from pathlib import Path + +# Default filename for the migrated genesis output +DEFAULT_OUT_NAME = "out_genesis.json" + +def rpc_call(rpc_url: str, method: str, params: list) -> dict: + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params + } + resp = requests.post(rpc_url, json=payload) + resp.raise_for_status() + data = resp.json() + if "error" in data: + raise RuntimeError(f"RPC error ({method}): {data['error']}") + return data["result"] + +def rpc_get_block_number(rpc_url: str) -> int: + """Fetch the latest block number (as an int).""" + hex_bn = rpc_call(rpc_url, "eth_blockNumber", []) + return int(hex_bn, 16) + +def rpc_debug_dump_block(rpc_url: str, block_param: str) -> dict: + """Call debug_dumpBlock at the given block tag or hex number.""" + return rpc_call(rpc_url, "debug_dumpBlock", [block_param]) + +def to_hex(x: str) -> str: + """Convert a decimal string to a hex string (0x-prefixed).""" + return hex(int(x)) + +def build_alloc(accounts: dict) -> dict: + """ + Given the 'accounts' map from debug_dumpBlock, return an 'alloc' + dictionary suitable for a genesis file: address → { balance, code?, storage? }. + """ + alloc = {} + for addr, acct in accounts.items(): + entry = {"balance": to_hex(acct["balance"])} + if acct.get("code"): + entry["code"] = acct["code"] + if acct.get("storage"): + entry["storage"] = acct["storage"] + alloc[addr] = entry + return alloc + +def main(): + p = argparse.ArgumentParser( + description="Snapshot world-state via debug_dumpBlock and merge into a genesis template" + ) + p.add_argument( + "--rpc", + default="http://127.0.0.1:8545", + help="Geth RPC endpoint" + ) + p.add_argument( + "--input-genesis", + required=True, + help="Path to your source chain genesis.json" + ) + p.add_argument( + "--block", "-b", + type=int, + help="Block number to snapshot (defaults to latest)" + ) + p.add_argument( + "--output", "-o", + help=( + "Path or directory for output genesis JSON " + f"(default: ./{DEFAULT_OUT_NAME})" + ) + ) + args = p.parse_args() + + # load template + tmpl_path = Path(args.input_genesis) + if not tmpl_path.is_file(): + raise SystemExit(f"❌ Input genesis not found: {tmpl_path}") + genesis_tpl = json.loads(tmpl_path.read_text()) + + # decide which block to dump + if args.block is None: + # fetch actual latest block number + block_no = rpc_get_block_number(args.rpc) + block_param = "latest" + print(f"⛓ Dumping world-state at latest (block {block_no})…") + else: + block_no = args.block + block_param = hex(block_no) + print(f"⛓ Dumping world-state at block {block_no}…") + + # fetch the dump + dump = rpc_debug_dump_block(args.rpc, block_param) + print(f" ↳ {len(dump['accounts'])} accounts loaded") + + # build alloc → merge → write out + latest_alloc = build_alloc(dump["accounts"]) + new_gen = genesis_tpl.copy() + orig_alloc = new_gen.get("alloc", {}) + new_gen["alloc"] = {**orig_alloc, **latest_alloc} + + # determine output path + if args.output: + out_path = Path(args.output) + if out_path.is_dir(): + out_path = out_path / DEFAULT_OUT_NAME + else: + out_path = Path.cwd() / DEFAULT_OUT_NAME + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(new_gen, indent=2)) + print(f"✅ Wrote merged genesis → {out_path}") + +if __name__ == "__main__": + main() From 07975daa4e11f13e78f5a56c3190c54de137afa3 Mon Sep 17 00:00:00 2001 From: kant Date: Wed, 2 Jul 2025 16:41:38 -0700 Subject: [PATCH 2/3] Adding support for multiple webhooks --- tools/state-dump/README.md | 66 ----- tools/state-dump/initial_genesis.json | 82 ------ tools/state-dump/requirements.txt | 40 --- tools/state-dump/world_state.py | 120 --------- .../notification/notifier.go | 204 ++++++++++++++ .../notification/notifier_test.go | 118 ++++++++ .../validators-monitor/notification/slack.go | 251 ------------------ .../notification/slack_test.go | 215 --------------- 8 files changed, 322 insertions(+), 774 deletions(-) delete mode 100644 tools/state-dump/README.md delete mode 100644 tools/state-dump/initial_genesis.json delete mode 100644 tools/state-dump/requirements.txt delete mode 100644 tools/state-dump/world_state.py create mode 100644 tools/validators-monitor/notification/notifier.go create mode 100644 tools/validators-monitor/notification/notifier_test.go delete mode 100644 tools/validators-monitor/notification/slack.go delete mode 100644 tools/validators-monitor/notification/slack_test.go diff --git a/tools/state-dump/README.md b/tools/state-dump/README.md deleted file mode 100644 index 8ee5e449d..000000000 --- a/tools/state-dump/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Geth World State Snapshot - -This repository provides a utility to snapshot the Ethereum world state at a given block and merge it into a new genesis file. The main script, `world_state.py`, connects to an archive node via JSON-RPC, retrieves account and storage data, and produces an updated genesis alloc. - ---- - -## Prerequisites - -- Python 3.8+ -- An Ethereum archive node exposing the JSON-RPC interface (e.g. Geth with `--gcmode=archive`). - ---- - -## Repository Structure - -```bash -/Users/kant/mev-commit/tools/state-dump -├── genesis_test.json -├── initial_genesis.json -├── requirements.txt -└── world_state.py -``` - ---- - -## Setup & Usage - -```bash -# 1. Create and activate a virtual environment -python3 -m venv geth-world-state -source geth-world-state/bin/activate - -# 2. Upgrade pip and install dependencies -pip install --upgrade pip -pip install -r requirements.txt - -# 3. Run the snapshot script and merge into out_genesis.json -python3 world_state.py \ - --rpc http://:8545 \ - --input-genesis initial_genesis.json \ - --output out_genesis.json -``` - -**Example:** - -```bash -python3 world_state.py \ - --rpc http://34.75.194.46:8545 \ - --input-genesis initial_genesis.json \ - --output out_genesis.json -``` - -- `--rpc` - JSON-RPC endpoint of your archive node (e.g. `http://127.0.0.1:8545`). - -- `--input-genesis` - Path to your existing genesis template (e.g. `initial_genesis.json`). - -- `--output` - Path where the merged genesis file will be written (e.g. `out_genesis.json`). - ---- - -## License - -This project is released under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/tools/state-dump/initial_genesis.json b/tools/state-dump/initial_genesis.json deleted file mode 100644 index cc57634bf..000000000 --- a/tools/state-dump/initial_genesis.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "config": { - "chainId": 141414, - "homesteadBlock": 0, - "daoForkSupport": true, - "eip150Block": 0, - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "muirGlacierBlock": 0, - "berlinBlock": 0, - "londonBlock": 0, - "arrowGlacierBlock": 0, - "grayGlacierBlock": 0, - "shanghaiTime": 0, - "cancunTime": 0, - "pragueTime": 0, - "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", - "blobSchedule": { - "cancun": { - "target": 3, - "max": 6, - "baseFeeUpdateFraction": 3338477 - }, - "prague": { - "target": 6, - "max": 9, - "baseFeeUpdateFraction": 5007716 - } - }, - "terminalTotalDifficulty": 0 - }, - "nonce": "0x0", - "timestamp": "0x0", - "gasLimit": "0x1c9c380", - "difficulty": "0x1", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x0000000000000000000000000000000000000000", - "alloc": { - "E666c8Fc7be06a4A88145Cd0C626FAe4D40fCDA7": { - "balance": "0xc097ce7bc90715b34b9f1000000000" - }, - "3Ba6a3318a7D55C73F743529E2ca69cCF112D538": { - "balance": "0xc097ce7bc90715b34b9f1000000000" - }, - "BADc9848e1e87E5017E7790Be7c4b5d35C304FC1": { - "balance": "0xc097ce7bc90715b34b9f1000000000" - }, - "c286beF43cEa547545d5b7179AEf6747F63Ac8Aa": { - "balance": "0xc097ce7bc90715b34b9f1000000000" - }, - "072F6D4d7A1F7Af547d47D927bEaf38E01Fcb33b": { - "balance": "0xc097ce7bc90715b34b9f1000000000" - }, - "00000961Ef480Eb55e80D19ad83579A64c007002": { - "balance": "0x0", - "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd" - }, - "0000bbddc7ce488642fb579f8b00f3a590007251": { - "balance": "0x0", - "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd" - }, - "0x0000F90827F1C53a10cb7A02335B175320002935": { - "balance": "0x0", - "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500" - }, - "0x00000000219ab540356cbb839cbe05303d7705fa": { - "balance": "0x0", - "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220dceca8706b29e917dacf25fceef95acac8d90d765ac926663ce4096195952b6164736f6c634300060b0033" - } - }, - "number": "0x0", - "gasUsed": "0x0", - "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "withdrawalsRoot":"0x0000000000000000000000000000000000000000000000000000000000000000", - "baseFeePerGas": "0x7", - "excessBlobGas": "0x0", - "blobGasUsed": "0x0" -} diff --git a/tools/state-dump/requirements.txt b/tools/state-dump/requirements.txt deleted file mode 100644 index c3aef21c9..000000000 --- a/tools/state-dump/requirements.txt +++ /dev/null @@ -1,40 +0,0 @@ -aiohappyeyeballs==2.6.1 -aiohttp==3.12.13 -aiosignal==1.3.2 -annotated-types==0.7.0 -attrs==25.3.0 -bitarray==3.4.2 -certifi==2025.6.15 -charset-normalizer==3.4.2 -ckzg==2.1.1 -cytoolz==1.0.1 -eth-account==0.13.7 -eth-hash==0.7.1 -eth-keyfile==0.8.1 -eth-keys==0.7.0 -eth-rlp==2.2.0 -eth-typing==5.2.1 -eth-utils==5.3.0 -eth_abi==5.2.0 -frozenlist==1.7.0 -hexbytes==1.3.1 -idna==3.10 -multidict==6.5.0 -parsimonious==0.10.0 -propcache==0.3.2 -pycryptodome==3.23.0 -pydantic==2.11.7 -pydantic_core==2.33.2 -pyunormalize==16.0.0 -regex==2024.11.6 -requests==2.32.4 -rlp==4.1.0 -toolz==1.0.0 -tqdm==4.67.1 -types-requests==2.32.4.20250611 -typing-inspection==0.4.1 -typing_extensions==4.14.0 -urllib3==2.5.0 -web3==7.12.0 -websockets==15.0.1 -yarl==1.20.1 diff --git a/tools/state-dump/world_state.py b/tools/state-dump/world_state.py deleted file mode 100644 index 5a6037406..000000000 --- a/tools/state-dump/world_state.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -import json -import argparse -import requests -from pathlib import Path - -# Default filename for the migrated genesis output -DEFAULT_OUT_NAME = "out_genesis.json" - -def rpc_call(rpc_url: str, method: str, params: list) -> dict: - payload = { - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params - } - resp = requests.post(rpc_url, json=payload) - resp.raise_for_status() - data = resp.json() - if "error" in data: - raise RuntimeError(f"RPC error ({method}): {data['error']}") - return data["result"] - -def rpc_get_block_number(rpc_url: str) -> int: - """Fetch the latest block number (as an int).""" - hex_bn = rpc_call(rpc_url, "eth_blockNumber", []) - return int(hex_bn, 16) - -def rpc_debug_dump_block(rpc_url: str, block_param: str) -> dict: - """Call debug_dumpBlock at the given block tag or hex number.""" - return rpc_call(rpc_url, "debug_dumpBlock", [block_param]) - -def to_hex(x: str) -> str: - """Convert a decimal string to a hex string (0x-prefixed).""" - return hex(int(x)) - -def build_alloc(accounts: dict) -> dict: - """ - Given the 'accounts' map from debug_dumpBlock, return an 'alloc' - dictionary suitable for a genesis file: address → { balance, code?, storage? }. - """ - alloc = {} - for addr, acct in accounts.items(): - entry = {"balance": to_hex(acct["balance"])} - if acct.get("code"): - entry["code"] = acct["code"] - if acct.get("storage"): - entry["storage"] = acct["storage"] - alloc[addr] = entry - return alloc - -def main(): - p = argparse.ArgumentParser( - description="Snapshot world-state via debug_dumpBlock and merge into a genesis template" - ) - p.add_argument( - "--rpc", - default="http://127.0.0.1:8545", - help="Geth RPC endpoint" - ) - p.add_argument( - "--input-genesis", - required=True, - help="Path to your source chain genesis.json" - ) - p.add_argument( - "--block", "-b", - type=int, - help="Block number to snapshot (defaults to latest)" - ) - p.add_argument( - "--output", "-o", - help=( - "Path or directory for output genesis JSON " - f"(default: ./{DEFAULT_OUT_NAME})" - ) - ) - args = p.parse_args() - - # load template - tmpl_path = Path(args.input_genesis) - if not tmpl_path.is_file(): - raise SystemExit(f"❌ Input genesis not found: {tmpl_path}") - genesis_tpl = json.loads(tmpl_path.read_text()) - - # decide which block to dump - if args.block is None: - # fetch actual latest block number - block_no = rpc_get_block_number(args.rpc) - block_param = "latest" - print(f"⛓ Dumping world-state at latest (block {block_no})…") - else: - block_no = args.block - block_param = hex(block_no) - print(f"⛓ Dumping world-state at block {block_no}…") - - # fetch the dump - dump = rpc_debug_dump_block(args.rpc, block_param) - print(f" ↳ {len(dump['accounts'])} accounts loaded") - - # build alloc → merge → write out - latest_alloc = build_alloc(dump["accounts"]) - new_gen = genesis_tpl.copy() - orig_alloc = new_gen.get("alloc", {}) - new_gen["alloc"] = {**orig_alloc, **latest_alloc} - - # determine output path - if args.output: - out_path = Path(args.output) - if out_path.is_dir(): - out_path = out_path / DEFAULT_OUT_NAME - else: - out_path = Path.cwd() / DEFAULT_OUT_NAME - - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(new_gen, indent=2)) - print(f"✅ Wrote merged genesis → {out_path}") - -if __name__ == "__main__": - main() diff --git a/tools/validators-monitor/notification/notifier.go b/tools/validators-monitor/notification/notifier.go new file mode 100644 index 000000000..e394c791e --- /dev/null +++ b/tools/validators-monitor/notification/notifier.go @@ -0,0 +1,204 @@ +package notification + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log/slog" + "math/big" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/params" + "github.com/primev/mev-commit/tools/validators-monitor/api" +) + +// Message represents a notification message structure +// generalized to support different platforms. +type Message struct { + Text string `json:"text,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +// Attachment represents a message attachment +type Attachment struct { + Color string `json:"color,omitempty"` + Title string `json:"title,omitempty"` + Text string `json:"text,omitempty"` + Fields []Field `json:"fields,omitempty"` + Footer string `json:"footer,omitempty"` + TS int64 `json:"ts,omitempty"` + MarkdownIn []string `json:"mrkdwn_in,omitempty"` +} + +// Field represents a field in a message attachment +type Field struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// Notifier sends notifications to multiple webhook endpoints +type Notifier struct { + webhookURLs []string + client *http.Client + logger *slog.Logger + enabled bool +} + +// NewNotifier creates a new notifier instance +func NewNotifier(webhookURLs []string, logger *slog.Logger) *Notifier { + enabled := len(webhookURLs) > 0 + + if !enabled { + logger.Warn("Notifications disabled - no webhook URLs provided") + } else { + logger.Info("Notifications enabled") + } + + return &Notifier{ + webhookURLs: webhookURLs, + client: &http.Client{ + Timeout: 10 * time.Second, + }, + logger: logger, + enabled: enabled, + } +} + +// SendMessage sends a message to all configured webhook endpoints +func (n *Notifier) SendMessage(ctx context.Context, message Message) error { + if !n.enabled { + n.logger.Debug("Notification skipped (disabled)") + return nil + } + + messageJSON, err := json.Marshal(message) + if err != nil { + return fmt.Errorf("failed to marshal message: %w", err) + } + + var errorOccurred bool + for _, webhookURL := range n.webhookURLs { + req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(messageJSON)) + if err != nil { + n.logger.Error("Failed to create request", "webhook", webhookURL, "error", err) + errorOccurred = true + continue + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := n.client.Do(req) + if err != nil { + n.logger.Error("Failed to send notification", "webhook", webhookURL, "error", err) + errorOccurred = true + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + n.logger.Error("Notification API returned non-200 status code", "webhook", webhookURL, "status", resp.StatusCode) + errorOccurred = true + continue + } + n.logger.Debug("Notification sent successfully", "webhook", webhookURL) + } + + if errorOccurred { + return fmt.Errorf("one or more notifications failed") + } + return nil +} + +// NotifyRelayData sends a notification about relay data for a validator +func (n *Notifier) NotifyRelayData( + ctx context.Context, + pubkey string, + validatorIndex, + blockNumber, + slot uint64, + mevReward *big.Int, + feeRecipient string, + relaysWithData, + allRelays []string, + dashboardInfo *api.DashboardResponse, +) error { + color := "#36a64f" + if len(relaysWithData) == 0 { + color = "#ff9900" + } + + relaysWithDataStr := "None" + if len(relaysWithData) > 0 { + relaysWithDataStr = formatRelayList(relaysWithData) + } + + fields := []Field{ + {"Validator Index", fmt.Sprintf("%d", validatorIndex), true}, + {"Slot", fmt.Sprintf("%d", slot), true}, + {"Block Number", fmt.Sprintf("%d", blockNumber), true}, + {"Validator Pubkey", pubkey, false}, + {"Relays With Data", fmt.Sprintf("```%s```", relaysWithDataStr), false}, + {"Data Availability", fmt.Sprintf("%d of %d relays have data", len(relaysWithData), len(allRelays)), false}, + } + + title := "Relay Data Available for Validator" + if len(relaysWithData) == 0 { + title = "No Relay Data Found for Validator" + } + + message := Message{ + Attachments: []Attachment{ + { + Color: color, + Title: title, + Text: "Report on relay data for opted-in validator", + Fields: fields, + Footer: "Validator Monitor", + TS: time.Now().Unix(), + MarkdownIn: []string{"text", "fields"}, + }, + }, + } + + if dashboardInfo != nil { + attach := &message.Attachments[0] + + attach.Fields = append(attach.Fields, Field{"Block Winner", dashboardInfo.Winner, false}) + attach.Fields = append(attach.Fields, Field{"Commitments", fmt.Sprintf("%d (Rewards: %d, Slashes: %d)", + dashboardInfo.TotalOpenedCommitments, + dashboardInfo.TotalRewards, + dashboardInfo.TotalSlashes), false}) + + if dashboardInfo.TotalAmount != "" { + amountWei, ok := new(big.Int).SetString(dashboardInfo.TotalAmount, 10) + if ok { + attach.Fields = append(attach.Fields, Field{"Total Bid Amount", formatWeiToEth(amountWei), true}) + } else { + attach.Fields = append(attach.Fields, Field{"Total Bid Amount (wei)", dashboardInfo.TotalAmount, true}) + } + } + attach.Fields = append(attach.Fields, Field{"MEV Reward", formatWeiToEth(mevReward), true}) + attach.Fields = append(attach.Fields, Field{"MEV Reward Recipient", feeRecipient, true}) + } + + return n.SendMessage(ctx, message) +} + +func formatRelayList(relays []string) string { + if len(relays) == 0 { + return "None" + } + var result string + for _, r := range relays { + result += "- " + r + "\n" + } + return result +} + +func formatWeiToEth(wei *big.Int) string { + ethValue := new(big.Float).Quo(new(big.Float).SetInt(wei), new(big.Float).SetFloat64(params.Ether)) + return fmt.Sprintf("%.6f ETH", ethValue) +} diff --git a/tools/validators-monitor/notification/notifier_test.go b/tools/validators-monitor/notification/notifier_test.go new file mode 100644 index 000000000..53d317d6b --- /dev/null +++ b/tools/validators-monitor/notification/notifier_test.go @@ -0,0 +1,118 @@ +package notification + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "math/big" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +func TestFormatRelayList(t *testing.T) { + if got := formatRelayList([]string{}); got != "None" { + t.Errorf("formatRelayList(empty) = %q; want \"None\"", got) + } + relays := []string{"relay1", "relay2"} + want := "- relay1\n- relay2\n" + if got := formatRelayList(relays); got != want { + t.Errorf("formatRelayList(%v) = %q; want %q", relays, got, want) + } +} + +func TestFormatWeiToEth(t *testing.T) { + cases := []struct { + wei *big.Int + want string + }{ + {big.NewInt(0), "0.000000 ETH"}, + {big.NewInt(params.Ether), "1.000000 ETH"}, + {big.NewInt(1234567890000000000), "1.234568 ETH"}, + } + for _, c := range cases { + if got := formatWeiToEth(c.wei); got != c.want { + t.Errorf("formatWeiToEth(%v) = %q; want %q", c.wei, got, c.want) + } + } +} + +func TestNewNotifier(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + n := NewNotifier([]string{}, logger) + if n.enabled { + t.Errorf("expected notifier disabled when webhookURLs are empty") + } + n2 := NewNotifier([]string{"http://example.com"}, logger) + if !n2.enabled { + t.Errorf("expected notifier enabled when webhookURLs provided") + } +} + +func TestSendMessage_Disabled(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + n := NewNotifier([]string{}, logger) + if err := n.SendMessage(context.Background(), Message{Text: "hello"}); err != nil { + t.Errorf("SendMessage disabled = error %v; want nil", err) + } +} + +func TestSendMessage_Success(t *testing.T) { + var received []byte + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + received = body + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + n := NewNotifier([]string{server.URL}, logger) + msg := Message{Text: "test"} + require.NoError(t, n.SendMessage(context.Background(), msg)) + + var got Message + require.NoError(t, json.Unmarshal(received, &got)) + require.Equal(t, msg.Text, got.Text) +} + +func TestSendMessage_NonOK(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + n := NewNotifier([]string{server.URL}, logger) + err := n.SendMessage(context.Background(), Message{Text: "err"}) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "one or more notifications failed")) +} + +func TestNotifyRelayData(t *testing.T) { + var payload Message + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&payload) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + n := NewNotifier([]string{server.URL}, logger) + relays := []string{"relay1"} + allRelays := []string{"relay1", "relay2"} + + err := n.NotifyRelayData(context.Background(), "0xabc", 123, 456, 789, big.NewInt(2e18), "0xfee", relays, allRelays, nil) + require.NoError(t, err) + + require.Len(t, payload.Attachments, 1) + attachment := payload.Attachments[0] + require.Equal(t, "#36a64f", attachment.Color) + require.Equal(t, "Relay Data Available for Validator", attachment.Title) +} diff --git a/tools/validators-monitor/notification/slack.go b/tools/validators-monitor/notification/slack.go deleted file mode 100644 index 2a00cbebd..000000000 --- a/tools/validators-monitor/notification/slack.go +++ /dev/null @@ -1,251 +0,0 @@ -package notification - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "log/slog" - "math/big" - "net/http" - "time" - - "github.com/ethereum/go-ethereum/params" - "github.com/primev/mev-commit/tools/validators-monitor/api" -) - -// SlackMessage represents a Slack message structure -type SlackMessage struct { - Text string `json:"text,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` -} - -// Attachment represents a Slack message attachment -type Attachment struct { - Color string `json:"color,omitempty"` - Title string `json:"title,omitempty"` - Text string `json:"text,omitempty"` - Fields []Field `json:"fields,omitempty"` - Footer string `json:"footer,omitempty"` - TS int64 `json:"ts,omitempty"` - MarkdownIn []string `json:"mrkdwn_in,omitempty"` -} - -// Field represents a field in a Slack message attachment -type Field struct { - Title string `json:"title"` - Value string `json:"value"` - Short bool `json:"short"` -} - -// SlackNotifier sends notifications to Slack -type SlackNotifier struct { - webhookURL string - client *http.Client - logger *slog.Logger - enabled bool -} - -// NewSlackNotifier creates a new Slack notifier -func NewSlackNotifier(webhookURL string, logger *slog.Logger) *SlackNotifier { - enabled := webhookURL != "" - - if !enabled { - logger.Warn("Slack notifications disabled - no webhook URL provided") - } else { - logger.Info("Slack notifications enabled") - } - - return &SlackNotifier{ - webhookURL: webhookURL, - client: &http.Client{ - Timeout: 10 * time.Second, - }, - logger: logger, - enabled: enabled, - } -} - -// SendMessage sends a message to Slack -func (n *SlackNotifier) SendMessage(ctx context.Context, message SlackMessage) error { - if !n.enabled { - n.logger.Debug("Slack notification skipped (disabled)") - return nil - } - - messageJSON, err := json.Marshal(message) - if err != nil { - return fmt.Errorf("failed to marshal slack message: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, "POST", n.webhookURL, bytes.NewBuffer(messageJSON)) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - - resp, err := n.client.Do(req) - if err != nil { - return fmt.Errorf("failed to send slack notification: %w", err) - } - //nolint:errcheck - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("slack API returned non-200 status code: %d", resp.StatusCode) - } - - n.logger.Debug("Slack notification sent successfully") - return nil -} - -// NotifyRelayData sends a notification about relay data for a validator -func (n *SlackNotifier) NotifyRelayData( - ctx context.Context, - pubkey string, - validatorIndex, - blockNumber, - slot uint64, - mevReward *big.Int, - feeReceipient string, - relaysWithData, - allRelays []string, - dashboardInfo *api.DashboardResponse, -) error { - if !n.enabled { - return nil - } - - color := "#36a64f" // Green for relays having data - if len(relaysWithData) == 0 { - color = "#ff9900" // Orange for no data available - } - - relaysWithDataStr := "None" - if len(relaysWithData) > 0 { - relaysWithDataStr = formatRelayList(relaysWithData) - } - - fields := []Field{ - { - Title: "Validator Index", - Value: fmt.Sprintf("%d", validatorIndex), - Short: true, - }, - { - Title: "Slot", - Value: fmt.Sprintf("%d", slot), - Short: true, - }, - { - Title: "Block Number", - Value: fmt.Sprintf("%d", blockNumber), - Short: true, - }, - { - Title: "Validator Pubkey", - Value: pubkey, - Short: false, - }, - { - Title: "Relays With Data", - Value: fmt.Sprintf("```%s```", relaysWithDataStr), - Short: false, - }, - { - Title: "Data Availability", - Value: fmt.Sprintf("%d of %d relays have data", len(relaysWithData), len(allRelays)), - Short: false, - }, - } - - title := "Relay Data Available for Validator" - if len(relaysWithData) == 0 { - title = "No Relay Data Found for Validator" - } - - message := SlackMessage{ - Attachments: []Attachment{ - { - Color: color, - Title: title, - Text: "Report on relay data for opted-in validator", - Fields: fields, - Footer: "Validator Monitor", - TS: time.Now().Unix(), - MarkdownIn: []string{"text", "fields"}, - }, - }, - } - - if dashboardInfo != nil { - attachment := &message.Attachments[0] - - attachment.Fields = append(attachment.Fields, Field{ - Title: "Block Winner", - Value: dashboardInfo.Winner, - Short: false, - }) - - attachment.Fields = append(attachment.Fields, Field{ - Title: "Commitments", - Value: fmt.Sprintf("%d (Rewards: %d, Slashes: %d)", - dashboardInfo.TotalOpenedCommitments, - dashboardInfo.TotalRewards, - dashboardInfo.TotalSlashes), - Short: false, - }) - - if dashboardInfo.TotalAmount != "" { - amountWei, ok := new(big.Int).SetString(dashboardInfo.TotalAmount, 10) - if ok { - attachment.Fields = append(attachment.Fields, Field{ - Title: "Total Bid Amount", - Value: formatWeiToEth(amountWei), - Short: true, - }) - } else { - // If conversion fails, show raw value - attachment.Fields = append(attachment.Fields, Field{ - Title: "Total Bid Amount (wei)", - Value: dashboardInfo.TotalAmount, - Short: true, - }) - } - } - attachment.Fields = append(attachment.Fields, Field{ - Title: "MEV Reward", - Value: formatWeiToEth(mevReward), - Short: true, - }) - attachment.Fields = append(attachment.Fields, Field{ - Title: "MEV Reward Recipient", - Value: feeReceipient, - Short: true, - }) - } - - return n.SendMessage(ctx, message) -} - -func formatRelayList(relays []string) string { - if len(relays) == 0 { - return "None" - } - - var result string - for _, r := range relays { - result += "- " + r + "\n" - } - return result -} - -func formatWeiToEth(wei *big.Int) string { - ethValue := new(big.Float).Quo( - new(big.Float).SetInt(wei), - new(big.Float).SetFloat64(params.Ether), - ) - - return fmt.Sprintf("%.6f ETH", ethValue) -} diff --git a/tools/validators-monitor/notification/slack_test.go b/tools/validators-monitor/notification/slack_test.go deleted file mode 100644 index 43564863c..000000000 --- a/tools/validators-monitor/notification/slack_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package notification - -import ( - "context" - "encoding/json" - "io" - "log/slog" - "math/big" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/params" - api "github.com/primev/mev-commit/tools/validators-monitor/api" - "github.com/stretchr/testify/require" -) - -func TestFormatRelayList(t *testing.T) { - if got := formatRelayList([]string{}); got != "None" { - t.Errorf("formatRelayList(empty) = %q; want \"None\"", got) - } - relays := []string{"relay1", "relay2"} - want := "- relay1\n- relay2\n" - if got := formatRelayList(relays); got != want { - t.Errorf("formatRelayList(%v) = %q; want %q", relays, got, want) - } -} - -func TestFormatWeiToEth(t *testing.T) { - cases := []struct { - wei *big.Int - want string - }{ - {big.NewInt(0), "0.000000 ETH"}, - {big.NewInt(params.Ether), "1.000000 ETH"}, - {big.NewInt(1234567890000000000), "1.234568 ETH"}, - } - for _, c := range cases { - if got := formatWeiToEth(c.wei); got != c.want { - t.Errorf("formatWeiToEth(%v) = %q; want %q", c.wei, got, c.want) - } - } -} - -func TestNewSlackNotifier(t *testing.T) { - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - n := NewSlackNotifier("", logger) - if n.enabled { - t.Errorf("expected notifier disabled when webhookURL is empty") - } - n2 := NewSlackNotifier("http://example.com", logger) - if !n2.enabled { - t.Errorf("expected notifier enabled when webhookURL provided") - } -} - -func TestSendMessage_Disabled(t *testing.T) { - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - n := NewSlackNotifier("", logger) - if err := n.SendMessage(context.Background(), SlackMessage{Text: "hello"}); err != nil { - t.Errorf("SendMessage disabled = error %v; want nil", err) - } -} - -func TestSendMessage_Success(t *testing.T) { - var received []byte - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - t.Errorf("expected method POST; got %s", r.Method) - } - if ct := r.Header.Get("Content-Type"); ct != "application/json" { - t.Errorf("expected Content-Type application/json; got %s", ct) - } - body, _ := io.ReadAll(r.Body) - received = body - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - n := NewSlackNotifier(server.URL, logger) - msg := SlackMessage{Text: "test"} - if err := n.SendMessage(context.Background(), msg); err != nil { - t.Fatalf("SendMessage = %v; want nil", err) - } - - var got SlackMessage - if err := json.Unmarshal(received, &got); err != nil { - t.Fatalf("failed to unmarshal request body: %v", err) - } - if got.Text != msg.Text { - t.Errorf("Sent message Text = %q; want %q", got.Text, msg.Text) - } -} - -func TestSendMessage_NonOK(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - n := NewSlackNotifier(server.URL, logger) - err := n.SendMessage(context.Background(), SlackMessage{Text: "err"}) - if err == nil || !strings.Contains(err.Error(), "non-200") { - t.Errorf("expected non-200 status error; got %v", err) - } -} - -func TestNotifyRelayData_NoDashboard(t *testing.T) { - var payload SlackMessage - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := json.NewDecoder(r.Body).Decode(&payload) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - n := NewSlackNotifier(server.URL, logger) - relays := []string{"r1", "r2"} - allRelays := []string{"r1", "r2", "r3"} - if err := n.NotifyRelayData(context.Background(), "0xabc", 1, 100, 10, big.NewInt(2e18), "0xabc", relays, allRelays, nil); err != nil { - t.Fatalf("NotifyRelayData = %v; want nil", err) - } - if len(payload.Attachments) != 1 { - t.Fatalf("expected 1 attachment; got %d", len(payload.Attachments)) - } - att := payload.Attachments[0] - if att.Color != "#36a64f" { - t.Errorf("Color = %q; want %q", att.Color, "#36a64f") - } - if att.Title != "Relay Data Available for Validator" { - t.Errorf("Title = %q; want %q", att.Title, "Relay Data Available for Validator") - } - fields := map[string]string{} - for _, f := range att.Fields { - fields[f.Title] = f.Value - } - wantFields := map[string]string{ - "Validator Index": "1", - "Slot": "10", - "Block Number": "100", - "Validator Pubkey": "0xabc", - "Relays With Data": "```- r1\n- r2\n```", - "Data Availability": "2 of 3 relays have data", - } - for k, v := range wantFields { - if got, ok := fields[k]; !ok { - t.Errorf("missing field %q", k) - } else if got != v { - t.Errorf("field %q = %q; want %q", k, got, v) - } - } -} - -func TestNotifyRelayData_WithDashboard(t *testing.T) { - var payload SlackMessage - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := json.NewDecoder(r.Body).Decode(&payload) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - n := NewSlackNotifier(server.URL, logger) - relays := []string{} - all := []string{"a", "b", "c"} - info := &api.DashboardResponse{ - Winner: "relayX", - TotalOpenedCommitments: 5, - TotalRewards: 8, - TotalSlashes: 1, - TotalAmount: "5000000000000000000", - } - if err := n.NotifyRelayData(context.Background(), "0xdef", 2, 200, 20, big.NewInt(3e18), "0xabc", relays, all, info); err != nil { - t.Fatalf("NotifyRelayData = %v; want nil", err) - } - if len(payload.Attachments) != 1 { - t.Fatalf("expected 1 attachment; got %d", len(payload.Attachments)) - } - att := payload.Attachments[0] - if att.Color != "#ff9900" { - t.Errorf("Color = %q; want %q", att.Color, "#ff9900") - } - if att.Title != "No Relay Data Found for Validator" { - t.Errorf("Title = %q; want %q", att.Title, "No Relay Data Found for Validator") - } - fields := map[string]string{} - for _, f := range att.Fields { - fields[f.Title] = f.Value - } - wantFields := map[string]string{ - "Validator Index": "2", - "Slot": "20", - "Block Number": "200", - "Validator Pubkey": "0xdef", - "Relays With Data": "```None```", - "Data Availability": "0 of 3 relays have data", - "Block Winner": "relayX", - "Commitments": "5 (Rewards: 8, Slashes: 1)", - "Total Bid Amount": "5.000000 ETH", - "MEV Reward": "3.000000 ETH", - "MEV Reward Recipient": "0xabc", - } - for k, v := range wantFields { - if got, ok := fields[k]; !ok { - t.Errorf("missing field %q", k) - } else if got != v { - t.Errorf("field %q = %q; want %q", k, got, v) - } - } -} From 4f85efbf62089023a91362afc53857a03946f53b Mon Sep 17 00:00:00 2001 From: kant Date: Wed, 2 Jul 2025 23:25:30 -0700 Subject: [PATCH 3/3] handling errors gracefully --- tools/validators-monitor/config/config.go | 2 +- tools/validators-monitor/main.go | 12 ++++----- tools/validators-monitor/monitor/monitor.go | 2 +- .../notification/notifier.go | 25 +++++++++++++------ tools/validators-monitor/service/service.go | 2 +- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/tools/validators-monitor/config/config.go b/tools/validators-monitor/config/config.go index 8f7f22881..581b4d308 100644 --- a/tools/validators-monitor/config/config.go +++ b/tools/validators-monitor/config/config.go @@ -9,7 +9,7 @@ type Config struct { EthereumRPCURL string `json:"ethereum_rpc_url"` ValidatorOptInContract string `json:"contract_address"` RelayURLs []string `json:"relay_urls"` - SlackWebhookURL string `json:"slack_webhook_url"` + WebhookURLs []string `json:"webhook_urls"` DashboardApiUrl string `json:"dashboard_api_url"` HealthPort int `json:"health_port"` LaggardMode *big.Int `json:"laggard_mode"` diff --git a/tools/validators-monitor/main.go b/tools/validators-monitor/main.go index 459184e3a..12978080a 100644 --- a/tools/validators-monitor/main.go +++ b/tools/validators-monitor/main.go @@ -48,10 +48,10 @@ var ( ), } - optionSlackWebhook = &cli.StringFlag{ - Name: "slack-webhook", - Usage: "Slack webhook URL for notifications", - EnvVars: []string{"SLACK_WEBHOOK_URL"}, + optionWebhookUrls = &cli.StringSliceFlag{ + Name: "webhooks", + Usage: "webhook URLs for notifications", + EnvVars: []string{"WEBHOOK_URLS"}, } optionDashboardApiUrl = &cli.StringFlag{ @@ -187,7 +187,7 @@ func main() { optionEthereumRpcUrl, optionValidatorOptInContract, optionTrackMissed, - optionSlackWebhook, + optionWebhookUrls, optionDashboardApiUrl, optionRelayUrls, optionHealthPort, @@ -220,7 +220,7 @@ func main() { EthereumRPCURL: c.String(optionEthereumRpcUrl.Name), ValidatorOptInContract: c.String(optionValidatorOptInContract.Name), FetchIntervalSec: 12, // Use epoch duration - SlackWebhookURL: c.String(optionSlackWebhook.Name), + WebhookURLs: c.StringSlice(optionWebhookUrls.Name), DashboardApiUrl: c.String(optionDashboardApiUrl.Name), RelayURLs: c.StringSlice(optionRelayUrls.Name), HealthPort: c.Int(optionHealthPort.Name), diff --git a/tools/validators-monitor/monitor/monitor.go b/tools/validators-monitor/monitor/monitor.go index d6fc0ef3f..b99e7967a 100644 --- a/tools/validators-monitor/monitor/monitor.go +++ b/tools/validators-monitor/monitor/monitor.go @@ -152,7 +152,7 @@ func New( beacon: beaconClient, relay: api.NewRelayClient(cfg.RelayURLs, log, httpClient), dashboard: dashboardClient, - notifier: notification.NewSlackNotifier(cfg.SlackWebhookURL, log), + notifier: notification.NewNotifier(cfg.WebhookURLs, log), optChecker: optInChecker, dutiesCache: make(map[uint64]cachedDuties), processedBlocks: make(map[uint64]time.Time), diff --git a/tools/validators-monitor/notification/notifier.go b/tools/validators-monitor/notification/notifier.go index e394c791e..362a07d17 100644 --- a/tools/validators-monitor/notification/notifier.go +++ b/tools/validators-monitor/notification/notifier.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" + "io" "log/slog" "math/big" "net/http" @@ -79,12 +81,12 @@ func (n *Notifier) SendMessage(ctx context.Context, message Message) error { return fmt.Errorf("failed to marshal message: %w", err) } - var errorOccurred bool + var errs []error for _, webhookURL := range n.webhookURLs { req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(messageJSON)) if err != nil { n.logger.Error("Failed to create request", "webhook", webhookURL, "error", err) - errorOccurred = true + errs = append(errs, fmt.Errorf("create request (%s): %w", webhookURL, err)) continue } @@ -93,21 +95,30 @@ func (n *Notifier) SendMessage(ctx context.Context, message Message) error { resp, err := n.client.Do(req) if err != nil { n.logger.Error("Failed to send notification", "webhook", webhookURL, "error", err) - errorOccurred = true + errs = append(errs, fmt.Errorf("send notification (%s): %w", webhookURL, err)) continue } - defer resp.Body.Close() + + if _, err := io.Copy(io.Discard, resp.Body); err != nil { + n.logger.Error("Failed to read response body", "webhook", webhookURL, "error", err) + errs = append(errs, fmt.Errorf("read response body (%s): %w", webhookURL, err)) + } + + if err := resp.Body.Close(); err != nil { + n.logger.Error("Failed to close response body", "webhook", webhookURL, "error", err) + errs = append(errs, fmt.Errorf("close response body (%s): %w", webhookURL, err)) + } if resp.StatusCode != http.StatusOK { n.logger.Error("Notification API returned non-200 status code", "webhook", webhookURL, "status", resp.StatusCode) - errorOccurred = true + errs = append(errs, fmt.Errorf("non-200 status (%s): %d", webhookURL, resp.StatusCode)) continue } n.logger.Debug("Notification sent successfully", "webhook", webhookURL) } - if errorOccurred { - return fmt.Errorf("one or more notifications failed") + if len(errs) > 0 { + return fmt.Errorf("one or more notifications failed: %w", errors.Join(errs...)) } return nil } diff --git a/tools/validators-monitor/service/service.go b/tools/validators-monitor/service/service.go index 2f89faadf..a38ac9d00 100644 --- a/tools/validators-monitor/service/service.go +++ b/tools/validators-monitor/service/service.go @@ -37,7 +37,7 @@ func New( FetchIntervalSec: cfg.FetchIntervalSec, EthereumRPCURL: cfg.EthereumRPCURL, ValidatorOptInContract: cfg.ValidatorOptInContract, - SlackWebhookURL: cfg.SlackWebhookURL, + WebhookURLs: cfg.WebhookURLs, RelayURLs: cfg.RelayURLs, DashboardApiUrl: cfg.DashboardApiUrl, LaggardMode: cfg.LaggardMode,