├── metrics ├── metrics_test.go ├── utils.go ├── networkstats_test.go ├── networkstats.go ├── blockdata_test.go ├── relayrewards_test.go ├── relayrewards.go ├── proposalduties_test.go ├── proposalduties.go ├── beaconstate_test.go ├── blockdata.go ├── metrics.go └── beaconstate.go ├── .gitignore ├── keys.txt ├── Dockerfile ├── .github └── workflows │ ├── tests.yml │ └── release.yml ├── db ├── db_test.go └── db.go ├── price └── price.go ├── schemas └── schemas.go ├── config └── config.go ├── main.go ├── pools ├── pools.go └── pools_test.go ├── README.md ├── go.mod └── go.sum /metrics/metrics_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | eth-metrics 3 | db.db 4 | eth-deposits 5 | .vscode -------------------------------------------------------------------------------- /keys.txt: -------------------------------------------------------------------------------- 1 | 0xaddc693f9090db30a9aae27c047a95245f60313f574fb32729dd06341db55c743e64ba0709ee74181750b6da5f234b44 2 | 0xb6ba7d587c26ca22fd9b306c2f6708c3d998269a81e09aa1298db37ed3ca0a355c46054cb3d3dfd220461465b1bdf267 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25-alpine AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN apk add --update gcc g++ 8 | RUN go mod download 9 | RUN go build -o /eth-metrics 10 | 11 | FROM golang:1.25-alpine 12 | 13 | WORKDIR / 14 | 15 | COPY --from=build /eth-metrics /eth-metrics 16 | 17 | ENTRYPOINT ["/eth-metrics"] 18 | -------------------------------------------------------------------------------- /metrics/utils.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | func BoolToUint64(in bool) uint64 { 8 | if in { 9 | return uint64(1) 10 | } 11 | return uint64(0) 12 | } 13 | 14 | func ToBytes48(x []byte) [48]byte { 15 | var y [48]byte 16 | copy(y[:], x) 17 | return y 18 | } 19 | 20 | func UToStr(x uint64) string { 21 | return strconv.FormatUint(x, 10) 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Tests 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.24.x] 8 | #macos-latest, windows-latest 9 | os: [ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | - name: Run tests 19 | run: | 20 | cd metrics 21 | go test -v . 22 | -------------------------------------------------------------------------------- /db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | "time" 7 | 8 | "github.com/bilinearlabs/eth-metrics/schemas" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_GetMissingEpochs(t *testing.T) { 13 | db, err := New(":memory:") 14 | require.NoError(t, err) 15 | 16 | db.CreateTables() 17 | 18 | db.StoreValidatorPerformance(schemas.ValidatorPerformanceMetrics{ 19 | Time: time.Now(), 20 | Epoch: 100, 21 | EarnedBalance: big.NewInt(100), 22 | LosedBalance: big.NewInt(100), 23 | EffectiveBalance: big.NewInt(100), 24 | MEVRewards: big.NewInt(100), 25 | ProposerTips: big.NewInt(100), 26 | }) 27 | 28 | epochs, err := db.GetMissingEpochs(200, 4) 29 | require.NoError(t, err) 30 | require.Equal(t, []uint64{197, 198, 199, 200}, epochs) 31 | 32 | db.StoreValidatorPerformance(schemas.ValidatorPerformanceMetrics{ 33 | Time: time.Now(), 34 | Epoch: 197, 35 | EarnedBalance: big.NewInt(100), 36 | LosedBalance: big.NewInt(100), 37 | EffectiveBalance: big.NewInt(100), 38 | MEVRewards: big.NewInt(100), 39 | ProposerTips: big.NewInt(100), 40 | }) 41 | 42 | epochs, err = db.GetMissingEpochs(200, 4) 43 | require.NoError(t, err) 44 | require.Equal(t, []uint64{198, 199, 200}, epochs) 45 | 46 | epochs, err = db.GetMissingEpochs(200, 0) 47 | require.NoError(t, err) 48 | require.Equal(t, []uint64{}, epochs) 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | if: startsWith(github.ref, 'refs/tags') 10 | steps: 11 | - name: Extract Version 12 | id: version_step 13 | run: | 14 | echo "##[set-output name=version;]VERSION=${GITHUB_REF#$"refs/tags/v"}" 15 | echo "##[set-output name=version_tag;]$GITHUB_REPOSITORY:${GITHUB_REF#$"refs/tags/v"}" 16 | echo "##[set-output name=latest_tag;]$GITHUB_REPOSITORY:latest" 17 | - name: Print Version 18 | run: | 19 | echo ${{steps.version_step.outputs.version_tag}} 20 | echo ${{steps.version_step.outputs.latest_tag}} 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v1 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | 27 | - name: Login to DockerHub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: PrepareReg Names 34 | id: read-docker-image-identifiers 35 | run: | 36 | echo VERSION_TAG=$(echo ${{ steps.version_step.outputs.version_tag }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 37 | echo LASTEST_TAG=$(echo ${{ steps.version_step.outputs.latest_tag }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 38 | - name: Build and push 39 | id: docker_build 40 | uses: docker/build-push-action@v2 41 | with: 42 | push: true 43 | tags: | 44 | ${{env.VERSION_TAG}} 45 | ${{env.LASTEST_TAG}} 46 | build-args: | 47 | ${{steps.version_step.outputs.version}} 48 | -------------------------------------------------------------------------------- /metrics/networkstats_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/attestantio/go-eth2-client/spec" 7 | "github.com/attestantio/go-eth2-client/spec/deneb" 8 | "github.com/attestantio/go-eth2-client/spec/fulu" 9 | "github.com/attestantio/go-eth2-client/spec/phase0" 10 | "github.com/bilinearlabs/eth-metrics/db" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestGetNetworkStats_Success(t *testing.T) { 15 | networkStats, err := NewNetworkStats(&db.Database{}) 16 | if err != nil { 17 | t.Fatalf("Error creating network stats: %v", err) 18 | } 19 | // 1 slashed validator, 2 exited validators, 1 active validator 20 | validator_0 := ToBytes48([]byte{10}) 21 | validator_1 := ToBytes48([]byte{20}) 22 | validator_2 := ToBytes48([]byte{30}) 23 | beaconState := &spec.VersionedBeaconState{ 24 | Fulu: &fulu.BeaconState{ 25 | Validators: []*phase0.Validator{ 26 | { 27 | PublicKey: validator_0, 28 | Slashed: true, 29 | ExitEpoch: 1, 30 | ActivationEpoch: 0, 31 | }, 32 | { 33 | PublicKey: validator_1, 34 | Slashed: false, 35 | ExitEpoch: 0, 36 | ActivationEpoch: 0, 37 | }, 38 | { 39 | PublicKey: validator_2, 40 | Slashed: false, 41 | ExitEpoch: 2, 42 | ActivationEpoch: 0, 43 | }, 44 | }, 45 | LatestExecutionPayloadHeader: &deneb.ExecutionPayloadHeader{ 46 | Timestamp: 1673308800, 47 | }, 48 | }, 49 | } 50 | networkStatsResult, err := networkStats.GetNetworkStats(1, beaconState) 51 | assert.NoError(t, err) 52 | assert.Equal(t, uint64(1), networkStatsResult.NOfSlashedValidators) 53 | assert.Equal(t, uint64(2), networkStatsResult.NOfExitedValidators) 54 | assert.Equal(t, uint64(1), networkStatsResult.NOfActiveValidators) 55 | assert.NotNil(t, networkStatsResult) 56 | } 57 | -------------------------------------------------------------------------------- /price/price.go: -------------------------------------------------------------------------------- 1 | package price 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bilinearlabs/eth-metrics/config" 7 | "github.com/bilinearlabs/eth-metrics/db" 8 | "github.com/pkg/errors" 9 | log "github.com/sirupsen/logrus" 10 | gecko "github.com/superoo7/go-gecko/v3" 11 | ) 12 | 13 | var vc = []string{"usd", "eurr"} 14 | 15 | type Price struct { 16 | database *db.Database 17 | coingecko *gecko.Client 18 | config *config.Config 19 | } 20 | 21 | func NewPrice(dbPath string, config *config.Config) (*Price, error) { 22 | 23 | cg := gecko.NewClient(nil) 24 | 25 | var database *db.Database 26 | var err error 27 | if dbPath != "" { 28 | database, err = db.New(dbPath) 29 | if err != nil { 30 | return nil, errors.Wrap(err, "could not create postgresql") 31 | } 32 | err = database.CreateEthPriceTable() 33 | if err != nil { 34 | return nil, errors.Wrap(err, "error creating pool table to store data") 35 | } 36 | } 37 | 38 | return &Price{ 39 | database: database, 40 | coingecko: cg, 41 | config: config, 42 | }, nil 43 | } 44 | 45 | func (p *Price) GetEthPrice() { 46 | id := "" 47 | if p.config.Network == "ethereum" { 48 | id = "ethereum" 49 | } else if p.config.Network == "gnosis" { 50 | id = "gnosis" 51 | } else { 52 | log.Fatal("Network not supported: ", p.config.Network) 53 | } 54 | 55 | sp, err := p.coingecko.SimplePrice([]string{id}, vc) 56 | if err != nil { 57 | log.Error(err) 58 | } 59 | 60 | eth := (*sp)[id] 61 | ethPriceUsd := eth["usd"] 62 | 63 | logPrice(ethPriceUsd) 64 | 65 | if p.database != nil { 66 | err := p.database.StoreEthPrice(ethPriceUsd) 67 | if err != nil { 68 | log.Error(err) 69 | } 70 | } 71 | } 72 | 73 | func (p *Price) Run() { 74 | todoSetAsFlag := 30 * time.Minute 75 | ticker := time.NewTicker(todoSetAsFlag) 76 | for ; true; <-ticker.C { 77 | p.GetEthPrice() 78 | } 79 | } 80 | 81 | func logPrice(price float32) { 82 | log.Info("Ethereum price in USD: ", price) 83 | } 84 | -------------------------------------------------------------------------------- /schemas/schemas.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | ) 7 | 8 | type ValidatorPerformanceMetrics struct { 9 | Time time.Time 10 | PoolName string 11 | Epoch uint64 12 | NOfActiveValidators uint64 13 | NOfTotalVotes uint64 14 | NOfIncorrectSource uint64 15 | NOfIncorrectTarget uint64 16 | NOfIncorrectHead uint64 17 | NOfValidatingKeys uint64 18 | NOfValsWithLessBalance uint64 // TODO: Deprecate, same as array length 19 | EarnedBalance *big.Int 20 | LosedBalance *big.Int 21 | MissedAttestationsKeys []string // TODO: Deprecate in favor of IndexesMissedAtt 22 | LostBalanceKeys []string // TODO: Depercate in favor of IndexesLessBalance 23 | IndexesMissedAtt []uint64 24 | IndexesLessBalance []uint64 25 | TotalBalance *big.Int 26 | EffectiveBalance *big.Int 27 | TotalRewards *big.Int 28 | DeltaEpochBalance *big.Int 29 | MEVRewards *big.Int 30 | ProposerTips *big.Int 31 | } 32 | 33 | type ValidatorStatusMetrics struct { 34 | // custom field: vals with active duties 35 | Validating uint64 36 | 37 | // TODO: num of slashed validators 38 | // note that after slashing->exited 39 | 40 | // maps 1:1 with eth2 spec status 41 | Unknown uint64 42 | Deposited uint64 43 | Pending uint64 44 | Active uint64 45 | Exiting uint64 46 | Slashing uint64 47 | Exited uint64 48 | Invalid uint64 49 | PartiallyDeposited uint64 50 | } 51 | 52 | type RewardsMetrics struct { 53 | Epoch uint64 54 | TotalDeposits *big.Int 55 | CumulativeRewards *big.Int 56 | } 57 | 58 | type ProposalDutiesMetrics struct { 59 | Epoch uint64 60 | Scheduled []Duty 61 | Proposed []Duty 62 | Missed []Duty 63 | } 64 | 65 | type Duty struct { 66 | ValIndex uint64 67 | Slot uint64 68 | Graffiti string 69 | } 70 | 71 | type NetworkStats struct { 72 | Time time.Time 73 | Epoch uint64 74 | NOfActiveValidators uint64 75 | NOfExitedValidators uint64 76 | NOfSlashedValidators uint64 77 | } 78 | -------------------------------------------------------------------------------- /metrics/networkstats.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/attestantio/go-eth2-client/spec" 7 | "github.com/bilinearlabs/eth-metrics/db" 8 | "github.com/bilinearlabs/eth-metrics/schemas" 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type NetworkStats struct { 14 | database *db.Database 15 | } 16 | 17 | func NewNetworkStats( 18 | database *db.Database, 19 | ) (*NetworkStats, error) { 20 | return &NetworkStats{ 21 | database: database, 22 | }, nil 23 | } 24 | 25 | func (n *NetworkStats) Run( 26 | currentEpoch uint64, 27 | currentBeaconState *spec.VersionedBeaconState, 28 | ) error { 29 | if n.database == nil { 30 | return errors.New("database is nil") 31 | } 32 | 33 | if currentBeaconState == nil { 34 | return errors.New("current beacon state is nil") 35 | } 36 | 37 | networkStats, err := n.GetNetworkStats(currentEpoch, currentBeaconState) 38 | if err != nil { 39 | return errors.Wrap(err, "error getting network stats") 40 | } 41 | 42 | if n.database != nil { 43 | err = n.database.StoreNetworkMetrics(networkStats) 44 | if err != nil { 45 | return errors.Wrap(err, "could not store network stats") 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (n *NetworkStats) GetNetworkStats( 53 | currentEpoch uint64, 54 | beaconState *spec.VersionedBeaconState, 55 | ) (schemas.NetworkStats, error) { 56 | networkStats := schemas.NetworkStats{ 57 | Time: time.Unix(int64(GetTimestamp(beaconState)), 0), 58 | Epoch: currentEpoch, 59 | NOfActiveValidators: 0, 60 | NOfExitedValidators: 0, 61 | NOfSlashedValidators: 0, 62 | } 63 | validators := GetValidators(beaconState) 64 | 65 | for _, val := range validators { 66 | if val.Slashed { 67 | networkStats.NOfSlashedValidators++ 68 | } 69 | if uint64(val.ExitEpoch) <= currentEpoch { 70 | networkStats.NOfExitedValidators++ 71 | } else if uint64(val.ActivationEpoch) <= currentEpoch { 72 | networkStats.NOfActiveValidators++ 73 | } 74 | } 75 | 76 | log.WithFields(log.Fields{ 77 | "Total Validators": len(validators), 78 | "Total Slashed Validators": networkStats.NOfSlashedValidators, 79 | "Total Exited Validators": networkStats.NOfExitedValidators, 80 | "Total Active Validators": networkStats.NOfActiveValidators, 81 | }).Info("Network stats:") 82 | 83 | return networkStats, nil 84 | } 85 | -------------------------------------------------------------------------------- /metrics/blockdata_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "math/big" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/attestantio/go-eth2-client/spec" 13 | "github.com/ethereum/go-ethereum/core/types" 14 | "github.com/pkg/errors" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func Test_GetProposerTip(t *testing.T) { 19 | bd := &BlockData{ 20 | networkParameters: &NetworkParameters{ 21 | slotsInEpoch: 32, 22 | }, 23 | } 24 | 25 | blockData, err := LoadBlockData(5214302) 26 | if err != nil { 27 | t.Fatalf("error loading block data: %s", err) 28 | } 29 | 30 | proposerTip, err := bd.GetProposerTip(blockData.BeaconBlock, blockData.Header, blockData.Receipts) 31 | if err != nil { 32 | t.Fatalf("error getting proposer tip: %s", err) 33 | } 34 | tip := big.NewInt(38657065851824731) 35 | assert.Equal(t, proposerTip, tip) 36 | } 37 | 38 | func Test_ExtractWithdrawals(t *testing.T) { 39 | bd := &BlockData{ 40 | networkParameters: &NetworkParameters{ 41 | slotsInEpoch: 32, 42 | }, 43 | } 44 | 45 | blockData, err := LoadBlockData(5214302) 46 | if err != nil { 47 | t.Fatalf("error loading block data: %s", err) 48 | } 49 | 50 | withdrawals := make(map[uint64]*big.Int) 51 | bd.ExtractWithdrawals(blockData.BeaconBlock, withdrawals) 52 | assert.Equal(t, withdrawals, map[uint64]*big.Int{ 53 | 416729: big.NewInt(1701196), 54 | 416730: big.NewInt(1731482), 55 | 416731: big.NewInt(1683530), 56 | 416732: big.NewInt(1765666), 57 | 416733: big.NewInt(1753893), 58 | 416734: big.NewInt(45764133), 59 | 416735: big.NewInt(1740038), 60 | 416736: big.NewInt(1736192), 61 | 416737: big.NewInt(1732742), 62 | 416738: big.NewInt(1776043), 63 | 416739: big.NewInt(1746233), 64 | 416740: big.NewInt(1713045), 65 | 416741: big.NewInt(1761575), 66 | 416742: big.NewInt(1719014), 67 | 416743: big.NewInt(1735415), 68 | 416744: big.NewInt(1714423), 69 | }) 70 | } 71 | 72 | type MockBlockData struct { 73 | BeaconBlock *spec.VersionedSignedBeaconBlock `json:"consensus_block"` 74 | Header *types.Header `json:"execution_header"` 75 | Receipts []*types.Receipt `json:"execution_receipts"` 76 | } 77 | 78 | func LoadBlockData(slot uint64) (*MockBlockData, error) { 79 | path := filepath.Join("../mock", fmt.Sprintf("fullblock_slot_%d.json", slot)) 80 | jsonFile, err := os.Open(path) 81 | if err != nil { 82 | return nil, errors.Wrap(err, "could not open json file") 83 | } 84 | defer jsonFile.Close() 85 | 86 | byteValue, err := io.ReadAll(jsonFile) 87 | if err != nil { 88 | return nil, errors.Wrap(err, "could not read json file") 89 | } 90 | 91 | var blockData MockBlockData 92 | 93 | err = json.Unmarshal(byteValue, &blockData) 94 | if err != nil { 95 | return nil, errors.Wrap(err, "could not unmarshal json file") 96 | } 97 | 98 | return &blockData, nil 99 | } 100 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // By default the release is a custom build. CI takes care of upgrading it with 11 | // go build -v -ldflags="-X 'github.com/bilinearlabs/eth-metrics/config.ReleaseVersion=x.y.z'" 12 | var ReleaseVersion = "custom-build" 13 | 14 | type Config struct { 15 | PoolNames []string 16 | ValidatorsFile string 17 | DatabasePath string 18 | Eth1Address string 19 | Eth2Address string 20 | EpochDebug string 21 | Verbosity string 22 | Network string 23 | Credentials string 24 | BackfillEpochs uint64 25 | StateTimeout int 26 | } 27 | 28 | // custom implementation to allow providing the same flag multiple times 29 | // --flag=value1 --flag=value2 30 | type arrayFlags []string 31 | 32 | func (i *arrayFlags) String() string { 33 | return "" 34 | } 35 | 36 | func (i *arrayFlags) Set(value string) error { 37 | *i = append(*i, value) 38 | return nil 39 | } 40 | 41 | func NewCliConfig() (*Config, error) { 42 | var poolNames arrayFlags 43 | 44 | // Allows passing multiple times 45 | flag.Var(&poolNames, "pool-name", "Pool name to monitor. Can be useed multiple times") 46 | 47 | var validatorsFile = flag.String("validators-file", "", "csv file with entities and their validator keys") 48 | var version = flag.Bool("version", false, "Prints the release version and exits") 49 | var network = flag.String("network", "ethereum", "ethereum|gnosis") 50 | var databasePath = flag.String("database-path", "", "Database path: db.db (optional)") 51 | var eth1Address = flag.String("eth1address", "", "Ethereum 1 http endpoint. To be used by rocket pool") 52 | var eth2Address = flag.String("eth2address", "", "Ethereum 2 http endpoint") 53 | var stateTimeout = flag.Int("state-timeout", 60, "Timeout in seconds for fetching the beacon state") 54 | var epochDebug = flag.String("epoch-debug", "", "Calculates the stats for a given epoch and exits, useful for debugging") 55 | var verbosity = flag.String("verbosity", "info", "Logging verbosity (trace, debug, info=default, warn, error, fatal, panic)") 56 | var credentials = flag.String("credentials", "", "Credentials for the http client (username:password)") 57 | var backfillEpochs = flag.Uint64("backfill-epochs", 0, "Number of epochs to backfill") 58 | 59 | flag.Parse() 60 | 61 | if *version { 62 | log.Info("Version: ", ReleaseVersion) 63 | os.Exit(0) 64 | } 65 | 66 | conf := &Config{ 67 | PoolNames: poolNames, 68 | ValidatorsFile: *validatorsFile, 69 | DatabasePath: *databasePath, 70 | Eth1Address: *eth1Address, 71 | Eth2Address: *eth2Address, 72 | EpochDebug: *epochDebug, 73 | Verbosity: *verbosity, 74 | Network: *network, 75 | Credentials: *credentials, 76 | BackfillEpochs: *backfillEpochs, 77 | StateTimeout: *stateTimeout, 78 | } 79 | logConfig(conf) 80 | return conf, nil 81 | } 82 | 83 | func logConfig(cfg *Config) { 84 | log.WithFields(log.Fields{ 85 | "PoolNames": cfg.PoolNames, 86 | "ValidatorsFile": cfg.ValidatorsFile, 87 | "DatabasePath": cfg.DatabasePath, 88 | "Eth1Address": cfg.Eth1Address, 89 | "Eth2Address": cfg.Eth2Address, 90 | "EpochDebug": cfg.EpochDebug, 91 | "Verbosity": cfg.Verbosity, 92 | "Network": cfg.Network, 93 | "Credentials": "***", 94 | "BackfillEpochs": cfg.BackfillEpochs, 95 | "StateTimeout": cfg.StateTimeout, 96 | }).Info("Cli Config:") 97 | } 98 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/bilinearlabs/eth-metrics/config" 13 | "github.com/bilinearlabs/eth-metrics/metrics" 14 | "github.com/bilinearlabs/eth-metrics/price" 15 | "github.com/gin-contrib/cors" 16 | "github.com/gin-gonic/gin" 17 | _ "github.com/mattn/go-sqlite3" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var db *sql.DB 22 | 23 | func main() { 24 | config, err := config.NewCliConfig() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | logLevel, err := log.ParseLevel(config.Verbosity) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | log.SetLevel(logLevel) 34 | 35 | metrics, err := metrics.NewMetrics( 36 | context.Background(), 37 | config) 38 | 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | price, err := price.NewPrice(config.DatabasePath, config) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // Initialize the database 49 | db, err = sql.Open("sqlite3", config.DatabasePath) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | defer db.Close() 54 | 55 | // Set up the Gin server 56 | r := gin.Default() 57 | r.Use(cors.Default()) 58 | 59 | gin.SetMode(gin.ReleaseMode) 60 | 61 | r.POST("/query", func(c *gin.Context) { 62 | var query struct { 63 | SQL string `json:"sql"` 64 | } 65 | 66 | if err := c.BindJSON(&query); err != nil { 67 | c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 68 | return 69 | } 70 | 71 | if !isSafeQuery(query.SQL) { 72 | c.JSON(http.StatusBadRequest, gin.H{"error": "Unsafe query detected"}) 73 | return 74 | } 75 | 76 | rows, err := executeQuery(query.SQL) 77 | if err != nil { 78 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 79 | return 80 | } 81 | 82 | c.JSON(http.StatusOK, gin.H{"data": rows}) 83 | }) 84 | 85 | // Run the server in a goroutine 86 | go func() { 87 | if err := r.Run(); err != nil { 88 | log.Fatal("Failed to run server: ", err) 89 | } 90 | }() 91 | 92 | go price.Run() 93 | metrics.Run() 94 | 95 | // Wait for signal. 96 | sigCh := make(chan os.Signal, 1) 97 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 98 | for { 99 | sig := <-sigCh 100 | if sig == syscall.SIGINT || sig == syscall.SIGTERM || sig == os.Interrupt || sig == os.Kill { 101 | break 102 | } 103 | } 104 | 105 | log.Info("Stopping eth-metrics") 106 | } 107 | 108 | // TODO: Move all api logic to a separate file 109 | func isSafeQuery(query string) bool { 110 | query = strings.ToLower(query) 111 | unsafeKeywords := []string{"drop", "delete", "update", "insert", "alter", "create", "replace"} 112 | for _, keyword := range unsafeKeywords { 113 | if strings.Contains(query, keyword) { 114 | return false 115 | } 116 | } 117 | return true 118 | } 119 | 120 | func executeQuery(query string) ([]map[string]interface{}, error) { 121 | rows, err := db.Query(query) 122 | if err != nil { 123 | return nil, err 124 | } 125 | defer rows.Close() 126 | 127 | columns, err := rows.Columns() 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | results := make([]map[string]interface{}, 0) 133 | for rows.Next() { 134 | columnsData := make([]interface{}, len(columns)) 135 | columnsPointers := make([]interface{}, len(columns)) 136 | for i := range columnsData { 137 | columnsPointers[i] = &columnsData[i] 138 | } 139 | 140 | if err := rows.Scan(columnsPointers...); err != nil { 141 | return nil, err 142 | } 143 | 144 | rowMap := make(map[string]interface{}) 145 | for i, colName := range columns { 146 | val := columnsData[i] 147 | b, ok := val.([]byte) 148 | if ok { 149 | rowMap[colName] = string(b) 150 | } else { 151 | rowMap[colName] = val 152 | } 153 | } 154 | results = append(results, rowMap) 155 | } 156 | 157 | return results, nil 158 | } 159 | -------------------------------------------------------------------------------- /metrics/relayrewards_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/avast/retry-go/v4" 11 | "github.com/bilinearlabs/eth-metrics/config" 12 | "github.com/flashbots/mev-boost-relay/common" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestGetRelayRewards_Success(t *testing.T) { 17 | // Create a test server that returns valid rewards 18 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | // Verify the request path 20 | assert.Contains(t, r.URL.Path, "/relay/v1/data/bidtraces/proposer_payload_delivered") 21 | 22 | // Return mock rewards data 23 | payloads := []common.BidTraceV2JSON{ 24 | { 25 | ProposerPubkey: "0x1234567890abcdef", 26 | Value: "1000000000000000000", 27 | }, 28 | { 29 | ProposerPubkey: "0xabcdef1234567890", 30 | Value: "2000000000000000000", 31 | }, 32 | } 33 | w.Header().Set("Content-Type", "application/json") 34 | json.NewEncoder(w).Encode(payloads) 35 | })) 36 | defer server.Close() 37 | 38 | RELAY_SERVERS = []string{server.URL} 39 | 40 | networkParams := &NetworkParameters{ 41 | slotsInEpoch: 2, 42 | } 43 | validatorKeyToPool := map[string]string{ 44 | "0x1234567890abcdef": "pool1", 45 | "0xabcdef1234567890": "pool2", 46 | } 47 | cfg := &config.Config{} 48 | 49 | relayRewards, err := NewRelayRewards(networkParams, validatorKeyToPool, cfg) 50 | assert.NoError(t, err) 51 | 52 | // Call GetRelayRewards 53 | rewards, slotsWithRewards, err := relayRewards.GetRelayRewards(0) 54 | assert.NoError(t, err) 55 | assert.NotNil(t, rewards) 56 | assert.NotNil(t, slotsWithRewards) 57 | 58 | // Verify rewards are aggregated correctly 59 | // Each slot (2 slots) * each relay server (1 server) = 2 requests 60 | // pool1: 2 * 1 ETH = 2 ETH 61 | // pool2: 2 * 2 ETH = 4 ETH 62 | assert.Equal(t, big.NewInt(2000000000000000000), rewards["pool1"]) 63 | assert.Equal(t, big.NewInt(4000000000000000000), rewards["pool2"]) 64 | assert.Len(t, slotsWithRewards, 2) 65 | } 66 | 67 | func TestGetRelayRewards_HTTPError(t *testing.T) { 68 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 | w.WriteHeader(http.StatusInternalServerError) 70 | w.Write([]byte("Internal Server Error")) 71 | })) 72 | defer server.Close() 73 | 74 | RELAY_SERVERS = []string{server.URL} 75 | 76 | networkParams := &NetworkParameters{ 77 | slotsInEpoch: 1, 78 | } 79 | validatorKeyToPool := map[string]string{ 80 | "0x1234567890abcdef": "pool1", 81 | } 82 | cfg := &config.Config{} 83 | 84 | relayRewards, err := NewRelayRewards(networkParams, validatorKeyToPool, cfg) 85 | assert.NoError(t, err) 86 | 87 | relayRewards.retryOpts = []retry.Option{retry.Attempts(1)} 88 | 89 | rewards, slotsWithRewards, err := relayRewards.GetRelayRewards(0) 90 | assert.Error(t, err) 91 | assert.Nil(t, rewards) 92 | assert.Nil(t, slotsWithRewards) 93 | } 94 | 95 | func TestGetRelayRewards_InvalidValue(t *testing.T) { 96 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 | w.WriteHeader(http.StatusOK) 98 | w.Write([]byte(`[{"proposer_pubkey": "0x1234567890abcdef", "value": "Invalid Value"}]`)) 99 | })) 100 | defer server.Close() 101 | 102 | RELAY_SERVERS = []string{server.URL} 103 | 104 | networkParams := &NetworkParameters{ 105 | slotsInEpoch: 1, 106 | } 107 | validatorKeyToPool := map[string]string{ 108 | "0x1234567890abcdef": "pool1", 109 | } 110 | cfg := &config.Config{} 111 | 112 | relayRewards, err := NewRelayRewards(networkParams, validatorKeyToPool, cfg) 113 | assert.NoError(t, err) 114 | 115 | relayRewards.retryOpts = []retry.Option{retry.Attempts(1)} 116 | 117 | rewards, slotsWithRewards, err := relayRewards.GetRelayRewards(0) 118 | assert.Error(t, err) 119 | assert.Nil(t, rewards) 120 | assert.Nil(t, slotsWithRewards) 121 | } 122 | -------------------------------------------------------------------------------- /pools/pools.go: -------------------------------------------------------------------------------- 1 | package pools 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/ethereum/go-ethereum/common/hexutil" 13 | ) 14 | 15 | func ReadCustomValidatorsFile(validatorKeysFile string) (validatorKeys [][]byte, err error) { 16 | log.Info("Reading validator keys from .txt: ", validatorKeysFile) 17 | validatorKeys = make([][]byte, 0) 18 | 19 | file, err := os.Open(validatorKeysFile) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer file.Close() 24 | 25 | scanner := bufio.NewScanner(file) 26 | for scanner.Scan() { 27 | line := scanner.Text() 28 | 29 | // Skip first line 30 | if (line == "f_validator_pubkey") || (line == "f0_") || (line == "f_public_key") { 31 | continue 32 | } 33 | keyStr := strings.Trim(line, "\"") 34 | if strings.Contains(keyStr, "\\x") { 35 | keyStr = strings.Replace(line, "\\x", "", -1) 36 | } 37 | if !strings.HasPrefix(keyStr, "0x") { 38 | keyStr = "0x" + keyStr 39 | } 40 | 41 | if len(keyStr) != 98 { 42 | return validatorKeys, errors.New(fmt.Sprintf("length of key is incorrect: %d", len(keyStr))) 43 | } 44 | 45 | valKey, err := hexutil.Decode(keyStr) 46 | if err != nil { 47 | return validatorKeys, errors.Wrap(err, fmt.Sprintf("could not decode key: %s", keyStr)) 48 | } 49 | validatorKeys = append(validatorKeys, valKey) 50 | } 51 | 52 | if err := scanner.Err(); err != nil { 53 | return nil, err 54 | } 55 | 56 | log.Info("Done reading ", len(validatorKeys), " from ", validatorKeysFile) 57 | return validatorKeys, nil 58 | } 59 | 60 | func ReadEthstaValidatorsFile(validatorKeysFile string) (validatorKeys [][]byte, err error) { 61 | log.Info("Reading validator keys from ethsta.com csv file: ", validatorKeysFile) 62 | validatorKeys = make([][]byte, 0) 63 | 64 | file, err := os.Open(validatorKeysFile) 65 | if err != nil { 66 | return nil, err 67 | } 68 | defer file.Close() 69 | 70 | scanner := bufio.NewScanner(file) 71 | for scanner.Scan() { 72 | 73 | // Skip first line 74 | line := scanner.Text() 75 | if line == "address,version,entity" { 76 | continue 77 | } 78 | fields := strings.Split(line, ",") 79 | if len(fields) != 3 { 80 | return validatorKeys, errors.New("the format of the file is not the expected, see ethsta.com") 81 | } 82 | keyStr := "0x" + fields[0] 83 | 84 | if len(keyStr) != 98 { 85 | return validatorKeys, errors.New(fmt.Sprintf("length of key is incorrect: %d", len(keyStr))) 86 | } 87 | valKey, err := hexutil.Decode(keyStr) 88 | if err != nil { 89 | return validatorKeys, errors.Wrap(err, fmt.Sprintf("could not decode key: %s", keyStr)) 90 | } 91 | validatorKeys = append(validatorKeys, valKey) 92 | } 93 | 94 | if err := scanner.Err(); err != nil { 95 | return nil, err 96 | } 97 | 98 | log.Info("Done reading ", len(validatorKeys), " from ", validatorKeysFile) 99 | return validatorKeys, nil 100 | } 101 | 102 | func ReadValidatorsFile(validatorsFile string) (poolValidatorKeys map[string][][]byte, validatorKeyToPool map[string]string, err error) { 103 | log.Info("Reading validators csv file: ", validatorsFile) 104 | poolValidatorKeys = make(map[string][][]byte) 105 | validatorKeyToPool = make(map[string]string) 106 | 107 | file, err := os.Open(validatorsFile) 108 | if err != nil { 109 | return nil, nil, err 110 | } 111 | defer file.Close() 112 | 113 | numKeys := 0 114 | scanner := bufio.NewScanner(file) 115 | for scanner.Scan() { 116 | line := scanner.Text() 117 | // Skip first line 118 | if line == "Validator Index,Public Key,Entity (Pool Name),Sub-Pool" { 119 | continue 120 | } 121 | fields := strings.Split(line, ",") 122 | if len(fields) != 4 { 123 | return poolValidatorKeys, validatorKeyToPool, errors.New("the format of the file is not the expected: Validator Index,Public Key,Entity (Pool Name),Sub-Pool") 124 | } 125 | entity := fields[2] 126 | keyStr := fields[1] 127 | 128 | if !strings.HasPrefix(keyStr, "0x") { 129 | keyStr = "0x" + keyStr 130 | } 131 | if len(keyStr) != 98 { 132 | return poolValidatorKeys, validatorKeyToPool, errors.New(fmt.Sprintf("length of key is incorrect: %d", len(keyStr))) 133 | } 134 | valKey, err := hexutil.Decode(keyStr) 135 | if err != nil { 136 | return poolValidatorKeys, validatorKeyToPool, errors.Wrap(err, fmt.Sprintf("could not decode key: %s", keyStr)) 137 | } 138 | if _, ok := poolValidatorKeys[entity]; !ok { 139 | poolValidatorKeys[entity] = make([][]byte, 0) 140 | } 141 | poolValidatorKeys[entity] = append(poolValidatorKeys[entity], valKey) 142 | validatorKeyToPool[keyStr] = entity 143 | numKeys++ 144 | } 145 | 146 | if err := scanner.Err(); err != nil { 147 | return nil, nil, err 148 | } 149 | 150 | log.Info("Done reading ", numKeys, " keys from ", validatorsFile) 151 | return poolValidatorKeys, validatorKeyToPool, nil 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eth-metrics 2 | 3 | [![Tag](https://img.shields.io/github/tag/bilinearlabs/eth-metrics.svg)](https://github.com/bilinearlabs/eth-metrics/releases/) 4 | [![Release](https://github.com/bilinearlabs/eth-metrics/actions/workflows/release.yml/badge.svg)](https://github.com/bilinearlabs/eth-metrics/actions/workflows/release.yml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/bilinearlabs/eth-metrics)](https://goreportcard.com/report/github.com/bilinearlabs/eth-metrics) 6 | [![Tests](https://github.com/bilinearlabs/eth-metrics/actions/workflows/tests.yml/badge.svg)](https://github.com/bilinearlabs/eth-metrics/actions/workflows/tests.yml) 7 | [![gitpoap badge](https://public-api.gitpoap.io/v1/repo/bilinearlabs/eth-metrics/badge)](https://www.gitpoap.io/gh/bilinearlabs/eth-metrics) 8 | 9 | ## Introduction 10 | 11 | This tool monitors Ethereum consensus staking–pool performance. Given a set of labeled validators, it calculates: 12 | 13 | * Rates of faulty head, source, and target votes (per the GASPER algorithm) 14 | * Changes in rewards and penalties between consecutive epochs 15 | * Proposed and missed blocks for each epoch 16 | 17 | It leverages the beacon state, which makes it resource-intensive when tracking only a few validators but scales efficiently to monitor hundreds of thousands. All data is persisted in a SQLite database. 18 | 19 | By default, metrics are computed from the latest head as the chain progresses in real time. You can backfill historical epochs using `--backfill-epochs` but note that this requires access to an archival node. 20 | 21 | ## Requirements 22 | 23 | This project requires: 24 | * An ethereum `consensus` client compliant with the http api running with `--enable-debug-rpc-endpoints`. 25 | * An ethereum `execution` client compliant with the http api. 26 | 27 | ## Build 28 | 29 | ### Docker 30 | 31 | Use the public docker image. 32 | 33 | ```console 34 | docker pull bilinearlabs/eth-metrics:latest 35 | ``` 36 | 37 | Build with docker: 38 | 39 | ```console 40 | git clone https://github.com/bilinearlabs/eth-metrics.git 41 | docker build -t eth-metrics . 42 | ``` 43 | 44 | ### Source 45 | 46 | ```console 47 | git clone https://github.com/bilinearlabs/eth-metrics.git 48 | go build 49 | ``` 50 | 51 | ## Usage 52 | 53 | The following flags are available: 54 | 55 | ```console 56 | ./eth-metrics --help 57 | ``` 58 | 59 | Place in `pool_a.txt` file the validators keys you want to track. 60 | 61 | ``` 62 | 0xaddc693f9090db30a9aae27c047a95245f60313f574fb32729dd06341db55c743e64ba0709ee74181750b6da5f234b44 63 | 0xb6ba7d587c26ca22fd9b306c2f6708c3d998269a81e09aa1298db37ed3ca0a355c46054cb3d3dfd220461465b1bdf267 64 | ``` 65 | 66 | And in `pool_b.txt` other ones. 67 | ``` 68 | 0xa59af0999c83f66de6cab8d833169fe10bce102d466c60c97c4e927210ac56e687c53feac8937c905cec5e87fccd72ce 69 | 0xb19b97fdf01ebd69ad69585e5c693f2ca251f16a315d65db0454e0632a1edc8ffdc21b24eabd26ba24a3a1228040fe8b 70 | 0x8f904676c4ca468ca9df4121bc7f7b1d969dfff93c8d2788b417dbe2e737aa1e644c31ebf36d933f8f1e5b6ebcfd6571 71 | ``` 72 | 73 | You can pass as many `--pool-name` as you want. The name of the pool will be taken from the file. 74 | This example will monitor the performance of `pool_a` and `pool_b` and store in a SQLite database their performance, using said names as labels. 75 | 76 | ```console 77 | ./eth-metrics \ 78 | --eth1address=https://your-execution-endpoint \ 79 | --eth2address=https://your-consensus-endpoint \ 80 | --verbosity=debug \ 81 | --database-path=db.db \ 82 | --pool-name=pool_a.txt \ 83 | --pool-name=pool_b.txt \ 84 | ``` 85 | 86 | Another option is to place in a `pools.csv` file the validators you want to track. The file must be a CSV with 4 columns: `Validator Index`, `Public Key`, `Entity (Pool Name)`, and `Sub-Pool`. The first line (header) is skipped if it matches the expected format. Only `Public Key` and `Entity (Pool Name)` fields are used at the moment. 87 | 88 | ```csv 89 | Validator Index,Public Key,Entity (Pool Name),Sub-Pool 90 | 123456,0xaddc693f9090db30a9aae27c047a95245f60313f574fb32729dd06341db55c743e64ba0709ee74181750b6da5f234b44,pool_a,subpool1 91 | 789012,0xa59af0999c83f66de6cab8d833169fe10bce102d466c60c97c4e927210ac56e687c53feac8937c905cec5e87fccd72ce,pool_b,subpool2 92 | ``` 93 | 94 | And pass the `--validators-file` flag: 95 | 96 | ```console 97 | ./eth-metrics \ 98 | --eth1address=https://your-execution-endpoint \ 99 | --eth2address=https://your-consensus-endpoint \ 100 | --verbosity=debug \ 101 | --database-path=db.db \ 102 | --validators-file=keys.csv \ 103 | ``` 104 | 105 | You can access the content of the database directly, or by using the API that allows to pass raw queries. For example, you can get the metrics from the latest epoch for `pool_a` as follows. 106 | 107 | ``` 108 | curl -X POST http://localhost:8080/query \ 109 | -H "Content-Type: application/json" \ 110 | -d "{\"sql\": \"SELECT * \ 111 | FROM t_pools_metrics_summary \ 112 | WHERE f_epoch = (SELECT MAX(f_epoch) FROM t_pools_metrics_summary) \ 113 | AND f_pool = 'pool_a';\"}" 114 | ``` 115 | 116 | ## Support 117 | 118 | This project gratefully acknowledges the Ethereum Foundation for its support through their grant FY22-0795. 119 | -------------------------------------------------------------------------------- /metrics/relayrewards.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "math/big" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "github.com/avast/retry-go/v4" 13 | "github.com/bilinearlabs/eth-metrics/config" 14 | "github.com/flashbots/mev-boost-relay/common" 15 | "github.com/pkg/errors" 16 | log "github.com/sirupsen/logrus" 17 | "golang.org/x/sync/errgroup" 18 | ) 19 | 20 | var RELAY_SERVERS = []string{ 21 | "https://relay-analytics.ultrasound.money", 22 | "https://titanrelay.xyz", 23 | "https://bloxroute.max-profit.blxrbdn.com", 24 | "https://bloxroute.regulated.blxrbdn.com", 25 | "https://boost-relay.flashbots.net", 26 | "https://aestus.live", 27 | "https://agnostic-relay.net", 28 | "https://relay.ethgas.com", 29 | "https://relay.btcs.com", 30 | } 31 | 32 | type RelayRewards struct { 33 | httpClient *http.Client 34 | networkParameters *NetworkParameters 35 | validatorKeyToPool map[string]string 36 | config *config.Config 37 | retryOpts []retry.Option 38 | } 39 | 40 | func NewRelayRewards( 41 | networkParameters *NetworkParameters, 42 | validatorKeyToPool map[string]string, 43 | config *config.Config) (*RelayRewards, error) { 44 | return &RelayRewards{ 45 | httpClient: &http.Client{Timeout: 60 * time.Second}, 46 | networkParameters: networkParameters, 47 | validatorKeyToPool: validatorKeyToPool, 48 | config: config, 49 | retryOpts: []retry.Option{ 50 | retry.Attempts(5), 51 | retry.Delay(5 * time.Second), 52 | }, 53 | }, nil 54 | } 55 | 56 | func (r *RelayRewards) GetRelayRewards( 57 | epoch uint64, 58 | ) (map[string]*big.Int, map[uint64]struct{}, error) { 59 | slotsInEpoch := r.networkParameters.slotsInEpoch 60 | poolRewards := make(map[string]*big.Int) 61 | slotsWithRewards := make(map[uint64]struct{}) 62 | 63 | results := make(chan struct { 64 | slot uint64 65 | pool string 66 | reward *big.Int 67 | }) 68 | var g errgroup.Group 69 | var consumerWg sync.WaitGroup 70 | 71 | // Create per-relay semaphores (limit to 1 concurrent request per relay) 72 | relaySem := make(map[string]chan struct{}) 73 | for _, relay := range RELAY_SERVERS { 74 | relaySem[relay] = make(chan struct{}, 1) 75 | } 76 | 77 | // Consumer 78 | consumerWg.Go(func() { 79 | for result := range results { 80 | if _, ok := poolRewards[result.pool]; !ok { 81 | poolRewards[result.pool] = big.NewInt(0) 82 | } 83 | poolRewards[result.pool] = new(big.Int).Add(poolRewards[result.pool], result.reward) 84 | slotsWithRewards[result.slot] = struct{}{} 85 | } 86 | }) 87 | 88 | for i := range slotsInEpoch { 89 | slot := epoch*slotsInEpoch + i 90 | for _, relayServer := range RELAY_SERVERS { 91 | g.Go(func() error { 92 | // Acquire semaphore for this relay (blocks if another request is in progress) 93 | relaySem[relayServer] <- struct{}{} 94 | defer func() { <-relaySem[relayServer] }() 95 | 96 | payloads, err := r.getRewards(relayServer, slot) 97 | if err != nil { 98 | return errors.Wrap(err, fmt.Sprintf("error getting rewards from %s", relayServer)) 99 | } 100 | for _, payload := range payloads { 101 | pool, ok := r.validatorKeyToPool[payload.ProposerPubkey] 102 | if !ok { 103 | continue 104 | } 105 | value, ok := big.NewInt(0).SetString(payload.Value, 10) 106 | if !ok { 107 | return errors.New(fmt.Sprintf("failed to parse value: %s", payload.Value)) 108 | } 109 | results <- struct { 110 | slot uint64 111 | pool string 112 | reward *big.Int 113 | }{slot, pool, value} 114 | } 115 | return nil 116 | }) 117 | } 118 | } 119 | if err := g.Wait(); err != nil { 120 | close(results) 121 | consumerWg.Wait() 122 | return nil, nil, errors.Wrap(err, "error getting rewards") 123 | } 124 | close(results) 125 | consumerWg.Wait() 126 | 127 | return poolRewards, slotsWithRewards, nil 128 | } 129 | 130 | func (r *RelayRewards) getRewards(relayServer string, slot uint64) ([]common.BidTraceV2JSON, error) { 131 | var body []byte 132 | 133 | err := retry.Do(func() error { 134 | resp, err := r.httpClient.Get(fmt.Sprintf("%s/relay/v1/data/bidtraces/proposer_payload_delivered?slot=%d", relayServer, slot)) 135 | if err != nil { 136 | log.Warnf("error getting rewards from %s: %s. Slot: %d. Retrying...", relayServer, err, slot) 137 | return errors.Wrap(err, "error getting rewards from "+relayServer) 138 | } 139 | defer resp.Body.Close() 140 | if resp.StatusCode != http.StatusOK { 141 | log.Warnf("non-200 status from %s: %d. Slot: %d. Retrying...", relayServer, resp.StatusCode, slot) 142 | return errors.New(fmt.Sprintf("non-200 status: %d", resp.StatusCode)) 143 | } 144 | body, err = io.ReadAll(resp.Body) 145 | if err != nil { 146 | return errors.Wrap(err, "error reading response body") 147 | } 148 | return nil 149 | }, r.retryOpts...) 150 | if err != nil { 151 | return nil, errors.Wrap(err, "error getting rewards") 152 | } 153 | var payloads []common.BidTraceV2JSON 154 | 155 | if err := json.Unmarshal(body, &payloads); err != nil { 156 | return nil, errors.Wrap(err, "error decoding proposer payload delivered") 157 | } 158 | 159 | return payloads, nil 160 | } 161 | -------------------------------------------------------------------------------- /pools/pools_test.go: -------------------------------------------------------------------------------- 1 | package pools 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // All the following key formats are accepted 13 | 14 | // Do not ident, its a raw string literal 15 | var rawKeys = `0x947265fae1dc387b143a913a6a5f6a4b5b5db897661b38de728dad62a4ac3a9a4232116338bb35a85944fe6263faac61 16 | 0x8253556022b09877b0de3de5a0e4b3c254be36f200dcd6ec2a285ce0758b83aae049a54ec3a73f5fa80d2ad84cfea3cf 17 | 0xb5dab3cfa45f981542b6f567aa09d602cd931d5017a4327159a12865728aaf58bb36029336249a5a289b7e991b5bbe0e 18 | 0x96bb5dbe6da351684b8bd076944abbcd1585ad6ea6e408adf248b4a4d48c8f4ecaf9a8e1f6edffa24236297c40323993` 19 | 20 | var rawEthstaCsv = `address,version,entity 21 | 820DA27909137F0B3A25DD423EE4B2FC30DF65A9EE2FC542C8BC3B202642C6C42C2CBD2F85DEFD479764F53C0884851C,ETH2.0,GenesisMining 22 | 84CAFEB98D6682059B8EC3EA07923A7B6CD73C2BF0D4CAA5982D448080D417D2AEA5106ABAE9277A136FBA7BF2BEAE4F,ETH2.0,GenesisMining 23 | 851BDDE732CCA22D8552B16BEC298267A7ADEAF5E1DC1399F73380D9E2CC8E193BCDC310EB085E86A489479183D72190,ETH2.0,GenesisMining` 24 | 25 | var rawKeysChaind = `f_validator_pubkey 26 | \xab09cb461411d7818f18215236eab18d962fab5551c66b881385b065259f6a723fb77d5635228bc6041cc7c0abb9ea8a 27 | \xa226def15506115277d61c3f705e5153f68bf368fa55fbb7c738e35a3e2f720f8549ed93bb7c1046dfd62c1a0023a207` 28 | 29 | var rawKeysBigQuery = `f0_ 30 | 96ee0717c7a9b3386e1179346f0692addb38400cd0c9f98a51c824d924ea2ac19c952cbe241890437a96df1fa3447c50 31 | a9f5204bd6ac0ffed12f7a1f977bd206a595e25d8f69d3f35dd2c423b0daa21b530406abeee1c96dd878761e51237800` 32 | 33 | // Expected byte value for the keys 34 | var expectedKeys = [][]byte{ 35 | {0x94, 0x72, 0x65, 0xfa, 0xe1, 0xdc, 0x38, 0x7b, 0x14, 0x3a, 0x91, 0x3a, 0x6a, 0x5f, 0x6a, 0x4b, 0x5b, 0x5d, 0xb8, 0x97, 0x66, 0x1b, 0x38, 0xde, 0x72, 0x8d, 0xad, 0x62, 0xa4, 0xac, 0x3a, 0x9a, 0x42, 0x32, 0x11, 0x63, 0x38, 0xbb, 0x35, 0xa8, 0x59, 0x44, 0xfe, 0x62, 0x63, 0xfa, 0xac, 0x61}, 36 | {0x82, 0x53, 0x55, 0x60, 0x22, 0xb0, 0x98, 0x77, 0xb0, 0xde, 0x3d, 0xe5, 0xa0, 0xe4, 0xb3, 0xc2, 0x54, 0xbe, 0x36, 0xf2, 0x0, 0xdc, 0xd6, 0xec, 0x2a, 0x28, 0x5c, 0xe0, 0x75, 0x8b, 0x83, 0xaa, 0xe0, 0x49, 0xa5, 0x4e, 0xc3, 0xa7, 0x3f, 0x5f, 0xa8, 0xd, 0x2a, 0xd8, 0x4c, 0xfe, 0xa3, 0xcf}, 37 | {0xb5, 0xda, 0xb3, 0xcf, 0xa4, 0x5f, 0x98, 0x15, 0x42, 0xb6, 0xf5, 0x67, 0xaa, 0x9, 0xd6, 0x2, 0xcd, 0x93, 0x1d, 0x50, 0x17, 0xa4, 0x32, 0x71, 0x59, 0xa1, 0x28, 0x65, 0x72, 0x8a, 0xaf, 0x58, 0xbb, 0x36, 0x2, 0x93, 0x36, 0x24, 0x9a, 0x5a, 0x28, 0x9b, 0x7e, 0x99, 0x1b, 0x5b, 0xbe, 0xe}, 38 | {0x96, 0xbb, 0x5d, 0xbe, 0x6d, 0xa3, 0x51, 0x68, 0x4b, 0x8b, 0xd0, 0x76, 0x94, 0x4a, 0xbb, 0xcd, 0x15, 0x85, 0xad, 0x6e, 0xa6, 0xe4, 0x8, 0xad, 0xf2, 0x48, 0xb4, 0xa4, 0xd4, 0x8c, 0x8f, 0x4e, 0xca, 0xf9, 0xa8, 0xe1, 0xf6, 0xed, 0xff, 0xa2, 0x42, 0x36, 0x29, 0x7c, 0x40, 0x32, 0x39, 0x93}} 39 | 40 | var expectedKeysEthsta = [][]byte{ 41 | {0x82, 0xd, 0xa2, 0x79, 0x9, 0x13, 0x7f, 0xb, 0x3a, 0x25, 0xdd, 0x42, 0x3e, 0xe4, 0xb2, 0xfc, 0x30, 0xdf, 0x65, 0xa9, 0xee, 0x2f, 0xc5, 0x42, 0xc8, 0xbc, 0x3b, 0x20, 0x26, 0x42, 0xc6, 0xc4, 0x2c, 0x2c, 0xbd, 0x2f, 0x85, 0xde, 0xfd, 0x47, 0x97, 0x64, 0xf5, 0x3c, 0x8, 0x84, 0x85, 0x1c}, 42 | {0x84, 0xca, 0xfe, 0xb9, 0x8d, 0x66, 0x82, 0x5, 0x9b, 0x8e, 0xc3, 0xea, 0x7, 0x92, 0x3a, 0x7b, 0x6c, 0xd7, 0x3c, 0x2b, 0xf0, 0xd4, 0xca, 0xa5, 0x98, 0x2d, 0x44, 0x80, 0x80, 0xd4, 0x17, 0xd2, 0xae, 0xa5, 0x10, 0x6a, 0xba, 0xe9, 0x27, 0x7a, 0x13, 0x6f, 0xba, 0x7b, 0xf2, 0xbe, 0xae, 0x4f}, 43 | {0x85, 0x1b, 0xdd, 0xe7, 0x32, 0xcc, 0xa2, 0x2d, 0x85, 0x52, 0xb1, 0x6b, 0xec, 0x29, 0x82, 0x67, 0xa7, 0xad, 0xea, 0xf5, 0xe1, 0xdc, 0x13, 0x99, 0xf7, 0x33, 0x80, 0xd9, 0xe2, 0xcc, 0x8e, 0x19, 0x3b, 0xcd, 0xc3, 0x10, 0xeb, 0x8, 0x5e, 0x86, 0xa4, 0x89, 0x47, 0x91, 0x83, 0xd7, 0x21, 0x90}} 44 | 45 | var expectedChaind = [][]byte{ 46 | {0xab, 0x9, 0xcb, 0x46, 0x14, 0x11, 0xd7, 0x81, 0x8f, 0x18, 0x21, 0x52, 0x36, 0xea, 0xb1, 0x8d, 0x96, 0x2f, 0xab, 0x55, 0x51, 0xc6, 0x6b, 0x88, 0x13, 0x85, 0xb0, 0x65, 0x25, 0x9f, 0x6a, 0x72, 0x3f, 0xb7, 0x7d, 0x56, 0x35, 0x22, 0x8b, 0xc6, 0x4, 0x1c, 0xc7, 0xc0, 0xab, 0xb9, 0xea, 0x8a}, 47 | {0xa2, 0x26, 0xde, 0xf1, 0x55, 0x6, 0x11, 0x52, 0x77, 0xd6, 0x1c, 0x3f, 0x70, 0x5e, 0x51, 0x53, 0xf6, 0x8b, 0xf3, 0x68, 0xfa, 0x55, 0xfb, 0xb7, 0xc7, 0x38, 0xe3, 0x5a, 0x3e, 0x2f, 0x72, 0xf, 0x85, 0x49, 0xed, 0x93, 0xbb, 0x7c, 0x10, 0x46, 0xdf, 0xd6, 0x2c, 0x1a, 0x0, 0x23, 0xa2, 0x7}} 48 | 49 | var expectedBigQuery = [][]byte{ 50 | {0x96, 0xee, 0x7, 0x17, 0xc7, 0xa9, 0xb3, 0x38, 0x6e, 0x11, 0x79, 0x34, 0x6f, 0x6, 0x92, 0xad, 0xdb, 0x38, 0x40, 0xc, 0xd0, 0xc9, 0xf9, 0x8a, 0x51, 0xc8, 0x24, 0xd9, 0x24, 0xea, 0x2a, 0xc1, 0x9c, 0x95, 0x2c, 0xbe, 0x24, 0x18, 0x90, 0x43, 0x7a, 0x96, 0xdf, 0x1f, 0xa3, 0x44, 0x7c, 0x50}, 51 | {0xa9, 0xf5, 0x20, 0x4b, 0xd6, 0xac, 0xf, 0xfe, 0xd1, 0x2f, 0x7a, 0x1f, 0x97, 0x7b, 0xd2, 0x6, 0xa5, 0x95, 0xe2, 0x5d, 0x8f, 0x69, 0xd3, 0xf3, 0x5d, 0xd2, 0xc4, 0x23, 0xb0, 0xda, 0xa2, 0x1b, 0x53, 0x4, 0x6, 0xab, 0xee, 0xe1, 0xc9, 0x6d, 0xd8, 0x78, 0x76, 0x1e, 0x51, 0x23, 0x78, 0x0}} 52 | 53 | func CreateMockKeysFile(customKeysFile string, content string) { 54 | f, err := os.Create(customKeysFile) 55 | 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | _, err = f.WriteString(content) 61 | 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | f.Close() 66 | } 67 | 68 | func TestReadCustomValidator(t *testing.T) { 69 | customKeysFile := "test_file.txt" 70 | chaindFile := "chaind_file.txt" 71 | bqueryFile := "bigquery_file.txt" 72 | 73 | // Test all 3 formats that ReadCustomValidatorsFile supports 74 | CreateMockKeysFile(customKeysFile, rawKeys) 75 | defer os.Remove(customKeysFile) 76 | 77 | CreateMockKeysFile(chaindFile, rawKeysChaind) 78 | defer os.Remove(chaindFile) 79 | 80 | CreateMockKeysFile(bqueryFile, rawKeysBigQuery) 81 | defer os.Remove(bqueryFile) 82 | 83 | keys, err := ReadCustomValidatorsFile(customKeysFile) 84 | require.NoError(t, err) 85 | require.Equal(t, 4, len(keys)) 86 | for i, key := range keys { 87 | require.Equal(t, expectedKeys[i], key) 88 | } 89 | 90 | keys, err = ReadCustomValidatorsFile(chaindFile) 91 | require.NoError(t, err) 92 | require.Equal(t, 2, len(keys)) 93 | for i, key := range keys { 94 | require.Equal(t, expectedChaind[i], key) 95 | } 96 | 97 | keys, err = ReadCustomValidatorsFile(bqueryFile) 98 | require.NoError(t, err) 99 | require.Equal(t, 2, len(keys)) 100 | for i, key := range keys { 101 | require.Equal(t, expectedBigQuery[i], key) 102 | } 103 | } 104 | 105 | func TestReadEthstaValidatorsFile(t *testing.T) { 106 | customKeysFile := "ethsta_example.csv" 107 | CreateMockKeysFile(customKeysFile, rawEthstaCsv) 108 | defer os.Remove(customKeysFile) 109 | 110 | keys, err := ReadEthstaValidatorsFile(customKeysFile) 111 | require.NoError(t, err) 112 | require.Equal(t, 3, len(keys)) 113 | 114 | for i, key := range keys { 115 | require.Equal(t, expectedKeysEthsta[i], key) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bilinearlabs/eth-metrics 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/attestantio/go-eth2-client v0.27.2 7 | github.com/ethereum/go-ethereum v1.16.7 8 | github.com/gin-contrib/cors v1.7.6 9 | github.com/gin-gonic/gin v1.10.1 10 | github.com/mattn/go-sqlite3 v1.14.28 11 | github.com/pkg/errors v0.9.1 12 | github.com/prometheus/client_golang v1.21.0 13 | github.com/rs/zerolog v1.33.0 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/stretchr/testify v1.11.1 16 | github.com/superoo7/go-gecko v1.0.0 17 | modernc.org/sqlite v1.38.0 18 | ) 19 | 20 | require ( 21 | github.com/Microsoft/go-winio v0.6.2 // indirect 22 | github.com/NYTimes/gziphandler v1.1.1 // indirect 23 | github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect 24 | github.com/aohorodnyk/mimeheader v0.0.6 // indirect 25 | github.com/attestantio/go-builder-client v0.7.2 // indirect 26 | github.com/avast/retry-go/v4 v4.7.0 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/bits-and-blooms/bitset v1.22.0 // indirect 29 | github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect 30 | github.com/buger/jsonparser v1.1.1 // indirect 31 | github.com/bytedance/sonic v1.13.3 // indirect 32 | github.com/bytedance/sonic/loader v0.2.4 // indirect 33 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 34 | github.com/cloudwego/base64x v0.1.5 // indirect 35 | github.com/consensys/gnark-crypto v0.18.0 // indirect 36 | github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect 37 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect 40 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 41 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 42 | github.com/dustin/go-humanize v1.0.1 // indirect 43 | github.com/emicklei/dot v1.8.0 // indirect 44 | github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect 45 | github.com/ethereum/go-verkle v0.2.2 // indirect 46 | github.com/fatih/color v1.18.0 // indirect 47 | github.com/ferranbt/fastssz v0.1.4 // indirect 48 | github.com/flashbots/go-boost-utils v1.10.0 // indirect 49 | github.com/flashbots/go-utils v0.11.0 // indirect 50 | github.com/flashbots/mev-boost-relay v0.32.0 // indirect 51 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 52 | github.com/gin-contrib/sse v1.1.0 // indirect 53 | github.com/go-gorp/gorp/v3 v3.1.0 // indirect 54 | github.com/go-logr/logr v1.4.2 // indirect 55 | github.com/go-logr/stdr v1.2.2 // indirect 56 | github.com/go-ole/go-ole v1.3.0 // indirect 57 | github.com/go-playground/locales v0.14.1 // indirect 58 | github.com/go-playground/universal-translator v0.18.1 // indirect 59 | github.com/go-playground/validator/v10 v10.26.0 // indirect 60 | github.com/goccy/go-json v0.10.5 // indirect 61 | github.com/goccy/go-yaml v1.17.1 // indirect 62 | github.com/gofrs/flock v0.12.1 // indirect 63 | github.com/golang/protobuf v1.5.4 // indirect 64 | github.com/golang/snappy v1.0.0 // indirect 65 | github.com/google/uuid v1.6.0 // indirect 66 | github.com/gorilla/mux v1.8.1 // indirect 67 | github.com/gorilla/websocket v1.4.2 // indirect 68 | github.com/holiman/uint256 v1.3.2 // indirect 69 | github.com/huandu/go-clone v1.7.2 // indirect 70 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 71 | github.com/jmoiron/sqlx v1.4.0 // indirect 72 | github.com/json-iterator/go v1.1.12 // indirect 73 | github.com/klauspost/compress v1.18.0 // indirect 74 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 75 | github.com/leodido/go-urn v1.4.0 // indirect 76 | github.com/lib/pq v1.10.9 // indirect 77 | github.com/mattn/go-colorable v0.1.14 // indirect 78 | github.com/mattn/go-isatty v0.0.20 // indirect 79 | github.com/mattn/go-runewidth v0.0.16 // indirect 80 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 81 | github.com/minio/sha256-simd v1.0.1 // indirect 82 | github.com/mitchellh/mapstructure v1.5.0 // indirect 83 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 84 | github.com/modern-go/reflect2 v1.0.2 // indirect 85 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 86 | github.com/ncruces/go-strftime v0.1.9 // indirect 87 | github.com/olekukonko/tablewriter v0.0.5 // indirect 88 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 89 | github.com/pk910/dynamic-ssz v0.0.4 // indirect 90 | github.com/pmezard/go-difflib v1.0.0 // indirect 91 | github.com/prometheus/client_model v0.6.1 // indirect 92 | github.com/prometheus/common v0.62.0 // indirect 93 | github.com/prometheus/procfs v0.15.1 // indirect 94 | github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 // indirect 95 | github.com/r3labs/sse/v2 v2.10.0 // indirect 96 | github.com/redis/go-redis/v9 v9.7.0 // indirect 97 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 98 | github.com/rivo/uniseg v0.4.7 // indirect 99 | github.com/rubenv/sql-migrate v1.7.1 // indirect 100 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect 101 | github.com/spf13/cobra v1.9.1 // indirect 102 | github.com/spf13/pflag v1.0.6 // indirect 103 | github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect 104 | github.com/tdewolff/minify v2.3.6+incompatible // indirect 105 | github.com/tdewolff/parse v2.3.4+incompatible // indirect 106 | github.com/tidwall/gjson v1.18.0 // indirect 107 | github.com/tidwall/match v1.1.1 // indirect 108 | github.com/tidwall/pretty v1.2.0 // indirect 109 | github.com/tklauser/go-sysconf v0.3.14 // indirect 110 | github.com/tklauser/numcpus v0.9.0 // indirect 111 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 112 | github.com/ugorji/go/codec v1.3.0 // indirect 113 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 114 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 115 | go.opentelemetry.io/otel v1.34.0 // indirect 116 | go.opentelemetry.io/otel/exporters/prometheus v0.56.0 // indirect 117 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 118 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 119 | go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect 120 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 121 | go.uber.org/atomic v1.11.0 // indirect 122 | go.uber.org/multierr v1.11.0 // indirect 123 | go.uber.org/zap v1.27.0 // indirect 124 | golang.org/x/arch v0.18.0 // indirect 125 | golang.org/x/crypto v0.39.0 // indirect 126 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 127 | golang.org/x/net v0.41.0 // indirect 128 | golang.org/x/sync v0.15.0 // indirect 129 | golang.org/x/sys v0.36.0 // indirect 130 | golang.org/x/text v0.26.0 // indirect 131 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 132 | google.golang.org/protobuf v1.36.6 // indirect 133 | gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect 134 | gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect 135 | gopkg.in/yaml.v2 v2.4.0 // indirect 136 | gopkg.in/yaml.v3 v3.0.1 // indirect 137 | modernc.org/libc v1.65.10 // indirect 138 | modernc.org/mathutil v1.7.1 // indirect 139 | modernc.org/memory v1.11.0 // indirect 140 | ) 141 | -------------------------------------------------------------------------------- /metrics/proposalduties_test.go: -------------------------------------------------------------------------------- 1 | // TODO: Outdated 2 | 3 | package metrics 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/bilinearlabs/eth-metrics/schemas" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_getPoolProposalDuties(t *testing.T) { 14 | 15 | validatorIndexes := []uint64{10, 20, 30} 16 | 17 | epochDuties := schemas.ProposalDutiesMetrics{ 18 | Epoch: 0, 19 | Scheduled: []schemas.Duty{ 20 | {ValIndex: 9, Slot: 1, Graffiti: ""}, 21 | {ValIndex: 10, Slot: 5, Graffiti: ""}, 22 | {ValIndex: 20, Slot: 7, Graffiti: ""}, 23 | {ValIndex: 155, Slot: 8, Graffiti: ""}, 24 | }, 25 | Proposed: []schemas.Duty{ 26 | {ValIndex: 9, Slot: 1, Graffiti: ""}, 27 | {ValIndex: 10, Slot: 5, Graffiti: ""}, 28 | {ValIndex: 20, Slot: 7, Graffiti: ""}, 29 | {ValIndex: 155, Slot: 8, Graffiti: ""}, 30 | }, 31 | Missed: []schemas.Duty{ 32 | // No missed duties 33 | }, 34 | } 35 | proposalMetrics := getPoolProposalDuties(&epochDuties, "poolName", validatorIndexes) 36 | 37 | fmt.Println("priposal m etrics", proposalMetrics) 38 | 39 | require.Equal(t, proposalMetrics.Scheduled[0].ValIndex, uint64(10)) 40 | require.Equal(t, proposalMetrics.Scheduled[0].Slot, uint64(5)) 41 | 42 | require.Equal(t, proposalMetrics.Scheduled[1].ValIndex, uint64(20)) 43 | require.Equal(t, proposalMetrics.Scheduled[1].Slot, uint64(7)) 44 | 45 | require.Equal(t, proposalMetrics.Proposed[0].ValIndex, uint64(10)) 46 | require.Equal(t, proposalMetrics.Proposed[0].Slot, uint64(5)) 47 | 48 | require.Equal(t, proposalMetrics.Proposed[1].ValIndex, uint64(20)) 49 | require.Equal(t, proposalMetrics.Proposed[1].Slot, uint64(7)) 50 | 51 | require.Equal(t, 0, len(proposalMetrics.Missed)) 52 | } 53 | 54 | func Test_getMissedDuties(t *testing.T) { 55 | epochDuties := schemas.ProposalDutiesMetrics{ 56 | Epoch: 0, 57 | Scheduled: []schemas.Duty{ 58 | {ValIndex: 9, Slot: 1, Graffiti: ""}, 59 | {ValIndex: 10, Slot: 5, Graffiti: ""}, 60 | {ValIndex: 20, Slot: 7, Graffiti: ""}, 61 | {ValIndex: 155, Slot: 8, Graffiti: ""}, 62 | }, 63 | Proposed: []schemas.Duty{ 64 | {ValIndex: 9, Slot: 1, Graffiti: ""}, 65 | {ValIndex: 10, Slot: 5, Graffiti: ""}, 66 | //{ValIndex: 20, Slot: 7, Graffiti: ""}, // missed 67 | //{ValIndex: 155, Slot: 8, Graffiti: ""}, // missed 68 | }, 69 | Missed: []schemas.Duty{ 70 | // No missed duties 71 | }, 72 | } 73 | 74 | epochDuties.Missed = getMissedDuties(epochDuties.Scheduled, epochDuties.Proposed) 75 | 76 | require.Equal(t, epochDuties.Missed[0].ValIndex, uint64(20)) 77 | require.Equal(t, epochDuties.Missed[0].Slot, uint64(7)) 78 | 79 | require.Equal(t, epochDuties.Missed[1].ValIndex, uint64(155)) 80 | require.Equal(t, epochDuties.Missed[1].Slot, uint64(8)) 81 | } 82 | 83 | //log "github.com/sirupsen/logrus" 84 | 85 | /* 86 | // Validators p1-p7 have active duties 87 | var p1 = ToBytes48([]byte{1}) 88 | var p2 = ToBytes48([]byte{2}) 89 | var p3 = ToBytes48([]byte{3}) 90 | var p4 = ToBytes48([]byte{4}) 91 | var p5 = ToBytes48([]byte{5}) 92 | 93 | // Simulate that p6-p7 fail 94 | var p6 = ToBytes48([]byte{6}) 95 | var p7 = ToBytes48([]byte{7}) 96 | 97 | // Assign duties to p1-p7 98 | var duties = ðpb.DutiesResponse{ 99 | CurrentEpochDuties: []*ethpb.DutiesResponse_Duty{ 100 | { 101 | ProposerSlots: []ethTypes.Slot{32000}, 102 | PublicKey: p1[:], 103 | ValidatorIndex: 1, 104 | }, 105 | { 106 | ProposerSlots: []ethTypes.Slot{32001}, 107 | PublicKey: p2[:], 108 | ValidatorIndex: 2, 109 | }, 110 | { 111 | ProposerSlots: []ethTypes.Slot{32002}, 112 | PublicKey: p3[:], 113 | ValidatorIndex: 3, 114 | }, 115 | { 116 | ProposerSlots: []ethTypes.Slot{32003}, 117 | PublicKey: p4[:], 118 | ValidatorIndex: 4, 119 | }, 120 | { 121 | ProposerSlots: []ethTypes.Slot{32004}, 122 | PublicKey: p5[:], 123 | ValidatorIndex: 5, 124 | }, 125 | { 126 | ProposerSlots: []ethTypes.Slot{32005}, 127 | PublicKey: p6[:], 128 | ValidatorIndex: 6, 129 | }, 130 | { 131 | ProposerSlots: []ethTypes.Slot{32006}, 132 | PublicKey: p7[:], 133 | ValidatorIndex: 7, 134 | }, 135 | }} 136 | 137 | // Blocks can be: 138 | // BeaconBlockContainer_Phase0Block 139 | // BeaconBlockContainer_AltairBlock 140 | // And soon: BeaconBlockMerge 141 | 142 | // Only p1-p5 duties are fulfilled 143 | var blocks = ðpb.ListBeaconBlocksResponse{ 144 | BlockContainers: []*ethpb.BeaconBlockContainer{ 145 | { 146 | Block: ðpb.BeaconBlockContainer_AltairBlock{ 147 | AltairBlock: ðpb.SignedBeaconBlockAltair{ 148 | Block: ðpb.BeaconBlockAltair{ 149 | ProposerIndex: 1, 150 | Slot: 32000, 151 | Body: ðpb.BeaconBlockBodyAltair{Graffiti: []byte("1")}}}}}, 152 | { 153 | Block: ðpb.BeaconBlockContainer_AltairBlock{ 154 | AltairBlock: ðpb.SignedBeaconBlockAltair{ 155 | Block: ðpb.BeaconBlockAltair{ 156 | ProposerIndex: 2, 157 | Slot: 32001, 158 | Body: ðpb.BeaconBlockBodyAltair{Graffiti: []byte("2")}}}}}, 159 | { 160 | Block: ðpb.BeaconBlockContainer_AltairBlock{ 161 | AltairBlock: ðpb.SignedBeaconBlockAltair{ 162 | Block: ðpb.BeaconBlockAltair{ 163 | ProposerIndex: 3, 164 | Slot: 32002, 165 | Body: ðpb.BeaconBlockBodyAltair{Graffiti: []byte("3")}}}}}, 166 | { 167 | Block: ðpb.BeaconBlockContainer_AltairBlock{ 168 | AltairBlock: ðpb.SignedBeaconBlockAltair{ 169 | Block: ðpb.BeaconBlockAltair{ 170 | ProposerIndex: 4, 171 | Slot: 32003, 172 | Body: ðpb.BeaconBlockBodyAltair{Graffiti: []byte("4")}}}}}, 173 | { 174 | Block: ðpb.BeaconBlockContainer_AltairBlock{ 175 | AltairBlock: ðpb.SignedBeaconBlockAltair{ 176 | Block: ðpb.BeaconBlockAltair{ 177 | ProposerIndex: 5, 178 | Slot: 32004, 179 | Body: ðpb.BeaconBlockBodyAltair{Graffiti: []byte("5")}}}}}, 180 | }, 181 | } 182 | 183 | func Test_getProposalDuties(t *testing.T) { 184 | metrics := getProposalDuties(duties, blocks) 185 | 186 | require.Equal(t, len(metrics.Scheduled), 7) 187 | require.Equal(t, len(metrics.Proposed), 5) 188 | require.Equal(t, len(metrics.Missed), 2) 189 | 190 | // Scheduled blocks 191 | for i := 0; i < 7; i++ { 192 | require.Equal(t, metrics.Scheduled[i].ValIndex, uint64(i+1)) 193 | require.Equal(t, metrics.Scheduled[i].Slot, ethTypes.Slot(32000+i)) 194 | } 195 | 196 | // Proposed blocks 197 | for i := 0; i < 5; i++ { 198 | require.Equal(t, metrics.Proposed[i].ValIndex, uint64(i+1)) 199 | require.Equal(t, metrics.Proposed[i].Slot, ethTypes.Slot(32000+i)) 200 | } 201 | 202 | // Missed blocks 203 | for i := 0; i < 2; i++ { 204 | require.Equal(t, metrics.Missed[i].ValIndex, uint64(i+6)) 205 | require.Equal(t, metrics.Missed[i].Slot, ethTypes.Slot(32005+i)) 206 | } 207 | } 208 | 209 | func Test_getMissedDuties(t *testing.T) { 210 | missedDuties := getMissedDuties( 211 | // Schedulled 212 | []schemas.Duty{ 213 | {ValIndex: 1, Slot: ethTypes.Slot(1000)}, 214 | {ValIndex: 2, Slot: ethTypes.Slot(2000)}, 215 | {ValIndex: 3, Slot: ethTypes.Slot(3000)}, 216 | {ValIndex: 4, Slot: ethTypes.Slot(4000)}, 217 | }, 218 | // Proposed 219 | []schemas.Duty{ 220 | {ValIndex: 1, Slot: ethTypes.Slot(1000)}, 221 | {ValIndex: 4, Slot: ethTypes.Slot(4000)}, 222 | }, 223 | ) 224 | 225 | require.Equal(t, missedDuties[0].ValIndex, uint64(2)) 226 | require.Equal(t, missedDuties[0].Slot, ethTypes.Slot(2000)) 227 | 228 | require.Equal(t, missedDuties[1].ValIndex, uint64(3)) 229 | require.Equal(t, missedDuties[1].Slot, ethTypes.Slot(3000)) 230 | } 231 | */ 232 | -------------------------------------------------------------------------------- /metrics/proposalduties.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | 8 | apiOther "github.com/attestantio/go-eth2-client/api" 9 | api "github.com/attestantio/go-eth2-client/api/v1" 10 | "github.com/attestantio/go-eth2-client/http" 11 | "github.com/attestantio/go-eth2-client/spec/phase0" 12 | "github.com/bilinearlabs/eth-metrics/config" 13 | "github.com/bilinearlabs/eth-metrics/db" 14 | 15 | "github.com/bilinearlabs/eth-metrics/schemas" 16 | "github.com/pkg/errors" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | type ProposalDuties struct { 21 | consensus *http.Service 22 | networkParameters *NetworkParameters 23 | database *db.Database 24 | config *config.Config 25 | } 26 | 27 | func NewProposalDuties( 28 | consensus *http.Service, 29 | networkParameters *NetworkParameters, 30 | database *db.Database, 31 | config *config.Config) (*ProposalDuties, error) { 32 | 33 | return &ProposalDuties{ 34 | consensus: consensus, 35 | networkParameters: networkParameters, 36 | database: database, 37 | config: config, 38 | }, nil 39 | } 40 | 41 | func (p *ProposalDuties) RunProposalMetrics( 42 | activeKeys []uint64, 43 | poolName string, 44 | metrics *schemas.ProposalDutiesMetrics) error { 45 | 46 | poolProposals := getPoolProposalDuties( 47 | metrics, 48 | poolName, 49 | activeKeys) 50 | 51 | logProposalDuties(poolProposals, poolName) 52 | 53 | if p.database != nil { 54 | err := p.database.StoreProposalDuties(metrics.Epoch, poolName, uint64(len(poolProposals.Scheduled)), uint64(len(poolProposals.Proposed))) 55 | if err != nil { 56 | return errors.Wrap(err, "could not store proposal duties") 57 | } 58 | } 59 | return nil 60 | 61 | } 62 | 63 | func (p *ProposalDuties) GetProposalDuties(epoch uint64) ([]*api.ProposerDuty, error) { 64 | log.Info("Fetching proposal duties for epoch: ", epoch) 65 | 66 | // Empty indexes to force fetching all duties 67 | indexes := make([]phase0.ValidatorIndex, 0) 68 | 69 | opts := apiOther.ProposerDutiesOpts{ 70 | Indices: indexes, 71 | Epoch: phase0.Epoch(epoch), 72 | } 73 | 74 | duties, err := p.consensus.ProposerDuties( 75 | context.Background(), 76 | &opts) 77 | 78 | if err != nil { 79 | return make([]*api.ProposerDuty, 0), err 80 | } 81 | 82 | return duties.Data, nil 83 | } 84 | 85 | func (p *ProposalDuties) GetProposedBlocks(epoch uint64) ([]*api.BeaconBlockHeader, error) { 86 | log.Info("Fetching proposed blocks for epoch: ", epoch) 87 | 88 | epochBlockHeaders := make([]*api.BeaconBlockHeader, 0) 89 | slotsInEpoch := uint64(p.networkParameters.slotsInEpoch) 90 | 91 | for i := uint64(0); i < slotsInEpoch; i++ { 92 | slot := epoch*slotsInEpoch + uint64(i) 93 | slotStr := strconv.FormatUint(slot, 10) 94 | log.Debug("Fetching block for slot:" + slotStr) 95 | 96 | opts := apiOther.BeaconBlockHeaderOpts{ 97 | Block: slotStr, 98 | } 99 | 100 | blockHeader, err := p.consensus.BeaconBlockHeader(context.Background(), &opts) 101 | if err != nil { 102 | // This error is expected in skipped or orphaned blocks 103 | if !strings.Contains(err.Error(), "NOT_FOUND") { 104 | return epochBlockHeaders, errors.Wrap(err, "error getting beacon block header") 105 | } 106 | log.Warn("Block at slot " + slotStr + " was not found") 107 | continue 108 | } 109 | epochBlockHeaders = append(epochBlockHeaders, blockHeader.Data) 110 | } 111 | 112 | return epochBlockHeaders, nil 113 | } 114 | 115 | func (p *ProposalDuties) GetProposalMetrics( 116 | proposalDuties []*api.ProposerDuty, 117 | proposedBlocks []*api.BeaconBlockHeader) (schemas.ProposalDutiesMetrics, error) { 118 | 119 | proposalMetrics := schemas.ProposalDutiesMetrics{ 120 | Epoch: 0, 121 | Scheduled: make([]schemas.Duty, 0), 122 | Proposed: make([]schemas.Duty, 0), 123 | Missed: make([]schemas.Duty, 0), 124 | } 125 | 126 | if len(proposalDuties) != len(proposedBlocks) { 127 | log.Warn("Duties and blocks have different sizes, ok if n blocks were missed/orphaned") 128 | //return proposalMetrics, errors.New("duties and blocks have different sizes") 129 | } 130 | 131 | if proposalDuties == nil || proposedBlocks == nil { 132 | return proposalMetrics, errors.New("duties and blocks can't be nil") 133 | } 134 | 135 | /* proposedBlocks[0].Header.Message.Slot is nil if the block was missed 136 | if proposalDuties[0].Slot != proposedBlocks[0].Header.Message.Slot { 137 | return proposalMetrics, errors.New("duties and proposals contains different slots") 138 | }*/ 139 | 140 | proposalMetrics.Epoch = uint64(proposalDuties[0].Slot) / p.networkParameters.slotsInEpoch 141 | 142 | for _, duty := range proposalDuties { 143 | proposalMetrics.Scheduled = append( 144 | proposalMetrics.Scheduled, 145 | schemas.Duty{ 146 | ValIndex: uint64(duty.ValidatorIndex), 147 | Slot: uint64(duty.Slot), 148 | Graffiti: "NA", 149 | }) 150 | } 151 | 152 | for _, block := range proposedBlocks { 153 | // If block was missed its nil 154 | if block == nil { 155 | continue 156 | } 157 | proposalMetrics.Proposed = append( 158 | proposalMetrics.Proposed, 159 | schemas.Duty{ 160 | ValIndex: uint64(block.Header.Message.ProposerIndex), 161 | Slot: uint64(block.Header.Message.Slot), 162 | Graffiti: "TODO", 163 | }) 164 | 165 | } 166 | 167 | return proposalMetrics, nil 168 | } 169 | 170 | func getMissedDuties(scheduled []schemas.Duty, proposed []schemas.Duty) []schemas.Duty { 171 | missed := make([]schemas.Duty, 0) 172 | 173 | for _, s := range scheduled { 174 | found := false 175 | for _, p := range proposed { 176 | if s.Slot == p.Slot && s.ValIndex == p.ValIndex { 177 | found = true 178 | break 179 | } 180 | } 181 | if found == false { 182 | missed = append(missed, s) 183 | } 184 | } 185 | 186 | return missed 187 | } 188 | 189 | // TODO: This is very inefficient 190 | func getPoolProposalDuties( 191 | metrics *schemas.ProposalDutiesMetrics, 192 | poolName string, 193 | activeValidatorIndexes []uint64) *schemas.ProposalDutiesMetrics { 194 | 195 | poolDuties := schemas.ProposalDutiesMetrics{ 196 | Epoch: metrics.Epoch, 197 | Scheduled: make([]schemas.Duty, 0), 198 | Proposed: make([]schemas.Duty, 0), 199 | Missed: make([]schemas.Duty, 0), 200 | } 201 | 202 | // Check if this pool has any assigned proposal duties 203 | for i := range metrics.Scheduled { 204 | if IsValidatorIn(metrics.Scheduled[i].ValIndex, activeValidatorIndexes) { 205 | poolDuties.Scheduled = append(poolDuties.Scheduled, metrics.Scheduled[i]) 206 | } 207 | } 208 | 209 | // Check the proposed blocks from the pool 210 | for i := range metrics.Proposed { 211 | if IsValidatorIn(metrics.Proposed[i].ValIndex, activeValidatorIndexes) { 212 | poolDuties.Proposed = append(poolDuties.Proposed, metrics.Proposed[i]) 213 | } 214 | } 215 | 216 | poolDuties.Missed = getMissedDuties(poolDuties.Scheduled, poolDuties.Proposed) 217 | 218 | return &poolDuties 219 | } 220 | 221 | func logProposalDuties( 222 | poolDuties *schemas.ProposalDutiesMetrics, 223 | poolName string) { 224 | 225 | for _, d := range poolDuties.Scheduled { 226 | log.WithFields(log.Fields{ 227 | "PoolName": poolName, 228 | "ValIndex": d.ValIndex, 229 | "Slot": d.Slot, 230 | "Epoch": poolDuties.Epoch, 231 | "TotalScheduled": len(poolDuties.Scheduled), 232 | }).Info("Scheduled Duty") 233 | } 234 | 235 | for _, d := range poolDuties.Proposed { 236 | log.WithFields(log.Fields{ 237 | "PoolName": poolName, 238 | "ValIndex": d.ValIndex, 239 | "Slot": d.Slot, 240 | "Epoch": poolDuties.Epoch, 241 | "Graffiti": d.Graffiti, 242 | "TotalProposed": len(poolDuties.Proposed), 243 | }).Info("Proposed Duty") 244 | } 245 | 246 | for _, d := range poolDuties.Missed { 247 | log.WithFields(log.Fields{ 248 | "PoolName": poolName, 249 | "ValIndex": d.ValIndex, 250 | "Slot": d.Slot, 251 | "Epoch": poolDuties.Epoch, 252 | "TotalMissed": len(poolDuties.Missed), 253 | }).Info("Missed Duty") 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/bilinearlabs/eth-metrics/schemas" 12 | "github.com/pkg/errors" 13 | _ "modernc.org/sqlite" 14 | ) 15 | 16 | var createPoolsMetricsTable = ` 17 | CREATE TABLE IF NOT EXISTS t_pools_metrics_summary ( 18 | f_timestamp TIMESTAMPTZ NOT NULL, 19 | f_epoch BIGINT, 20 | f_pool TEXT, 21 | f_epoch_timestamp TIMESTAMPTZ NOT NULL, 22 | 23 | f_n_active_validators BIGINT, 24 | f_n_total_votes BIGINT, 25 | f_n_incorrect_source BIGINT, 26 | f_n_incorrect_target BIGINT, 27 | f_n_incorrect_head BIGINT, 28 | f_n_validating_keys BIGINT, 29 | f_n_valitadors_with_less_balace BIGINT, 30 | f_validator_indexes_with_less_balance TEXT, 31 | f_epoch_earned_balance_gwei BIGINT, 32 | f_epoch_lost_balace_gwei BIGINT, 33 | f_epoch_effective_balance_gwei BIGINT, 34 | f_mev_rewards_wei BIGINT, 35 | f_proposer_tips_wei BIGINT, 36 | 37 | f_n_scheduled_blocks BIGINT, 38 | f_n_proposed_blocks BIGINT, 39 | 40 | PRIMARY KEY (f_epoch, f_pool) 41 | ); 42 | ` 43 | 44 | var createProposalDutiesTable = ` 45 | CREATE TABLE IF NOT EXISTS t_proposal_duties ( 46 | f_epoch BIGINT, 47 | f_pool TEXT, 48 | f_n_scheduled_blocks BIGINT, 49 | f_n_proposed_blocks BIGINT, 50 | PRIMARY KEY (f_epoch, f_pool) 51 | ); 52 | ` 53 | 54 | var createEthPriceTable = ` 55 | CREATE TABLE IF NOT EXISTS t_eth_price ( 56 | f_timestamp TIMESTAMPTZ NOT NULL PRIMARY KEY, 57 | f_eth_price_usd FLOAT 58 | ); 59 | ` 60 | 61 | var createNetworkStatsTable = ` 62 | CREATE TABLE IF NOT EXISTS t_network_stats ( 63 | f_timestamp TIMESTAMPTZ NOT NULL, 64 | f_epoch BIGINT, 65 | f_n_active_validators BIGINT, 66 | f_n_exited_validators BIGINT, 67 | f_n_slashed_validators BIGINT, 68 | PRIMARY KEY (f_epoch) 69 | ); 70 | ` 71 | 72 | var insertEthPrice = ` 73 | INSERT INTO t_eth_price( 74 | f_timestamp, 75 | f_eth_price_usd) 76 | VALUES (?, ?) 77 | ON CONFLICT (f_timestamp) 78 | DO UPDATE SET 79 | f_eth_price_usd=EXCLUDED.f_eth_price_usd 80 | ` 81 | 82 | // TODO: Add missing 83 | // MissedAttestationsKeys []string 84 | // LostBalanceKeys []string 85 | var insertValidatorPerformance = ` 86 | INSERT INTO t_pools_metrics_summary( 87 | f_timestamp, 88 | f_epoch, 89 | f_pool, 90 | f_epoch_timestamp, 91 | f_n_active_validators, 92 | f_n_total_votes, 93 | f_n_incorrect_source, 94 | f_n_incorrect_target, 95 | f_n_incorrect_head, 96 | f_n_validating_keys, 97 | f_n_valitadors_with_less_balace, 98 | f_validator_indexes_with_less_balance, 99 | f_epoch_effective_balance_gwei, 100 | f_epoch_earned_balance_gwei, 101 | f_epoch_lost_balace_gwei, 102 | f_mev_rewards_wei, 103 | f_proposer_tips_wei) 104 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 105 | ON CONFLICT (f_epoch, f_pool) 106 | DO UPDATE SET 107 | f_timestamp=EXCLUDED.f_timestamp, 108 | f_epoch_timestamp=EXCLUDED.f_epoch_timestamp, 109 | f_n_active_validators=EXCLUDED.f_n_active_validators, 110 | f_n_total_votes=EXCLUDED.f_n_total_votes, 111 | f_n_incorrect_source=EXCLUDED.f_n_incorrect_source, 112 | f_n_incorrect_target=EXCLUDED.f_n_incorrect_target, 113 | f_n_incorrect_head=EXCLUDED.f_n_incorrect_head, 114 | f_n_validating_keys=EXCLUDED.f_n_validating_keys, 115 | f_n_valitadors_with_less_balace=EXCLUDED.f_n_valitadors_with_less_balace, 116 | f_validator_indexes_with_less_balance=EXCLUDED.f_validator_indexes_with_less_balance, 117 | f_epoch_effective_balance_gwei=EXCLUDED.f_epoch_effective_balance_gwei, 118 | f_epoch_earned_balance_gwei=EXCLUDED.f_epoch_earned_balance_gwei, 119 | f_epoch_lost_balace_gwei=EXCLUDED.f_epoch_lost_balace_gwei, 120 | f_mev_rewards_wei=EXCLUDED.f_mev_rewards_wei, 121 | f_proposer_tips_wei=EXCLUDED.f_proposer_tips_wei 122 | ` 123 | 124 | // TODO: Add f_epoch_timestamp 125 | var insertProposalDuties = ` 126 | INSERT INTO t_proposal_duties( 127 | f_epoch, 128 | f_pool, 129 | f_n_scheduled_blocks, 130 | f_n_proposed_blocks) 131 | VALUES (?, ?, ?, ?) 132 | ON CONFLICT (f_epoch, f_pool) 133 | DO UPDATE SET 134 | f_n_scheduled_blocks=EXCLUDED.f_n_scheduled_blocks, 135 | f_n_proposed_blocks=EXCLUDED.f_n_proposed_blocks 136 | ` 137 | 138 | var insertNetworkStats = ` 139 | INSERT INTO t_network_stats( 140 | f_timestamp, 141 | f_epoch, 142 | f_n_active_validators, 143 | f_n_exited_validators, 144 | f_n_slashed_validators) 145 | VALUES (?, ?, ?, ?, ?) 146 | ON CONFLICT (f_epoch) 147 | DO UPDATE SET 148 | f_timestamp=EXCLUDED.f_timestamp, 149 | f_n_active_validators=EXCLUDED.f_n_active_validators, 150 | f_n_exited_validators=EXCLUDED.f_n_exited_validators, 151 | f_n_slashed_validators=EXCLUDED.f_n_slashed_validators 152 | ` 153 | 154 | type Database struct { 155 | db *sql.DB 156 | PoolName string 157 | } 158 | 159 | func New(dbPath string) (*Database, error) { 160 | db, err := sql.Open("sqlite", dbPath) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return &Database{ 166 | db: db, 167 | }, nil 168 | } 169 | 170 | func (a *Database) CreateTables() error { 171 | if _, err := a.db.ExecContext( 172 | context.Background(), 173 | createPoolsMetricsTable); err != nil { 174 | return err 175 | } 176 | 177 | if _, err := a.db.ExecContext( 178 | context.Background(), 179 | createProposalDutiesTable); err != nil { 180 | return err 181 | } 182 | 183 | if _, err := a.db.ExecContext( 184 | context.Background(), 185 | createNetworkStatsTable); err != nil { 186 | return err 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func (a *Database) CreateEthPriceTable() error { 193 | if _, err := a.db.ExecContext( 194 | context.Background(), 195 | createEthPriceTable); err != nil { 196 | return err 197 | } 198 | return nil 199 | } 200 | 201 | func (a *Database) StoreProposalDuties(epoch uint64, poolName string, scheduledBlocks uint64, proposedBlocks uint64) error { 202 | _, err := a.db.ExecContext( 203 | context.Background(), 204 | insertProposalDuties, 205 | epoch, 206 | poolName, 207 | scheduledBlocks, 208 | proposedBlocks) 209 | 210 | if err != nil { 211 | return err 212 | } 213 | return nil 214 | } 215 | 216 | func (a *Database) StoreValidatorPerformance(validatorPerformance schemas.ValidatorPerformanceMetrics) error { 217 | _, err := a.db.ExecContext( 218 | context.Background(), 219 | insertValidatorPerformance, 220 | validatorPerformance.Time, 221 | validatorPerformance.Epoch, 222 | validatorPerformance.PoolName, 223 | validatorPerformance.Time, 224 | validatorPerformance.NOfActiveValidators, 225 | validatorPerformance.NOfTotalVotes, 226 | validatorPerformance.NOfIncorrectSource, 227 | validatorPerformance.NOfIncorrectTarget, 228 | validatorPerformance.NOfIncorrectHead, 229 | validatorPerformance.NOfValidatingKeys, 230 | validatorPerformance.NOfValsWithLessBalance, 231 | func(indexes []uint64) string { 232 | strs := make([]string, len(indexes)) 233 | for i, v := range indexes { 234 | strs[i] = strconv.FormatUint(v, 10) 235 | } 236 | return strings.Join(strs, ",") 237 | }(validatorPerformance.IndexesLessBalance), 238 | validatorPerformance.EffectiveBalance.Int64(), 239 | validatorPerformance.EarnedBalance.Int64(), 240 | validatorPerformance.LosedBalance.Int64(), 241 | validatorPerformance.MEVRewards.Int64(), 242 | validatorPerformance.ProposerTips.Int64(), 243 | ) 244 | 245 | if err != nil { 246 | return err 247 | } 248 | return nil 249 | } 250 | 251 | func (a *Database) StoreEthPrice(ethPriceUsd float32) error { 252 | _, err := a.db.ExecContext( 253 | context.Background(), 254 | insertEthPrice, 255 | time.Now(), // not really correct 256 | ethPriceUsd) 257 | 258 | if err != nil { 259 | return err 260 | } 261 | return nil 262 | } 263 | 264 | func (a *Database) StoreNetworkMetrics(networkMetrics schemas.NetworkStats) error { 265 | _, err := a.db.ExecContext( 266 | context.Background(), 267 | insertNetworkStats, 268 | networkMetrics.Time, 269 | networkMetrics.Epoch, 270 | networkMetrics.NOfActiveValidators, 271 | networkMetrics.NOfExitedValidators, 272 | networkMetrics.NOfSlashedValidators, 273 | ) 274 | 275 | if err != nil { 276 | return err 277 | } 278 | return nil 279 | } 280 | 281 | func (a *Database) GetMissingEpochs(currentEpoch uint64, backfillEpochs uint64) ([]uint64, error) { 282 | // Generate the expected range of epochs 283 | expectedEpochs := make(map[uint64]bool) 284 | for epoch := currentEpoch - backfillEpochs + 1; epoch <= currentEpoch; epoch++ { 285 | expectedEpochs[epoch] = true 286 | } 287 | 288 | // Query existing epochs in the range 289 | query := ` 290 | SELECT f_epoch 291 | FROM t_pools_metrics_summary 292 | WHERE f_epoch BETWEEN ? AND ? 293 | ` 294 | 295 | rows, err := a.db.QueryContext(context.Background(), query, currentEpoch-backfillEpochs+1, currentEpoch) 296 | if err != nil { 297 | return nil, errors.Wrap(err, "could not get existing epochs") 298 | } 299 | 300 | defer rows.Close() 301 | for rows.Next() { 302 | var epoch uint64 303 | if err := rows.Scan(&epoch); err != nil { 304 | return nil, err 305 | } 306 | delete(expectedEpochs, epoch) 307 | } 308 | 309 | // Collect missing epochs 310 | missingEpochs := make([]uint64, 0, len(expectedEpochs)) 311 | for epoch := range expectedEpochs { 312 | missingEpochs = append(missingEpochs, epoch) 313 | } 314 | 315 | // Sort the missing epochs in descending order 316 | sort.Slice(missingEpochs, func(i, j int) bool { return missingEpochs[i] < missingEpochs[j] }) 317 | 318 | return missingEpochs, nil 319 | } 320 | -------------------------------------------------------------------------------- /metrics/beaconstate_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/hex" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/attestantio/go-eth2-client/spec" 9 | "github.com/attestantio/go-eth2-client/spec/altair" 10 | "github.com/attestantio/go-eth2-client/spec/electra" 11 | "github.com/attestantio/go-eth2-client/spec/phase0" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | var validator_0 = ToBytes48([]byte{10}) 17 | var validator_1 = ToBytes48([]byte{20}) 18 | var validator_2 = ToBytes48([]byte{30}) 19 | var validator_3 = ToBytes48([]byte{40}) 20 | 21 | func Test_GetIndexesFromKeys(t *testing.T) { 22 | beaconState := &spec.VersionedBeaconState{ 23 | Altair: &altair.BeaconState{ 24 | Validators: []*phase0.Validator{ 25 | {PublicKey: validator_0}, 26 | {PublicKey: validator_1}, 27 | {PublicKey: validator_2}, 28 | {PublicKey: validator_3}, 29 | }, 30 | }, 31 | } 32 | 33 | inputKeys := [][][]byte{ 34 | {validator_3[:], validator_0[:]}, // test 1 35 | {validator_0[:]}, // test 2 36 | {validator_3[:], validator_0[:], validator_1[:]}, // test 3 37 | } 38 | 39 | expectedIndexes := [][]uint64{ 40 | {3, 0}, // test 1 41 | {0}, // test 2 42 | {3, 0, 1}, // test 3 43 | } 44 | 45 | keyToIndexMapping := PopulateKeysToIndexesMap(beaconState) 46 | 47 | for test := 0; test < len(inputKeys); test++ { 48 | indexes := GetIndexesFromKeys( 49 | inputKeys[test], 50 | keyToIndexMapping) 51 | // Ignore order 52 | require.ElementsMatch(t, indexes, expectedIndexes[test]) 53 | } 54 | } 55 | 56 | func Test_GetValidatorsWithLessBalance(t *testing.T) { 57 | b := &BeaconState{ 58 | networkParameters: &NetworkParameters{ 59 | slotsInEpoch: 32, 60 | }, 61 | } 62 | 63 | prevBeaconState := &spec.VersionedBeaconState{ 64 | Altair: &altair.BeaconState{ 65 | Slot: 34 * 32, 66 | Balances: []phase0.Gwei{ 67 | 1000, 68 | 9000, 69 | 2000, 70 | 1, 71 | }, 72 | }, 73 | } 74 | 75 | currentBeaconState := &spec.VersionedBeaconState{ 76 | Altair: &altair.BeaconState{ 77 | Slot: 35 * 32, 78 | Balances: []phase0.Gwei{ 79 | 900, 80 | 9500, 81 | 1000, 82 | 2, 83 | }, 84 | }, 85 | } 86 | 87 | indexLessBalance, earnedBalance, lostBalance, err := b.GetValidatorsWithLessBalance( 88 | []uint64{0, 1, 2, 3}, 89 | prevBeaconState, 90 | currentBeaconState, 91 | map[uint64]*big.Int{}, 92 | map[uint64][]*electra.PendingConsolidation{}, 93 | ) 94 | 95 | require.NoError(t, err) 96 | require.Equal(t, indexLessBalance, []uint64{0, 2}) 97 | require.Equal(t, earnedBalance, big.NewInt(501)) 98 | require.Equal(t, lostBalance, big.NewInt(-1100)) 99 | 100 | } 101 | 102 | func Test_GetValidatorsWithLessBalance_NonConsecutive(t *testing.T) { 103 | b := &BeaconState{ 104 | networkParameters: &NetworkParameters{ 105 | slotsInEpoch: 32, 106 | }, 107 | } 108 | 109 | currentBeaconState := &spec.VersionedBeaconState{ 110 | Altair: &altair.BeaconState{ 111 | Slot: 54 * 32, 112 | }, 113 | } 114 | prevBeaconState := &spec.VersionedBeaconState{ 115 | Altair: &altair.BeaconState{ 116 | Slot: 52 * 32, 117 | }, 118 | } 119 | 120 | _, _, _, err := b.GetValidatorsWithLessBalance( 121 | []uint64{}, 122 | prevBeaconState, 123 | currentBeaconState, 124 | map[uint64]*big.Int{}, 125 | map[uint64][]*electra.PendingConsolidation{}) 126 | 127 | require.Error(t, err) 128 | } 129 | 130 | // TODO: Test that slashed validators are ignored 131 | func Test_GetParticipation(t *testing.T) { 132 | b := &BeaconState{ 133 | networkParameters: &NetworkParameters{ 134 | slotsInEpoch: 32, 135 | }, 136 | } 137 | 138 | // Use 6 validators 139 | validatorIndexes := []uint64{0, 1, 2, 3, 4, 5} 140 | 141 | // Mock a beaconstate with 6 validators 142 | beaconState := &spec.VersionedBeaconState{ 143 | Altair: &altair.BeaconState{ 144 | // See spec: https://github.com/ethereum/consensus-specs/blob/master/specs/altair/beacon-chain.md#participation-flag-indices 145 | // b7 to b0: UNUSED,UNUSED,UNUSED,UNUSED UNUSED,HEAD,TARGET,SOURCE 146 | // i.e. 0000 0111 means head, target and source OK 147 | //. 0000 0001 means only source OK 148 | PreviousEpochParticipation: []altair.ParticipationFlags{ 149 | 0b00000111, 150 | 0b00000011, 151 | 0b00000011, 152 | 0b00000100, 153 | 0b00000000, 154 | 0b00000011, 155 | 0b00000011, // skipped (see validatorIndexes) 156 | 0b00000011, // skipped (see validatorIndexes) 157 | 0b00000011, // skipped (see validatorIndexes) 158 | }, 159 | // TODO: Different eth2 endpoints return wrong data for this. Bug? 160 | CurrentEpochParticipation: []altair.ParticipationFlags{}, 161 | Validators: []*phase0.Validator{ 162 | {Slashed: false}, 163 | {Slashed: false}, 164 | {Slashed: false}, 165 | {Slashed: false}, 166 | {Slashed: false}, 167 | {Slashed: false}, 168 | {Slashed: false}, 169 | {Slashed: false}, 170 | {Slashed: false}, 171 | }, 172 | }, 173 | } 174 | 175 | source, target, head, indexesMissedAtt := b.GetParticipation( 176 | validatorIndexes, 177 | beaconState) 178 | 179 | require.Equal(t, uint64(2), source) 180 | require.Equal(t, uint64(2), target) 181 | require.Equal(t, uint64(4), head) 182 | require.Equal(t, []uint64{3, 4}, indexesMissedAtt) 183 | } 184 | 185 | func Test_PopulateKeysToIndexesMap(t *testing.T) { 186 | beaconState := &spec.VersionedBeaconState{ 187 | Altair: &altair.BeaconState{ 188 | Validators: []*phase0.Validator{ 189 | {PublicKey: validator_0}, 190 | {PublicKey: validator_1}, 191 | {PublicKey: validator_2}, 192 | {PublicKey: validator_3}, 193 | }, 194 | }, 195 | } 196 | valKeyToIndex := PopulateKeysToIndexesMap(beaconState) 197 | require.Equal(t, uint64(0), valKeyToIndex[hex.EncodeToString(validator_0[:])]) 198 | require.Equal(t, uint64(1), valKeyToIndex[hex.EncodeToString(validator_1[:])]) 199 | require.Equal(t, uint64(2), valKeyToIndex[hex.EncodeToString(validator_2[:])]) 200 | require.Equal(t, uint64(3), valKeyToIndex[hex.EncodeToString(validator_3[:])]) 201 | } 202 | 203 | // TODO: Should be in utils 204 | func Test_BLSPubKeyToByte(t *testing.T) { 205 | blsKeys := []phase0.BLSPubKey{{0x01}, {0x02}} 206 | keys := BLSPubKeyToByte(blsKeys) 207 | 208 | // Too lazy to simplify this 209 | require.Equal(t, keys[0], []byte{0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}) 210 | require.Equal(t, keys[1], []byte{0x02, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}) 211 | } 212 | 213 | func Test_GetValidatorsIn(t *testing.T) { 214 | allSyncCommitteeIndexes := []uint64{5, 10, 20} 215 | poolValidatorIndexes := []uint64{1, 2, 3, 10, 6, 7, 8, 5, 22} 216 | 217 | poolSyncIndexes := GetValidatorsIn(allSyncCommitteeIndexes, poolValidatorIndexes) 218 | 219 | require.Equal(t, uint64(5), poolSyncIndexes[0]) 220 | require.Equal(t, uint64(10), poolSyncIndexes[1]) 221 | require.Equal(t, 2, len(poolSyncIndexes)) 222 | } 223 | 224 | func Test_IsValidatorIn(t *testing.T) { 225 | 226 | element := uint64(10) 227 | set := []uint64{5, 10, 20} 228 | 229 | isIn := IsValidatorIn(element, set) 230 | require.Equal(t, true, isIn) 231 | 232 | } 233 | 234 | func Test_IsBitSet(t *testing.T) { 235 | is := isBitSet(0, 0) 236 | require.Equal(t, false, is) 237 | 238 | is = isBitSet(7, 0) 239 | require.Equal(t, true, is) 240 | 241 | is = isBitSet(7, 1) 242 | require.Equal(t, true, is) 243 | 244 | is = isBitSet(7, 2) 245 | require.Equal(t, true, is) 246 | 247 | is = isBitSet(1, 0) 248 | require.Equal(t, true, is) 249 | 250 | is = isBitSet(5, 0) 251 | require.Equal(t, true, is) 252 | 253 | is = isBitSet(5, 1) 254 | require.Equal(t, false, is) 255 | 256 | is = isBitSet(5, 2) 257 | require.Equal(t, true, is) 258 | } 259 | 260 | func Test_GetProcessedConsolidations(t *testing.T) { 261 | prevBeaconState := &spec.VersionedBeaconState{ 262 | Electra: &electra.BeaconState{ 263 | PendingConsolidations: []*electra.PendingConsolidation{ 264 | {SourceIndex: 0, TargetIndex: 1}, 265 | {SourceIndex: 2, TargetIndex: 1}, 266 | }, 267 | Validators: []*phase0.Validator{ 268 | {Slashed: false}, 269 | {Slashed: false}, 270 | {Slashed: false}, 271 | }, 272 | }, 273 | } 274 | currentBeaconState := &spec.VersionedBeaconState{ 275 | Electra: &electra.BeaconState{ 276 | PendingConsolidations: []*electra.PendingConsolidation{}, 277 | Validators: []*phase0.Validator{ 278 | {Slashed: false}, 279 | {Slashed: false}, 280 | {Slashed: true}, 281 | }, 282 | }, 283 | } 284 | processedConsolidations, err := GetProcessedConsolidations(prevBeaconState, currentBeaconState) 285 | require.NoError(t, err) 286 | 287 | require.Equal(t, len(processedConsolidations), 1) 288 | require.Equal(t, len(processedConsolidations[1]), 1) 289 | require.Equal(t, processedConsolidations[1][0].SourceIndex, phase0.ValidatorIndex(0)) 290 | require.Equal(t, processedConsolidations[1][0].TargetIndex, phase0.ValidatorIndex(1)) 291 | } 292 | 293 | func Test_GetProcessedConsolidations_NilPendingConsolidations(t *testing.T) { 294 | prevBeaconState := &spec.VersionedBeaconState{ 295 | Electra: &electra.BeaconState{ 296 | PendingConsolidations: nil, 297 | }, 298 | } 299 | currentBeaconState := &spec.VersionedBeaconState{ 300 | Electra: &electra.BeaconState{ 301 | PendingConsolidations: nil, 302 | }, 303 | } 304 | processedConsolidations, err := GetProcessedConsolidations(prevBeaconState, currentBeaconState) 305 | require.Error(t, err) 306 | require.Nil(t, processedConsolidations) 307 | } 308 | -------------------------------------------------------------------------------- /metrics/blockdata.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/attestantio/go-eth2-client/api" 12 | "github.com/attestantio/go-eth2-client/http" 13 | "github.com/attestantio/go-eth2-client/spec" 14 | "github.com/attestantio/go-eth2-client/spec/bellatrix" 15 | "github.com/attestantio/go-eth2-client/spec/capella" 16 | "github.com/avast/retry-go/v4" 17 | "github.com/bilinearlabs/eth-metrics/config" 18 | "github.com/ethereum/go-ethereum/core/types" 19 | "github.com/ethereum/go-ethereum/ethclient" 20 | "github.com/pkg/errors" 21 | log "github.com/sirupsen/logrus" 22 | "golang.org/x/sync/errgroup" 23 | ) 24 | 25 | type EpochBlockData struct { 26 | Withdrawals map[uint64]*big.Int 27 | ProposerTips map[uint64]*big.Int 28 | } 29 | 30 | type BlockData struct { 31 | consensusClient *http.Service 32 | executionClient *ethclient.Client 33 | networkParameters *NetworkParameters 34 | config *config.Config 35 | retryOpts []retry.Option 36 | } 37 | 38 | func NewBlockData( 39 | consensusClient *http.Service, 40 | executionClient *ethclient.Client, 41 | networkParameters *NetworkParameters, 42 | config *config.Config, 43 | ) (*BlockData, error) { 44 | return &BlockData{ 45 | consensusClient: consensusClient, 46 | executionClient: executionClient, 47 | networkParameters: networkParameters, 48 | config: config, 49 | retryOpts: []retry.Option{ 50 | retry.Attempts(5), 51 | retry.Delay(5 * time.Second), 52 | }, 53 | }, nil 54 | } 55 | 56 | func (b *BlockData) GetEpochBlockData(epoch uint64, slotsWithMEVRewards map[uint64]struct{}) (*EpochBlockData, error) { 57 | log.Info("Fetching block data for epoch: ", epoch) 58 | 59 | data := &EpochBlockData{ 60 | Withdrawals: make(map[uint64]*big.Int), 61 | ProposerTips: make(map[uint64]*big.Int), 62 | } 63 | 64 | firstSlot := epoch * b.networkParameters.slotsInEpoch 65 | for slot := firstSlot; slot < firstSlot+b.networkParameters.slotsInEpoch; slot++ { 66 | slotStr := strconv.FormatUint(slot, 10) 67 | opts := api.SignedBeaconBlockOpts{ 68 | Block: slotStr, 69 | } 70 | 71 | beaconBlock, err := b.consensusClient.SignedBeaconBlock( 72 | context.Background(), 73 | &opts, 74 | ) 75 | if err != nil { 76 | // This error is expected in skipped or orphaned blocks 77 | if !strings.Contains(err.Error(), "NOT_FOUND") { 78 | return nil, errors.Wrap(err, "error getting signed beacon block") 79 | } 80 | log.Warn("block not found for slot: ", slot) 81 | continue 82 | } 83 | 84 | block := beaconBlock.Data 85 | 86 | b.ExtractWithdrawals(block, data.Withdrawals) 87 | 88 | // Extract transaction fees if block has no MEV rewards 89 | if _, ok := slotsWithMEVRewards[slot]; !ok { 90 | blockNumber := b.GetBlockNumber(block) 91 | 92 | header, err := b.getBlockHeader(blockNumber) 93 | if err != nil { 94 | return nil, errors.Wrap(err, "error getting block header and receipts") 95 | } 96 | rawTxs := b.GetBlockTransactions(block) 97 | receipts, err := b.getBlockReceipts(rawTxs) 98 | if err != nil { 99 | return nil, errors.Wrap(err, "error getting block receipts") 100 | } 101 | 102 | proposerTip, err := b.GetProposerTip(block, header, receipts) 103 | if err != nil { 104 | return nil, errors.Wrap(err, "error getting proposer tip") 105 | } 106 | proposerIndex := b.GetProposerIndex(block) 107 | if _, ok := data.ProposerTips[proposerIndex]; !ok { 108 | data.ProposerTips[proposerIndex] = big.NewInt(0) 109 | } 110 | data.ProposerTips[proposerIndex].Add(data.ProposerTips[proposerIndex], proposerTip) 111 | } 112 | } 113 | 114 | return data, nil 115 | } 116 | 117 | func (b *BlockData) ExtractWithdrawals(beaconBlock *spec.VersionedSignedBeaconBlock, withdrawals map[uint64]*big.Int) { 118 | blockWithdrawals := b.GetBlockWithdrawals(beaconBlock) 119 | for _, withdrawal := range blockWithdrawals { 120 | idx := uint64(withdrawal.ValidatorIndex) 121 | if _, ok := withdrawals[idx]; !ok { 122 | withdrawals[idx] = big.NewInt(0) 123 | } 124 | withdrawals[idx].Add(withdrawals[idx], big.NewInt(int64(withdrawal.Amount))) 125 | } 126 | } 127 | 128 | func (b *BlockData) GetProposerTip( 129 | beaconBlock *spec.VersionedSignedBeaconBlock, 130 | header *types.Header, 131 | receipts []*types.Receipt, 132 | ) (*big.Int, error) { 133 | rawTxs := b.GetBlockTransactions(beaconBlock) 134 | 135 | baseFeePerGasBytes := b.GetBaseFeePerGas(beaconBlock) 136 | baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBytes[:]) 137 | 138 | tips := big.NewInt(0) 139 | 140 | for i, rawTx := range rawTxs { 141 | var tx types.Transaction 142 | err := tx.UnmarshalBinary(rawTx) 143 | if err != nil { 144 | return nil, errors.Wrap(err, "error unmarshalling transaction") 145 | } 146 | txReceipt := receipts[i] 147 | 148 | if tx.Hash() != txReceipt.TxHash { 149 | return nil, errors.New("transaction hash mismatch") 150 | } 151 | 152 | tipFee := new(big.Int) 153 | gasPrice := tx.GasPrice() 154 | gasUsed := big.NewInt(int64(txReceipt.GasUsed)) 155 | 156 | switch tx.Type() { 157 | case 0, 1: 158 | tipFee.Mul(gasPrice, gasUsed) 159 | case 2, 3, 4: 160 | tip := new(big.Int).Add(tx.GasTipCap(), header.BaseFee) 161 | gasFeeCap := tx.GasFeeCap() 162 | var usedGasPrice *big.Int 163 | if gasFeeCap.Cmp(tip) < 0 { 164 | usedGasPrice = gasFeeCap 165 | } else { 166 | usedGasPrice = tip 167 | } 168 | tipFee = new(big.Int).Mul(usedGasPrice, gasUsed) 169 | default: 170 | return nil, errors.Errorf("unknown transaction type: %d, hash: %s", tx.Type(), tx.Hash().String()) 171 | } 172 | tips.Add(tips, tipFee) 173 | } 174 | 175 | burnt := new(big.Int).Mul(big.NewInt(int64(b.GetGasUsed(beaconBlock))), baseFeePerGas) 176 | proposerReward := new(big.Int).Sub(tips, burnt) 177 | return proposerReward, nil 178 | } 179 | 180 | func (b *BlockData) getBlockHeader( 181 | blockNumber uint64, 182 | ) (*types.Header, error) { 183 | var header *types.Header 184 | var err error 185 | 186 | blockNumberBig := new(big.Int).SetUint64(blockNumber) 187 | 188 | err = retry.Do(func() error { 189 | header, err = b.executionClient.HeaderByNumber(context.Background(), blockNumberBig) 190 | if err != nil { 191 | log.Warnf("error getting header for block %d: %s. Retrying...", blockNumber, err) 192 | return errors.Wrap(err, "error getting header for block") 193 | } 194 | return nil 195 | }, b.retryOpts...) 196 | if err != nil { 197 | return nil, errors.Wrap(err, "error getting header for block "+blockNumberBig.String()) 198 | } 199 | 200 | return header, nil 201 | } 202 | 203 | func (b *BlockData) getBlockReceipts(rawTxs []bellatrix.Transaction) ([]*types.Receipt, error) { 204 | receipts := make([]*types.Receipt, len(rawTxs)) 205 | var err error 206 | 207 | var g errgroup.Group 208 | var mu sync.Mutex 209 | // Limit concurrent requests 210 | g.SetLimit(10) 211 | 212 | for i, rawTx := range rawTxs { 213 | g.Go(func() error { 214 | var tx types.Transaction 215 | err = tx.UnmarshalBinary(rawTx) 216 | if err != nil { 217 | return errors.Wrap(err, "error unmarshalling transaction") 218 | } 219 | receipt, err := b.getTransactionReceipt(&tx) 220 | if err != nil { 221 | return errors.Wrap(err, "error getting transaction receipt") 222 | } 223 | mu.Lock() 224 | receipts[i] = receipt 225 | mu.Unlock() 226 | return nil 227 | }) 228 | } 229 | if err := g.Wait(); err != nil { 230 | return nil, errors.Wrap(err, "error getting block receipts") 231 | } 232 | return receipts, nil 233 | } 234 | 235 | func (b *BlockData) getTransactionReceipt(tx *types.Transaction) (*types.Receipt, error) { 236 | var receipt *types.Receipt 237 | var err error 238 | err = retry.Do(func() error { 239 | receipt, err = b.executionClient.TransactionReceipt(context.Background(), tx.Hash()) 240 | if err != nil { 241 | log.Warnf("error getting transaction receipt for tx %s: %s. Retrying...", tx.Hash().String(), err) 242 | return errors.Wrap(err, "error getting transaction receipt") 243 | } 244 | return nil 245 | }, b.retryOpts...) 246 | 247 | if err != nil { 248 | return nil, errors.Wrap(err, "error getting transaction receipt for tx "+tx.Hash().String()) 249 | } 250 | 251 | return receipt, nil 252 | } 253 | 254 | func (b *BlockData) GetBlockWithdrawals(beaconBlock *spec.VersionedSignedBeaconBlock) []*capella.Withdrawal { 255 | var withdrawals []*capella.Withdrawal 256 | if beaconBlock.Altair != nil { 257 | withdrawals = []*capella.Withdrawal{} 258 | } else if beaconBlock.Bellatrix != nil { 259 | withdrawals = []*capella.Withdrawal{} 260 | } else if beaconBlock.Capella != nil { 261 | withdrawals = beaconBlock.Capella.Message.Body.ExecutionPayload.Withdrawals 262 | } else if beaconBlock.Deneb != nil { 263 | withdrawals = beaconBlock.Deneb.Message.Body.ExecutionPayload.Withdrawals 264 | } else if beaconBlock.Electra != nil { 265 | withdrawals = beaconBlock.Electra.Message.Body.ExecutionPayload.Withdrawals 266 | } else if beaconBlock.Fulu != nil { 267 | withdrawals = beaconBlock.Fulu.Message.Body.ExecutionPayload.Withdrawals 268 | } else { 269 | log.Fatal("Beacon block was empty") 270 | } 271 | return withdrawals 272 | } 273 | 274 | func (b *BlockData) GetBlockTransactions(beaconBlock *spec.VersionedSignedBeaconBlock) []bellatrix.Transaction { 275 | var transactions []bellatrix.Transaction 276 | if beaconBlock.Altair != nil { 277 | transactions = []bellatrix.Transaction{} 278 | } else if beaconBlock.Bellatrix != nil { 279 | transactions = beaconBlock.Bellatrix.Message.Body.ExecutionPayload.Transactions 280 | } else if beaconBlock.Capella != nil { 281 | transactions = beaconBlock.Capella.Message.Body.ExecutionPayload.Transactions 282 | } else if beaconBlock.Deneb != nil { 283 | transactions = beaconBlock.Deneb.Message.Body.ExecutionPayload.Transactions 284 | } else if beaconBlock.Electra != nil { 285 | transactions = beaconBlock.Electra.Message.Body.ExecutionPayload.Transactions 286 | } else if beaconBlock.Fulu != nil { 287 | transactions = beaconBlock.Fulu.Message.Body.ExecutionPayload.Transactions 288 | } else { 289 | log.Fatal("Beacon block was empty") 290 | } 291 | return transactions 292 | } 293 | 294 | func (b *BlockData) GetBlockNumber(beaconBlock *spec.VersionedSignedBeaconBlock) uint64 { 295 | var blockNumber uint64 296 | if beaconBlock.Altair != nil { 297 | log.Fatal("Altair block has no block number") 298 | } else if beaconBlock.Bellatrix != nil { 299 | blockNumber = beaconBlock.Bellatrix.Message.Body.ExecutionPayload.BlockNumber 300 | } else if beaconBlock.Capella != nil { 301 | blockNumber = beaconBlock.Capella.Message.Body.ExecutionPayload.BlockNumber 302 | } else if beaconBlock.Deneb != nil { 303 | blockNumber = beaconBlock.Deneb.Message.Body.ExecutionPayload.BlockNumber 304 | } else if beaconBlock.Electra != nil { 305 | blockNumber = beaconBlock.Electra.Message.Body.ExecutionPayload.BlockNumber 306 | } else if beaconBlock.Fulu != nil { 307 | blockNumber = beaconBlock.Fulu.Message.Body.ExecutionPayload.BlockNumber 308 | } else { 309 | log.Fatal("Beacon block was empty") 310 | } 311 | return blockNumber 312 | } 313 | 314 | // Returns base fee per gas in big endian 315 | func (b *BlockData) GetBaseFeePerGas(beaconBlock *spec.VersionedSignedBeaconBlock) [32]byte { 316 | var baseFeePerGas [32]byte 317 | 318 | if beaconBlock.Altair != nil { 319 | log.Fatal("Altair block has no base fee per gas") 320 | } else if beaconBlock.Bellatrix != nil { 321 | baseFeePerGasLE := beaconBlock.Bellatrix.Message.Body.ExecutionPayload.BaseFeePerGas 322 | for i := range 32 { 323 | baseFeePerGas[i] = baseFeePerGasLE[32-1-i] 324 | } 325 | } else if beaconBlock.Capella != nil { 326 | baseFeePerGasLE := beaconBlock.Capella.Message.Body.ExecutionPayload.BaseFeePerGas 327 | for i := range 32 { 328 | baseFeePerGas[i] = baseFeePerGasLE[32-1-i] 329 | } 330 | } else if beaconBlock.Deneb != nil { 331 | baseFeePerGas = beaconBlock.Deneb.Message.Body.ExecutionPayload.BaseFeePerGas.Bytes32() 332 | } else if beaconBlock.Electra != nil { 333 | baseFeePerGas = beaconBlock.Electra.Message.Body.ExecutionPayload.BaseFeePerGas.Bytes32() 334 | } else if beaconBlock.Fulu != nil { 335 | baseFeePerGas = beaconBlock.Fulu.Message.Body.ExecutionPayload.BaseFeePerGas.Bytes32() 336 | } else { 337 | log.Fatal("Beacon block was empty") 338 | } 339 | return baseFeePerGas 340 | } 341 | 342 | func (b *BlockData) GetGasUsed(beaconBlock *spec.VersionedSignedBeaconBlock) uint64 { 343 | var gasUsed uint64 344 | 345 | if beaconBlock.Altair != nil { 346 | log.Fatal("Altair block has no gas used") 347 | } else if beaconBlock.Bellatrix != nil { 348 | gasUsed = beaconBlock.Bellatrix.Message.Body.ExecutionPayload.GasUsed 349 | } else if beaconBlock.Capella != nil { 350 | gasUsed = beaconBlock.Capella.Message.Body.ExecutionPayload.GasUsed 351 | } else if beaconBlock.Deneb != nil { 352 | gasUsed = beaconBlock.Deneb.Message.Body.ExecutionPayload.GasUsed 353 | } else if beaconBlock.Electra != nil { 354 | gasUsed = beaconBlock.Electra.Message.Body.ExecutionPayload.GasUsed 355 | } else if beaconBlock.Fulu != nil { 356 | gasUsed = beaconBlock.Fulu.Message.Body.ExecutionPayload.GasUsed 357 | } else { 358 | log.Fatal("Beacon block was empty") 359 | } 360 | return gasUsed 361 | } 362 | 363 | func (b *BlockData) GetProposerIndex(beaconBlock *spec.VersionedSignedBeaconBlock) uint64 { 364 | var proposerIndex uint64 365 | if beaconBlock.Altair != nil { 366 | proposerIndex = uint64(beaconBlock.Altair.Message.ProposerIndex) 367 | } else if beaconBlock.Bellatrix != nil { 368 | proposerIndex = uint64(beaconBlock.Bellatrix.Message.ProposerIndex) 369 | } else if beaconBlock.Capella != nil { 370 | proposerIndex = uint64(beaconBlock.Capella.Message.ProposerIndex) 371 | } else if beaconBlock.Deneb != nil { 372 | proposerIndex = uint64(beaconBlock.Deneb.Message.ProposerIndex) 373 | } else if beaconBlock.Electra != nil { 374 | proposerIndex = uint64(beaconBlock.Electra.Message.ProposerIndex) 375 | } else if beaconBlock.Fulu != nil { 376 | proposerIndex = uint64(beaconBlock.Fulu.Message.ProposerIndex) 377 | } else { 378 | log.Fatal("Beacon block was empty") 379 | } 380 | return proposerIndex 381 | } 382 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "math/big" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | nethttp "net/http" 14 | 15 | "github.com/attestantio/go-eth2-client/api" 16 | "github.com/attestantio/go-eth2-client/http" 17 | "github.com/attestantio/go-eth2-client/spec" 18 | "github.com/ethereum/go-ethereum/common/hexutil" 19 | "github.com/ethereum/go-ethereum/ethclient" 20 | "github.com/ethereum/go-ethereum/rpc" 21 | "github.com/rs/zerolog" 22 | 23 | "github.com/bilinearlabs/eth-metrics/config" 24 | "github.com/bilinearlabs/eth-metrics/db" 25 | "github.com/bilinearlabs/eth-metrics/pools" 26 | "github.com/pkg/errors" 27 | log "github.com/sirupsen/logrus" 28 | ) 29 | 30 | type NetworkParameters struct { 31 | genesisSeconds uint64 32 | slotsInEpoch uint64 33 | secondsPerSlot uint64 34 | } 35 | 36 | type Metrics struct { 37 | networkParameters *NetworkParameters 38 | config *config.Config 39 | db *db.Database 40 | httpClient *http.Service 41 | executionClient *ethclient.Client 42 | validatorKeysPerPool map[string][][]byte 43 | validatorKeyToPool map[string]string 44 | beaconState *BeaconState 45 | proposalDuties *ProposalDuties 46 | relayRewards *RelayRewards 47 | networkStats *NetworkStats 48 | blockData *BlockData 49 | } 50 | 51 | func NewMetrics( 52 | ctx context.Context, 53 | config *config.Config) (*Metrics, error) { 54 | 55 | var database *db.Database 56 | var err error 57 | 58 | if config.DatabasePath != "" { 59 | database, err = db.New(config.DatabasePath) 60 | if err != nil { 61 | return nil, errors.Wrap(err, "could not create postgresql") 62 | } 63 | err = database.CreateTables() 64 | if err != nil { 65 | return nil, errors.Wrap(err, "error creating pool table to store data") 66 | } 67 | } 68 | 69 | var validatorKeysPerPool map[string][][]byte 70 | var validatorKeyToPool map[string]string 71 | 72 | if config.ValidatorsFile != "" { 73 | validatorKeysPerPool, validatorKeyToPool, err = pools.ReadValidatorsFile(config.ValidatorsFile) 74 | if err != nil { 75 | return nil, errors.Wrap(err, "error reading validators file") 76 | } 77 | } else { 78 | // TODO check if mantain reading from txt files 79 | validatorKeysPerPool = make(map[string][][]byte) 80 | validatorKeyToPool = make(map[string]string) 81 | for _, poolName := range config.PoolNames { 82 | if strings.HasSuffix(poolName, ".txt") { 83 | pubKeysDeposited, err := pools.ReadCustomValidatorsFile(poolName) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | validatorKeysPerPool[poolName] = pubKeysDeposited 88 | for _, key := range pubKeysDeposited { 89 | keyStr := hexutil.Encode(key) 90 | validatorKeyToPool[keyStr] = poolName 91 | } 92 | log.Info("File: ", poolName, " contains ", len(pubKeysDeposited), " keys") 93 | } 94 | } 95 | } 96 | 97 | // Add header with credentials if provided 98 | encodedCredentials := base64.StdEncoding.EncodeToString([]byte(config.Credentials)) 99 | cred := map[string]string{} 100 | if config.Credentials != "" { 101 | cred["Authorization"] = "Basic " + encodedCredentials 102 | } 103 | 104 | client, err := http.New(context.Background(), 105 | http.WithTimeout(60*time.Second), 106 | http.WithAddress(config.Eth2Address), 107 | http.WithLogLevel(zerolog.WarnLevel), 108 | http.WithExtraHeaders(cred), 109 | ) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | httpClient := client.(*http.Service) 115 | 116 | genesis, err := httpClient.Genesis(context.Background(), &api.GenesisOpts{}) 117 | if err != nil { 118 | return nil, errors.Wrap(err, "error getting genesis info") 119 | } 120 | 121 | spec, err := httpClient.Spec(context.Background(), &api.SpecOpts{}) 122 | if err != nil { 123 | return nil, errors.Wrap(err, "error getting spec info") 124 | } 125 | 126 | slotsPerEpochInterface, found := spec.Data["SLOTS_PER_EPOCH"] 127 | if !found { 128 | return nil, errors.New("SLOTS_PER_EPOCH not found in spec") 129 | } 130 | 131 | secondsPerSlotInterface, found := spec.Data["SECONDS_PER_SLOT"] 132 | if !found { 133 | return nil, errors.New("SECONDS_PER_SLOT not found in spec") 134 | } 135 | 136 | slotsPerEpoch := slotsPerEpochInterface.(uint64) 137 | 138 | secondsPerSlot := uint64(secondsPerSlotInterface.(time.Duration).Seconds()) 139 | 140 | log.Info("Genesis time: ", genesis.Data.GenesisTime.Unix()) 141 | log.Info("Slots per epoch: ", slotsPerEpoch) 142 | log.Info("Seconds per slot: ", secondsPerSlot) 143 | 144 | rcpClient, err := rpc.DialOptions( 145 | context.Background(), 146 | config.Eth1Address, 147 | rpc.WithHTTPAuth(func(h nethttp.Header) error { 148 | h.Set("Authorization", "Basic "+encodedCredentials) 149 | return nil 150 | }), 151 | rpc.WithHTTPClient(&nethttp.Client{Timeout: 60 * time.Second}), 152 | ) 153 | if err != nil { 154 | return nil, errors.Wrap(err, "error dialing execution client") 155 | } 156 | 157 | executionClient := ethclient.NewClient(rcpClient) 158 | 159 | networkParameters := &NetworkParameters{ 160 | genesisSeconds: uint64(genesis.Data.GenesisTime.Unix()), 161 | slotsInEpoch: slotsPerEpoch, 162 | secondsPerSlot: secondsPerSlot, 163 | } 164 | 165 | return &Metrics{ 166 | networkParameters: networkParameters, 167 | db: database, 168 | httpClient: httpClient, 169 | executionClient: executionClient, 170 | config: config, 171 | validatorKeysPerPool: validatorKeysPerPool, 172 | validatorKeyToPool: validatorKeyToPool, 173 | }, nil 174 | } 175 | 176 | func (a *Metrics) Run() { 177 | bc, err := NewBeaconState( 178 | a.httpClient, 179 | a.networkParameters, 180 | a.db, 181 | a.config, 182 | a.networkParameters.slotsInEpoch, 183 | ) 184 | if err != nil { 185 | log.Fatal(err) 186 | // TODO: Add return here. 187 | } 188 | a.beaconState = bc 189 | 190 | pd, err := NewProposalDuties( 191 | a.httpClient, 192 | a.networkParameters, 193 | a.db, 194 | a.config, 195 | ) 196 | 197 | if err != nil { 198 | log.Fatal(err) 199 | } 200 | a.proposalDuties = pd 201 | 202 | rr, err := NewRelayRewards(a.networkParameters, a.validatorKeyToPool, a.config) 203 | if err != nil { 204 | log.Fatal(err) 205 | } 206 | a.relayRewards = rr 207 | 208 | ns, err := NewNetworkStats(a.db) 209 | if err != nil { 210 | log.Fatal(err) 211 | } 212 | a.networkStats = ns 213 | 214 | bd, err := NewBlockData(a.httpClient, a.executionClient, a.networkParameters, a.config) 215 | if err != nil { 216 | log.Fatal(err) 217 | } 218 | a.blockData = bd 219 | 220 | for _, poolName := range a.config.PoolNames { 221 | // Check that the validator keys are correct 222 | _, _, err := a.GetValidatorKeys(poolName) 223 | if err != nil { 224 | log.Fatal(err) 225 | } 226 | 227 | } 228 | go a.Loop() 229 | } 230 | 231 | func (a *Metrics) Loop() { 232 | var prevEpoch uint64 = uint64(0) 233 | var prevBeaconState *spec.VersionedBeaconState = nil 234 | // TODO: Refactor and hoist some stuff out to a function 235 | for { 236 | // Before doing anything, check if we are in the next epoch 237 | opts := api.NodeSyncingOpts{ 238 | Common: api.CommonOpts{ 239 | Timeout: 5 * time.Second, 240 | }, 241 | } 242 | headSlot, err := a.httpClient.NodeSyncing(context.Background(), &opts) 243 | if err != nil { 244 | log.Error("Could not get node sync status:", err) 245 | time.Sleep(5 * time.Second) 246 | continue 247 | } 248 | 249 | if headSlot.Data.IsSyncing { 250 | log.Error("Node is not in sync") 251 | time.Sleep(5 * time.Second) 252 | continue 253 | } 254 | 255 | // Leave some maring of 2 epochs 256 | currentEpoch := uint64(headSlot.Data.HeadSlot)/uint64(a.networkParameters.slotsInEpoch) - 2 257 | 258 | // If a debug epoch is set, overwrite the slot. Will compute just metrics for that epoch 259 | if a.config.EpochDebug != "" { 260 | epochDebugUint64, err := strconv.ParseUint(a.config.EpochDebug, 10, 64) 261 | if err != nil { 262 | log.Fatal(err) 263 | } 264 | log.Warn("Debugging mode, calculating metrics for epoch: ", a.config.EpochDebug) 265 | currentEpoch = epochDebugUint64 266 | } 267 | 268 | if prevEpoch >= currentEpoch { 269 | // do nothing 270 | time.Sleep(5 * time.Second) 271 | continue 272 | } 273 | 274 | missingEpochs, err := a.db.GetMissingEpochs(currentEpoch, a.config.BackfillEpochs) 275 | if err != nil { 276 | log.Error(err) 277 | time.Sleep(5 * time.Second) 278 | continue 279 | } 280 | 281 | if len(missingEpochs) > 0 { 282 | log.Info("Backfilling epochs: ", missingEpochs) 283 | } 284 | 285 | // Do backfilling. 286 | for _, epoch := range missingEpochs { 287 | if prevBeaconState != nil { 288 | prevSlot, err := prevBeaconState.Slot() 289 | prevEpoch = uint64(prevSlot) % a.networkParameters.slotsInEpoch 290 | if err != nil { 291 | // TODO: Handle this gracefully 292 | log.Fatal(err, "error getting slot from previous beacon state") 293 | } 294 | if (prevEpoch + 1) != epoch { 295 | prevBeaconState = nil 296 | } 297 | } 298 | currentBeaconState, err := a.ProcessEpoch(epoch, prevBeaconState) 299 | if err != nil { 300 | log.Error(err) 301 | time.Sleep(5 * time.Second) 302 | continue 303 | } 304 | prevBeaconState = currentBeaconState 305 | } 306 | 307 | currentBeaconState, err := a.ProcessEpoch(currentEpoch, prevBeaconState) 308 | if err != nil { 309 | log.Error(err) 310 | time.Sleep(5 * time.Second) 311 | continue 312 | } 313 | 314 | prevBeaconState = currentBeaconState 315 | prevEpoch = currentEpoch 316 | 317 | if a.config.EpochDebug != "" { 318 | log.Warn("Running in debug mode, exiting ok.") 319 | os.Exit(0) 320 | } 321 | } 322 | } 323 | 324 | func (a *Metrics) ProcessEpoch( 325 | currentEpoch uint64, 326 | prevBeaconState *spec.VersionedBeaconState) (*spec.VersionedBeaconState, error) { 327 | // Fetch proposal duties, meaning who shall propose each block within this epoch 328 | duties, err := a.proposalDuties.GetProposalDuties(currentEpoch) 329 | if err != nil { 330 | return nil, errors.Wrap(err, "error getting proposal duties") 331 | } 332 | 333 | // Fetch who actually proposed the blocks in this epoch 334 | proposed, err := a.proposalDuties.GetProposedBlocks(currentEpoch) 335 | if err != nil { 336 | return nil, errors.Wrap(err, "error getting proposed blocks") 337 | } 338 | 339 | // Summarize duties + proposed in a struct 340 | proposalMetrics, err := a.proposalDuties.GetProposalMetrics(duties, proposed) 341 | if err != nil { 342 | return nil, errors.Wrap(err, "error getting proposal metrics") 343 | } 344 | 345 | currentBeaconState, err := a.beaconState.GetBeaconState(currentEpoch) 346 | if err != nil { 347 | return nil, errors.Wrap(err, "error fetching beacon state") 348 | } 349 | 350 | // if no prev beacon state is known, fetch it 351 | if prevBeaconState == nil { 352 | prevBeaconState, err = a.beaconState.GetBeaconState(currentEpoch - 1) 353 | if err != nil { 354 | return nil, errors.Wrap(err, "error fetching previous beacon state") 355 | } 356 | } 357 | 358 | // Map to quickly convert public keys to index 359 | valKeyToIndex := PopulateKeysToIndexesMap(currentBeaconState) 360 | 361 | processedConsolidations, err := GetProcessedConsolidations(prevBeaconState, currentBeaconState) 362 | if err != nil { 363 | return nil, errors.Wrap(err, "error getting processed consolidations") 364 | } 365 | 366 | relayRewardsPerPool, slotsWithMEVRewards, err := a.relayRewards.GetRelayRewards(currentEpoch) 367 | if err != nil { 368 | return nil, errors.Wrap(err, "error getting relay rewards") 369 | } 370 | 371 | // Get withdrawals and proposer tips from all blocks of the epoch 372 | epochBlockData, err := a.blockData.GetEpochBlockData(currentEpoch, slotsWithMEVRewards) 373 | if err != nil { 374 | return nil, errors.Wrap(err, "error getting epoch block data") 375 | } 376 | validatorIndexToWithdrawalAmount := epochBlockData.Withdrawals 377 | proposerTips := epochBlockData.ProposerTips 378 | 379 | err = a.networkStats.Run(currentEpoch, currentBeaconState) 380 | if err != nil { 381 | return nil, errors.Wrap(err, "error getting network stats") 382 | } 383 | 384 | // Iterate all pools and calculate metrics using the fetched data 385 | for poolName, pubKeys := range a.validatorKeysPerPool { 386 | validatorIndexes := GetIndexesFromKeys(pubKeys, valKeyToIndex) 387 | 388 | relayRewards := big.NewInt(0) 389 | if reward, ok := relayRewardsPerPool[poolName]; ok { 390 | relayRewards.Add(relayRewards, reward) 391 | } 392 | err = a.beaconState.Run( 393 | pubKeys, 394 | poolName, 395 | currentBeaconState, 396 | prevBeaconState, 397 | valKeyToIndex, 398 | relayRewards, 399 | validatorIndexToWithdrawalAmount, 400 | proposerTips, 401 | processedConsolidations, 402 | ) 403 | if err != nil { 404 | return nil, errors.Wrap(err, "error running beacon state") 405 | } 406 | 407 | err = a.proposalDuties.RunProposalMetrics(validatorIndexes, poolName, &proposalMetrics) 408 | if err != nil { 409 | return nil, errors.Wrap(err, "error running proposal metrics") 410 | } 411 | } 412 | 413 | return currentBeaconState, nil 414 | } 415 | 416 | func (a *Metrics) GetValidatorKeys(poolName string) (string, [][]byte, error) { 417 | var pubKeysDeposited [][]byte 418 | var err error 419 | if strings.HasSuffix(poolName, ".txt") { 420 | // Vanila file, one key per line 421 | pubKeysDeposited, err = pools.ReadCustomValidatorsFile(poolName) 422 | if err != nil { 423 | log.Fatal(err) 424 | } 425 | // trim the file path and extension 426 | poolName = filepath.Base(poolName) 427 | poolName = strings.TrimSuffix(poolName, filepath.Ext(poolName)) 428 | } else if strings.HasSuffix(poolName, ".csv") { 429 | // ethsta.com format 430 | pubKeysDeposited, err = pools.ReadEthstaValidatorsFile(poolName) 431 | if err != nil { 432 | log.Fatal(err) 433 | } 434 | // trim the file path and extension 435 | poolName = filepath.Base(poolName) 436 | poolName = strings.TrimSuffix(poolName, filepath.Ext(poolName)) 437 | 438 | } 439 | return poolName, pubKeysDeposited, nil 440 | } 441 | -------------------------------------------------------------------------------- /metrics/beaconstate.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "math/big" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/attestantio/go-eth2-client/api" 12 | "github.com/attestantio/go-eth2-client/http" 13 | "github.com/attestantio/go-eth2-client/spec" 14 | "github.com/attestantio/go-eth2-client/spec/altair" 15 | "github.com/attestantio/go-eth2-client/spec/electra" 16 | "github.com/attestantio/go-eth2-client/spec/phase0" 17 | "github.com/pkg/errors" 18 | 19 | "github.com/bilinearlabs/eth-metrics/config" 20 | "github.com/bilinearlabs/eth-metrics/db" 21 | "github.com/bilinearlabs/eth-metrics/schemas" 22 | 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | type BeaconState struct { 27 | consensus *http.Service 28 | networkParameters *NetworkParameters 29 | database *db.Database 30 | config *config.Config 31 | slotsInEpoch uint64 32 | } 33 | 34 | func NewBeaconState( 35 | httpClient *http.Service, 36 | networkParameters *NetworkParameters, 37 | database *db.Database, 38 | config *config.Config, 39 | slotsInEpoch uint64, 40 | ) (*BeaconState, error) { 41 | 42 | return &BeaconState{ 43 | consensus: httpClient, 44 | networkParameters: networkParameters, 45 | database: database, 46 | config: config, 47 | slotsInEpoch: slotsInEpoch, 48 | }, nil 49 | } 50 | 51 | func (p *BeaconState) Run( 52 | validatorKeys [][]byte, 53 | poolName string, 54 | currentBeaconState *spec.VersionedBeaconState, 55 | prevBeaconState *spec.VersionedBeaconState, 56 | valKeyToIndex map[string]uint64, 57 | relayRewards *big.Int, 58 | validatorIndexToWithdrawalAmount map[uint64]*big.Int, 59 | proposerTips map[uint64]*big.Int, 60 | validatorIndexToProcessedConsolidation map[uint64][]*electra.PendingConsolidation) error { 61 | 62 | if currentBeaconState == nil || prevBeaconState == nil { 63 | return errors.New("current or previous beacon state is nil") 64 | } 65 | if len(validatorKeys) == 0 { 66 | return errors.New("no validator keys provided") 67 | } 68 | 69 | currentSlot, err := currentBeaconState.Slot() 70 | if err != nil { 71 | return errors.Wrap(err, "error getting slot from current beacon state") 72 | } 73 | 74 | prevSlot, err := prevBeaconState.Slot() 75 | if err != nil { 76 | return errors.Wrap(err, "error getting slot from previous beacon state") 77 | } 78 | 79 | // Distance shall be the slots in an epoch 80 | if currentSlot != (prevSlot + phase0.Slot(p.slotsInEpoch)) { 81 | return errors.New(fmt.Sprintf("slot mismatch between current and previous beacon state: %d vs %d", 82 | currentSlot, prevSlot)) 83 | } 84 | 85 | validatorIndexes := GetIndexesFromKeys(validatorKeys, valKeyToIndex) 86 | activeValidatorIndexes := p.GetActiveIndexes(validatorIndexes, currentBeaconState) 87 | 88 | // TODO: Redundant parameters already in the class 89 | metrics, err := p.PopulateParticipationAndBalance( 90 | poolName, 91 | activeValidatorIndexes, 92 | currentBeaconState, 93 | prevBeaconState, 94 | validatorIndexToWithdrawalAmount, 95 | validatorIndexToProcessedConsolidation) 96 | 97 | if err != nil { 98 | return errors.Wrap(err, "error populating participation and balance") 99 | } 100 | 101 | metrics.NOfActiveValidators = uint64(len(activeValidatorIndexes)) 102 | metrics.MEVRewards = relayRewards 103 | 104 | aggregatedProposerTips := big.NewInt(0) 105 | for _, activeValidatorIndex := range activeValidatorIndexes { 106 | if tip, ok := proposerTips[activeValidatorIndex]; ok { 107 | aggregatedProposerTips.Add(aggregatedProposerTips, tip) 108 | } 109 | } 110 | metrics.ProposerTips = aggregatedProposerTips 111 | 112 | syncCommitteeKeys := BLSPubKeyToByte(GetCurrentSyncCommittee(currentBeaconState)) 113 | syncCommitteeIndexes := GetIndexesFromKeys(syncCommitteeKeys, valKeyToIndex) 114 | poolSyncIndexes := GetValidatorsIn(syncCommitteeIndexes, activeValidatorIndexes) 115 | 116 | // Temporal to debug: 117 | p.ParticipationDebug(activeValidatorIndexes, currentBeaconState) 118 | 119 | log.Info("The pool:", poolName, " contains ", len(validatorKeys), " keys (may be hardcoded)") 120 | log.Info("The pool:", poolName, " contains ", len(validatorIndexes), " validators detected in the beacon state") 121 | log.Info("The pool:", poolName, " contains ", len(activeValidatorIndexes), " active validators detected in the beacon state") 122 | log.Info("Pool: ", poolName, " sync committee validators ", poolSyncIndexes) 123 | 124 | logMetrics(metrics, poolName) 125 | 126 | if p.database != nil { 127 | err := p.database.StoreValidatorPerformance(metrics) 128 | if err != nil { 129 | return errors.Wrap(err, "could not store validator performance") 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // TODO: Very naive approach 137 | func GetValidatorsIn(allSyncCommitteeIndexes []uint64, poolValidatorIndexes []uint64) []uint64 { 138 | poolCommmitteeIndexes := make([]uint64, 0) 139 | for i := range allSyncCommitteeIndexes { 140 | for j := range poolValidatorIndexes { 141 | if allSyncCommitteeIndexes[i] == poolValidatorIndexes[j] { 142 | poolCommmitteeIndexes = append(poolCommmitteeIndexes, allSyncCommitteeIndexes[i]) 143 | break 144 | } 145 | } 146 | } 147 | return poolCommmitteeIndexes 148 | } 149 | 150 | // Check if element is in set 151 | // TODO: Move to utils 152 | func IsValidatorIn(element uint64, set []uint64) bool { 153 | for i := range set { 154 | if element == set[i] { 155 | return true 156 | } 157 | } 158 | return false 159 | } 160 | 161 | func PopulateKeysToIndexesMap(beaconState *spec.VersionedBeaconState) map[string]uint64 { 162 | // TODO: Naive approach. Reset the map every time 163 | valKeyToIndex := make(map[string]uint64, 0) 164 | for index, beaconStateKey := range GetValidators(beaconState) { 165 | valKeyToIndex[hex.EncodeToString(beaconStateKey.PublicKey[:])] = uint64(index) 166 | } 167 | return valKeyToIndex 168 | } 169 | 170 | // TODO: Move to utils 171 | func BLSPubKeyToByte(blsKeys []phase0.BLSPubKey) [][]byte { 172 | keys := make([][]byte, 0) 173 | for i := range blsKeys { 174 | keys = append(keys, blsKeys[i][:]) 175 | } 176 | return keys 177 | } 178 | 179 | // Make sure the validator indexes are active 180 | func (p *BeaconState) PopulateParticipationAndBalance( 181 | poolName string, 182 | activeValidatorIndexes []uint64, 183 | beaconState *spec.VersionedBeaconState, 184 | prevBeaconState *spec.VersionedBeaconState, 185 | validatorIndexToWithdrawalAmount map[uint64]*big.Int, 186 | validatorIndexToProcessedConsolidation map[uint64][]*electra.PendingConsolidation) (schemas.ValidatorPerformanceMetrics, error) { 187 | 188 | metrics := schemas.ValidatorPerformanceMetrics{ 189 | EarnedBalance: big.NewInt(0), 190 | LosedBalance: big.NewInt(0), 191 | TotalBalance: big.NewInt(0), 192 | EffectiveBalance: big.NewInt(0), 193 | TotalRewards: big.NewInt(0), 194 | } 195 | 196 | nOfIncorrectSource, nOfIncorrectTarget, nOfIncorrectHead, indexesMissedAtt := p.GetParticipation( 197 | activeValidatorIndexes, 198 | beaconState) 199 | 200 | currentBalance, currentEffectiveBalance := GetTotalBalanceAndEffective(activeValidatorIndexes, beaconState) 201 | prevBalance, prevEffectiveBalance := GetTotalBalanceAndEffective(activeValidatorIndexes, prevBeaconState) 202 | 203 | // Make sure we are comparing apples to apples 204 | effectiveBalanceDiff := new(big.Int).Sub(currentEffectiveBalance, prevEffectiveBalance) 205 | effectiveBalanceDiff.Abs(effectiveBalanceDiff) 206 | if effectiveBalanceDiff.Cmp(big.NewInt(0)) != 0 && effectiveBalanceDiff.Cmp(big.NewInt(1000000000)) < 0 { // > 0 && < 1 ETH 207 | return schemas.ValidatorPerformanceMetrics{}, 208 | errors.New(fmt.Sprint("Can't calculate delta balances for pool: ", poolName, ", effective balances are different by less than 1 ETH:", 209 | currentEffectiveBalance, " vs ", prevEffectiveBalance)) 210 | } 211 | 212 | rewards := big.NewInt(0).Sub(currentBalance, currentEffectiveBalance) 213 | deltaEpochBalance := big.NewInt(0).Sub(currentBalance, prevBalance) 214 | 215 | lessBalanceIndexes, earnedBalance, lostBalance, err := p.GetValidatorsWithLessBalance( 216 | activeValidatorIndexes, 217 | prevBeaconState, 218 | beaconState, 219 | validatorIndexToWithdrawalAmount, 220 | validatorIndexToProcessedConsolidation) 221 | 222 | if err != nil { 223 | return schemas.ValidatorPerformanceMetrics{}, err 224 | } 225 | 226 | metrics.IndexesLessBalance = lessBalanceIndexes 227 | metrics.EarnedBalance = earnedBalance 228 | metrics.LosedBalance = lostBalance 229 | metrics.PoolName = poolName 230 | metrics.Time = time.Unix(int64(GetTimestamp(beaconState)), 0) 231 | 232 | metrics.Epoch = GetSlot(beaconState) / p.networkParameters.slotsInEpoch 233 | 234 | metrics.NOfTotalVotes = uint64(len(activeValidatorIndexes)) * 3 235 | metrics.NOfIncorrectSource = nOfIncorrectSource 236 | metrics.NOfIncorrectTarget = nOfIncorrectTarget 237 | metrics.NOfIncorrectHead = nOfIncorrectHead 238 | metrics.NOfValidatingKeys = uint64(len(activeValidatorIndexes)) 239 | //metrics.NOfValsWithLessBalance = nOfValsWithDecreasedBalance 240 | //metrics.EarnedBalance = earned 241 | //metrics.LosedBalance = losed 242 | metrics.IndexesMissedAtt = indexesMissedAtt 243 | //metrics.LostBalanceKeys = lostKeys 244 | metrics.TotalBalance = currentBalance 245 | metrics.EffectiveBalance = currentEffectiveBalance 246 | metrics.TotalRewards = rewards 247 | metrics.DeltaEpochBalance = deltaEpochBalance 248 | 249 | return metrics, nil 250 | } 251 | 252 | // TODO: Get slashed validators 253 | 254 | func (p *BeaconState) GetBeaconState(epoch uint64) (*spec.VersionedBeaconState, error) { 255 | log.Info("Fetching beacon state for epoch: ", epoch) 256 | // Its important to get the beacon state from the last slot of each epoch 257 | // to allow all attestations to be included 258 | // If epoch=1, slot = epoch*32 = 32, which is the first slot of epoch 1 259 | // but we want to run the metrics on the last slot, so -1 260 | // goes to the last slot of the previous epoch 261 | slotStr := strconv.FormatUint((epoch+1)*p.networkParameters.slotsInEpoch-1, 10) 262 | 263 | ctxTimeout, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(p.config.StateTimeout)) 264 | defer cancel() 265 | opts := api.BeaconStateOpts{ 266 | State: slotStr, 267 | // Override http client timeout 268 | Common: api.CommonOpts{ 269 | Timeout: time.Second * time.Duration(p.config.StateTimeout), 270 | }, 271 | } 272 | beaconState, err := p.consensus.BeaconState( 273 | ctxTimeout, 274 | &opts) 275 | if err != nil { 276 | return nil, err 277 | } 278 | log.Info("Got beacon state for epoch:", GetSlot(beaconState.Data)/p.networkParameters.slotsInEpoch) 279 | return beaconState.Data, nil 280 | } 281 | 282 | func GetTotalBalanceAndEffective( 283 | activeValidatorIndexes []uint64, 284 | beaconState *spec.VersionedBeaconState) (*big.Int, *big.Int) { 285 | 286 | totalBalances := big.NewInt(0).SetUint64(0) 287 | effectiveBalance := big.NewInt(0).SetUint64(0) 288 | validators := GetValidators(beaconState) 289 | balances := GetBalances(beaconState) 290 | 291 | for _, valIdx := range activeValidatorIndexes { 292 | // Skip if index is not present in the beacon state 293 | if valIdx >= uint64(len(balances)) { 294 | log.Warn("validator index goes beyond the beacon state indexes") 295 | continue 296 | } 297 | valBalance := big.NewInt(0).SetUint64(balances[valIdx]) 298 | valEffBalance := big.NewInt(0).SetUint64(uint64(validators[valIdx].EffectiveBalance)) 299 | totalBalances.Add(totalBalances, valBalance) 300 | effectiveBalance.Add(effectiveBalance, valEffBalance) 301 | } 302 | return totalBalances, effectiveBalance 303 | } 304 | 305 | // Returns the indexes of the validator keys. Note that the indexes 306 | // may belong to active, inactive or even slashed keys. 307 | func GetIndexesFromKeys( 308 | validatorKeys [][]byte, 309 | valKeyToIndex map[string]uint64) []uint64 { 310 | 311 | indexes := make([]uint64, 0) 312 | 313 | // Use global prepopulated map 314 | for _, key := range validatorKeys { 315 | if valIndex, ok := valKeyToIndex[hex.EncodeToString(key)]; ok { 316 | indexes = append(indexes, valIndex) 317 | } else { 318 | log.Warn("Index for key: ", hex.EncodeToString(key), " not found in beacon state") 319 | } 320 | } 321 | 322 | return indexes 323 | } 324 | 325 | func (p *BeaconState) GetActiveIndexes( 326 | validatorIndexes []uint64, 327 | beaconState *spec.VersionedBeaconState) []uint64 { 328 | 329 | activeIndexes := make([]uint64, 0) 330 | 331 | validators := GetValidators(beaconState) 332 | beaconStateEpoch := GetSlot(beaconState) / p.networkParameters.slotsInEpoch 333 | 334 | for _, valIdx := range validatorIndexes { 335 | if beaconStateEpoch >= uint64(validators[valIdx].ActivationEpoch) && 336 | beaconStateEpoch < uint64(validators[valIdx].ExitEpoch) { 337 | activeIndexes = append(activeIndexes, valIdx) 338 | } 339 | } 340 | 341 | return activeIndexes 342 | } 343 | 344 | func (p *BeaconState) GetValidatorsWithLessBalance( 345 | activeValidatorIndexes []uint64, 346 | prevBeaconState *spec.VersionedBeaconState, 347 | currentBeaconState *spec.VersionedBeaconState, 348 | validatorIndexToWithdrawalAmount map[uint64]*big.Int, 349 | validatorIndexToProcessedConsolidation map[uint64][]*electra.PendingConsolidation) ([]uint64, *big.Int, *big.Int, error) { 350 | 351 | prevEpoch := GetSlot(prevBeaconState) / p.networkParameters.slotsInEpoch 352 | currEpoch := GetSlot(currentBeaconState) / p.networkParameters.slotsInEpoch 353 | prevBalances := GetBalances(prevBeaconState) 354 | prevValidators := GetValidators(prevBeaconState) 355 | currBalances := GetBalances(currentBeaconState) 356 | 357 | if (prevEpoch + 1) != currEpoch { 358 | return nil, nil, nil, errors.New(fmt.Sprintf( 359 | "epochs are not consecutive: slot %d vs %d", 360 | prevEpoch, 361 | currEpoch)) 362 | } 363 | 364 | indexesWithLessBalance := make([]uint64, 0) 365 | earnedBalance := big.NewInt(0) 366 | lostBalance := big.NewInt(0) 367 | 368 | for _, valIdx := range activeValidatorIndexes { 369 | // handle if there was a new validator index not register in the prev state 370 | if valIdx >= uint64(len(prevBalances)) { 371 | log.Warn("validator index goes beyond the beacon state indexes") 372 | continue 373 | } 374 | 375 | prevEpochValBalance := big.NewInt(0).SetUint64(prevBalances[valIdx]) 376 | currentEpochValBalance := big.NewInt(0).SetUint64(currBalances[valIdx]) 377 | // Check if there is a withdrawal amount and add it to the balance 378 | if valWithdrawalAmount, ok := validatorIndexToWithdrawalAmount[valIdx]; ok { 379 | currentEpochValBalance.Add(currentEpochValBalance, valWithdrawalAmount) 380 | } 381 | // Check if there are consolidations and substract source effective balance 382 | if consolidations, ok := validatorIndexToProcessedConsolidation[valIdx]; ok { 383 | for _, consolidation := range consolidations { 384 | sourceBalance := big.NewInt(0).SetUint64(uint64(prevValidators[consolidation.SourceIndex].EffectiveBalance)) 385 | currentEpochValBalance.Sub(currentEpochValBalance, sourceBalance) 386 | } 387 | } 388 | 389 | delta := big.NewInt(0).Sub(currentEpochValBalance, prevEpochValBalance) 390 | 391 | if delta.Cmp(big.NewInt(0)) == -1 { 392 | indexesWithLessBalance = append(indexesWithLessBalance, valIdx) 393 | lostBalance.Add(lostBalance, delta) 394 | } else { 395 | earnedBalance.Add(earnedBalance, delta) 396 | } 397 | } 398 | 399 | return indexesWithLessBalance, earnedBalance, lostBalance, nil 400 | } 401 | 402 | func (p *BeaconState) ParticipationDebug( 403 | activeValidatorIndexes []uint64, 404 | beaconState *spec.VersionedBeaconState) { 405 | 406 | validators := GetValidators(beaconState) 407 | previousEpochParticipation := GetPreviousEpochParticipation(beaconState) 408 | 409 | nActiveValidators := uint64(0) 410 | 411 | beaconStateEpoch := GetSlot(beaconState) / p.networkParameters.slotsInEpoch 412 | 413 | var nCorrectSource, nCorrectTarget, nCorrectHead uint64 414 | 415 | for _, valIndx := range activeValidatorIndexes { 416 | // Ignore slashed validators 417 | if validators[valIndx].Slashed { 418 | continue 419 | } 420 | 421 | // Ignore not yet active validators 422 | if uint64(validators[valIndx].ActivationEpoch) > beaconStateEpoch { 423 | continue 424 | } 425 | 426 | epochAttestations := previousEpochParticipation[valIndx] 427 | if isBitSet(uint8(epochAttestations), 0) { 428 | nCorrectSource++ 429 | } 430 | if isBitSet(uint8(epochAttestations), 1) { 431 | nCorrectTarget++ 432 | } 433 | if isBitSet(uint8(epochAttestations), 2) { 434 | nCorrectHead++ 435 | } 436 | nActiveValidators++ 437 | } 438 | 439 | log.Info("Active validators: ", nActiveValidators) 440 | log.Info("Correct Source: ", (float64(nCorrectSource) / float64(nActiveValidators) * 100)) 441 | log.Info("Correct Target: ", (float64(nCorrectTarget) / float64(nActiveValidators) * 100)) 442 | log.Info("Correct Head: ", (float64(nCorrectHead) / float64(nActiveValidators) * 100)) 443 | } 444 | 445 | // See spec: from LSB to MSB: source, target, head. 446 | // https://github.com/ethereum/consensus-specs/blob/master/specs/altair/beacon-chain.md#participation-flag-indices 447 | func (p *BeaconState) GetParticipation( 448 | activeValidatorIndexes []uint64, 449 | beaconState *spec.VersionedBeaconState) (uint64, uint64, uint64, []uint64) { 450 | 451 | indexesMissedAtt := make([]uint64, 0) 452 | 453 | validators := GetValidators(beaconState) 454 | previousEpochParticipation := GetPreviousEpochParticipation(beaconState) 455 | 456 | var nIncorrectSource, nIncorrectTarget, nIncorrectHead uint64 457 | 458 | for _, valIndx := range activeValidatorIndexes { 459 | // Ignore slashed validators 460 | if validators[valIndx].Slashed { 461 | continue 462 | } 463 | beaconStateEpoch := GetSlot(beaconState) / p.networkParameters.slotsInEpoch 464 | // Ignore not yet active validators 465 | // TODO: Test this 466 | if uint64(validators[valIndx].ActivationEpoch) > beaconStateEpoch { 467 | //log.Warn("index: ", valIndx, " is not active yet") 468 | continue 469 | } 470 | 471 | // TODO: Dont know why but Infura returns 0 for all CurrentEpochAttestations 472 | epochAttestations := previousEpochParticipation[valIndx] 473 | // TODO: Count if bit is set instead if not set. Easier. 474 | if !isBitSet(uint8(epochAttestations), 0) { 475 | nIncorrectSource++ 476 | indexesMissedAtt = append(indexesMissedAtt, valIndx) 477 | } 478 | if !isBitSet(uint8(epochAttestations), 1) { 479 | nIncorrectTarget++ 480 | } 481 | if !isBitSet(uint8(epochAttestations), 2) { 482 | nIncorrectHead++ 483 | } 484 | } 485 | return nIncorrectSource, nIncorrectTarget, nIncorrectHead, indexesMissedAtt 486 | } 487 | 488 | // Check if bit n (0..7) is set where 0 is the LSB in little endian 489 | func isBitSet(input uint8, n int) bool { 490 | return (input & (1 << n)) > uint8(0) 491 | } 492 | 493 | func GetProcessedConsolidations( 494 | prevBeaconState *spec.VersionedBeaconState, 495 | currentBeaconState *spec.VersionedBeaconState, 496 | ) (map[uint64][]*electra.PendingConsolidation, error) { 497 | consolidations := make(map[uint64][]*electra.PendingConsolidation) 498 | 499 | validators := GetValidators(currentBeaconState) 500 | prevPendingConsolidations := GetPendingConsolidations(prevBeaconState) 501 | currPendingConsolidations := GetPendingConsolidations(currentBeaconState) 502 | 503 | if prevPendingConsolidations == nil || currPendingConsolidations == nil { 504 | return nil, errors.New("state with nil pending consolidations found") 505 | } 506 | 507 | if len(validators) == 0 { 508 | return consolidations, nil 509 | } 510 | 511 | // Set of current pending consolidations 512 | currPendingConsolidationsSet := make(map[electra.PendingConsolidation]bool) 513 | for _, consolidation := range currPendingConsolidations { 514 | currPendingConsolidationsSet[*consolidation] = true 515 | } 516 | 517 | // If the consolidation is not in the current set, it was processed or source slashed 518 | for _, consolidation := range prevPendingConsolidations { 519 | if _, ok := currPendingConsolidationsSet[*consolidation]; !ok { 520 | sourceValidator := validators[consolidation.SourceIndex] 521 | if sourceValidator.Slashed { 522 | continue 523 | } 524 | consolidations[uint64(consolidation.TargetIndex)] = append(consolidations[uint64(consolidation.TargetIndex)], consolidation) 525 | } 526 | } 527 | return consolidations, nil 528 | } 529 | 530 | func logMetrics( 531 | metrics schemas.ValidatorPerformanceMetrics, 532 | poolName string) { 533 | balanceDecreasedPercent := (float64(len(metrics.IndexesLessBalance)) / float64(metrics.NOfValidatingKeys)) * 100 534 | 535 | log.WithFields(log.Fields{ 536 | "PoolName": poolName, 537 | "Epoch": metrics.Epoch, 538 | "nOfActiveValidators": metrics.NOfActiveValidators, 539 | "nOfTotalVotes": metrics.NOfTotalVotes, 540 | "nOfIncorrectSource": metrics.NOfIncorrectSource, 541 | "nOfIncorrectTarget": metrics.NOfIncorrectTarget, 542 | "nOfIncorrectHead": metrics.NOfIncorrectHead, 543 | "nOfValidators": metrics.NOfValidatingKeys, 544 | "PercentIncorrectSource": (float64(metrics.NOfIncorrectSource) / float64(metrics.NOfValidatingKeys)) * 100, 545 | "PercentIncorrectTarget": (float64(metrics.NOfIncorrectTarget) / float64(metrics.NOfValidatingKeys)) * 100, 546 | "PercentIncorrectHead": (float64(metrics.NOfIncorrectHead) / float64(metrics.NOfValidatingKeys)) * 100, 547 | "nOfValsWithDecreasedBalance": len(metrics.IndexesLessBalance), 548 | "balanceDecreasedPercent": balanceDecreasedPercent, 549 | "epochEarnedBalance": metrics.EarnedBalance, 550 | "epochLostBalance": metrics.LosedBalance, 551 | "totalBalance": metrics.TotalBalance, 552 | "effectiveBalance": metrics.EffectiveBalance, 553 | "totalRewards": metrics.TotalRewards, 554 | "ValidadorKeyMissedAtt": metrics.IndexesMissedAtt, 555 | "ValidadorKeyLessBalance": metrics.IndexesLessBalance, 556 | "DeltaEpochBalance": metrics.DeltaEpochBalance, 557 | "epochMEVRewards": metrics.MEVRewards, 558 | }).Info(poolName + " Stats:") 559 | } 560 | 561 | // Wrappers on top of the beacon state to fetch some fields regardless of Altair or Bellatrix 562 | // Note that this is needed because both block types do not implement the same interface, since 563 | // the state differs accross versions. 564 | // Note also that this functions only make sense for the beacon state fields that are common 565 | // to all the versioned beacon states. 566 | func GetValidators(beaconState *spec.VersionedBeaconState) []*phase0.Validator { 567 | var validators []*phase0.Validator 568 | if beaconState.Altair != nil { 569 | validators = beaconState.Altair.Validators 570 | } else if beaconState.Bellatrix != nil { 571 | validators = beaconState.Bellatrix.Validators 572 | } else if beaconState.Capella != nil { 573 | validators = beaconState.Capella.Validators 574 | } else if beaconState.Deneb != nil { 575 | validators = beaconState.Deneb.Validators 576 | } else if beaconState.Electra != nil { 577 | validators = beaconState.Electra.Validators 578 | } else if beaconState.Fulu != nil { 579 | validators = beaconState.Fulu.Validators 580 | } else { 581 | log.Fatal("Beacon state was empty") 582 | } 583 | return validators 584 | } 585 | 586 | func GetBalances(beaconState *spec.VersionedBeaconState) []uint64 { 587 | var tmpBalances []phase0.Gwei 588 | if beaconState.Altair != nil { 589 | tmpBalances = beaconState.Altair.Balances 590 | } else if beaconState.Bellatrix != nil { 591 | tmpBalances = beaconState.Bellatrix.Balances 592 | } else if beaconState.Capella != nil { 593 | tmpBalances = beaconState.Capella.Balances 594 | } else if beaconState.Deneb != nil { 595 | tmpBalances = beaconState.Deneb.Balances 596 | } else if beaconState.Electra != nil { 597 | tmpBalances = beaconState.Electra.Balances 598 | } else if beaconState.Fulu != nil { 599 | tmpBalances = beaconState.Fulu.Balances 600 | } else { 601 | log.Fatal("Beacon state was empty") 602 | } 603 | 604 | balances := make([]uint64, len(tmpBalances)) 605 | for i := range tmpBalances { 606 | balances[i] = uint64(tmpBalances[i]) 607 | } 608 | return balances 609 | } 610 | 611 | func GetPreviousEpochParticipation(beaconState *spec.VersionedBeaconState) []altair.ParticipationFlags { 612 | var previousEpochParticipation []altair.ParticipationFlags 613 | if beaconState.Altair != nil { 614 | previousEpochParticipation = beaconState.Altair.PreviousEpochParticipation 615 | } else if beaconState.Bellatrix != nil { 616 | previousEpochParticipation = beaconState.Bellatrix.PreviousEpochParticipation 617 | } else if beaconState.Capella != nil { 618 | previousEpochParticipation = beaconState.Capella.PreviousEpochParticipation 619 | } else if beaconState.Deneb != nil { 620 | previousEpochParticipation = beaconState.Deneb.PreviousEpochParticipation 621 | } else if beaconState.Electra != nil { 622 | previousEpochParticipation = beaconState.Electra.PreviousEpochParticipation 623 | } else if beaconState.Fulu != nil { 624 | previousEpochParticipation = beaconState.Fulu.PreviousEpochParticipation 625 | } else { 626 | log.Fatal("Beacon state was empty") 627 | } 628 | return previousEpochParticipation 629 | } 630 | 631 | func GetSlot(beaconState *spec.VersionedBeaconState) uint64 { 632 | var slot uint64 633 | if beaconState.Altair != nil { 634 | slot = uint64(beaconState.Altair.Slot) 635 | } else if beaconState.Bellatrix != nil { 636 | slot = uint64(beaconState.Bellatrix.Slot) 637 | } else if beaconState.Capella != nil { 638 | slot = uint64(beaconState.Capella.Slot) 639 | } else if beaconState.Deneb != nil { 640 | slot = uint64(beaconState.Deneb.Slot) 641 | } else if beaconState.Electra != nil { 642 | slot = uint64(beaconState.Electra.Slot) 643 | } else if beaconState.Fulu != nil { 644 | slot = uint64(beaconState.Fulu.Slot) 645 | } else { 646 | log.Fatal("Beacon state was empty") 647 | } 648 | return slot 649 | } 650 | 651 | func GetTimestamp(beaconState *spec.VersionedBeaconState) uint64 { 652 | var timestamp uint64 653 | if beaconState.Bellatrix != nil { 654 | timestamp = uint64(beaconState.Bellatrix.LatestExecutionPayloadHeader.Timestamp) 655 | } else if beaconState.Capella != nil { 656 | timestamp = uint64(beaconState.Capella.LatestExecutionPayloadHeader.Timestamp) 657 | } else if beaconState.Deneb != nil { 658 | timestamp = uint64(beaconState.Deneb.LatestExecutionPayloadHeader.Timestamp) 659 | } else if beaconState.Electra != nil { 660 | timestamp = uint64(beaconState.Electra.LatestExecutionPayloadHeader.Timestamp) 661 | } else if beaconState.Fulu != nil { 662 | timestamp = uint64(beaconState.Fulu.LatestExecutionPayloadHeader.Timestamp) 663 | } else { 664 | log.Fatal("Could not get timestamp from beacon state") 665 | } 666 | return timestamp 667 | } 668 | 669 | func GetCurrentSyncCommittee(beaconState *spec.VersionedBeaconState) []phase0.BLSPubKey { 670 | var pubKeys []phase0.BLSPubKey 671 | if beaconState.Altair != nil { 672 | pubKeys = beaconState.Altair.CurrentSyncCommittee.Pubkeys 673 | } else if beaconState.Bellatrix != nil { 674 | pubKeys = beaconState.Bellatrix.CurrentSyncCommittee.Pubkeys 675 | } else if beaconState.Capella != nil { 676 | pubKeys = beaconState.Capella.CurrentSyncCommittee.Pubkeys 677 | } else if beaconState.Deneb != nil { 678 | pubKeys = beaconState.Deneb.CurrentSyncCommittee.Pubkeys 679 | } else if beaconState.Electra != nil { 680 | pubKeys = beaconState.Electra.CurrentSyncCommittee.Pubkeys 681 | } else if beaconState.Fulu != nil { 682 | pubKeys = beaconState.Fulu.CurrentSyncCommittee.Pubkeys 683 | } else { 684 | log.Fatal("Beacon state was empty") 685 | } 686 | return pubKeys 687 | } 688 | 689 | func GetPendingConsolidations(beaconState *spec.VersionedBeaconState) []*electra.PendingConsolidation { 690 | var pendingConsolidations []*electra.PendingConsolidation 691 | if beaconState.Electra != nil { 692 | pendingConsolidations = beaconState.Electra.PendingConsolidations 693 | } else if beaconState.Fulu != nil { 694 | pendingConsolidations = beaconState.Fulu.PendingConsolidations 695 | } else { 696 | log.Fatal("Beacon state was empty") 697 | } 698 | return pendingConsolidations 699 | } 700 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 2 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 3 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 4 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 5 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 6 | github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= 7 | github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= 8 | github.com/aohorodnyk/mimeheader v0.0.6 h1:WCV4NQjtbqnd2N3FT5MEPesan/lfvaLYmt5v4xSaX/M= 9 | github.com/aohorodnyk/mimeheader v0.0.6/go.mod h1:/Gd3t3vszyZYwjNJo2qDxoftZjjVzMdkQZxkiINp3vM= 10 | github.com/attestantio/go-builder-client v0.7.2 h1:bOrtysEIZd9bEM+mAeT6OtAo6LSAft/qylBLwFoFwZ0= 11 | github.com/attestantio/go-builder-client v0.7.2/go.mod h1:+NADxbaknI5yxl+0mCkMa/VciVsesxRMGNP/poDfV08= 12 | github.com/attestantio/go-eth2-client v0.26.0 h1:oDWKvIUJfvr1EBi/w9L6mawYZHOCymjHkml7fZplT20= 13 | github.com/attestantio/go-eth2-client v0.26.0/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= 14 | github.com/attestantio/go-eth2-client v0.27.1 h1:g7bm+gG/p+gfzYdEuxuAepVWYb8EO+2KojV5/Lo2BxM= 15 | github.com/attestantio/go-eth2-client v0.27.1/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= 16 | github.com/attestantio/go-eth2-client v0.27.2 h1:VjA9R39ovy8ryb7IpFfD5eLYBg/20biztxh6fKZ7/K0= 17 | github.com/attestantio/go-eth2-client v0.27.2/go.mod h1:i56XBegxVt7wXupnLBOj9IyGwy5cqaoTsCSKlwTubEU= 18 | github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= 19 | github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= 20 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 21 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 22 | github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= 23 | github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 24 | github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= 25 | github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 26 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 27 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 28 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 29 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 30 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 31 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 32 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 33 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 34 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 35 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 36 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 37 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 38 | github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= 39 | github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= 40 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 41 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 42 | github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= 43 | github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= 44 | github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= 45 | github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= 46 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= 47 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= 48 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 49 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= 52 | github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= 53 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 54 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 55 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 56 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 57 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 58 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 59 | github.com/emicklei/dot v1.6.4 h1:cG9ycT67d9Yw22G+mAb4XiuUz6E6H1S0zePp/5Cwe/c= 60 | github.com/emicklei/dot v1.6.4/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= 61 | github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= 62 | github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= 63 | github.com/ethereum/c-kzg-4844 v1.0.3 h1:IEnbOHwjixW2cTvKRUlAAUOeleV7nNM/umJR+qy4WDs= 64 | github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= 65 | github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= 66 | github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= 67 | github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= 68 | github.com/ethereum/go-ethereum v1.16.1 h1:7684NfKCb1+IChudzdKyZJ12l1Tq4ybPZOITiCDXqCk= 69 | github.com/ethereum/go-ethereum v1.16.1/go.mod h1:ngYIvmMAYdo4sGW9cGzLvSsPGhDOOzL0jK5S5iXpj0g= 70 | github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= 71 | github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= 72 | github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= 73 | github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= 74 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 75 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 76 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 77 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 78 | github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= 79 | github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= 80 | github.com/flashbots/go-boost-utils v1.10.0 h1:AGihhYtOjGF/efaBoQefYfmqzKsba6Y7SlfEK2iEPGY= 81 | github.com/flashbots/go-boost-utils v1.10.0/go.mod h1:vCtklzlENAGLqDrf6JteivgANjzXFqVSQQ3LtoQxyV8= 82 | github.com/flashbots/go-utils v0.11.0 h1:MuI9OOl40MukSL2ucKKQG1sxxl5Cqjla41TRubGNu0w= 83 | github.com/flashbots/go-utils v0.11.0/go.mod h1:i4xxEB6sHDFfNWEIfh+rP6nx3LxynEn8AOZa05EYgwA= 84 | github.com/flashbots/mev-boost-relay v0.32.0 h1:4foPhTs6YTi1+UcnAZX05m0617eA+rlE969sI3qeFS8= 85 | github.com/flashbots/mev-boost-relay v0.32.0/go.mod h1:PfuOEMk5vrnt8uVcpg45RNYZUYRYxUpBoKA9k5zfGqM= 86 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 87 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 88 | github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= 89 | github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= 90 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 91 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 92 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 93 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 94 | github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= 95 | github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= 96 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 97 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 98 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 99 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 100 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 101 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 102 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 103 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 104 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 105 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 106 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 107 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 108 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 109 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 110 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 111 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 112 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 113 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 114 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 115 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 116 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 117 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 118 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 119 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 120 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 121 | github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0= 122 | github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= 123 | github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= 124 | github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 125 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 126 | github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 127 | github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 128 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 129 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 130 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 131 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 132 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 133 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 134 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 135 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 136 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 137 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 138 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 139 | github.com/google/pprof v0.0.0-20190404155422-f8f10df84213/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 140 | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= 141 | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 142 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 143 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 144 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 145 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 146 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 147 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 148 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 149 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 150 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= 151 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 152 | github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= 153 | github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= 154 | github.com/huandu/go-clone v1.6.0 h1:HMo5uvg4wgfiy5FoGOqlFLQED/VGRm2D9Pi8g1FXPGc= 155 | github.com/huandu/go-clone v1.6.0/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= 156 | github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo= 157 | github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= 158 | github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= 159 | github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= 160 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 161 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 162 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 163 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 164 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 165 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 166 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 167 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 168 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 169 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 170 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 171 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 172 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 173 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 174 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 175 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 176 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 177 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 178 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 179 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 180 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 181 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 182 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 183 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 184 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 185 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 186 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 187 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 188 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 189 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 190 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 191 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 192 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 193 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 194 | github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= 195 | github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 196 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 197 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 198 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 199 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 200 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 201 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 202 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 203 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 204 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 205 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 206 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 207 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 208 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 209 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 210 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 211 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 212 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 213 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 214 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 215 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 216 | github.com/pk910/dynamic-ssz v0.0.4 h1:DT29+1055tCEPCaR4V/ez+MOKW7BzBsmjyFvBRqx0ME= 217 | github.com/pk910/dynamic-ssz v0.0.4/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= 218 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 219 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 220 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 221 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 222 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 223 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 224 | github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= 225 | github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 226 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 227 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 228 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 229 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 230 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 231 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 232 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 233 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 234 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 235 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 236 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 237 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 238 | github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= 239 | github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= 240 | github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= 241 | github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= 242 | github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= 243 | github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= 244 | github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= 245 | github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= 246 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 247 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 248 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 249 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 250 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 251 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 252 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 253 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 254 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= 255 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 256 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 257 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 258 | github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= 259 | github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= 260 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 261 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= 262 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 263 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 264 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 265 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 266 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 267 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 268 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 269 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 270 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 271 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 272 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 273 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 274 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 275 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 276 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 277 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 278 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 279 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 280 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 281 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 282 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 283 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 284 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 285 | github.com/superoo7/go-gecko v1.0.0 h1:Xa1hZu2AYSA20eVMEd4etY0fcJoEI5deja1mdRmqlpI= 286 | github.com/superoo7/go-gecko v1.0.0/go.mod h1:6AMYHL2wP2EN8AB9msPM76Lbo8L/MQOknYjvak5coaY= 287 | github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= 288 | github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 289 | github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= 290 | github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 291 | github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo= 292 | github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= 293 | github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38= 294 | github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= 295 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 296 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 297 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 298 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 299 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 300 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 301 | github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= 302 | github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= 303 | github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= 304 | github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= 305 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 306 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 307 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 308 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 309 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 310 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 311 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 312 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 313 | go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= 314 | go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= 315 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 316 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 317 | go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E= 318 | go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk= 319 | go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= 320 | go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= 321 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 322 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 323 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 324 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 325 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 326 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 327 | go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= 328 | go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= 329 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 330 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 331 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 332 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 333 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 334 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 335 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 336 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 337 | golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 338 | golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= 339 | golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 340 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 341 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 342 | golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= 343 | golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 344 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 345 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 346 | golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= 347 | golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 348 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 349 | golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 350 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 351 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 352 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 354 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 355 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 356 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 362 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 363 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 364 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 365 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 366 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 367 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 368 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 369 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 370 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 371 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 372 | golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= 373 | golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 374 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 375 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 376 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 377 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 378 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 379 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 380 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 381 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 382 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 383 | gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= 384 | gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= 385 | gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= 386 | gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= 387 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 388 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 389 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 390 | gopkg.in/h2non/gock.v1 v1.0.14 h1:fTeu9fcUvSnLNacYvYI54h+1/XEteDyHvrVCZEEEYNM= 391 | gopkg.in/h2non/gock.v1 v1.0.14/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= 392 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 393 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 394 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 395 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 396 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 397 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 398 | modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= 399 | modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 400 | modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= 401 | modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= 402 | modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA= 403 | modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 404 | modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 405 | modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 406 | modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= 407 | modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= 408 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 409 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 410 | modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= 411 | modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 412 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 413 | modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 414 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 415 | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 416 | modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= 417 | modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= 418 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 419 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 420 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 421 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 422 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 423 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 424 | --------------------------------------------------------------------------------