diff --git a/api/server/httprest/options.go b/api/server/httprest/options.go index 3c89f22be2..15b86465c5 100644 --- a/api/server/httprest/options.go +++ b/api/server/httprest/options.go @@ -1,9 +1,8 @@ package httprest import ( - "time" - "net/http" + "time" "github.com/OffchainLabs/prysm/v6/api/server/middleware" ) diff --git a/beacon-chain/core/blocks/attester_slashing.go b/beacon-chain/core/blocks/attester_slashing.go index 80e31d7585..ea70a90027 100644 --- a/beacon-chain/core/blocks/attester_slashing.go +++ b/beacon-chain/core/blocks/attester_slashing.go @@ -5,6 +5,7 @@ import ( "sort" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" "github.com/OffchainLabs/prysm/v6/container/slice" @@ -39,11 +40,11 @@ func ProcessAttesterSlashings( ctx context.Context, beaconState state.BeaconState, slashings []ethpb.AttSlashing, - slashFunc slashValidatorFunc, + exitInfo *validators.ExitInfo, ) (state.BeaconState, error) { var err error for _, slashing := range slashings { - beaconState, err = ProcessAttesterSlashing(ctx, beaconState, slashing, slashFunc) + beaconState, err = ProcessAttesterSlashing(ctx, beaconState, slashing, exitInfo) if err != nil { return nil, err } @@ -56,7 +57,7 @@ func ProcessAttesterSlashing( ctx context.Context, beaconState state.BeaconState, slashing ethpb.AttSlashing, - slashFunc slashValidatorFunc, + exitInfo *validators.ExitInfo, ) (state.BeaconState, error) { if err := VerifyAttesterSlashing(ctx, beaconState, slashing); err != nil { return nil, errors.Wrap(err, "could not verify attester slashing") @@ -75,10 +76,9 @@ func ProcessAttesterSlashing( return nil, err } if helpers.IsSlashableValidator(val.ActivationEpoch(), val.WithdrawableEpoch(), val.Slashed(), currentEpoch) { - beaconState, err = slashFunc(ctx, beaconState, primitives.ValidatorIndex(validatorIndex)) + beaconState, err = validators.SlashValidator(ctx, beaconState, primitives.ValidatorIndex(validatorIndex), exitInfo) if err != nil { - return nil, errors.Wrapf(err, "could not slash validator index %d", - validatorIndex) + return nil, errors.Wrapf(err, "could not slash validator index %d", validatorIndex) } slashedAny = true } diff --git a/beacon-chain/core/blocks/attester_slashing_test.go b/beacon-chain/core/blocks/attester_slashing_test.go index 14b6795bc3..a71598a205 100644 --- a/beacon-chain/core/blocks/attester_slashing_test.go +++ b/beacon-chain/core/blocks/attester_slashing_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/blocks" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing" v "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" @@ -44,11 +45,10 @@ func TestProcessAttesterSlashings_DataNotSlashable(t *testing.T) { Target: ðpb.Checkpoint{Epoch: 1}}, })}} - var registry []*ethpb.Validator currentSlot := primitives.Slot(0) beaconState, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{ - Validators: registry, + Validators: []*ethpb.Validator{{}}, Slot: currentSlot, }) require.NoError(t, err) @@ -62,16 +62,15 @@ func TestProcessAttesterSlashings_DataNotSlashable(t *testing.T) { for i, s := range b.Block.Body.AttesterSlashings { ss[i] = s } - _, err = blocks.ProcessAttesterSlashings(t.Context(), beaconState, ss, v.SlashValidator) + _, err = blocks.ProcessAttesterSlashings(t.Context(), beaconState, ss, v.ExitInformation(beaconState)) assert.ErrorContains(t, "attestations are not slashable", err) } func TestProcessAttesterSlashings_IndexedAttestationFailedToVerify(t *testing.T) { - var registry []*ethpb.Validator currentSlot := primitives.Slot(0) beaconState, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{ - Validators: registry, + Validators: []*ethpb.Validator{{}}, Slot: currentSlot, }) require.NoError(t, err) @@ -101,7 +100,7 @@ func TestProcessAttesterSlashings_IndexedAttestationFailedToVerify(t *testing.T) for i, s := range b.Block.Body.AttesterSlashings { ss[i] = s } - _, err = blocks.ProcessAttesterSlashings(t.Context(), beaconState, ss, v.SlashValidator) + _, err = blocks.ProcessAttesterSlashings(t.Context(), beaconState, ss, v.ExitInformation(beaconState)) assert.ErrorContains(t, "validator indices count exceeds MAX_VALIDATORS_PER_COMMITTEE", err) } @@ -243,7 +242,7 @@ func TestProcessAttesterSlashings_AppliesCorrectStatus(t *testing.T) { currentSlot := 2 * params.BeaconConfig().SlotsPerEpoch require.NoError(t, tc.st.SetSlot(currentSlot)) - newState, err := blocks.ProcessAttesterSlashings(t.Context(), tc.st, []ethpb.AttSlashing{tc.slashing}, v.SlashValidator) + newState, err := blocks.ProcessAttesterSlashings(t.Context(), tc.st, []ethpb.AttSlashing{tc.slashing}, v.ExitInformation(tc.st)) require.NoError(t, err) newRegistry := newState.Validators() @@ -265,3 +264,83 @@ func TestProcessAttesterSlashings_AppliesCorrectStatus(t *testing.T) { }) } } + +func TestProcessAttesterSlashing_ExitEpochGetsUpdated(t *testing.T) { + st, keys := util.DeterministicGenesisStateElectra(t, 8) + bal, err := helpers.TotalActiveBalance(st) + require.NoError(t, err) + perEpochChurn := helpers.ActivationExitChurnLimit(primitives.Gwei(bal)) + vals := st.Validators() + + // We set the total effective balance of slashed validators + // higher than the churn limit for a single epoch. + vals[0].EffectiveBalance = uint64(perEpochChurn / 3) + vals[1].EffectiveBalance = uint64(perEpochChurn / 3) + vals[2].EffectiveBalance = uint64(perEpochChurn / 3) + vals[3].EffectiveBalance = uint64(perEpochChurn / 3) + require.NoError(t, st.SetValidators(vals)) + + sl1att1 := util.HydrateIndexedAttestationElectra(ðpb.IndexedAttestationElectra{ + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{Epoch: 1}, + }, + AttestingIndices: []uint64{0, 1}, + }) + sl1att2 := util.HydrateIndexedAttestationElectra(ðpb.IndexedAttestationElectra{ + AttestingIndices: []uint64{0, 1}, + }) + slashing1 := ðpb.AttesterSlashingElectra{ + Attestation_1: sl1att1, + Attestation_2: sl1att2, + } + sl2att1 := util.HydrateIndexedAttestationElectra(ðpb.IndexedAttestationElectra{ + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{Epoch: 1}, + }, + AttestingIndices: []uint64{2, 3}, + }) + sl2att2 := util.HydrateIndexedAttestationElectra(ðpb.IndexedAttestationElectra{ + AttestingIndices: []uint64{2, 3}, + }) + slashing2 := ðpb.AttesterSlashingElectra{ + Attestation_1: sl2att1, + Attestation_2: sl2att2, + } + + domain, err := signing.Domain(st.Fork(), 0, params.BeaconConfig().DomainBeaconAttester, st.GenesisValidatorsRoot()) + require.NoError(t, err) + + signingRoot, err := signing.ComputeSigningRoot(sl1att1.GetData(), domain) + assert.NoError(t, err, "Could not get signing root of beacon block header") + sig0 := keys[0].Sign(signingRoot[:]) + sig1 := keys[1].Sign(signingRoot[:]) + aggregateSig := bls.AggregateSignatures([]bls.Signature{sig0, sig1}) + sl1att1.Signature = aggregateSig.Marshal() + + signingRoot, err = signing.ComputeSigningRoot(sl1att2.GetData(), domain) + assert.NoError(t, err, "Could not get signing root of beacon block header") + sig0 = keys[0].Sign(signingRoot[:]) + sig1 = keys[1].Sign(signingRoot[:]) + aggregateSig = bls.AggregateSignatures([]bls.Signature{sig0, sig1}) + sl1att2.Signature = aggregateSig.Marshal() + + signingRoot, err = signing.ComputeSigningRoot(sl2att1.GetData(), domain) + assert.NoError(t, err, "Could not get signing root of beacon block header") + sig0 = keys[2].Sign(signingRoot[:]) + sig1 = keys[3].Sign(signingRoot[:]) + aggregateSig = bls.AggregateSignatures([]bls.Signature{sig0, sig1}) + sl2att1.Signature = aggregateSig.Marshal() + + signingRoot, err = signing.ComputeSigningRoot(sl2att2.GetData(), domain) + assert.NoError(t, err, "Could not get signing root of beacon block header") + sig0 = keys[2].Sign(signingRoot[:]) + sig1 = keys[3].Sign(signingRoot[:]) + aggregateSig = bls.AggregateSignatures([]bls.Signature{sig0, sig1}) + sl2att2.Signature = aggregateSig.Marshal() + + exitInfo := v.ExitInformation(st) + assert.Equal(t, primitives.Epoch(0), exitInfo.HighestExitEpoch) + _, err = blocks.ProcessAttesterSlashings(t.Context(), st, []ethpb.AttSlashing{slashing1, slashing2}, exitInfo) + require.NoError(t, err) + assert.Equal(t, primitives.Epoch(6), exitInfo.HighestExitEpoch) +} diff --git a/beacon-chain/core/blocks/block_operations_fuzz_test.go b/beacon-chain/core/blocks/block_operations_fuzz_test.go index f81e1bad61..778a399455 100644 --- a/beacon-chain/core/blocks/block_operations_fuzz_test.go +++ b/beacon-chain/core/blocks/block_operations_fuzz_test.go @@ -191,7 +191,7 @@ func TestFuzzProcessProposerSlashings_10000(t *testing.T) { fuzzer.Fuzz(p) s, err := state_native.InitializeFromProtoUnsafePhase0(state) require.NoError(t, err) - r, err := ProcessProposerSlashings(ctx, s, []*ethpb.ProposerSlashing{p}, v.SlashValidator) + r, err := ProcessProposerSlashings(ctx, s, []*ethpb.ProposerSlashing{p}, v.ExitInformation(s)) if err != nil && r != nil { t.Fatalf("return value should be nil on err. found: %v on error: %v for state: %v and slashing: %v", r, err, state, p) } @@ -224,7 +224,7 @@ func TestFuzzProcessAttesterSlashings_10000(t *testing.T) { fuzzer.Fuzz(a) s, err := state_native.InitializeFromProtoUnsafePhase0(state) require.NoError(t, err) - r, err := ProcessAttesterSlashings(ctx, s, []ethpb.AttSlashing{a}, v.SlashValidator) + r, err := ProcessAttesterSlashings(ctx, s, []ethpb.AttSlashing{a}, v.ExitInformation(s)) if err != nil && r != nil { t.Fatalf("return value should be nil on err. found: %v on error: %v for state: %v and slashing: %v", r, err, state, a) } @@ -334,7 +334,7 @@ func TestFuzzProcessVoluntaryExits_10000(t *testing.T) { fuzzer.Fuzz(e) s, err := state_native.InitializeFromProtoUnsafePhase0(state) require.NoError(t, err) - r, err := ProcessVoluntaryExits(ctx, s, []*ethpb.SignedVoluntaryExit{e}) + r, err := ProcessVoluntaryExits(ctx, s, []*ethpb.SignedVoluntaryExit{e}, v.ExitInformation(s)) if err != nil && r != nil { t.Fatalf("return value should be nil on err. found: %v on error: %v for state: %v and exit: %v", r, err, state, e) } @@ -351,7 +351,7 @@ func TestFuzzProcessVoluntaryExitsNoVerify_10000(t *testing.T) { fuzzer.Fuzz(e) s, err := state_native.InitializeFromProtoUnsafePhase0(state) require.NoError(t, err) - r, err := ProcessVoluntaryExits(t.Context(), s, []*ethpb.SignedVoluntaryExit{e}) + r, err := ProcessVoluntaryExits(t.Context(), s, []*ethpb.SignedVoluntaryExit{e}, v.ExitInformation(s)) if err != nil && r != nil { t.Fatalf("return value should be nil on err. found: %v on error: %v for state: %v and block: %v", r, err, state, e) } diff --git a/beacon-chain/core/blocks/block_regression_test.go b/beacon-chain/core/blocks/block_regression_test.go index d6519f0062..9a57e6af92 100644 --- a/beacon-chain/core/blocks/block_regression_test.go +++ b/beacon-chain/core/blocks/block_regression_test.go @@ -94,7 +94,7 @@ func TestProcessAttesterSlashings_RegressionSlashableIndices(t *testing.T) { for i, s := range b.Block.Body.AttesterSlashings { ss[i] = s } - newState, err := blocks.ProcessAttesterSlashings(t.Context(), beaconState, ss, v.SlashValidator) + newState, err := blocks.ProcessAttesterSlashings(t.Context(), beaconState, ss, v.ExitInformation(beaconState)) require.NoError(t, err) newRegistry := newState.Validators() if !newRegistry[expectedSlashedVal].Slashed { diff --git a/beacon-chain/core/blocks/exit.go b/beacon-chain/core/blocks/exit.go index 7ad203a9b4..d60398c4c0 100644 --- a/beacon-chain/core/blocks/exit.go +++ b/beacon-chain/core/blocks/exit.go @@ -9,7 +9,6 @@ import ( v "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" "github.com/OffchainLabs/prysm/v6/config/params" - "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/runtime/version" "github.com/OffchainLabs/prysm/v6/time/slots" @@ -50,13 +49,12 @@ func ProcessVoluntaryExits( ctx context.Context, beaconState state.BeaconState, exits []*ethpb.SignedVoluntaryExit, + exitInfo *v.ExitInfo, ) (state.BeaconState, error) { // Avoid calculating the epoch churn if no exits exist. if len(exits) == 0 { return beaconState, nil } - maxExitEpoch, churn := v.MaxExitEpochAndChurn(beaconState) - var exitEpoch primitives.Epoch for idx, exit := range exits { if exit == nil || exit.Exit == nil { return nil, errors.New("nil voluntary exit in block body") @@ -68,15 +66,8 @@ func ProcessVoluntaryExits( if err := VerifyExitAndSignature(val, beaconState, exit); err != nil { return nil, errors.Wrapf(err, "could not verify exit %d", idx) } - beaconState, exitEpoch, err = v.InitiateValidatorExit(ctx, beaconState, exit.Exit.ValidatorIndex, maxExitEpoch, churn) - if err == nil { - if exitEpoch > maxExitEpoch { - maxExitEpoch = exitEpoch - churn = 1 - } else if exitEpoch == maxExitEpoch { - churn++ - } - } else if !errors.Is(err, v.ErrValidatorAlreadyExited) { + beaconState, err = v.InitiateValidatorExit(ctx, beaconState, exit.Exit.ValidatorIndex, exitInfo) + if err != nil && !errors.Is(err, v.ErrValidatorAlreadyExited) { return nil, err } } diff --git a/beacon-chain/core/blocks/exit_test.go b/beacon-chain/core/blocks/exit_test.go index d9d4add8ec..33ac17c567 100644 --- a/beacon-chain/core/blocks/exit_test.go +++ b/beacon-chain/core/blocks/exit_test.go @@ -7,6 +7,7 @@ import ( "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/time" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" state_native "github.com/OffchainLabs/prysm/v6/beacon-chain/state/state-native" "github.com/OffchainLabs/prysm/v6/config/params" @@ -46,7 +47,7 @@ func TestProcessVoluntaryExits_NotActiveLongEnoughToExit(t *testing.T) { } want := "validator has not been active long enough to exit" - _, err = blocks.ProcessVoluntaryExits(t.Context(), state, b.Block.Body.VoluntaryExits) + _, err = blocks.ProcessVoluntaryExits(t.Context(), state, b.Block.Body.VoluntaryExits, validators.ExitInformation(state)) assert.ErrorContains(t, want, err) } @@ -76,7 +77,7 @@ func TestProcessVoluntaryExits_ExitAlreadySubmitted(t *testing.T) { } want := "validator with index 0 has already submitted an exit, which will take place at epoch: 10" - _, err = blocks.ProcessVoluntaryExits(t.Context(), state, b.Block.Body.VoluntaryExits) + _, err = blocks.ProcessVoluntaryExits(t.Context(), state, b.Block.Body.VoluntaryExits, validators.ExitInformation(state)) assert.ErrorContains(t, want, err) } @@ -124,7 +125,7 @@ func TestProcessVoluntaryExits_AppliesCorrectStatus(t *testing.T) { }, } - newState, err := blocks.ProcessVoluntaryExits(t.Context(), state, b.Block.Body.VoluntaryExits) + newState, err := blocks.ProcessVoluntaryExits(t.Context(), state, b.Block.Body.VoluntaryExits, validators.ExitInformation(state)) require.NoError(t, err, "Could not process exits") newRegistry := newState.Validators() if newRegistry[0].ExitEpoch != helpers.ActivationExitEpoch(primitives.Epoch(state.Slot()/params.BeaconConfig().SlotsPerEpoch)) { diff --git a/beacon-chain/core/blocks/proposer_slashing.go b/beacon-chain/core/blocks/proposer_slashing.go index 1fe9ade9a9..850ff98eb1 100644 --- a/beacon-chain/core/blocks/proposer_slashing.go +++ b/beacon-chain/core/blocks/proposer_slashing.go @@ -7,9 +7,9 @@ import ( "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/time" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" "github.com/OffchainLabs/prysm/v6/config/params" - "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/time/slots" "github.com/pkg/errors" @@ -19,11 +19,6 @@ import ( // ErrCouldNotVerifyBlockHeader is returned when a block header's signature cannot be verified. var ErrCouldNotVerifyBlockHeader = errors.New("could not verify beacon block header") -type slashValidatorFunc func( - ctx context.Context, - st state.BeaconState, - vid primitives.ValidatorIndex) (state.BeaconState, error) - // ProcessProposerSlashings is one of the operations performed // on each processed beacon block to slash proposers based on // slashing conditions if any slashable events occurred. @@ -54,11 +49,11 @@ func ProcessProposerSlashings( ctx context.Context, beaconState state.BeaconState, slashings []*ethpb.ProposerSlashing, - slashFunc slashValidatorFunc, + exitInfo *validators.ExitInfo, ) (state.BeaconState, error) { var err error for _, slashing := range slashings { - beaconState, err = ProcessProposerSlashing(ctx, beaconState, slashing, slashFunc) + beaconState, err = ProcessProposerSlashing(ctx, beaconState, slashing, exitInfo) if err != nil { return nil, err } @@ -71,7 +66,7 @@ func ProcessProposerSlashing( ctx context.Context, beaconState state.BeaconState, slashing *ethpb.ProposerSlashing, - slashFunc slashValidatorFunc, + exitInfo *validators.ExitInfo, ) (state.BeaconState, error) { var err error if slashing == nil { @@ -80,7 +75,7 @@ func ProcessProposerSlashing( if err = VerifyProposerSlashing(beaconState, slashing); err != nil { return nil, errors.Wrap(err, "could not verify proposer slashing") } - beaconState, err = slashFunc(ctx, beaconState, slashing.Header_1.Header.ProposerIndex) + beaconState, err = validators.SlashValidator(ctx, beaconState, slashing.Header_1.Header.ProposerIndex, exitInfo) if err != nil { return nil, errors.Wrapf(err, "could not slash proposer index %d", slashing.Header_1.Header.ProposerIndex) } diff --git a/beacon-chain/core/blocks/proposer_slashing_test.go b/beacon-chain/core/blocks/proposer_slashing_test.go index 7fe013a397..0c3d231eff 100644 --- a/beacon-chain/core/blocks/proposer_slashing_test.go +++ b/beacon-chain/core/blocks/proposer_slashing_test.go @@ -50,7 +50,7 @@ func TestProcessProposerSlashings_UnmatchedHeaderSlots(t *testing.T) { }, } want := "mismatched header slots" - _, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, b.Block.Body.ProposerSlashings, v.SlashValidator) + _, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, b.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) assert.ErrorContains(t, want, err) } @@ -83,7 +83,7 @@ func TestProcessProposerSlashings_SameHeaders(t *testing.T) { }, } want := "expected slashing headers to differ" - _, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, b.Block.Body.ProposerSlashings, v.SlashValidator) + _, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, b.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) assert.ErrorContains(t, want, err) } @@ -133,7 +133,7 @@ func TestProcessProposerSlashings_ValidatorNotSlashable(t *testing.T) { "validator with key %#x is not slashable", bytesutil.ToBytes48(beaconState.Validators()[0].PublicKey), ) - _, err = blocks.ProcessProposerSlashings(t.Context(), beaconState, b.Block.Body.ProposerSlashings, v.SlashValidator) + _, err = blocks.ProcessProposerSlashings(t.Context(), beaconState, b.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) assert.ErrorContains(t, want, err) } @@ -172,7 +172,7 @@ func TestProcessProposerSlashings_AppliesCorrectStatus(t *testing.T) { block := util.NewBeaconBlock() block.Block.Body.ProposerSlashings = slashings - newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.SlashValidator) + newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) require.NoError(t, err) newStateVals := newState.Validators() @@ -220,7 +220,7 @@ func TestProcessProposerSlashings_AppliesCorrectStatusAltair(t *testing.T) { block := util.NewBeaconBlock() block.Block.Body.ProposerSlashings = slashings - newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.SlashValidator) + newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) require.NoError(t, err) newStateVals := newState.Validators() @@ -268,7 +268,7 @@ func TestProcessProposerSlashings_AppliesCorrectStatusBellatrix(t *testing.T) { block := util.NewBeaconBlock() block.Block.Body.ProposerSlashings = slashings - newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.SlashValidator) + newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) require.NoError(t, err) newStateVals := newState.Validators() @@ -316,7 +316,7 @@ func TestProcessProposerSlashings_AppliesCorrectStatusCapella(t *testing.T) { block := util.NewBeaconBlock() block.Block.Body.ProposerSlashings = slashings - newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.SlashValidator) + newState, err := blocks.ProcessProposerSlashings(t.Context(), beaconState, block.Block.Body.ProposerSlashings, v.ExitInformation(beaconState)) require.NoError(t, err) newStateVals := newState.Validators() diff --git a/beacon-chain/core/electra/registry_updates.go b/beacon-chain/core/electra/registry_updates.go index 8dd91b8206..8b4cbc4421 100644 --- a/beacon-chain/core/electra/registry_updates.go +++ b/beacon-chain/core/electra/registry_updates.go @@ -84,8 +84,8 @@ func ProcessRegistryUpdates(ctx context.Context, st state.BeaconState) error { // Handle validator ejections. for _, idx := range eligibleForEjection { var err error - // exitQueueEpoch and churn arguments are not used in electra. - st, _, err = validators.InitiateValidatorExit(ctx, st, idx, 0 /*exitQueueEpoch*/, 0 /*churn*/) + // exit info is not used in electra + st, err = validators.InitiateValidatorExit(ctx, st, idx, &validators.ExitInfo{}) if err != nil && !errors.Is(err, validators.ErrValidatorAlreadyExited) { return fmt.Errorf("failed to initiate validator exit at index %d: %w", idx, err) } diff --git a/beacon-chain/core/electra/transition_no_verify_sig.go b/beacon-chain/core/electra/transition_no_verify_sig.go index b7702ae3ef..04338031bd 100644 --- a/beacon-chain/core/electra/transition_no_verify_sig.go +++ b/beacon-chain/core/electra/transition_no_verify_sig.go @@ -4,6 +4,7 @@ import ( "context" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/blocks" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" v "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" "github.com/OffchainLabs/prysm/v6/consensus-types/interfaces" @@ -46,18 +47,21 @@ var ( // # [New in Electra:EIP7251] // for_ops(body.execution_payload.consolidation_requests, process_consolidation_request) -func ProcessOperations( - ctx context.Context, - st state.BeaconState, - block interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { +func ProcessOperations(ctx context.Context, st state.BeaconState, block interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { + var err error + // 6110 validations are in VerifyOperationLengths bb := block.Body() // Electra extends the altair operations. - st, err := ProcessProposerSlashings(ctx, st, bb.ProposerSlashings(), v.SlashValidator) + exitInfo := v.ExitInformation(st) + if err := helpers.UpdateTotalActiveBalanceCache(st, exitInfo.TotalActiveBalance); err != nil { + return nil, errors.Wrap(err, "could not update total active balance cache") + } + st, err = ProcessProposerSlashings(ctx, st, bb.ProposerSlashings(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process altair proposer slashing") } - st, err = ProcessAttesterSlashings(ctx, st, bb.AttesterSlashings(), v.SlashValidator) + st, err = ProcessAttesterSlashings(ctx, st, bb.AttesterSlashings(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process altair attester slashing") } @@ -68,7 +72,7 @@ func ProcessOperations( if _, err := ProcessDeposits(ctx, st, bb.Deposits()); err != nil { // new in electra return nil, errors.Wrap(err, "could not process altair deposit") } - st, err = ProcessVoluntaryExits(ctx, st, bb.VoluntaryExits()) + st, err = ProcessVoluntaryExits(ctx, st, bb.VoluntaryExits(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process voluntary exits") } diff --git a/beacon-chain/core/electra/withdrawals.go b/beacon-chain/core/electra/withdrawals.go index d0e5fd2364..5f78ab0c29 100644 --- a/beacon-chain/core/electra/withdrawals.go +++ b/beacon-chain/core/electra/withdrawals.go @@ -147,9 +147,8 @@ func ProcessWithdrawalRequests(ctx context.Context, st state.BeaconState, wrs [] if isFullExitRequest { // Only exit validator if it has no pending withdrawals in the queue if pendingBalanceToWithdraw == 0 { - maxExitEpoch, churn := validators.MaxExitEpochAndChurn(st) var err error - st, _, err = validators.InitiateValidatorExit(ctx, st, vIdx, maxExitEpoch, churn) + st, err = validators.InitiateValidatorExit(ctx, st, vIdx, validators.ExitInformation(st)) if err != nil { return nil, err } diff --git a/beacon-chain/core/epoch/epoch_processing.go b/beacon-chain/core/epoch/epoch_processing.go index 731bdacbee..24ce0579d9 100644 --- a/beacon-chain/core/epoch/epoch_processing.go +++ b/beacon-chain/core/epoch/epoch_processing.go @@ -99,8 +99,7 @@ func ProcessRegistryUpdates(ctx context.Context, st state.BeaconState) (state.Be for _, idx := range eligibleForEjection { // Here is fine to do a quadratic loop since this should // barely happen - maxExitEpoch, churn := validators.MaxExitEpochAndChurn(st) - st, _, err = validators.InitiateValidatorExit(ctx, st, idx, maxExitEpoch, churn) + st, err = validators.InitiateValidatorExit(ctx, st, idx, validators.ExitInformation(st)) if err != nil && !errors.Is(err, validators.ErrValidatorAlreadyExited) { return nil, errors.Wrapf(err, "could not initiate exit for validator %d", idx) } diff --git a/beacon-chain/core/helpers/rewards_penalties.go b/beacon-chain/core/helpers/rewards_penalties.go index 6f69cb3d2b..73a3c41083 100644 --- a/beacon-chain/core/helpers/rewards_penalties.go +++ b/beacon-chain/core/helpers/rewards_penalties.go @@ -87,6 +87,11 @@ func TotalActiveBalance(s state.ReadOnlyBeaconState) (uint64, error) { return total, nil } +// UpdateTotalActiveBalanceCache updates the cache with the given total active balance. +func UpdateTotalActiveBalanceCache(s state.BeaconState, total uint64) error { + return balanceCache.AddTotalEffectiveBalance(s, total) +} + // IncreaseBalance increases validator with the given 'index' balance by 'delta' in Gwei. // // Spec pseudocode definition: diff --git a/beacon-chain/core/helpers/rewards_penalties_test.go b/beacon-chain/core/helpers/rewards_penalties_test.go index 7e1d13112f..64192ad352 100644 --- a/beacon-chain/core/helpers/rewards_penalties_test.go +++ b/beacon-chain/core/helpers/rewards_penalties_test.go @@ -297,3 +297,30 @@ func TestIncreaseBadBalance_NotOK(t *testing.T) { require.ErrorContains(t, "addition overflows", helpers.IncreaseBalance(state, test.i, test.nb)) } } + +func TestUpdateTotalActiveBalanceCache(t *testing.T) { + helpers.ClearCache() + + // Create a test state with some validators + validators := []*ethpb.Validator{ + {EffectiveBalance: 32 * 1e9, ExitEpoch: params.BeaconConfig().FarFutureEpoch, ActivationEpoch: 0}, + {EffectiveBalance: 32 * 1e9, ExitEpoch: params.BeaconConfig().FarFutureEpoch, ActivationEpoch: 0}, + {EffectiveBalance: 31 * 1e9, ExitEpoch: params.BeaconConfig().FarFutureEpoch, ActivationEpoch: 0}, + } + state, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{ + Validators: validators, + Slot: 0, + }) + require.NoError(t, err) + + // Test updating cache with a specific total + testTotal := uint64(95 * 1e9) // 32 + 32 + 31 = 95 + err = helpers.UpdateTotalActiveBalanceCache(state, testTotal) + require.NoError(t, err) + + // Verify the cache was updated by retrieving the total active balance + // which should now return the cached value + cachedTotal, err := helpers.TotalActiveBalance(state) + require.NoError(t, err) + assert.Equal(t, testTotal, cachedTotal, "Cache should return the updated total") +} diff --git a/beacon-chain/core/transition/transition_no_verify_sig.go b/beacon-chain/core/transition/transition_no_verify_sig.go index 7ca7437834..4bf9d89f48 100644 --- a/beacon-chain/core/transition/transition_no_verify_sig.go +++ b/beacon-chain/core/transition/transition_no_verify_sig.go @@ -8,6 +8,7 @@ import ( "github.com/OffchainLabs/prysm/v6/beacon-chain/core/altair" b "github.com/OffchainLabs/prysm/v6/beacon-chain/core/blocks" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/electra" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/transition/interop" v "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" @@ -374,15 +375,18 @@ func ProcessBlockForStateRoot( } // This calls altair block operations. -func altairOperations( - ctx context.Context, - st state.BeaconState, - beaconBlock interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { - st, err := b.ProcessProposerSlashings(ctx, st, beaconBlock.Body().ProposerSlashings(), v.SlashValidator) +func altairOperations(ctx context.Context, st state.BeaconState, beaconBlock interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { + var err error + + exitInfo := v.ExitInformation(st) + if err := helpers.UpdateTotalActiveBalanceCache(st, exitInfo.TotalActiveBalance); err != nil { + return nil, errors.Wrap(err, "could not update total active balance cache") + } + st, err = b.ProcessProposerSlashings(ctx, st, beaconBlock.Body().ProposerSlashings(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process altair proposer slashing") } - st, err = b.ProcessAttesterSlashings(ctx, st, beaconBlock.Body().AttesterSlashings(), v.SlashValidator) + st, err = b.ProcessAttesterSlashings(ctx, st, beaconBlock.Body().AttesterSlashings(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process altair attester slashing") } @@ -393,7 +397,7 @@ func altairOperations( if _, err := altair.ProcessDeposits(ctx, st, beaconBlock.Body().Deposits()); err != nil { return nil, errors.Wrap(err, "could not process altair deposit") } - st, err = b.ProcessVoluntaryExits(ctx, st, beaconBlock.Body().VoluntaryExits()) + st, err = b.ProcessVoluntaryExits(ctx, st, beaconBlock.Body().VoluntaryExits(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process voluntary exits") } @@ -401,15 +405,18 @@ func altairOperations( } // This calls phase 0 block operations. -func phase0Operations( - ctx context.Context, - st state.BeaconState, - beaconBlock interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { - st, err := b.ProcessProposerSlashings(ctx, st, beaconBlock.Body().ProposerSlashings(), v.SlashValidator) +func phase0Operations(ctx context.Context, st state.BeaconState, beaconBlock interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { + var err error + + exitInfo := v.ExitInformation(st) + if err := helpers.UpdateTotalActiveBalanceCache(st, exitInfo.TotalActiveBalance); err != nil { + return nil, errors.Wrap(err, "could not update total active balance cache") + } + st, err = b.ProcessProposerSlashings(ctx, st, beaconBlock.Body().ProposerSlashings(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process block proposer slashings") } - st, err = b.ProcessAttesterSlashings(ctx, st, beaconBlock.Body().AttesterSlashings(), v.SlashValidator) + st, err = b.ProcessAttesterSlashings(ctx, st, beaconBlock.Body().AttesterSlashings(), exitInfo) if err != nil { return nil, errors.Wrap(err, "could not process block attester slashings") } @@ -420,5 +427,9 @@ func phase0Operations( if _, err := altair.ProcessDeposits(ctx, st, beaconBlock.Body().Deposits()); err != nil { return nil, errors.Wrap(err, "could not process deposits") } - return b.ProcessVoluntaryExits(ctx, st, beaconBlock.Body().VoluntaryExits()) + st, err = b.ProcessVoluntaryExits(ctx, st, beaconBlock.Body().VoluntaryExits(), exitInfo) + if err != nil { + return nil, errors.Wrap(err, "could not process voluntary exits") + } + return st, nil } diff --git a/beacon-chain/core/validators/validator.go b/beacon-chain/core/validators/validator.go index 2837ed0f53..29b56277b0 100644 --- a/beacon-chain/core/validators/validator.go +++ b/beacon-chain/core/validators/validator.go @@ -13,34 +13,55 @@ import ( "github.com/OffchainLabs/prysm/v6/config/params" "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" "github.com/OffchainLabs/prysm/v6/math" + mathutil "github.com/OffchainLabs/prysm/v6/math" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/runtime/version" "github.com/OffchainLabs/prysm/v6/time/slots" "github.com/pkg/errors" ) +// ExitInfo provides information about validator exits in the state. +type ExitInfo struct { + HighestExitEpoch primitives.Epoch + Churn uint64 + TotalActiveBalance uint64 +} + // ErrValidatorAlreadyExited is an error raised when trying to process an exit of // an already exited validator var ErrValidatorAlreadyExited = errors.New("validator already exited") -// MaxExitEpochAndChurn returns the maximum non-FAR_FUTURE_EPOCH exit -// epoch and the number of them -func MaxExitEpochAndChurn(s state.BeaconState) (maxExitEpoch primitives.Epoch, churn uint64) { +// ExitInformation returns information about validator exits. +func ExitInformation(s state.BeaconState) *ExitInfo { + exitInfo := &ExitInfo{} + farFutureEpoch := params.BeaconConfig().FarFutureEpoch + currentEpoch := slots.ToEpoch(s.Slot()) + totalActiveBalance := uint64(0) + err := s.ReadFromEveryValidator(func(idx int, val state.ReadOnlyValidator) error { e := val.ExitEpoch() if e != farFutureEpoch { - if e > maxExitEpoch { - maxExitEpoch = e - churn = 1 - } else if e == maxExitEpoch { - churn++ + if e > exitInfo.HighestExitEpoch { + exitInfo.HighestExitEpoch = e + exitInfo.Churn = 1 + } else if e == exitInfo.HighestExitEpoch { + exitInfo.Churn++ } } + + // Calculate total active balance in the same loop + if helpers.IsActiveValidatorUsingTrie(val, currentEpoch) { + totalActiveBalance += val.EffectiveBalance() + } + return nil }) _ = err - return + + // Apply minimum balance as per spec + exitInfo.TotalActiveBalance = mathutil.Max(params.BeaconConfig().EffectiveBalanceIncrement, totalActiveBalance) + return exitInfo } // InitiateValidatorExit takes in validator index and updates @@ -64,59 +85,117 @@ func MaxExitEpochAndChurn(s state.BeaconState) (maxExitEpoch primitives.Epoch, c // # Set validator exit epoch and withdrawable epoch // validator.exit_epoch = exit_queue_epoch // validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) -func InitiateValidatorExit(ctx context.Context, s state.BeaconState, idx primitives.ValidatorIndex, exitQueueEpoch primitives.Epoch, churn uint64) (state.BeaconState, primitives.Epoch, error) { +func InitiateValidatorExit( + ctx context.Context, + s state.BeaconState, + idx primitives.ValidatorIndex, + exitInfo *ExitInfo, +) (state.BeaconState, error) { validator, err := s.ValidatorAtIndex(idx) if err != nil { - return nil, 0, err + return nil, err } if validator.ExitEpoch != params.BeaconConfig().FarFutureEpoch { - return s, validator.ExitEpoch, ErrValidatorAlreadyExited + return s, ErrValidatorAlreadyExited } // Compute exit queue epoch. if s.Version() < version.Electra { - // Relevant spec code from phase0: - // - // exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] - // exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) - // exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) - // if exit_queue_churn >= get_validator_churn_limit(state): - // exit_queue_epoch += Epoch(1) - exitableEpoch := helpers.ActivationExitEpoch(time.CurrentEpoch(s)) - if exitableEpoch > exitQueueEpoch { - exitQueueEpoch = exitableEpoch - churn = 0 - } - activeValidatorCount, err := helpers.ActiveValidatorCount(ctx, s, time.CurrentEpoch(s)) - if err != nil { - return nil, 0, errors.Wrap(err, "could not get active validator count") - } - currentChurn := helpers.ValidatorExitChurnLimit(activeValidatorCount) - - if churn >= currentChurn { - exitQueueEpoch, err = exitQueueEpoch.SafeAdd(1) - if err != nil { - return nil, 0, err - } + if err = initiateValidatorExitPreElectra(ctx, s, exitInfo); err != nil { + return nil, err } } else { // [Modified in Electra:EIP7251] // exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) var err error - exitQueueEpoch, err = s.ExitEpochAndUpdateChurn(primitives.Gwei(validator.EffectiveBalance)) + exitInfo.HighestExitEpoch, err = s.ExitEpochAndUpdateChurn(primitives.Gwei(validator.EffectiveBalance)) if err != nil { - return nil, 0, err + return nil, err } } - validator.ExitEpoch = exitQueueEpoch - validator.WithdrawableEpoch, err = exitQueueEpoch.SafeAddEpoch(params.BeaconConfig().MinValidatorWithdrawabilityDelay) + validator.ExitEpoch = exitInfo.HighestExitEpoch + validator.WithdrawableEpoch, err = exitInfo.HighestExitEpoch.SafeAddEpoch(params.BeaconConfig().MinValidatorWithdrawabilityDelay) if err != nil { - return nil, 0, err + return nil, err } if err := s.UpdateValidatorAtIndex(idx, validator); err != nil { - return nil, 0, err + return nil, err } - return s, exitQueueEpoch, nil + return s, nil +} + +// InitiateValidatorExitForTotalBal has the same functionality as InitiateValidatorExit, +// the only difference being how total active balance is obtained. In InitiateValidatorExit +// it is calculated inside the function and in InitiateValidatorExitForTotalBal it's a +// function argument. +func InitiateValidatorExitForTotalBal( + ctx context.Context, + s state.BeaconState, + idx primitives.ValidatorIndex, + exitInfo *ExitInfo, + totalActiveBalance primitives.Gwei, +) (state.BeaconState, error) { + validator, err := s.ValidatorAtIndex(idx) + if err != nil { + return nil, err + } + if validator.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return s, ErrValidatorAlreadyExited + } + + // Compute exit queue epoch. + if s.Version() < version.Electra { + if err = initiateValidatorExitPreElectra(ctx, s, exitInfo); err != nil { + return nil, err + } + } else { + // [Modified in Electra:EIP7251] + // exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) + var err error + exitInfo.HighestExitEpoch, err = s.ExitEpochAndUpdateChurnForTotalBal(totalActiveBalance, primitives.Gwei(validator.EffectiveBalance)) + if err != nil { + return nil, err + } + } + validator.ExitEpoch = exitInfo.HighestExitEpoch + validator.WithdrawableEpoch, err = exitInfo.HighestExitEpoch.SafeAddEpoch(params.BeaconConfig().MinValidatorWithdrawabilityDelay) + if err != nil { + return nil, err + } + if err := s.UpdateValidatorAtIndex(idx, validator); err != nil { + return nil, err + } + return s, nil +} + +func initiateValidatorExitPreElectra(ctx context.Context, s state.BeaconState, exitInfo *ExitInfo) error { + // Relevant spec code from phase0: + // + // exit_epochs = [v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH] + // exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) + // exit_queue_churn = len([v for v in state.validators if v.exit_epoch == exit_queue_epoch]) + // if exit_queue_churn >= get_validator_churn_limit(state): + // exit_queue_epoch += Epoch(1) + exitableEpoch := helpers.ActivationExitEpoch(time.CurrentEpoch(s)) + if exitableEpoch > exitInfo.HighestExitEpoch { + exitInfo.HighestExitEpoch = exitableEpoch + exitInfo.Churn = 0 + } + activeValidatorCount, err := helpers.ActiveValidatorCount(ctx, s, time.CurrentEpoch(s)) + if err != nil { + return errors.Wrap(err, "could not get active validator count") + } + currentChurn := helpers.ValidatorExitChurnLimit(activeValidatorCount) + if exitInfo.Churn >= currentChurn { + exitInfo.HighestExitEpoch, err = exitInfo.HighestExitEpoch.SafeAdd(1) + if err != nil { + return err + } + exitInfo.Churn = 1 + } else { + exitInfo.Churn = exitInfo.Churn + 1 + } + return nil } // SlashValidator slashes the malicious validator's balance and awards @@ -152,9 +231,12 @@ func InitiateValidatorExit(ctx context.Context, s state.BeaconState, idx primiti func SlashValidator( ctx context.Context, s state.BeaconState, - slashedIdx primitives.ValidatorIndex) (state.BeaconState, error) { - maxExitEpoch, churn := MaxExitEpochAndChurn(s) - s, _, err := InitiateValidatorExit(ctx, s, slashedIdx, maxExitEpoch, churn) + slashedIdx primitives.ValidatorIndex, + exitInfo *ExitInfo, +) (state.BeaconState, error) { + var err error + + s, err = InitiateValidatorExitForTotalBal(ctx, s, slashedIdx, exitInfo, primitives.Gwei(exitInfo.TotalActiveBalance)) if err != nil && !errors.Is(err, ErrValidatorAlreadyExited) { return nil, errors.Wrapf(err, "could not initiate validator %d exit", slashedIdx) } diff --git a/beacon-chain/core/validators/validator_test.go b/beacon-chain/core/validators/validator_test.go index c1dfb366e8..e0ec103235 100644 --- a/beacon-chain/core/validators/validator_test.go +++ b/beacon-chain/core/validators/validator_test.go @@ -49,9 +49,11 @@ func TestInitiateValidatorExit_AlreadyExited(t *testing.T) { }} state, err := state_native.InitializeFromProtoPhase0(base) require.NoError(t, err) - newState, epoch, err := validators.InitiateValidatorExit(t.Context(), state, 0, 199, 1) + exitInfo := &validators.ExitInfo{HighestExitEpoch: 199, Churn: 1} + newState, err := validators.InitiateValidatorExit(t.Context(), state, 0, exitInfo) require.ErrorIs(t, err, validators.ErrValidatorAlreadyExited) - require.Equal(t, exitEpoch, epoch) + assert.Equal(t, primitives.Epoch(199), exitInfo.HighestExitEpoch) + assert.Equal(t, uint64(1), exitInfo.Churn) v, err := newState.ValidatorAtIndex(0) require.NoError(t, err) assert.Equal(t, exitEpoch, v.ExitEpoch, "Already exited") @@ -68,9 +70,11 @@ func TestInitiateValidatorExit_ProperExit(t *testing.T) { }} state, err := state_native.InitializeFromProtoPhase0(base) require.NoError(t, err) - newState, epoch, err := validators.InitiateValidatorExit(t.Context(), state, idx, exitedEpoch+2, 1) + exitInfo := &validators.ExitInfo{HighestExitEpoch: exitedEpoch + 2, Churn: 1} + newState, err := validators.InitiateValidatorExit(t.Context(), state, idx, exitInfo) require.NoError(t, err) - require.Equal(t, exitedEpoch+2, epoch) + assert.Equal(t, exitedEpoch+2, exitInfo.HighestExitEpoch) + assert.Equal(t, uint64(2), exitInfo.Churn) v, err := newState.ValidatorAtIndex(idx) require.NoError(t, err) assert.Equal(t, exitedEpoch+2, v.ExitEpoch, "Exit epoch was not the highest") @@ -88,9 +92,11 @@ func TestInitiateValidatorExit_ChurnOverflow(t *testing.T) { }} state, err := state_native.InitializeFromProtoPhase0(base) require.NoError(t, err) - newState, epoch, err := validators.InitiateValidatorExit(t.Context(), state, idx, exitedEpoch+2, 4) + exitInfo := &validators.ExitInfo{HighestExitEpoch: exitedEpoch + 2, Churn: 4} + newState, err := validators.InitiateValidatorExit(t.Context(), state, idx, exitInfo) require.NoError(t, err) - require.Equal(t, exitedEpoch+3, epoch) + assert.Equal(t, exitedEpoch+3, exitInfo.HighestExitEpoch) + assert.Equal(t, uint64(1), exitInfo.Churn) // Because of exit queue overflow, // validator who init exited has to wait one more epoch. @@ -110,7 +116,8 @@ func TestInitiateValidatorExit_WithdrawalOverflows(t *testing.T) { }} state, err := state_native.InitializeFromProtoPhase0(base) require.NoError(t, err) - _, _, err = validators.InitiateValidatorExit(t.Context(), state, 1, params.BeaconConfig().FarFutureEpoch-1, 1) + exitInfo := &validators.ExitInfo{HighestExitEpoch: params.BeaconConfig().FarFutureEpoch - 1, Churn: 1} + _, err = validators.InitiateValidatorExit(t.Context(), state, 1, exitInfo) require.ErrorContains(t, "addition overflows", err) } @@ -146,12 +153,11 @@ func TestInitiateValidatorExit_ProperExit_Electra(t *testing.T) { require.NoError(t, err) require.Equal(t, primitives.Gwei(0), ebtc) - newState, epoch, err := validators.InitiateValidatorExit(t.Context(), state, idx, 0, 0) // exitQueueEpoch and churn are not used in electra + newState, err := validators.InitiateValidatorExit(t.Context(), state, idx, &validators.ExitInfo{}) // exit info is not used in electra require.NoError(t, err) // Expect that the exit epoch is the next available epoch with max seed lookahead. want := helpers.ActivationExitEpoch(exitedEpoch + 1) - require.Equal(t, want, epoch) v, err := newState.ValidatorAtIndex(idx) require.NoError(t, err) assert.Equal(t, want, v.ExitEpoch, "Exit epoch was not the highest") @@ -190,7 +196,7 @@ func TestSlashValidator_OK(t *testing.T) { require.NoError(t, err, "Could not get proposer") proposerBal, err := state.BalanceAtIndex(proposer) require.NoError(t, err) - slashedState, err := validators.SlashValidator(t.Context(), state, slashedIdx) + slashedState, err := validators.SlashValidator(t.Context(), state, slashedIdx, validators.ExitInformation(state)) require.NoError(t, err, "Could not slash validator") require.Equal(t, true, slashedState.Version() == version.Phase0) @@ -244,7 +250,7 @@ func TestSlashValidator_Electra(t *testing.T) { require.NoError(t, err, "Could not get proposer") proposerBal, err := state.BalanceAtIndex(proposer) require.NoError(t, err) - slashedState, err := validators.SlashValidator(t.Context(), state, slashedIdx) + slashedState, err := validators.SlashValidator(t.Context(), state, slashedIdx, validators.ExitInformation(state)) require.NoError(t, err, "Could not slash validator") require.Equal(t, true, slashedState.Version() == version.Electra) @@ -505,8 +511,8 @@ func TestValidatorMaxExitEpochAndChurn(t *testing.T) { for _, tt := range tests { s, err := state_native.InitializeFromProtoPhase0(tt.state) require.NoError(t, err) - epoch, churn := validators.MaxExitEpochAndChurn(s) - require.Equal(t, tt.wantedEpoch, epoch) - require.Equal(t, tt.wantedChurn, churn) + exitInfo := validators.ExitInformation(s) + require.Equal(t, tt.wantedEpoch, exitInfo.HighestExitEpoch) + require.Equal(t, tt.wantedChurn, exitInfo.Churn) } } diff --git a/beacon-chain/rpc/eth/rewards/service.go b/beacon-chain/rpc/eth/rewards/service.go index e350d2a2a8..0383c9e892 100644 --- a/beacon-chain/rpc/eth/rewards/service.go +++ b/beacon-chain/rpc/eth/rewards/service.go @@ -68,7 +68,8 @@ func (rs *BlockRewardService) GetBlockRewardsData(ctx context.Context, blk inter Code: http.StatusInternalServerError, } } - st, err = coreblocks.ProcessAttesterSlashings(ctx, st, blk.Body().AttesterSlashings(), validators.SlashValidator) + exitInfo := validators.ExitInformation(st) + st, err = coreblocks.ProcessAttesterSlashings(ctx, st, blk.Body().AttesterSlashings(), exitInfo) if err != nil { return nil, &httputil.DefaultJsonError{ Message: "Could not get attester slashing rewards: " + err.Error(), @@ -82,7 +83,7 @@ func (rs *BlockRewardService) GetBlockRewardsData(ctx context.Context, blk inter Code: http.StatusInternalServerError, } } - st, err = coreblocks.ProcessProposerSlashings(ctx, st, blk.Body().ProposerSlashings(), validators.SlashValidator) + st, err = coreblocks.ProcessProposerSlashings(ctx, st, blk.Body().ProposerSlashings(), exitInfo) if err != nil { return nil, &httputil.DefaultJsonError{ Message: "Could not get proposer slashing rewards: " + err.Error(), diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_slashings.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_slashings.go index 2badca5274..21fa2721d2 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_slashings.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_slashings.go @@ -4,16 +4,23 @@ import ( "context" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/blocks" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers" v "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" ) func (vs *Server) getSlashings(ctx context.Context, head state.BeaconState) ([]*ethpb.ProposerSlashing, []ethpb.AttSlashing) { + var err error + + exitInfo := v.ExitInformation(head) + if err := helpers.UpdateTotalActiveBalanceCache(head, exitInfo.TotalActiveBalance); err != nil { + log.WithError(err).Warn("Could not update total active balance cache") + } proposerSlashings := vs.SlashingsPool.PendingProposerSlashings(ctx, head, false /*noLimit*/) validProposerSlashings := make([]*ethpb.ProposerSlashing, 0, len(proposerSlashings)) for _, slashing := range proposerSlashings { - _, err := blocks.ProcessProposerSlashing(ctx, head, slashing, v.SlashValidator) + _, err = blocks.ProcessProposerSlashing(ctx, head, slashing, exitInfo) if err != nil { log.WithError(err).Warn("Could not validate proposer slashing for block inclusion") continue @@ -23,7 +30,7 @@ func (vs *Server) getSlashings(ctx context.Context, head state.BeaconState) ([]* attSlashings := vs.SlashingsPool.PendingAttesterSlashings(ctx, head, false /*noLimit*/) validAttSlashings := make([]ethpb.AttSlashing, 0, len(attSlashings)) for _, slashing := range attSlashings { - _, err := blocks.ProcessAttesterSlashing(ctx, head, slashing, v.SlashValidator) + _, err = blocks.ProcessAttesterSlashing(ctx, head, slashing, exitInfo) if err != nil { log.WithError(err).Warn("Could not validate attester slashing for block inclusion") continue diff --git a/beacon-chain/state/interfaces.go b/beacon-chain/state/interfaces.go index ae1e78e863..428e548ca7 100644 --- a/beacon-chain/state/interfaces.go +++ b/beacon-chain/state/interfaces.go @@ -265,6 +265,7 @@ type WriteOnlyEth1Data interface { AppendEth1DataVotes(val *ethpb.Eth1Data) error SetEth1DepositIndex(val uint64) error ExitEpochAndUpdateChurn(exitBalance primitives.Gwei) (primitives.Epoch, error) + ExitEpochAndUpdateChurnForTotalBal(totalActiveBalance primitives.Gwei, exitBalance primitives.Gwei) (primitives.Epoch, error) } // WriteOnlyValidators defines a struct which only has write access to validators methods. diff --git a/beacon-chain/state/state-native/setters_churn.go b/beacon-chain/state/state-native/setters_churn.go index cee1f34531..c4ed930ba9 100644 --- a/beacon-chain/state/state-native/setters_churn.go +++ b/beacon-chain/state/state-native/setters_churn.go @@ -44,11 +44,27 @@ func (b *BeaconState) ExitEpochAndUpdateChurn(exitBalance primitives.Gwei) (prim return 0, err } + return b.exitEpochAndUpdateChurn(primitives.Gwei(activeBal), exitBalance) +} + +// ExitEpochAndUpdateChurnForTotalBal has the same functionality as ExitEpochAndUpdateChurn, +// the only difference being how total active balance is obtained. In ExitEpochAndUpdateChurn +// it is calculated inside the function and in ExitEpochAndUpdateChurnForTotalBal it's a +// function argument. +func (b *BeaconState) ExitEpochAndUpdateChurnForTotalBal(totalActiveBalance primitives.Gwei, exitBalance primitives.Gwei) (primitives.Epoch, error) { + if b.version < version.Electra { + return 0, errNotSupported("ExitEpochAndUpdateChurnForTotalBal", b.version) + } + + return b.exitEpochAndUpdateChurn(totalActiveBalance, exitBalance) +} + +func (b *BeaconState) exitEpochAndUpdateChurn(totalActiveBalance primitives.Gwei, exitBalance primitives.Gwei) (primitives.Epoch, error) { b.lock.Lock() defer b.lock.Unlock() earliestExitEpoch := max(b.earliestExitEpoch, helpers.ActivationExitEpoch(slots.ToEpoch(b.slot))) - perEpochChurn := helpers.ActivationExitChurnLimit(primitives.Gwei(activeBal)) // Guaranteed to be non-zero. + perEpochChurn := helpers.ActivationExitChurnLimit(totalActiveBalance) // Guaranteed to be non-zero. // New epoch for exits var exitBalanceToConsume primitives.Gwei diff --git a/changelog/radek_fix-max-epoch-calculation-once.md b/changelog/radek_fix-max-epoch-calculation-once.md new file mode 100644 index 0000000000..e3c1575651 --- /dev/null +++ b/changelog/radek_fix-max-epoch-calculation-once.md @@ -0,0 +1,3 @@ +### Changed + +- Pre-calculate exit epoch, churn and active balance before processing slashings to reduce CPU load. \ No newline at end of file diff --git a/runtime/logging/logrus-prefixed-formatter/README.md b/runtime/logging/logrus-prefixed-formatter/README.md index e51b5d2e0f..5ccc068fa5 100644 --- a/runtime/logging/logrus-prefixed-formatter/README.md +++ b/runtime/logging/logrus-prefixed-formatter/README.md @@ -35,8 +35,8 @@ Here is how it should be used: package main import ( - "github.com/sirupsen/logrus" prefixed "github.com/prysmaticlabs/prysm/runtime/logging/logrus-prefixed-formatter" + "github.com/sirupsen/logrus" ) var log = logrus.New() diff --git a/testing/spectest/shared/common/operations/attester_slashing.go b/testing/spectest/shared/common/operations/attester_slashing.go index 525b654a27..6919950754 100644 --- a/testing/spectest/shared/common/operations/attester_slashing.go +++ b/testing/spectest/shared/common/operations/attester_slashing.go @@ -12,6 +12,6 @@ import ( func RunAttesterSlashingTest(t *testing.T, config string, fork string, block blockWithSSZObject, sszToState SSZToState) { runSlashingTest(t, config, fork, "attester_slashing", block, sszToState, func(ctx context.Context, s state.BeaconState, b interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) { - return blocks.ProcessAttesterSlashings(ctx, s, b.Block().Body().AttesterSlashings(), v.SlashValidator) + return blocks.ProcessAttesterSlashings(ctx, s, b.Block().Body().AttesterSlashings(), v.ExitInformation(s)) }) } diff --git a/testing/spectest/shared/common/operations/proposer_slashing.go b/testing/spectest/shared/common/operations/proposer_slashing.go index d2d63d4ff1..40f19b62ae 100644 --- a/testing/spectest/shared/common/operations/proposer_slashing.go +++ b/testing/spectest/shared/common/operations/proposer_slashing.go @@ -12,6 +12,6 @@ import ( func RunProposerSlashingTest(t *testing.T, config string, fork string, block blockWithSSZObject, sszToState SSZToState) { runSlashingTest(t, config, fork, "proposer_slashing", block, sszToState, func(ctx context.Context, s state.BeaconState, b interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) { - return blocks.ProcessProposerSlashings(ctx, s, b.Block().Body().ProposerSlashings(), v.SlashValidator) + return blocks.ProcessProposerSlashings(ctx, s, b.Block().Body().ProposerSlashings(), v.ExitInformation(s)) }) } diff --git a/testing/spectest/shared/common/operations/voluntary_exit.go b/testing/spectest/shared/common/operations/voluntary_exit.go index 62577569ce..5a8646b51f 100644 --- a/testing/spectest/shared/common/operations/voluntary_exit.go +++ b/testing/spectest/shared/common/operations/voluntary_exit.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/OffchainLabs/prysm/v6/beacon-chain/core/blocks" + "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators" "github.com/OffchainLabs/prysm/v6/beacon-chain/state" "github.com/OffchainLabs/prysm/v6/consensus-types/interfaces" "github.com/OffchainLabs/prysm/v6/testing/require" @@ -30,7 +31,7 @@ func RunVoluntaryExitTest(t *testing.T, config string, fork string, block blockW blk, err := block(exitSSZ) require.NoError(t, err) RunBlockOperationTest(t, folderPath, blk, sszToState, func(ctx context.Context, s state.BeaconState, b interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) { - return blocks.ProcessVoluntaryExits(ctx, s, b.Block().Body().VoluntaryExits()) + return blocks.ProcessVoluntaryExits(ctx, s, b.Block().Body().VoluntaryExits(), validators.ExitInformation(s)) }) }) }