diff --git a/Makefile b/Makefile index 206038a4..2456332a 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,6 @@ run: $(BIN) $(STATE_ANALYZER_CMD) \ --log-level=${STATE_ANALYZER_LOG_LEVEL} \ --bn-endpoint=${STATE_ANALYZER_BN_ENDPOINT} \ - --outfolder=${STATE_ANALYZER_OUTFOLDER} \ --init-slot=${STATE_ANALYZER_INIT_SLOT} \ --final-slot=${STATE_ANALYZER_FINAL_SLOT} \ --validator-indexes=${STATE_ANALYZER_VALIDATOR_INDEXES} \ diff --git a/README.md b/README.md index 74e4bba3..3e7e869b 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ -# Eth2 State Analyzer +# Eth CL State Analyzer + +The CL State Analyzer is a go-written client that indexes all validator-related duties and parameters from Ethereum's beaconchain by fetching the CL States from a node (preferable a locally running archival node). + +The client indexes all the validator/epoch related metrics into a set of postgreSQL tables. Which later on can be used to monitor the performance of validators in the beaconchain. + +This tool has been used to power the [pandametrics.xyz](https://pandametrics.xyz/) public dashboard. + +### Prerequisites +To use the tool, the following requirements need to be installed in the machine: +- [go](https://go.dev/doc/install) preferably on its 1.17 version or above. Go also needs to be executable from the terminal. +- PostgreSQL DB +- Access to a Ethereum CL beacon node (preferably an archive node to index the slots faster) + +### Installation +The repository provides a Makefile that will take care of all your problems. + +To compile locally the client, just type the following command at the root of the directory: +``` +make build +``` + +Or if you prefer to install the client locally type: +``` +make install +``` + +### Running the tool +To execute the tool, you can simply modify the `.env` file with your own configuration. The `.env` file first exports all the variables as system environment variables, and then uses them as arguments when calling the tool. + +*Running the tool (configurable in the `.env` file)*: +``` +make run +``` + +*Available Commands*: +``` +COMMANDS: + rewards analyze the Beacon State of a given slot range + help, h Shows a list of commands or help for one command +``` + +*Available Options (configurable in the `.env` file)* +``` +OPTIONS: + --bn-endpoint value beacon node endpoint (to request the BeaconStates) + --init-slot value init slot from where to start (default: 0) + --final-slot value init slot from where to finish (default: 0) + --validator-indexes value json file including the list of validator indexes (leave the json `[]` to index all the existing validators) + --log-level value log level: debug, warn, info, error + --db-url value example: postgresql://beaconchain:beaconchain@localhost:5432/beacon_states + --workers-num value example: 50 (default: 0) + --db-workers-num value example: 50 (default: 0) + --help, -h show help (default: false) +``` + + +# Maintainers +@cortze , @tadahar + +# Contributing +The project is open for everyone to contribute! \ No newline at end of file diff --git a/cmd/reward_cmd.go b/cmd/reward_cmd.go index c25f48cb..41ca0749 100644 --- a/cmd/reward_cmd.go +++ b/cmd/reward_cmd.go @@ -24,10 +24,6 @@ var RewardsCommand = &cli.Command{ Name: "bn-endpoint", Usage: "beacon node endpoint (to request the BeaconStates)", }, - &cli.StringFlag{ - Name: "outfolder", - Usage: "output result folder", - }, &cli.IntFlag{ Name: "init-slot", Usage: "init slot from where to start", @@ -73,9 +69,6 @@ func LaunchRewardsCalculator(c *cli.Context) error { if !c.IsSet("bn-endpoint") { return errors.New("bn endpoint not provided") } - if !c.IsSet("outfolder") { - return errors.New("outputfolder no provided") - } if !c.IsSet("init-slot") { return errors.New("final slot not provided") } diff --git a/launcher.sh b/launcher.sh deleted file mode 100755 index 171e31c8..00000000 --- a/launcher.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -CLI_NAME="state-analyzer" - -echo "launching State-Analyzer" - - -BN_ENDPOINT="http://localhost:5052" -OUT_FOLDER="results" -INIT_SLOT="300000" -FINAL_SLOT="300063" -VALIDATOR_LIST_FILE="test_validators.json" - -go get -go build -o $CLI_NAME - - - -"./$CLI_NAME" rewards --log-level=$1 --bn-endpoint="$BN_ENDPOINT" --outfolder="$OUT_FOLDER" --init-slot="$INIT_SLOT" --final-slot="$FINAL_SLOT" --validator-indexes="$VALIDATOR_LIST_FILE" - - diff --git a/main.go b/main.go index 579846fb..573ca662 100644 --- a/main.go +++ b/main.go @@ -33,11 +33,14 @@ func main() { app := &cli.App{ Name: CliName, Usage: "Tinny client that requests and processes the Beacon State for the slot range defined.", - UsageText: "state-analyzer [commands] [arguments...]", + UsageText: "eth2-state-analyzer [commands] [arguments...]", Authors: []*cli.Author{ { Name: "Cortze", Email: "cortze@protonmail.com", + }, { + Name: "Tdahar", + Email: "tarsuno@gmail.com", }, }, EnableBashCompletion: true, diff --git a/pkg/fork_metrics/fork_state/state_test.go b/pkg/fork_metrics/fork_state/state_test.go new file mode 100644 index 00000000..886fbd0d --- /dev/null +++ b/pkg/fork_metrics/fork_state/state_test.go @@ -0,0 +1,83 @@ +package fork_state + +import ( + "testing" + + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/require" +) + +func TestState(t *testing.T) { + + balancesArray := make([]uint64, 0) + balancesArray = append(balancesArray, 34000000000, 31200000000) + validator1 := phase0.Validator{ + EffectiveBalance: 32000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + } + + validator2 := phase0.Validator{ + EffectiveBalance: 31000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + } + + validatorArray := make([]*phase0.Validator, 0) + validatorArray = append(validatorArray, &validator1, &validator2) + + state := ForkStateContentBase{ + Validators: validatorArray, + Balances: balancesArray, + BlockRoots: make([][]byte, 0), + } + state.Setup() + + require.Equal(t, state.TotalActiveBalance, uint64(validator1.EffectiveBalance+validator2.EffectiveBalance)) + + state.Validators = append(state.Validators, &validator1, &validator2) + + state = ForkStateContentBase{ + Validators: validatorArray, + Balances: balancesArray, + BlockRoots: make([][]byte, 0), + } + state.Setup() + attestations := make([]altair.ParticipationFlags, 0) + attestations = append(attestations, altair.ParticipationFlags(7)) + + ProcessAttestations(&state, attestations) + + require.Equal(t, state.AttestingBalance[0], uint64(validator1.EffectiveBalance)) + require.Equal(t, state.AttestingVals[0], true) + require.Equal(t, state.AttestingVals[1], false) + + attestations = append(attestations, altair.ParticipationFlags(7)) + state = ForkStateContentBase{ + Validators: validatorArray, + Balances: balancesArray, + BlockRoots: make([][]byte, 0), + } + state.Setup() + ProcessAttestations(&state, attestations) + + require.Equal(t, state.AttestingBalance[0], uint64(validator1.EffectiveBalance+validator2.EffectiveBalance)) + require.Equal(t, state.AttestingVals[0], true) + require.Equal(t, state.AttestingVals[1], true) + + state = ForkStateContentBase{ + Validators: validatorArray, + Balances: balancesArray, + BlockRoots: make([][]byte, 0), + } + state.Setup() + attestations[1] = altair.ParticipationFlags(6) + ProcessAttestations(&state, attestations) + + // no source attesting + require.Equal(t, state.AttestingBalance[0], uint64(validator1.EffectiveBalance)) + require.Equal(t, state.AttestingVals[0], true) + require.Equal(t, state.AttestingVals[1], true) + +} diff --git a/pkg/fork_metrics/rewards_test.go b/pkg/fork_metrics/rewards_test.go new file mode 100644 index 00000000..453ee049 --- /dev/null +++ b/pkg/fork_metrics/rewards_test.go @@ -0,0 +1,201 @@ +package fork_metrics + +import ( + "math" + "testing" + + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/cortze/eth2-state-analyzer/pkg/fork_metrics/fork_state" + "github.com/stretchr/testify/require" +) + +func TestMaxAttestationReward(t *testing.T) { + + balancesArray := make([]uint64, 0) + balancesArray = append(balancesArray, 34000000000, 31200000000) + validator1 := phase0.Validator{ + EffectiveBalance: 32000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + } + + validator2 := phase0.Validator{ + EffectiveBalance: 31000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + } + + validatorArray := make([]*phase0.Validator, 0) + validatorArray = append(validatorArray, &validator1, &validator2) + + state := fork_state.ForkStateContentBase{ + Validators: validatorArray, + Balances: balancesArray, + BlockRoots: make([][]byte, 0), + Epoch: 10, + } + state.Setup() + + balancesArray1 := make([]uint64, 0) + balancesArray1 = append(balancesArray1, 34000000000, 31200000000) + validator1 = phase0.Validator{ + EffectiveBalance: 32000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + } + + validator2 = phase0.Validator{ + EffectiveBalance: 31000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + } + + validatorArray1 := make([]*phase0.Validator, 0) + validatorArray1 = append(validatorArray1, &validator1, &validator2) + + statePrev := fork_state.ForkStateContentBase{ + Validators: validatorArray1, + Balances: balancesArray1, + BlockRoots: make([][]byte, 0), + Epoch: 9, + } + + attestations := make([]altair.ParticipationFlags, 0) + attestations = append(attestations, altair.ParticipationFlags(6)) + statePrev.Setup() + fork_state.ProcessAttestations(&state, attestations) + + stateNext := statePrev + stateNext.Epoch = 11 + rewardsObj := NewAltairMetrics( + statePrev, + state, + stateNext) + + baseRewardPerInc := uint64(fork_state.EFFECTIVE_BALANCE_INCREMENT * fork_state.BASE_REWARD_FACTOR) + baseRewardPerInc = baseRewardPerInc / uint64(math.Sqrt(float64(rewardsObj.CurrentState.TotalActiveBalance))) + require.Equal(t, + rewardsObj.GetBaseRewardPerInc(rewardsObj.CurrentState.TotalActiveBalance), + uint64(baseRewardPerInc)) + + require.Equal(t, + rewardsObj.GetBaseReward(0, uint64(validator1.EffectiveBalance), rewardsObj.CurrentState.TotalActiveBalance), + uint64(baseRewardPerInc*32)) + + require.Equal(t, + rewardsObj.GetBaseReward(1, uint64(validator2.EffectiveBalance), rewardsObj.CurrentState.TotalActiveBalance), + uint64(baseRewardPerInc*31)) + + attReward := 0 + + // Source + reward := uint64(14) * baseRewardPerInc * 31 * uint64(rewardsObj.CurrentState.AttestingBalance[0]/1000000000) + reward = reward / (uint64(rewardsObj.CurrentState.TotalActiveBalance / 1000000000)) / 64 + attReward += int(reward) + + // Target + reward = uint64(26) * baseRewardPerInc * 31 * uint64(rewardsObj.CurrentState.AttestingBalance[1]/1000000000) + reward = reward / (uint64(rewardsObj.CurrentState.TotalActiveBalance / 1000000000)) / 64 + attReward += int(reward) + + // Head + reward = uint64(14) * baseRewardPerInc * 31 * uint64(rewardsObj.CurrentState.AttestingBalance[2]/1000000000) + reward = reward / (uint64(rewardsObj.CurrentState.TotalActiveBalance / 1000000000)) / 64 + attReward += int(reward) + + require.Equal(t, + rewardsObj.GetMaxAttestationReward(1), + uint64(attReward)) + + attReward = 0 + + // Source + reward = uint64(14) * baseRewardPerInc * 32 * uint64(rewardsObj.CurrentState.AttestingBalance[0]/1000000000) + reward = reward / (uint64(rewardsObj.CurrentState.TotalActiveBalance / 1000000000)) / 64 + attReward += int(reward) + + // Target + reward = uint64(26) * baseRewardPerInc * 32 * uint64(rewardsObj.CurrentState.AttestingBalance[1]/1000000000) + reward = reward / (uint64(rewardsObj.CurrentState.TotalActiveBalance / 1000000000)) / 64 + attReward += int(reward) + + // Head + reward = uint64(14) * baseRewardPerInc * 32 * uint64(rewardsObj.CurrentState.AttestingBalance[2]/1000000000) + reward = reward / (uint64(rewardsObj.CurrentState.TotalActiveBalance / 1000000000)) / 64 + attReward += int(reward) + require.Equal(t, + rewardsObj.GetMaxAttestationReward(0), + uint64(2590292)) + +} + +func TestMaxSyncCommitteeReward(t *testing.T) { + + // create state + balancesArray := make([]uint64, 0) + balancesArray = append(balancesArray, 34000000000, 31200000000) + validator1Pubkey := "0x8b9cfaf7480d7bb848cc2017b9770bef00f8d9e761bea9ea06a0534c449a98f50b587db18a93c4da8ae805476edee55a" + validator1 := phase0.Validator{ + EffectiveBalance: 32000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + PublicKey: phase0.BLSPubKey{}, + } + + copy(validator1.PublicKey[:], validator1Pubkey) + + validator2Pubkey := "0x9b9cfaf7480d7bb848cc2017b9770bef00f8d9e761bea9ea06a0534c449a98f50b587db18a93c4da8ae805476edee55a" + validator2 := phase0.Validator{ + EffectiveBalance: 31000000000, + ActivationEpoch: 0, + ExitEpoch: 10000000000, + PublicKey: phase0.BLSPubKey{}, + } + copy(validator1.PublicKey[:], validator2Pubkey) + + validatorArray := make([]*phase0.Validator, 0) + validatorArray = append(validatorArray, &validator1, &validator2) + syncCommittee := altair.SyncCommittee{ + Pubkeys: make([]phase0.BLSPubKey, 0), + } + syncCommittee.Pubkeys = append(syncCommittee.Pubkeys, validator1.PublicKey) // validator 1 is in sync committee + + // state creation + stateNext := fork_state.ForkStateContentBase{ + Validators: validatorArray, + Balances: balancesArray, + BlockRoots: make([][]byte, 0), + Epoch: 11, + SyncCommittee: syncCommittee, + } + stateNext.Setup() + + // create fork metrics + rewardsObj := NewAltairMetrics( + stateNext, + fork_state.ForkStateContentBase{}, + fork_state.ForkStateContentBase{}) + + baseRewardPerInc := uint64(fork_state.EFFECTIVE_BALANCE_INCREMENT * fork_state.BASE_REWARD_FACTOR) + baseRewardPerInc = baseRewardPerInc / uint64(math.Sqrt(float64(rewardsObj.NextState.TotalActiveBalance))) + + participantReward := uint64(rewardsObj.NextState.TotalActiveBalance / 1000000000) + participantReward = participantReward * baseRewardPerInc + participantReward = participantReward * 2 / 64 / 32 + participantReward = participantReward / 512 + require.Equal(t, + rewardsObj.GetMaxSyncComReward(0), + uint64(participantReward*32)) + + require.Equal(t, + rewardsObj.GetMaxSyncComReward(1), + uint64(0)) // not in sync committee + + rewardsObj.NextState.MissedBlocks = append(rewardsObj.NextState.MissedBlocks, 1) + + require.Equal(t, + rewardsObj.GetMaxSyncComReward(0), + uint64(participantReward*31)) // one missed block + +} diff --git a/pkg/utils/validator_indexes.go b/pkg/utils/validator_indexes.go index 2ca6b8f9..1408bf86 100644 --- a/pkg/utils/validator_indexes.go +++ b/pkg/utils/validator_indexes.go @@ -16,7 +16,7 @@ func GetValIndexesFromJson(filePath string) ([]uint64, error) { err = json.Unmarshal(fbytes, &validatorIndex) if err != nil { - log.Errorf("Error unmarshalling val list", err.Error()) + log.Errorf("Error unmarshalling val list: %s", err.Error()) } log.Infof("Readed %d validators", len(validatorIndex))