diff --git a/cl/clparams/config.go b/cl/clparams/config.go index 89ddc13a06f..06558cd1dfc 100644 --- a/cl/clparams/config.go +++ b/cl/clparams/config.go @@ -446,6 +446,14 @@ func configForkNames(b *BeaconChainConfig) map[[VersionLength]byte]string { return fvn } +func (b *BeaconChainConfig) ParticipationWeights() []uint64 { + return []uint64{ + b.TimelySourceWeight, + b.TimelyTargetWeight, + b.TimelyHeadWeight, + } +} + var MainnetBeaconConfig BeaconChainConfig = BeaconChainConfig{ // Constants (Non-configurable) FarFutureEpoch: math.MaxUint64, diff --git a/cl/utils/bytes.go b/cl/utils/bytes.go index df9989260c3..13e7c2bc7f9 100644 --- a/cl/utils/bytes.go +++ b/cl/utils/bytes.go @@ -83,6 +83,20 @@ func DecodeSSZSnappy(dst ssz_utils.Unmarshaler, src []byte) error { return nil } +func DecodeSSZSnappyWithVersion(dst ssz_utils.Unmarshaler, src []byte, version int) error { + dec, err := snappy.Decode(nil, src) + if err != nil { + return err + } + + err = dst.DecodeSSZWithVersion(dec, version) + if err != nil { + return err + } + + return nil +} + func CompressZstd(b []byte) []byte { wr, err := zstd.NewWriter(nil) if err != nil { diff --git a/cmd/erigon-cl/core/state/accessors.go b/cmd/erigon-cl/core/state/accessors.go index c5b946fabfc..05394e28613 100644 --- a/cmd/erigon-cl/core/state/accessors.go +++ b/cmd/erigon-cl/core/state/accessors.go @@ -13,6 +13,8 @@ import ( eth2_shuffle "github.com/protolambda/eth2-shuffle" ) +const PreAllocatedRewardsAndPenalties = 8192 + // GetActiveValidatorsIndices returns the list of validator indices active for the given epoch. func (b *BeaconState) GetActiveValidatorsIndices(epoch uint64) (indicies []uint64) { if cachedIndicies, ok := b.activeValidatorsCache.Get(epoch); ok { @@ -92,11 +94,11 @@ func (b *BeaconState) GetTotalBalance(validatorSet []uint64) (uint64, error) { } // GetTotalActiveBalance return the sum of all balances within active validators. -func (b *BeaconState) GetTotalActiveBalance() (uint64, error) { +func (b *BeaconState) GetTotalActiveBalance() uint64 { if b.totalActiveBalanceCache < b.beaconConfig.EffectiveBalanceIncrement { - return b.beaconConfig.EffectiveBalanceIncrement, nil + return b.beaconConfig.EffectiveBalanceIncrement } - return b.totalActiveBalanceCache, nil + return b.totalActiveBalanceCache } // GetTotalSlashingAmount return the sum of all slashings. @@ -267,7 +269,7 @@ func (b *BeaconState) BaseReward(totalActiveBalance, index uint64) (uint64, erro // SyncRewards returns the proposer reward and the sync participant reward given the total active balance in state. func (b *BeaconState) SyncRewards() (proposerReward, participantReward uint64, err error) { - activeBalance, err := b.GetTotalActiveBalance() + activeBalance := b.GetTotalActiveBalance() if err != nil { return 0, 0, err } @@ -400,3 +402,129 @@ func (b *BeaconState) GetAttestingIndicies(attestation *cltypes.AttestationData, } return attestingIndices, nil } + +// Implementation of get_eligible_validator_indices as defined in the eth 2.0 specs. +func (b *BeaconState) EligibleValidatorsIndicies() (eligibleValidators []uint64) { + eligibleValidators = make([]uint64, 0, len(b.validators)) + previousEpoch := b.PreviousEpoch() + // TODO(Giulio2002): Proper caching + for i, validator := range b.validators { + if validator.Active(previousEpoch) || (validator.Slashed && previousEpoch+1 < validator.WithdrawableEpoch) { + eligibleValidators = append(eligibleValidators, uint64(i)) + } + } + return +} + +// Implementation of is_in_inactivity_leak. tells us if network is in danger pretty much. defined in ETH 2.0 specs. +func (b *BeaconState) inactivityLeaking() bool { + return (b.PreviousEpoch() - b.finalizedCheckpoint.Epoch) > b.beaconConfig.MinEpochsToInactivityPenalty +} + +// Implementation defined in ETH 2.0 specs: https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#get_flag_index_deltas. +// Although giulio made it efficient hopefully. results will be written in the input map. +func (b *BeaconState) processFlagIndexDeltas(flagIdx int, balanceDeltaMap map[uint64]int64, eligibleValidators []uint64) (err error) { + // Initialize variables + var ( + unslashedParticipatingIndicies []uint64 + unslashedParticipatingTotalBalance uint64 + baseReward uint64 + ) + // Find unslashedParticipatingIndicies for this specific flag index. + previousEpoch := b.PreviousEpoch() + unslashedParticipatingIndicies, err = b.GetUnslashedParticipatingIndices(flagIdx, previousEpoch) + if err != nil { + return + } + // Find current weight for flag index. + weights := b.beaconConfig.ParticipationWeights() + weight := weights[flagIdx] + // Compute participating indices total balance (required in rewards/penalties computation). + unslashedParticipatingTotalBalance, err = b.GetTotalBalance(unslashedParticipatingIndicies) + if err != nil { + return + } + // Make it a map to make the existence check O(1) time-complexity. + isUnslashedParticipatingIndicies := make(map[uint64]struct{}) + for _, index := range unslashedParticipatingIndicies { + isUnslashedParticipatingIndicies[index] = struct{}{} + } + // Compute relative increments. + unslashedParticipatingIncrements := unslashedParticipatingTotalBalance / b.beaconConfig.EffectiveBalanceIncrement + activeIncrements := b.GetTotalActiveBalance() / b.beaconConfig.EffectiveBalanceIncrement + totalActiveBalance := b.GetTotalActiveBalance() + // Now process deltas and whats nots. + for _, index := range eligibleValidators { + if _, ok := isUnslashedParticipatingIndicies[index]; ok { + if b.inactivityLeaking() { + continue + } + rewardNumerator := baseReward * weight * unslashedParticipatingIncrements + balanceDeltaMap[index] += int64(rewardNumerator / (activeIncrements * b.beaconConfig.WeightDenominator)) + } else if flagIdx != int(b.beaconConfig.TimelyHeadFlagIndex) { + baseReward, err = b.BaseReward(totalActiveBalance, index) + if err != nil { + return + } + balanceDeltaMap[index] -= int64(baseReward * weight / b.beaconConfig.WeightDenominator) + } + } + return +} + +// Implemention defined in https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#modified-get_inactivity_penalty_deltas. +func (b *BeaconState) processInactivityDeltas(balanceDeltaMap map[uint64]int64, eligibleValidators []uint64) (err error) { + var ( + unslashedParticipatingIndicies []uint64 + validator cltypes.Validator + ) + // Find unslashedParticipatingIndicies for this specific flag index. + previousEpoch := b.PreviousEpoch() + unslashedParticipatingIndicies, err = b.GetUnslashedParticipatingIndices(int(b.beaconConfig.TimelyHeadFlagIndex), previousEpoch) + if err != nil { + return + } + // Make it a map to make the existence check O(1) time-complexity. + isUnslashedParticipatingIndicies := make(map[uint64]struct{}) + for _, index := range unslashedParticipatingIndicies { + isUnslashedParticipatingIndicies[index] = struct{}{} + } + // retrieve penalty quotient based on fork + var penaltyQuotient uint64 + switch b.version { + case clparams.Phase0Version: + penaltyQuotient = b.beaconConfig.InactivityPenaltyQuotient + case clparams.AltairVersion: + penaltyQuotient = b.beaconConfig.InactivityPenaltyQuotientAltair + case clparams.BellatrixVersion: + penaltyQuotient = b.beaconConfig.InactivityPenaltyQuotientBellatrix + } + for _, index := range eligibleValidators { + if _, ok := isUnslashedParticipatingIndicies[index]; ok { + continue + } + // Process inactivity penalties. + validator, err = b.ValidatorAt(int(index)) + if err != nil { + return err + } + penaltyNumerator := validator.EffectiveBalance * b.inactivityScores[index] + balanceDeltaMap[index] -= int64(penaltyNumerator / (b.beaconConfig.InactivityScoreBias * penaltyQuotient)) + } + return +} + +// BalanceDeltas return the delta for each validator index. +func (b *BeaconState) BalanceDeltas() (balanceDeltaMap map[uint64]int64, err error) { + balanceDeltaMap = map[uint64]int64{} + eligibleValidators := b.EligibleValidatorsIndicies() + // process each flag indexes by weight. + for i := range b.beaconConfig.ParticipationWeights() { + if err = b.processFlagIndexDeltas(i, balanceDeltaMap, eligibleValidators); err != nil { + return + } + } + // process inactivity scores now. + err = b.processInactivityDeltas(balanceDeltaMap, eligibleValidators) + return +} diff --git a/cmd/erigon-cl/core/state/accessors_test.go b/cmd/erigon-cl/core/state/accessors_test.go index 82a044f3752..9aa08e68c20 100644 --- a/cmd/erigon-cl/core/state/accessors_test.go +++ b/cmd/erigon-cl/core/state/accessors_test.go @@ -57,9 +57,7 @@ func TestActiveValidatorIndices(t *testing.T) { require.NoError(t, err) require.Equal(t, set, []uint64{1}) // Check if balances are retrieved correctly - totalBalance, err := testState.GetTotalActiveBalance() - require.NoError(t, err) - require.Equal(t, totalBalance, uint64(2e9)) + require.Equal(t, testState.GetTotalActiveBalance(), uint64(2e9)) } func TestGetBlockRoot(t *testing.T) { diff --git a/cmd/erigon-cl/core/state/mutators.go b/cmd/erigon-cl/core/state/mutators.go index 51ac7f55a97..f5d218e8bdb 100644 --- a/cmd/erigon-cl/core/state/mutators.go +++ b/cmd/erigon-cl/core/state/mutators.go @@ -4,12 +4,12 @@ import ( "fmt" ) -func (b *BeaconState) IncreaseBalance(index int, delta uint64) error { - currentBalance, err := b.ValidatorBalance(index) +func (b *BeaconState) IncreaseBalance(index, delta uint64) error { + currentBalance, err := b.ValidatorBalance(int(index)) if err != nil { return err } - return b.SetValidatorBalance(index, currentBalance+delta) + return b.SetValidatorBalance(int(index), currentBalance+delta) } func (b *BeaconState) DecreaseBalance(index, delta uint64) error { @@ -101,8 +101,8 @@ func (b *BeaconState) SlashValidator(slashedInd, whistleblowerInd uint64) error } whistleBlowerReward := newValidator.EffectiveBalance / b.beaconConfig.WhistleBlowerRewardQuotient proposerReward := whistleBlowerReward / b.beaconConfig.ProposerRewardQuotient - if err := b.IncreaseBalance(int(proposerInd), proposerReward); err != nil { + if err := b.IncreaseBalance(proposerInd, proposerReward); err != nil { return err } - return b.IncreaseBalance(int(whistleblowerInd), whistleBlowerReward) + return b.IncreaseBalance(whistleblowerInd, whistleBlowerReward) } diff --git a/cmd/erigon-cl/core/state/mutators_test.go b/cmd/erigon-cl/core/state/mutators_test.go index b0bc171dd90..f6eb80e3be2 100644 --- a/cmd/erigon-cl/core/state/mutators_test.go +++ b/cmd/erigon-cl/core/state/mutators_test.go @@ -41,7 +41,7 @@ func TestIncreaseBalance(t *testing.T) { testInd := uint64(42) amount := uint64(100) beforeBalance := state.Balances()[testInd] - state.IncreaseBalance(int(testInd), amount) + state.IncreaseBalance(testInd, amount) afterBalance := state.Balances()[testInd] require.Equal(t, afterBalance, beforeBalance+amount) } diff --git a/cmd/erigon-cl/core/transition/finalization_and_justification.go b/cmd/erigon-cl/core/transition/finalization_and_justification.go index d91d523e109..f3e545060a7 100644 --- a/cmd/erigon-cl/core/transition/finalization_and_justification.go +++ b/cmd/erigon-cl/core/transition/finalization_and_justification.go @@ -1,8 +1,6 @@ package transition import ( - "fmt" - "github.com/ledgerwatch/erigon/cl/clparams" "github.com/ledgerwatch/erigon/cl/cltypes" ) @@ -87,11 +85,10 @@ func (s *StateTransistor) processJustificationBitsAndFinalityAltair() error { if err != nil { return err } - totalActiveBalance, err := s.state.GetTotalActiveBalance() + totalActiveBalance := s.state.GetTotalActiveBalance() if err != nil { return err } - fmt.Println(totalActiveBalance > 1000000000) previousTargetBalance, err := s.state.GetTotalBalance(previousIndices) if err != nil { return err diff --git a/cmd/erigon-cl/core/transition/operations.go b/cmd/erigon-cl/core/transition/operations.go index f300a57f2d7..a67a178064d 100644 --- a/cmd/erigon-cl/core/transition/operations.go +++ b/cmd/erigon-cl/core/transition/operations.go @@ -240,7 +240,7 @@ func (s *StateTransistor) ProcessDeposit(deposit *cltypes.Deposit) error { return nil } // Increase the balance if exists already - return s.state.IncreaseBalance(int(validatorIndex), amount) + return s.state.IncreaseBalance(validatorIndex, amount) } diff --git a/cmd/erigon-cl/core/transition/process_attestations.go b/cmd/erigon-cl/core/transition/process_attestations.go index ad6e657e60e..11f84273af7 100644 --- a/cmd/erigon-cl/core/transition/process_attestations.go +++ b/cmd/erigon-cl/core/transition/process_attestations.go @@ -28,16 +28,7 @@ func (s *StateTransistor) ProcessAttestations(attestations []*cltypes.Attestatio // ProcessAttestation takes an attestation and process it. func (s *StateTransistor) processAttestation(attestation *cltypes.Attestation) ([]uint64, error) { - participationFlagWeights := []uint64{ - s.beaconConfig.TimelySourceWeight, - s.beaconConfig.TimelyTargetWeight, - s.beaconConfig.TimelyHeadWeight, - } - - totalActiveBalance, err := s.state.GetTotalActiveBalance() - if err != nil { - return nil, err - } + totalActiveBalance := s.state.GetTotalActiveBalance() data := attestation.Data currentEpoch := s.state.Epoch() previousEpoch := s.state.PreviousEpoch() @@ -70,7 +61,7 @@ func (s *StateTransistor) processAttestation(attestation *cltypes.Attestation) ( } for _, attesterIndex := range attestingIndicies { - for flagIndex, weight := range participationFlagWeights { + for flagIndex, weight := range s.beaconConfig.ParticipationWeights() { if !slices.Contains(participationFlagsIndicies, uint8(flagIndex)) || epochParticipation[attesterIndex].HasFlag(flagIndex) { continue } @@ -95,7 +86,7 @@ func (s *StateTransistor) processAttestation(attestation *cltypes.Attestation) ( } proposerRewardDenominator := (s.beaconConfig.WeightDenominator - s.beaconConfig.ProposerWeight) * s.beaconConfig.WeightDenominator / s.beaconConfig.ProposerWeight reward := proposerRewardNumerator / proposerRewardDenominator - return attestingIndicies, s.state.IncreaseBalance(int(proposer), reward) + return attestingIndicies, s.state.IncreaseBalance(proposer, reward) } type verifyAttestationWorkersResult struct { diff --git a/cmd/erigon-cl/core/transition/process_rewards_and_penalties.go b/cmd/erigon-cl/core/transition/process_rewards_and_penalties.go new file mode 100644 index 00000000000..7aeb8e1578d --- /dev/null +++ b/cmd/erigon-cl/core/transition/process_rewards_and_penalties.go @@ -0,0 +1,25 @@ +package transition + +// ProcessRewardsAndPenalties applies rewards/penalties accumulated during previous epoch. +func (s *StateTransistor) ProcessRewardsAndPenalties() error { + // Get deltas for epoch transition. + deltas, err := s.state.BalanceDeltas() + if err != nil { + return err + } + // Apply deltas + for index, delta := range deltas { + if delta > 0 { + // Increment + if err := s.state.IncreaseBalance(index, uint64(delta)); err != nil { + return err + } + continue + } + // Decrease balance + if err := s.state.DecreaseBalance(index, uint64(-delta)); err != nil { + return err + } + } + return nil +} diff --git a/cmd/erigon-cl/core/transition/process_rewards_and_penalties_test.go b/cmd/erigon-cl/core/transition/process_rewards_and_penalties_test.go new file mode 100644 index 00000000000..178d8e082b3 --- /dev/null +++ b/cmd/erigon-cl/core/transition/process_rewards_and_penalties_test.go @@ -0,0 +1,37 @@ +package transition_test + +import ( + _ "embed" + "testing" + + "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/erigon/cl/utils" + "github.com/ledgerwatch/erigon/cmd/erigon-cl/core/state" + "github.com/ledgerwatch/erigon/cmd/erigon-cl/core/transition" + "github.com/stretchr/testify/require" +) + +//go:embed test_data/rewards_finality_test_expected.ssz_snappy +var expectedState []byte + +//go:embed test_data/rewards_finality_test_state.ssz_snappy +var startingState []byte + +func TestProcessRewardsAndPenalties(t *testing.T) { + // Load test states. + testState := state.New(&clparams.MainnetBeaconConfig) + require.NoError(t, utils.DecodeSSZSnappyWithVersion(testState, startingState, int(clparams.BellatrixVersion))) + expected := state.New(&clparams.MainnetBeaconConfig) + require.NoError(t, utils.DecodeSSZSnappyWithVersion(expected, expectedState, int(clparams.BellatrixVersion))) + // Make up state transistor + s := transition.New(testState, &clparams.MainnetBeaconConfig, nil, false) + // Do processing + require.NoError(t, s.ProcessRewardsAndPenalties()) + // Now compare if the two states are the same by taking their root and comparing. + haveRoot, err := testState.HashSSZ() + require.NoError(t, err) + expectedRoot, err := testState.HashSSZ() + require.NoError(t, err) + // Lastly compare + require.Equal(t, expectedRoot, haveRoot) +} diff --git a/cmd/erigon-cl/core/transition/process_slashings.go b/cmd/erigon-cl/core/transition/process_slashings.go index 3cf8d9a7841..22bea4cd8b3 100644 --- a/cmd/erigon-cl/core/transition/process_slashings.go +++ b/cmd/erigon-cl/core/transition/process_slashings.go @@ -6,10 +6,7 @@ func (s *StateTransistor) processSlashings(slashingMultiplier uint64) error { // Get the current epoch epoch := s.state.Epoch() // Get the total active balance - totalBalance, err := s.state.GetTotalActiveBalance() - if err != nil { - return err - } + totalBalance := s.state.GetTotalActiveBalance() // Calculate the total slashing amount // by summing all slashings and multiplying by the provided multiplier slashing := s.state.GetTotalSlashingAmount() * slashingMultiplier diff --git a/cmd/erigon-cl/core/transition/process_sync_aggregate.go b/cmd/erigon-cl/core/transition/process_sync_aggregate.go index 7abd1107226..009719f89c0 100644 --- a/cmd/erigon-cl/core/transition/process_sync_aggregate.go +++ b/cmd/erigon-cl/core/transition/process_sync_aggregate.go @@ -45,7 +45,7 @@ func (s *StateTransistor) processSyncAggregate(sync *cltypes.SyncAggregate) ([][ currByte := sync.SyncCommiteeBits[i/8] if (currByte & (1 << bit)) > 0 { votedKeys = append(votedKeys, committeeKeys[i][:]) - if err := s.state.IncreaseBalance(int(vIdx), participantReward); err != nil { + if err := s.state.IncreaseBalance(vIdx, participantReward); err != nil { return nil, err } earnedProposerReward += proposerReward @@ -55,7 +55,7 @@ func (s *StateTransistor) processSyncAggregate(sync *cltypes.SyncAggregate) ([][ } } } - return votedKeys, s.state.IncreaseBalance(int(proposerIndex), earnedProposerReward) + return votedKeys, s.state.IncreaseBalance(proposerIndex, earnedProposerReward) } func (s *StateTransistor) ProcessSyncAggregate(sync *cltypes.SyncAggregate) error { diff --git a/cmd/erigon-cl/core/transition/test_data/rewards_finality_test_expected.ssz_snappy b/cmd/erigon-cl/core/transition/test_data/rewards_finality_test_expected.ssz_snappy new file mode 100644 index 00000000000..0adffd2e5d2 Binary files /dev/null and b/cmd/erigon-cl/core/transition/test_data/rewards_finality_test_expected.ssz_snappy differ diff --git a/cmd/erigon-cl/core/transition/test_data/rewards_finality_test_state.ssz_snappy b/cmd/erigon-cl/core/transition/test_data/rewards_finality_test_state.ssz_snappy new file mode 100644 index 00000000000..ab3bf299a9a Binary files /dev/null and b/cmd/erigon-cl/core/transition/test_data/rewards_finality_test_state.ssz_snappy differ