diff --git a/beacon-chain/state/state-native/getters_participation.go b/beacon-chain/state/state-native/getters_participation.go index 7df34103f8a9..2693695b330b 100644 --- a/beacon-chain/state/state-native/getters_participation.go +++ b/beacon-chain/state/state-native/getters_participation.go @@ -58,9 +58,9 @@ func (b *BeaconState) UnrealizedCheckpointBalances() (uint64, uint64, uint64, er } if features.Get().EnableExperimentalState { - return stateutil.UnrealizedCheckpointBalances(cp, pp, b.validatorsVal(), currentEpoch) + return stateutil.UnrealizedCheckpointBalances(cp, pp, stateutil.NewValMultiValueSliceReader(b.validatorsMultiValue, b), currentEpoch) } else { - return stateutil.UnrealizedCheckpointBalances(cp, pp, b.validators, currentEpoch) + return stateutil.UnrealizedCheckpointBalances(cp, pp, stateutil.NewValSliceReader(b.validators), currentEpoch) } } diff --git a/beacon-chain/state/stateutil/BUILD.bazel b/beacon-chain/state/stateutil/BUILD.bazel index cb30eb8ad6db..cc83efb14a4c 100644 --- a/beacon-chain/state/stateutil/BUILD.bazel +++ b/beacon-chain/state/stateutil/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "trie_helpers.go", "unrealized_justification.go", "validator_map_handler.go", + "validator_reader.go", "validator_root.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/stateutil", @@ -26,6 +27,7 @@ go_library( "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//container/multi-value-slice:go_default_library", "//container/trie:go_default_library", "//crypto/hash:go_default_library", "//crypto/hash/htr:go_default_library", @@ -56,6 +58,7 @@ go_test( "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//container/multi-value-slice:go_default_library", "//crypto/hash:go_default_library", "//encoding/bytesutil:go_default_library", "//encoding/ssz:go_default_library", diff --git a/beacon-chain/state/stateutil/unrealized_justification.go b/beacon-chain/state/stateutil/unrealized_justification.go index d1a427c05ace..d6dc40a24277 100644 --- a/beacon-chain/state/stateutil/unrealized_justification.go +++ b/beacon-chain/state/stateutil/unrealized_justification.go @@ -5,7 +5,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/math" - ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" ) // UnrealizedCheckpointBalances returns the total current active balance, the @@ -13,17 +12,21 @@ import ( // current epoch correctly attested for target balance. It takes the current and // previous epoch participation bits as parameters so implicitly only works for // beacon states post-Altair. -func UnrealizedCheckpointBalances(cp, pp []byte, validators []*ethpb.Validator, currentEpoch primitives.Epoch) (uint64, uint64, uint64, error) { +func UnrealizedCheckpointBalances(cp, pp []byte, validators ValReader, currentEpoch primitives.Epoch) (uint64, uint64, uint64, error) { targetIdx := params.BeaconConfig().TimelyTargetFlagIndex activeBalance := uint64(0) currentTarget := uint64(0) prevTarget := uint64(0) - if len(cp) < len(validators) || len(pp) < len(validators) { + if len(cp) < validators.Len() || len(pp) < validators.Len() { return 0, 0, 0, errors.New("participation does not match validator set") } - var err error - for i, v := range validators { + valLength := validators.Len() + for i := 0; i < valLength; i++ { + v, err := validators.At(i) + if err != nil { + return 0, 0, 0, err + } active := v.ActivationEpoch <= currentEpoch && currentEpoch < v.ExitEpoch if active && !v.Slashed { activeBalance, err = math.Add64(activeBalance, v.EffectiveBalance) diff --git a/beacon-chain/state/stateutil/unrealized_justification_test.go b/beacon-chain/state/stateutil/unrealized_justification_test.go index ccd064d4cc76..1ba9b37f22c4 100644 --- a/beacon-chain/state/stateutil/unrealized_justification_test.go +++ b/beacon-chain/state/stateutil/unrealized_justification_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/prysmaticlabs/prysm/v5/config/params" + multi_value_slice "github.com/prysmaticlabs/prysm/v5/container/multi-value-slice" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/require" ) @@ -25,7 +26,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { pp := make([]byte, len(validators)) t.Run("No one voted last two epochs", func(tt *testing.T) { - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 0) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 0) require.NoError(tt, err) require.Equal(tt, expectedActive, active) require.Equal(tt, uint64(0), current) @@ -35,7 +36,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { t.Run("bad votes in last two epochs", func(tt *testing.T) { copy(cp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00}) copy(pp, []byte{0x00, 0x00, 0x00, 0x00}) - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 1) require.NoError(tt, err) require.Equal(tt, expectedActive, active) require.Equal(tt, uint64(0), current) @@ -45,7 +46,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { t.Run("two votes in last epoch", func(tt *testing.T) { copy(cp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 1 << targetFlag, 1 << targetFlag}) copy(pp, []byte{0x00, 0x00, 0x00, 0x00, 0xFF ^ (1 << targetFlag)}) - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 1) require.NoError(tt, err) require.Equal(tt, expectedActive, active) require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, current) @@ -55,7 +56,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { t.Run("two votes in previous epoch", func(tt *testing.T) { copy(cp, []byte{0x00, 0x00, 0x00, 0x00, 0xFF ^ (1 << targetFlag), 0x00}) copy(pp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 1 << targetFlag, 1 << targetFlag}) - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 1) require.NoError(tt, err) require.Equal(tt, expectedActive, active) require.Equal(tt, uint64(0), current) @@ -66,7 +67,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { validators[0].EffectiveBalance = params.BeaconConfig().MaxEffectiveBalance - params.BeaconConfig().MinDepositAmount copy(cp, []byte{0xFF, 0xFF, 0x00, 0x00, 0xFF ^ (1 << targetFlag), 0}) copy(pp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 0xFF, 0xFF}) - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 1) require.NoError(tt, err) expectedActive -= params.BeaconConfig().MinDepositAmount require.Equal(tt, expectedActive, active) @@ -76,7 +77,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { t.Run("slash a validator", func(tt *testing.T) { validators[1].Slashed = true - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 1) require.NoError(tt, err) expectedActive -= params.BeaconConfig().MaxEffectiveBalance require.Equal(tt, expectedActive, active) @@ -85,7 +86,7 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { }) t.Run("Exit a validator", func(tt *testing.T) { validators[4].ExitEpoch = 1 - active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 2) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValSliceReader(validators), 2) require.NoError(tt, err) expectedActive -= params.BeaconConfig().MaxEffectiveBalance require.Equal(tt, expectedActive, active) @@ -93,3 +94,105 @@ func TestState_UnrealizedCheckpointBalances(t *testing.T) { require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance, previous) }) } + +func TestState_MVSlice_UnrealizedCheckpointBalances(t *testing.T) { + validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) + targetFlag := params.BeaconConfig().TimelyTargetFlagIndex + expectedActive := params.BeaconConfig().MinGenesisActiveValidatorCount * params.BeaconConfig().MaxEffectiveBalance + + balances := make([]uint64, params.BeaconConfig().MinGenesisActiveValidatorCount) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + } + balances[i] = params.BeaconConfig().MaxEffectiveBalance + } + + mv := &multi_value_slice.Slice[*ethpb.Validator]{} + mv.Init(validators) + + cp := make([]byte, len(validators)) + pp := make([]byte, len(validators)) + + t.Run("No one voted last two epochs", func(tt *testing.T) { + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 0) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, uint64(0), current) + require.Equal(tt, uint64(0), previous) + }) + + t.Run("bad votes in last two epochs", func(tt *testing.T) { + copy(cp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00}) + copy(pp, []byte{0x00, 0x00, 0x00, 0x00}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 1) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, uint64(0), current) + require.Equal(tt, uint64(0), previous) + }) + + t.Run("two votes in last epoch", func(tt *testing.T) { + copy(cp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 1 << targetFlag, 1 << targetFlag}) + copy(pp, []byte{0x00, 0x00, 0x00, 0x00, 0xFF ^ (1 << targetFlag)}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 1) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, current) + require.Equal(tt, uint64(0), previous) + }) + + t.Run("two votes in previous epoch", func(tt *testing.T) { + copy(cp, []byte{0x00, 0x00, 0x00, 0x00, 0xFF ^ (1 << targetFlag), 0x00}) + copy(pp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 1 << targetFlag, 1 << targetFlag}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 1) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, uint64(0), current) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + }) + + t.Run("votes in both epochs, decreased balance in first validator", func(tt *testing.T) { + validators[0].EffectiveBalance = params.BeaconConfig().MaxEffectiveBalance - params.BeaconConfig().MinDepositAmount + copy(cp, []byte{0xFF, 0xFF, 0x00, 0x00, 0xFF ^ (1 << targetFlag), 0}) + copy(pp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 0xFF, 0xFF}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 1) + require.NoError(tt, err) + expectedActive -= params.BeaconConfig().MinDepositAmount + require.Equal(tt, expectedActive, active) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance-params.BeaconConfig().MinDepositAmount, current) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + }) + + t.Run("slash a validator", func(tt *testing.T) { + validators[1].Slashed = true + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 1) + require.NoError(tt, err) + expectedActive -= params.BeaconConfig().MaxEffectiveBalance + require.Equal(tt, expectedActive, active) + require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance-params.BeaconConfig().MinDepositAmount, current) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + }) + t.Run("Exit a validator", func(tt *testing.T) { + validators[4].ExitEpoch = 1 + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, NewValMultiValueSliceReader(mv, &testObject{id: 0}), 2) + require.NoError(tt, err) + expectedActive -= params.BeaconConfig().MaxEffectiveBalance + require.Equal(tt, expectedActive, active) + require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance-params.BeaconConfig().MinDepositAmount, current) + require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance, previous) + }) +} + +type testObject struct { + id uint64 +} + +func (o *testObject) Id() uint64 { + return o.id +} + +func (o *testObject) SetId(id uint64) { + o.id = id +} diff --git a/beacon-chain/state/stateutil/validator_reader.go b/beacon-chain/state/stateutil/validator_reader.go new file mode 100644 index 000000000000..26f950c83181 --- /dev/null +++ b/beacon-chain/state/stateutil/validator_reader.go @@ -0,0 +1,59 @@ +package stateutil + +import ( + multi_value_slice "github.com/prysmaticlabs/prysm/v5/container/multi-value-slice" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" +) + +// ValReader specifies an interface through which we can access the validator registry. +type ValReader interface { + Len() int + At(i int) (*ethpb.Validator, error) +} + +// ValSliceReader describes a struct that conforms to the ValReader interface +type ValSliceReader struct { + Validators []*ethpb.Validator +} + +// NewValSliceReader constructs a ValSliceReader object. +func NewValSliceReader(vals []*ethpb.Validator) ValSliceReader { + return ValSliceReader{Validators: vals} +} + +// Len is the length of the validator registry. +func (v ValSliceReader) Len() int { + return len(v.Validators) +} + +// At returns the validator at the provided index. +func (v ValSliceReader) At(i int) (*ethpb.Validator, error) { + return v.Validators[i], nil +} + +// ValMultiValueSliceReader describes a struct that conforms to the ValReader interface. +// This struct is specifically designed for accessing validator data from a +// multivalue slice. +type ValMultiValueSliceReader struct { + ValMVSlice *multi_value_slice.Slice[*ethpb.Validator] + Identifier multi_value_slice.Identifiable +} + +// NewValMultiValueSliceReader constructs a new val reader object. +func NewValMultiValueSliceReader(valSlice *multi_value_slice.Slice[*ethpb.Validator], + identifier multi_value_slice.Identifiable) ValMultiValueSliceReader { + return ValMultiValueSliceReader{ + ValMVSlice: valSlice, + Identifier: identifier, + } +} + +// Len is the length of the validator registry. +func (v ValMultiValueSliceReader) Len() int { + return v.ValMVSlice.Len(v.Identifier) +} + +// At returns the validator at the provided index. +func (v ValMultiValueSliceReader) At(i int) (*ethpb.Validator, error) { + return v.ValMVSlice.At(v.Identifier, uint64(i)) +}