From abb9151eea70111da1f87784b32fa98f82865675 Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Fri, 10 May 2024 10:13:59 -0500 Subject: [PATCH 1/7] eip-7251: process_pending_consolidations and process_consolidations --- beacon-chain/core/electra/BUILD.bazel | 4 + beacon-chain/core/electra/consolidations.go | 247 ++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 beacon-chain/core/electra/consolidations.go diff --git a/beacon-chain/core/electra/BUILD.bazel b/beacon-chain/core/electra/BUILD.bazel index 697b9df7291b..7c3c9e04dc67 100644 --- a/beacon-chain/core/electra/BUILD.bazel +++ b/beacon-chain/core/electra/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "churn.go", + "consolidations.go", "transition.go", "upgrade.go", "validator.go", @@ -14,17 +15,20 @@ go_library( "//beacon-chain/core/altair:go_default_library", "//beacon-chain/core/epoch:go_default_library", "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/state-native:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", "//math:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//time/slots:go_default_library", "@com_github_pkg_errors//:go_default_library", + "@io_opencensus_go//trace:go_default_library", ], ) diff --git a/beacon-chain/core/electra/consolidations.go b/beacon-chain/core/electra/consolidations.go new file mode 100644 index 000000000000..98fe0e968175 --- /dev/null +++ b/beacon-chain/core/electra/consolidations.go @@ -0,0 +1,247 @@ +package electra + +import ( + "context" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" + "github.com/prysmaticlabs/prysm/v5/math" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/time/slots" + "go.opencensus.io/trace" +) + +var ErrNilConsolidations = errors.New("nil consolidations") + +// ProcessPendingConsolidations -- +// +// Spec definition: +// +// def process_pending_consolidations(state: BeaconState) -> None: +// next_pending_consolidation = 0 +// for pending_consolidation in state.pending_consolidations: +// source_validator = state.validators[pending_consolidation.source_index] +// if source_validator.slashed: +// next_pending_consolidation += 1 +// continue +// if source_validator.withdrawable_epoch > get_current_epoch(state): +// break +// +// # Churn any target excess active balance of target and raise its max +// switch_to_compounding_validator(state, pending_consolidation.target_index) +// # Move active balance to target. Excess balance is withdrawable. +// active_balance = get_active_balance(state, pending_consolidation.source_index) +// decrease_balance(state, pending_consolidation.source_index, active_balance) +// increase_balance(state, pending_consolidation.target_index, active_balance) +// next_pending_consolidation += 1 +// +// state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:] +func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState, activeBalance uint64) (state.BeaconState, error) { + ctx, span := trace.StartSpan(ctx, "electra.ProcessPendingConsolidations") + defer span.End() + + if st == nil || st.IsNil() { + return nil, errors.New("nil state") + } + + var nextPendingConsolidation uint64 + pendingConsolidations, err := st.PendingConsolidations() + if err != nil { + return nil, err + } + for _, pc := range pendingConsolidations { + sourceValidator, err := st.ValidatorAtIndex(pc.SourceIndex) + if err != nil { + return nil, err + } + if sourceValidator.Slashed { + nextPendingConsolidation++ + continue + } + if sourceValidator.WithdrawableEpoch > slots.ToEpoch(st.Slot()) { + break + } + + if err := SwitchToCompoundingValidator(ctx, st, pc.TargetIndex); err != nil { + return nil, err + } + + if err := helpers.DecreaseBalance(st, pc.SourceIndex, activeBalance); err != nil { + return nil, err + } + if err := helpers.IncreaseBalance(st, pc.TargetIndex, activeBalance); err != nil { + return nil, err + } + nextPendingConsolidation++ + } + + // TODO: Check OOB + if err := st.SetPendingConsolidations(pendingConsolidations[nextPendingConsolidation:]); err != nil { + return nil, err + } + + return st, nil +} + +// ProcessConsolidations -- +// +// Spec definition: +// +// def process_consolidation(state: BeaconState, signed_consolidation: SignedConsolidation) -> None: +// # If the pending consolidations queue is full, no consolidations are allowed in the block +// assert len(state.pending_consolidations) < PENDING_CONSOLIDATIONS_LIMIT +// # If there is too little available consolidation churn limit, no consolidations are allowed in the block +// assert get_consolidation_churn_limit(state) > MIN_ACTIVATION_BALANCE +// consolidation = signed_consolidation.message +// # Verify that source != target, so a consolidation cannot be used as an exit. +// assert consolidation.source_index != consolidation.target_index +// +// source_validator = state.validators[consolidation.source_index] +// target_validator = state.validators[consolidation.target_index] +// # Verify the source and the target are active +// current_epoch = get_current_epoch(state) +// assert is_active_validator(source_validator, current_epoch) +// assert is_active_validator(target_validator, current_epoch) +// # Verify exits for source and target have not been initiated +// assert source_validator.exit_epoch == FAR_FUTURE_EPOCH +// assert target_validator.exit_epoch == FAR_FUTURE_EPOCH +// # Consolidations must specify an epoch when they become valid; they are not valid before then +// assert current_epoch >= consolidation.epoch +// +// # Verify the source and the target have Execution layer withdrawal credentials +// assert has_execution_withdrawal_credential(source_validator) +// assert has_execution_withdrawal_credential(target_validator) +// # Verify the same withdrawal address +// assert source_validator.withdrawal_credentials[12:] == target_validator.withdrawal_credentials[12:] +// +// # Verify consolidation is signed by the source and the target +// domain = compute_domain(DOMAIN_CONSOLIDATION, genesis_validators_root=state.genesis_validators_root) +// signing_root = compute_signing_root(consolidation, domain) +// pubkeys = [source_validator.pubkey, target_validator.pubkey] +// assert bls.FastAggregateVerify(pubkeys, signing_root, signed_consolidation.signature) +// +// # Initiate source validator exit and append pending consolidation +// source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn( +// state, source_validator.effective_balance) +// source_validator.withdrawable_epoch = Epoch( +// source_validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY +// ) +// state.pending_consolidations.append(PendingConsolidation( +// source_index=consolidation.source_index, +// target_index=consolidation.target_index +// )) +func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethpb.SignedConsolidation) (state.BeaconState, error) { + _, span := trace.StartSpan(ctx, "electra.ProcessConsolidations") + defer span.End() + + if st == nil || st.IsNil() { + return nil, errors.New("nil state") + } + if cs == nil { + return nil, ErrNilConsolidations + } + + domain, err := signing.ComputeDomain(params.BeaconConfig().DomainConsolidation, st.Fork().CurrentVersion, st.GenesisValidatorsRoot()) + if err != nil { + return nil, err + } + + for _, c := range cs { + if c == nil || c.Message == nil { + return nil, errors.New("nil consolidation") + } + + // TODO(preston): can these be moved outside of the loop? + if n, err := st.NumPendingConsolidations(); err != nil { + return nil, err + } else if n >= params.BeaconConfig().PendingConsolidationsLimit { + return nil, errors.New("pending consolidations queue is full") + } + + totalBalance, err := helpers.TotalActiveBalance(st) + if err != nil { + return nil, err + } + if helpers.ConsolidationChurnLimit(math.Gwei(totalBalance)) <= math.Gwei(params.BeaconConfig().MinActivationBalance) { + return nil, errors.New("too little available consolidation churn limit") + } + currentEpoch := slots.ToEpoch(st.Slot()) + // END TODO + + if c.Message.SourceIndex == c.Message.TargetIndex { + return nil, errors.New("source and target index are the same") + } + source, err := st.ValidatorAtIndex(c.Message.SourceIndex) + if err != nil { + return nil, err + } + target, err := st.ValidatorAtIndex(c.Message.TargetIndex) + if err != nil { + return nil, err + } + if !helpers.IsActiveValidator(source, currentEpoch) { + return nil, errors.New("source is not active") + } + if !helpers.IsActiveValidator(target, currentEpoch) { + return nil, errors.New("target is not active") + } + if source.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return nil, errors.New("source exit epoch has been initiated") + } + if target.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return nil, errors.New("target exit epoch has been initiated") + } + if currentEpoch < c.Message.Epoch { + return nil, errors.New("consolidation is not valid yet") + } + + if !helpers.HasExecutionWithdrawalCredentials(source) { + return nil, errors.New("source does not have execution withdrawal credentials") + } + if !helpers.HasExecutionWithdrawalCredentials(target) { + return nil, errors.New("target does not have execution withdrawal credentials") + } + if !helpers.IsSameWithdrawalCredentials(source, target) { + return nil, errors.New("source and target have different withdrawal credentials") + } + + sr, err := signing.ComputeSigningRoot(c.Message, domain) + if err != nil { + return nil, err + } + sourcePk, err := bls.PublicKeyFromBytes(source.PublicKey) + if err != nil { + return nil, errors.Wrap(err, "could not convert bytes to public key") + } + targetPk, err := bls.PublicKeyFromBytes(target.PublicKey) + if err != nil { + return nil, errors.Wrap(err, "could not convert bytes to public key") + } + sig, err := bls.SignatureFromBytes(c.Signature) + if err != nil { + return nil, errors.Wrap(err, "could not convert bytes to signature") + } + if !sig.FastAggregateVerify([]bls.PublicKey{sourcePk, targetPk}, sr) { + return nil, errors.New("consolidation signature verification failed") + } + + sEE, err := ComputeConsolidationEpochAndUpdateChurn(ctx, st, math.Gwei(source.EffectiveBalance)) + if err != nil { + return nil, err + } + source.ExitEpoch = sEE + source.WithdrawableEpoch = sEE + params.BeaconConfig().MinValidatorWithdrawabilityDelay + if err := st.UpdateValidatorAtIndex(c.Message.SourceIndex, source); err != nil { + return nil, err + } + if err := st.AppendPendingConsolidation(c.Message.ToPendingConsolidation()); err != nil { + return nil, err + } + } + + return st, nil +} From 2babebdb204bbd29bee3ef150dc78bb29bf22908 Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Wed, 8 May 2024 11:13:20 -0500 Subject: [PATCH 2/7] Consolidate unit tests + spectests Fix failing spectest //testing/spectest/minimal/electra/operations:go_default_test --- beacon-chain/core/electra/BUILD.bazel | 6 + beacon-chain/core/electra/consolidations.go | 47 +- .../core/electra/consolidations_test.go | 444 ++++++++++++++++++ .../electra/epoch_processing/BUILD.bazel | 1 + .../pending_consolidations_test.go | 11 + .../mainnet/electra/operations/BUILD.bazel | 1 + .../electra/operations/consolidation_test.go | 11 + .../electra/epoch_processing/BUILD.bazel | 1 + .../pending_consolidations_test.go | 11 + .../minimal/electra/operations/BUILD.bazel | 1 + .../electra/operations/consolidation_test.go | 11 + .../electra/epoch_processing/BUILD.bazel | 1 + .../pending_consolidations.go | 28 ++ .../shared/electra/operations/BUILD.bazel | 1 + .../electra/operations/consolidations.go | 48 ++ 15 files changed, 605 insertions(+), 18 deletions(-) create mode 100644 beacon-chain/core/electra/consolidations_test.go create mode 100644 testing/spectest/mainnet/electra/epoch_processing/pending_consolidations_test.go create mode 100644 testing/spectest/mainnet/electra/operations/consolidation_test.go create mode 100644 testing/spectest/minimal/electra/epoch_processing/pending_consolidations_test.go create mode 100644 testing/spectest/minimal/electra/operations/consolidation_test.go create mode 100644 testing/spectest/shared/electra/epoch_processing/pending_consolidations.go create mode 100644 testing/spectest/shared/electra/operations/consolidations.go diff --git a/beacon-chain/core/electra/BUILD.bazel b/beacon-chain/core/electra/BUILD.bazel index 7c3c9e04dc67..acb1f7dceded 100644 --- a/beacon-chain/core/electra/BUILD.bazel +++ b/beacon-chain/core/electra/BUILD.bazel @@ -36,21 +36,27 @@ go_test( name = "go_default_test", srcs = [ "churn_test.go", + "consolidations_test.go", "upgrade_test.go", "validator_test.go", ], deps = [ ":go_default_library", "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/state-native:go_default_library", + "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", + "//crypto/bls/blst:go_default_library", + "//crypto/bls/common:go_default_library", "//encoding/bytesutil:go_default_library", "//math:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//runtime/interop:go_default_library", "//testing/require:go_default_library", "//testing/util:go_default_library", "//time/slots:go_default_library", diff --git a/beacon-chain/core/electra/consolidations.go b/beacon-chain/core/electra/consolidations.go index 98fe0e968175..36ff13b3ffff 100644 --- a/beacon-chain/core/electra/consolidations.go +++ b/beacon-chain/core/electra/consolidations.go @@ -15,9 +15,8 @@ import ( "go.opencensus.io/trace" ) -var ErrNilConsolidations = errors.New("nil consolidations") - -// ProcessPendingConsolidations -- +// ProcessPendingConsolidations implements the spec definition below. This method makes mutating +// calls to the beacon state. // // Spec definition: // @@ -40,7 +39,7 @@ var ErrNilConsolidations = errors.New("nil consolidations") // next_pending_consolidation += 1 // // state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:] -func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState, activeBalance uint64) (state.BeaconState, error) { +func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) (state.BeaconState, error) { ctx, span := trace.StartSpan(ctx, "electra.ProcessPendingConsolidations") defer span.End() @@ -48,6 +47,8 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState, act return nil, errors.New("nil state") } + currentEpoch := slots.ToEpoch(st.Slot()) + var nextPendingConsolidation uint64 pendingConsolidations, err := st.PendingConsolidations() if err != nil { @@ -62,7 +63,7 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState, act nextPendingConsolidation++ continue } - if sourceValidator.WithdrawableEpoch > slots.ToEpoch(st.Slot()) { + if sourceValidator.WithdrawableEpoch > currentEpoch { break } @@ -70,6 +71,10 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState, act return nil, err } + activeBalance, err := st.ActiveBalanceAtIndex(pc.SourceIndex) + if err != nil { + return nil, err + } if err := helpers.DecreaseBalance(st, pc.SourceIndex, activeBalance); err != nil { return nil, err } @@ -79,15 +84,17 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState, act nextPendingConsolidation++ } - // TODO: Check OOB - if err := st.SetPendingConsolidations(pendingConsolidations[nextPendingConsolidation:]); err != nil { - return nil, err + if nextPendingConsolidation > 0 { + if err := st.SetPendingConsolidations(pendingConsolidations[nextPendingConsolidation:]); err != nil { + return nil, err + } } return st, nil } -// ProcessConsolidations -- +// ProcessConsolidations implements the spec definition below. This method makes mutating calls to +// the beacon state. // // Spec definition: // @@ -141,11 +148,21 @@ func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethp if st == nil || st.IsNil() { return nil, errors.New("nil state") } - if cs == nil { - return nil, ErrNilConsolidations + + if len(cs) == 0 { + return st, nil // Nothing to process. + } + + domain, err := signing.ComputeDomain( + params.BeaconConfig().DomainConsolidation, + nil, // Use genesis fork version + st.GenesisValidatorsRoot(), + ) + if err != nil { + return nil, err } - domain, err := signing.ComputeDomain(params.BeaconConfig().DomainConsolidation, st.Fork().CurrentVersion, st.GenesisValidatorsRoot()) + totalBalance, err := helpers.TotalActiveBalance(st) if err != nil { return nil, err } @@ -155,22 +172,16 @@ func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethp return nil, errors.New("nil consolidation") } - // TODO(preston): can these be moved outside of the loop? if n, err := st.NumPendingConsolidations(); err != nil { return nil, err } else if n >= params.BeaconConfig().PendingConsolidationsLimit { return nil, errors.New("pending consolidations queue is full") } - totalBalance, err := helpers.TotalActiveBalance(st) - if err != nil { - return nil, err - } if helpers.ConsolidationChurnLimit(math.Gwei(totalBalance)) <= math.Gwei(params.BeaconConfig().MinActivationBalance) { return nil, errors.New("too little available consolidation churn limit") } currentEpoch := slots.ToEpoch(st.Slot()) - // END TODO if c.Message.SourceIndex == c.Message.TargetIndex { return nil, errors.New("source and target index are the same") diff --git a/beacon-chain/core/electra/consolidations_test.go b/beacon-chain/core/electra/consolidations_test.go new file mode 100644 index 000000000000..e52ef30f5229 --- /dev/null +++ b/beacon-chain/core/electra/consolidations_test.go @@ -0,0 +1,444 @@ +package electra_test + +import ( + "context" + "testing" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/crypto/bls/blst" + "github.com/prysmaticlabs/prysm/v5/crypto/bls/common" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/interop" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" +) + +func TestProcessPendingConsolidations(t *testing.T) { + tests := []struct { + name string + state state.BeaconState + check func(*testing.T, state.BeaconState) + wantErr bool + }{ + { + name: "nil state", + state: nil, + wantErr: true, + }, + { + name: "no pending consolidations", + state: func() state.BeaconState { + pb := ð.BeaconStateElectra{} + + st, err := state_native.InitializeFromProtoUnsafeElectra(pb) + require.NoError(t, err) + return st + }(), + wantErr: false, + }, + { + name: "processes pending consolidation successfully", + state: func() state.BeaconState { + pb := ð.BeaconStateElectra{ + Validators: []*eth.Validator{ + { + WithdrawalCredentials: []byte{0x01, 0xFF}, + }, + { + WithdrawalCredentials: []byte{0x01, 0xAB}, + }, + }, + Balances: []uint64{ + params.BeaconConfig().MinActivationBalance, + params.BeaconConfig().MinActivationBalance, + }, + PendingConsolidations: []*eth.PendingConsolidation{ + { + SourceIndex: 0, + TargetIndex: 1, + }, + }, + } + + st, err := state_native.InitializeFromProtoUnsafeElectra(pb) + require.NoError(t, err) + return st + }(), + check: func(t *testing.T, st state.BeaconState) { + // Balances are transferred from v0 to v1. + bal0, err := st.BalanceAtIndex(0) + require.NoError(t, err) + require.Equal(t, uint64(0), bal0) + bal1, err := st.BalanceAtIndex(1) + require.NoError(t, err) + require.Equal(t, 2*params.BeaconConfig().MinActivationBalance, bal1) + + // The pending consolidation is removed from the list. + num, err := st.NumPendingConsolidations() + require.NoError(t, err) + require.Equal(t, uint64(0), num) + + // v1 is switched to compounding validator. + v1, err := st.ValidatorAtIndex(1) + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().CompoundingWithdrawalPrefixByte, v1.WithdrawalCredentials[0]) + }, + wantErr: false, + }, + { + name: "stop processing when a source val withdrawable epoch is in the future", + state: func() state.BeaconState { + pb := ð.BeaconStateElectra{ + Validators: []*eth.Validator{ + { + WithdrawalCredentials: []byte{0x01, 0xFF}, + WithdrawableEpoch: 100, + }, + { + WithdrawalCredentials: []byte{0x01, 0xAB}, + }, + }, + Balances: []uint64{ + params.BeaconConfig().MinActivationBalance, + params.BeaconConfig().MinActivationBalance, + }, + PendingConsolidations: []*eth.PendingConsolidation{ + { + SourceIndex: 0, + TargetIndex: 1, + }, + }, + } + + st, err := state_native.InitializeFromProtoUnsafeElectra(pb) + require.NoError(t, err) + return st + }(), + check: func(t *testing.T, st state.BeaconState) { + // No balances are transferred from v0 to v1. + bal0, err := st.BalanceAtIndex(0) + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().MinActivationBalance, bal0) + bal1, err := st.BalanceAtIndex(1) + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().MinActivationBalance, bal1) + + // The pending consolidation is still in the list. + num, err := st.NumPendingConsolidations() + require.NoError(t, err) + require.Equal(t, uint64(1), num) + }, + wantErr: false, + }, + { + name: "slashed validator is not consolidated", + state: func() state.BeaconState { + pb := ð.BeaconStateElectra{ + Validators: []*eth.Validator{ + { + WithdrawalCredentials: []byte{0x01, 0xFF}, + }, + { + WithdrawalCredentials: []byte{0x01, 0xAB}, + }, + { + Slashed: true, + }, + { + WithdrawalCredentials: []byte{0x01, 0xCC}, + }, + }, + Balances: []uint64{ + params.BeaconConfig().MinActivationBalance, + params.BeaconConfig().MinActivationBalance, + params.BeaconConfig().MinActivationBalance, + params.BeaconConfig().MinActivationBalance, + }, + PendingConsolidations: []*eth.PendingConsolidation{ + { + SourceIndex: 2, + TargetIndex: 3, + }, + { + SourceIndex: 0, + TargetIndex: 1, + }, + }, + } + + st, err := state_native.InitializeFromProtoUnsafeElectra(pb) + require.NoError(t, err) + return st + }(), + check: func(t *testing.T, st state.BeaconState) { + // No balances are transferred from v2 to v3. + bal0, err := st.BalanceAtIndex(2) + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().MinActivationBalance, bal0) + bal1, err := st.BalanceAtIndex(3) + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().MinActivationBalance, bal1) + + // No pending consolidation remaining. + num, err := st.NumPendingConsolidations() + require.NoError(t, err) + require.Equal(t, uint64(0), num) + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := electra.ProcessPendingConsolidations(context.TODO(), tt.state) + require.Equal(t, tt.wantErr, err != nil) + if tt.check != nil { + tt.check(t, res) + } + }) + } +} + +func stateWithActiveBalanceETH(t *testing.T, balETH uint64) state.BeaconState { + gwei := balETH * 1_000_000_000 + balPerVal := params.BeaconConfig().MinActivationBalance + numVals := gwei / balPerVal + + vals := make([]*eth.Validator, numVals) + bals := make([]uint64, numVals) + for i := uint64(0); i < numVals; i++ { + wc := make([]byte, 32) + wc[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte + wc[31] = byte(i) + vals[i] = ð.Validator{ + ActivationEpoch: 0, + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: balPerVal, + WithdrawalCredentials: wc, + } + bals[i] = balPerVal + } + st, err := state_native.InitializeFromProtoUnsafeElectra(ð.BeaconStateElectra{ + Slot: 10 * params.BeaconConfig().SlotsPerEpoch, + Validators: vals, + Balances: bals, + Fork: ð.Fork{ + CurrentVersion: params.BeaconConfig().ElectraForkVersion, + }, + }) + require.NoError(t, err) + + return st +} + +func TestProcessConsolidations(t *testing.T) { + secretKeys, publicKeys, err := interop.DeterministicallyGenerateKeys(0, 2) + require.NoError(t, err) + + genesisValidatorRoot := bytesutil.PadTo([]byte("genesisValidatorRoot"), fieldparams.RootLength) + + _ = secretKeys + + tests := []struct { + name string + state state.BeaconState + scs []*eth.SignedConsolidation + check func(*testing.T, state.BeaconState) + wantErr string + }{ + { + name: "nil state", + scs: make([]*eth.SignedConsolidation, 10), + wantErr: "nil state", + }, + { + name: "nil consolidation in slice", + state: func() state.BeaconState { + st, _ := util.DeterministicGenesisStateElectra(t, 1) + return st + }(), + scs: []*eth.SignedConsolidation{nil, nil}, + wantErr: "nil consolidation", + }, + { + name: "state is 100% full of pending consolidations", + state: func() state.BeaconState { + st, _ := util.DeterministicGenesisStateElectra(t, 1) + pc := make([]*eth.PendingConsolidation, params.BeaconConfig().PendingConsolidationsLimit) + require.NoError(t, st.SetPendingConsolidations(pc)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{}}}, + wantErr: "pending consolidations queue is full", + }, + { + name: "state has too little consolidation churn limit available to process a consolidation", + state: func() state.BeaconState { + st, _ := util.DeterministicGenesisStateElectra(t, 1) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{}}}, + wantErr: "too little available consolidation churn limit", + }, + { + name: "consolidation with source and target as the same index is rejected", + state: stateWithActiveBalanceETH(t, 19_000_000), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 100}}}, + wantErr: "source and target index are the same", + }, + { + name: "consolidation with inactive source is rejected", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + val, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + val.ActivationEpoch = params.BeaconConfig().FarFutureEpoch + require.NoError(t, st.UpdateValidatorAtIndex(25, val)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 25, TargetIndex: 100}}}, + wantErr: "source is not active", + }, + { + name: "consolidation with inactive target is rejected", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + val, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + val.ActivationEpoch = params.BeaconConfig().FarFutureEpoch + require.NoError(t, st.UpdateValidatorAtIndex(25, val)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 25}}}, + wantErr: "target is not active", + }, + { + name: "consolidation with exiting source", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + val, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + val.ExitEpoch = 256 + require.NoError(t, st.UpdateValidatorAtIndex(25, val)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 25, TargetIndex: 100}}}, + wantErr: "source exit epoch has been initiated", + }, + { + name: "consolidation with exiting target", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + val, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + val.ExitEpoch = 256 + require.NoError(t, st.UpdateValidatorAtIndex(25, val)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 25}}}, + wantErr: "target exit epoch has been initiated", + }, + { + name: "consolidation with future epoch is rejected", + state: stateWithActiveBalanceETH(t, 19_000_000), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 25, Epoch: 55}}}, + wantErr: "consolidation is not valid yet", + }, + { + name: "source validator without withdrawal credentials is rejected", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + val, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + val.WithdrawalCredentials = []byte{} + require.NoError(t, st.UpdateValidatorAtIndex(25, val)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 25, TargetIndex: 100}}}, + wantErr: "source does not have execution withdrawal credentials", + }, + { + name: "target validator without withdrawal credentials is rejected", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + val, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + val.WithdrawalCredentials = []byte{} + require.NoError(t, st.UpdateValidatorAtIndex(25, val)) + return st + }(), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 25}}}, + wantErr: "target does not have execution withdrawal credentials", + }, + { + name: "source and target with different withdrawal credentials is rejected", + state: stateWithActiveBalanceETH(t, 19_000_000), + scs: []*eth.SignedConsolidation{{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 25}}}, + wantErr: "source and target have different withdrawal credentials", + }, + { + name: "consolidation with valid signatures is OK", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 19_000_000) + require.NoError(t, st.SetGenesisValidatorsRoot(genesisValidatorRoot)) + source, err := st.ValidatorAtIndex(100) + require.NoError(t, err) + target, err := st.ValidatorAtIndex(25) + require.NoError(t, err) + source.PublicKey = publicKeys[0].Marshal() + source.WithdrawalCredentials = target.WithdrawalCredentials + require.NoError(t, st.UpdateValidatorAtIndex(100, source)) + target.PublicKey = publicKeys[1].Marshal() + require.NoError(t, st.UpdateValidatorAtIndex(25, target)) + return st + }(), + scs: func() []*eth.SignedConsolidation { + sc := ð.SignedConsolidation{Message: ð.Consolidation{SourceIndex: 100, TargetIndex: 25, Epoch: 8}} + + domain, err := signing.ComputeDomain( + params.BeaconConfig().DomainConsolidation, + nil, + genesisValidatorRoot, + ) + require.NoError(t, err) + sr, err := signing.ComputeSigningRoot(sc.Message, domain) + require.NoError(t, err) + + sig0 := secretKeys[0].Sign(sr[:]) + sig1 := secretKeys[1].Sign(sr[:]) + + sc.Signature = blst.AggregateSignatures([]common.Signature{sig0, sig1}).Marshal() + + return []*eth.SignedConsolidation{sc} + }(), + check: func(t *testing.T, st state.BeaconState) { + source, err := st.ValidatorAtIndex(100) + require.NoError(t, err) + // The consolidated validator is exiting. + require.Equal(t, primitives.Epoch(15), source.ExitEpoch) // 15 = state.Epoch(10) + MIN_SEED_LOOKAHEAD(4) + 1 + require.Equal(t, primitives.Epoch(15+params.BeaconConfig().MinValidatorWithdrawabilityDelay), source.WithdrawableEpoch) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := electra.ProcessConsolidations(context.TODO(), tt.state, tt.scs) + if len(tt.wantErr) > 0 { + require.ErrorContains(t, tt.wantErr, err) + } else { + require.NoError(t, err) + } + if tt.check != nil { + tt.check(t, res) + } + }) + } +} diff --git a/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel b/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel index 9bf424ef6062..c256d5da8516 100644 --- a/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel +++ b/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel @@ -8,6 +8,7 @@ go_test( "inactivity_updates_test.go", "justification_and_finalization_test.go", "participation_flag_updates_test.go", + "pending_consolidations_test.go", "randao_mixes_reset_test.go", "rewards_and_penalties_test.go", "slashings_reset_test.go", diff --git a/testing/spectest/mainnet/electra/epoch_processing/pending_consolidations_test.go b/testing/spectest/mainnet/electra/epoch_processing/pending_consolidations_test.go new file mode 100644 index 000000000000..9dbc628d062f --- /dev/null +++ b/testing/spectest/mainnet/electra/epoch_processing/pending_consolidations_test.go @@ -0,0 +1,11 @@ +package epoch_processing + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing" +) + +func TestMainnet_Electra_EpochProcessing_PendingConsolidations(t *testing.T) { + epoch_processing.RunPendingConsolidationsTests(t, "mainnet") +} diff --git a/testing/spectest/mainnet/electra/operations/BUILD.bazel b/testing/spectest/mainnet/electra/operations/BUILD.bazel index 48fd331379dd..a8a3ba231ec0 100644 --- a/testing/spectest/mainnet/electra/operations/BUILD.bazel +++ b/testing/spectest/mainnet/electra/operations/BUILD.bazel @@ -6,6 +6,7 @@ go_test( "attester_slashing_test.go", "block_header_test.go", "bls_to_execution_change_test.go", + "consolidation_test.go", "execution_payload_test.go", "proposer_slashing_test.go", "sync_committee_test.go", diff --git a/testing/spectest/mainnet/electra/operations/consolidation_test.go b/testing/spectest/mainnet/electra/operations/consolidation_test.go new file mode 100644 index 000000000000..3afe9874ec60 --- /dev/null +++ b/testing/spectest/mainnet/electra/operations/consolidation_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMainnet_Electra_Operations_Consolidation(t *testing.T) { + operations.RunConsolidationTest(t, "mainnet") +} diff --git a/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel b/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel index 0f4b31514b71..02bf1f72aba4 100644 --- a/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel +++ b/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel @@ -8,6 +8,7 @@ go_test( "inactivity_updates_test.go", "justification_and_finalization_test.go", "participation_flag_updates_test.go", + "pending_consolidations_test.go", "randao_mixes_reset_test.go", "rewards_and_penalties_test.go", "slashings_reset_test.go", diff --git a/testing/spectest/minimal/electra/epoch_processing/pending_consolidations_test.go b/testing/spectest/minimal/electra/epoch_processing/pending_consolidations_test.go new file mode 100644 index 000000000000..0fcbb76608d0 --- /dev/null +++ b/testing/spectest/minimal/electra/epoch_processing/pending_consolidations_test.go @@ -0,0 +1,11 @@ +package epoch_processing + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing" +) + +func TestMinimal_Electra_EpochProcessing_PendingConsolidations(t *testing.T) { + epoch_processing.RunPendingConsolidationsTests(t, "minimal") +} diff --git a/testing/spectest/minimal/electra/operations/BUILD.bazel b/testing/spectest/minimal/electra/operations/BUILD.bazel index b54d95de73d0..d95b1cb0eb0a 100644 --- a/testing/spectest/minimal/electra/operations/BUILD.bazel +++ b/testing/spectest/minimal/electra/operations/BUILD.bazel @@ -6,6 +6,7 @@ go_test( "attester_slashing_test.go", "block_header_test.go", "bls_to_execution_change_test.go", + "consolidation_test.go", "execution_payload_test.go", "proposer_slashing_test.go", "sync_committee_test.go", diff --git a/testing/spectest/minimal/electra/operations/consolidation_test.go b/testing/spectest/minimal/electra/operations/consolidation_test.go new file mode 100644 index 000000000000..cc46d13998d2 --- /dev/null +++ b/testing/spectest/minimal/electra/operations/consolidation_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMinimal_Electra_Operations_Consolidation(t *testing.T) { + operations.RunConsolidationTest(t, "minimal") +} diff --git a/testing/spectest/shared/electra/epoch_processing/BUILD.bazel b/testing/spectest/shared/electra/epoch_processing/BUILD.bazel index 90d8ef842817..7140a280dab4 100644 --- a/testing/spectest/shared/electra/epoch_processing/BUILD.bazel +++ b/testing/spectest/shared/electra/epoch_processing/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "inactivity_updates.go", "justification_and_finalization.go", "participation_flag_updates.go", + "pending_consolidations.go", "randao_mixes_reset.go", "rewards_and_penalties.go", "slashings.go", diff --git a/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go b/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go new file mode 100644 index 000000000000..674fa40187bd --- /dev/null +++ b/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go @@ -0,0 +1,28 @@ +package epoch_processing + +import ( + "context" + "path" + "testing" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/spectest/utils" +) + +func RunPendingConsolidationsTests(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "epoch_processing/pending_consolidations/pyspec_tests") + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + RunEpochOperationTest(t, folderPath, processPendingConsolidations) + }) + } +} + +func processPendingConsolidations(t *testing.T, st state.BeaconState) (state.BeaconState, error) { + return electra.ProcessPendingConsolidations(context.TODO(), st) +} diff --git a/testing/spectest/shared/electra/operations/BUILD.bazel b/testing/spectest/shared/electra/operations/BUILD.bazel index 7da4295b226b..9596456ef58d 100644 --- a/testing/spectest/shared/electra/operations/BUILD.bazel +++ b/testing/spectest/shared/electra/operations/BUILD.bazel @@ -7,6 +7,7 @@ go_library( "attester_slashing.go", "block_header.go", "bls_to_execution_changes.go", + "consolidations.go", "deposit_receipt.go", "execution_layer_withdrawal_request.go", "execution_payload.go", diff --git a/testing/spectest/shared/electra/operations/consolidations.go b/testing/spectest/shared/electra/operations/consolidations.go new file mode 100644 index 000000000000..622682eb5fd7 --- /dev/null +++ b/testing/spectest/shared/electra/operations/consolidations.go @@ -0,0 +1,48 @@ +package operations + +import ( + "context" + "path" + "testing" + + "github.com/golang/snappy" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/spectest/utils" + "github.com/prysmaticlabs/prysm/v5/testing/util" +) + +func RunConsolidationTest(t *testing.T, config string) { + t.Skip("These tests were temporarily deleted in v1.5.0-alpha.2. See https://github.com/ethereum/consensus-specs/pull/3736") + require.NoError(t, utils.SetConfig(t, config)) + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "operations/consolidation/pyspec_tests") + require.NotEqual(t, 0, len(testFolders), "missing tests for consolidation operation in folder") + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + consolidationFile, err := util.BazelFileBytes(folderPath, "consolidation.ssz_snappy") + require.NoError(t, err) + consolidationSSZ, err := snappy.Decode(nil /* dst */, consolidationFile) + require.NoError(t, err, "Failed to decompress") + consolidation := ðpb.SignedConsolidation{} + require.NoError(t, consolidation.UnmarshalSSZ(consolidationSSZ), "Failed to unmarshal") + + body := ðpb.BeaconBlockBodyElectra{Consolidations: []*ethpb.SignedConsolidation{consolidation}} + processConsolidationFunc := func(ctx context.Context, s state.BeaconState, b interfaces.SignedBeaconBlock) (state.BeaconState, error) { + body, ok := b.Block().Body().(interfaces.ROBlockBodyElectra) + if !ok { + t.Error("block body is not electra") + } + cs := body.Consolidations() + if len(cs) == 0 { + t.Error("no consolidations to test") + } + return electra.ProcessConsolidations(ctx, s, cs) + } + RunBlockOperationTest(t, folderPath, body, processConsolidationFunc) + }) + } +} From 84ff2bad872d8c8dc6a452b2cff19998ce8987be Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 14 May 2024 02:40:15 -0500 Subject: [PATCH 3/7] Unskip consolidation processing for minimal spectests --- .../spectest/mainnet/electra/operations/consolidation_test.go | 1 + testing/spectest/shared/electra/operations/consolidations.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/spectest/mainnet/electra/operations/consolidation_test.go b/testing/spectest/mainnet/electra/operations/consolidation_test.go index 3afe9874ec60..9f6b0208d8cf 100644 --- a/testing/spectest/mainnet/electra/operations/consolidation_test.go +++ b/testing/spectest/mainnet/electra/operations/consolidation_test.go @@ -7,5 +7,6 @@ import ( ) func TestMainnet_Electra_Operations_Consolidation(t *testing.T) { + t.Skip("These tests were temporarily deleted in v1.5.0-alpha.2. See https://github.com/ethereum/consensus-specs/pull/3736") operations.RunConsolidationTest(t, "mainnet") } diff --git a/testing/spectest/shared/electra/operations/consolidations.go b/testing/spectest/shared/electra/operations/consolidations.go index 622682eb5fd7..09f3e04a8fab 100644 --- a/testing/spectest/shared/electra/operations/consolidations.go +++ b/testing/spectest/shared/electra/operations/consolidations.go @@ -16,7 +16,6 @@ import ( ) func RunConsolidationTest(t *testing.T, config string) { - t.Skip("These tests were temporarily deleted in v1.5.0-alpha.2. See https://github.com/ethereum/consensus-specs/pull/3736") require.NoError(t, utils.SetConfig(t, config)) testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "operations/consolidation/pyspec_tests") require.NotEqual(t, 0, len(testFolders), "missing tests for consolidation operation in folder") From 28323f867b4f65f36fe97db1ac418f0b7907152f Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 14 May 2024 04:37:33 -0500 Subject: [PATCH 4/7] PR feedback --- beacon-chain/core/electra/consolidations.go | 84 +++++++++---------- .../core/electra/consolidations_test.go | 8 +- .../pending_consolidations.go | 2 +- .../electra/operations/consolidations.go | 2 +- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/beacon-chain/core/electra/consolidations.go b/beacon-chain/core/electra/consolidations.go index 36ff13b3ffff..031faf370c1b 100644 --- a/beacon-chain/core/electra/consolidations.go +++ b/beacon-chain/core/electra/consolidations.go @@ -39,12 +39,12 @@ import ( // next_pending_consolidation += 1 // // state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:] -func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) (state.BeaconState, error) { +func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) error { ctx, span := trace.StartSpan(ctx, "electra.ProcessPendingConsolidations") defer span.End() if st == nil || st.IsNil() { - return nil, errors.New("nil state") + return errors.New("nil state") } currentEpoch := slots.ToEpoch(st.Slot()) @@ -52,12 +52,13 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) (st var nextPendingConsolidation uint64 pendingConsolidations, err := st.PendingConsolidations() if err != nil { - return nil, err + return err } + for _, pc := range pendingConsolidations { sourceValidator, err := st.ValidatorAtIndex(pc.SourceIndex) if err != nil { - return nil, err + return err } if sourceValidator.Slashed { nextPendingConsolidation++ @@ -68,29 +69,27 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) (st } if err := SwitchToCompoundingValidator(ctx, st, pc.TargetIndex); err != nil { - return nil, err + return err } activeBalance, err := st.ActiveBalanceAtIndex(pc.SourceIndex) if err != nil { - return nil, err + return err } if err := helpers.DecreaseBalance(st, pc.SourceIndex, activeBalance); err != nil { - return nil, err + return err } if err := helpers.IncreaseBalance(st, pc.TargetIndex, activeBalance); err != nil { - return nil, err + return err } nextPendingConsolidation++ } if nextPendingConsolidation > 0 { - if err := st.SetPendingConsolidations(pendingConsolidations[nextPendingConsolidation:]); err != nil { - return nil, err - } + return st.SetPendingConsolidations(pendingConsolidations[nextPendingConsolidation:]) } - return st, nil + return nil } // ProcessConsolidations implements the spec definition below. This method makes mutating calls to @@ -141,16 +140,16 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) (st // source_index=consolidation.source_index, // target_index=consolidation.target_index // )) -func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethpb.SignedConsolidation) (state.BeaconState, error) { +func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethpb.SignedConsolidation) error { _, span := trace.StartSpan(ctx, "electra.ProcessConsolidations") defer span.End() if st == nil || st.IsNil() { - return nil, errors.New("nil state") + return errors.New("nil state") } if len(cs) == 0 { - return st, nil // Nothing to process. + return nil // Nothing to process. } domain, err := signing.ComputeDomain( @@ -159,100 +158,101 @@ func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethp st.GenesisValidatorsRoot(), ) if err != nil { - return nil, err + return err } totalBalance, err := helpers.TotalActiveBalance(st) if err != nil { - return nil, err + return err } + currentEpoch := slots.ToEpoch(st.Slot()) + for _, c := range cs { if c == nil || c.Message == nil { - return nil, errors.New("nil consolidation") + return errors.New("nil consolidation") } if n, err := st.NumPendingConsolidations(); err != nil { - return nil, err + return err } else if n >= params.BeaconConfig().PendingConsolidationsLimit { - return nil, errors.New("pending consolidations queue is full") + return errors.New("pending consolidations queue is full") } if helpers.ConsolidationChurnLimit(math.Gwei(totalBalance)) <= math.Gwei(params.BeaconConfig().MinActivationBalance) { - return nil, errors.New("too little available consolidation churn limit") + return errors.New("too little available consolidation churn limit") } - currentEpoch := slots.ToEpoch(st.Slot()) if c.Message.SourceIndex == c.Message.TargetIndex { - return nil, errors.New("source and target index are the same") + return errors.New("source and target index are the same") } source, err := st.ValidatorAtIndex(c.Message.SourceIndex) if err != nil { - return nil, err + return err } target, err := st.ValidatorAtIndex(c.Message.TargetIndex) if err != nil { - return nil, err + return err } if !helpers.IsActiveValidator(source, currentEpoch) { - return nil, errors.New("source is not active") + return errors.New("source is not active") } if !helpers.IsActiveValidator(target, currentEpoch) { - return nil, errors.New("target is not active") + return errors.New("target is not active") } if source.ExitEpoch != params.BeaconConfig().FarFutureEpoch { - return nil, errors.New("source exit epoch has been initiated") + return errors.New("source exit epoch has been initiated") } if target.ExitEpoch != params.BeaconConfig().FarFutureEpoch { - return nil, errors.New("target exit epoch has been initiated") + return errors.New("target exit epoch has been initiated") } if currentEpoch < c.Message.Epoch { - return nil, errors.New("consolidation is not valid yet") + return errors.New("consolidation is not valid yet") } if !helpers.HasExecutionWithdrawalCredentials(source) { - return nil, errors.New("source does not have execution withdrawal credentials") + return errors.New("source does not have execution withdrawal credentials") } if !helpers.HasExecutionWithdrawalCredentials(target) { - return nil, errors.New("target does not have execution withdrawal credentials") + return errors.New("target does not have execution withdrawal credentials") } if !helpers.IsSameWithdrawalCredentials(source, target) { - return nil, errors.New("source and target have different withdrawal credentials") + return errors.New("source and target have different withdrawal credentials") } sr, err := signing.ComputeSigningRoot(c.Message, domain) if err != nil { - return nil, err + return err } sourcePk, err := bls.PublicKeyFromBytes(source.PublicKey) if err != nil { - return nil, errors.Wrap(err, "could not convert bytes to public key") + return errors.Wrap(err, "could not convert source public key bytes to bls public key") } targetPk, err := bls.PublicKeyFromBytes(target.PublicKey) if err != nil { - return nil, errors.Wrap(err, "could not convert bytes to public key") + return errors.Wrap(err, "could not convert target public key bytes to bls public key") } sig, err := bls.SignatureFromBytes(c.Signature) if err != nil { - return nil, errors.Wrap(err, "could not convert bytes to signature") + return errors.Wrap(err, "could not convert bytes to signature") } if !sig.FastAggregateVerify([]bls.PublicKey{sourcePk, targetPk}, sr) { - return nil, errors.New("consolidation signature verification failed") + return errors.New("consolidation signature verification failed") } sEE, err := ComputeConsolidationEpochAndUpdateChurn(ctx, st, math.Gwei(source.EffectiveBalance)) if err != nil { - return nil, err + return err } source.ExitEpoch = sEE source.WithdrawableEpoch = sEE + params.BeaconConfig().MinValidatorWithdrawabilityDelay if err := st.UpdateValidatorAtIndex(c.Message.SourceIndex, source); err != nil { - return nil, err + return err } if err := st.AppendPendingConsolidation(c.Message.ToPendingConsolidation()); err != nil { - return nil, err + return err } } - return st, nil + return nil } diff --git a/beacon-chain/core/electra/consolidations_test.go b/beacon-chain/core/electra/consolidations_test.go index e52ef30f5229..ae134fa1ee3f 100644 --- a/beacon-chain/core/electra/consolidations_test.go +++ b/beacon-chain/core/electra/consolidations_test.go @@ -197,10 +197,10 @@ func TestProcessPendingConsolidations(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - res, err := electra.ProcessPendingConsolidations(context.TODO(), tt.state) + err := electra.ProcessPendingConsolidations(context.TODO(), tt.state) require.Equal(t, tt.wantErr, err != nil) if tt.check != nil { - tt.check(t, res) + tt.check(t, tt.state) } }) } @@ -430,14 +430,14 @@ func TestProcessConsolidations(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - res, err := electra.ProcessConsolidations(context.TODO(), tt.state, tt.scs) + err := electra.ProcessConsolidations(context.TODO(), tt.state, tt.scs) if len(tt.wantErr) > 0 { require.ErrorContains(t, tt.wantErr, err) } else { require.NoError(t, err) } if tt.check != nil { - tt.check(t, res) + tt.check(t, tt.state) } }) } diff --git a/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go b/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go index 674fa40187bd..06e7f8161b9d 100644 --- a/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go +++ b/testing/spectest/shared/electra/epoch_processing/pending_consolidations.go @@ -24,5 +24,5 @@ func RunPendingConsolidationsTests(t *testing.T, config string) { } func processPendingConsolidations(t *testing.T, st state.BeaconState) (state.BeaconState, error) { - return electra.ProcessPendingConsolidations(context.TODO(), st) + return st, electra.ProcessPendingConsolidations(context.TODO(), st) } diff --git a/testing/spectest/shared/electra/operations/consolidations.go b/testing/spectest/shared/electra/operations/consolidations.go index 09f3e04a8fab..967ddafa2400 100644 --- a/testing/spectest/shared/electra/operations/consolidations.go +++ b/testing/spectest/shared/electra/operations/consolidations.go @@ -39,7 +39,7 @@ func RunConsolidationTest(t *testing.T, config string) { if len(cs) == 0 { t.Error("no consolidations to test") } - return electra.ProcessConsolidations(ctx, s, cs) + return s, electra.ProcessConsolidations(ctx, s, cs) } RunBlockOperationTest(t, folderPath, body, processConsolidationFunc) }) From 5af7914ebc70a07f9153f1ebfb8624360cdf2f26 Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 14 May 2024 04:56:07 -0500 Subject: [PATCH 5/7] Update beacon-chain/core/electra/consolidations_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RadosÅ‚aw Kapka --- beacon-chain/core/electra/consolidations_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon-chain/core/electra/consolidations_test.go b/beacon-chain/core/electra/consolidations_test.go index ae134fa1ee3f..9869d0f5802e 100644 --- a/beacon-chain/core/electra/consolidations_test.go +++ b/beacon-chain/core/electra/consolidations_test.go @@ -333,7 +333,7 @@ func TestProcessConsolidations(t *testing.T) { wantErr: "source exit epoch has been initiated", }, { - name: "consolidation with exiting target", + name: "consolidation with exiting target is rejected", state: func() state.BeaconState { st := stateWithActiveBalanceETH(t, 19_000_000) val, err := st.ValidatorAtIndex(25) From cba374a6cd17a48ef8a46fa4f99175b3bba5e79f Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 14 May 2024 04:58:44 -0500 Subject: [PATCH 6/7] Update beacon-chain/core/electra/consolidations_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RadosÅ‚aw Kapka --- beacon-chain/core/electra/consolidations_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon-chain/core/electra/consolidations_test.go b/beacon-chain/core/electra/consolidations_test.go index 9869d0f5802e..c4da23d91367 100644 --- a/beacon-chain/core/electra/consolidations_test.go +++ b/beacon-chain/core/electra/consolidations_test.go @@ -320,7 +320,7 @@ func TestProcessConsolidations(t *testing.T) { wantErr: "target is not active", }, { - name: "consolidation with exiting source", + name: "consolidation with exiting source is rejected", state: func() state.BeaconState { st := stateWithActiveBalanceETH(t, 19_000_000) val, err := st.ValidatorAtIndex(25) From 8141096bf3ced3793cf0737603af7d3e28c711cc Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 14 May 2024 08:49:35 -0500 Subject: [PATCH 7/7] Move consolidation limit check outside of the loop --- beacon-chain/core/electra/consolidations.go | 8 ++++---- beacon-chain/core/electra/consolidations_test.go | 7 ++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/beacon-chain/core/electra/consolidations.go b/beacon-chain/core/electra/consolidations.go index 031faf370c1b..603e40d893e0 100644 --- a/beacon-chain/core/electra/consolidations.go +++ b/beacon-chain/core/electra/consolidations.go @@ -166,6 +166,10 @@ func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethp return err } + if helpers.ConsolidationChurnLimit(math.Gwei(totalBalance)) <= math.Gwei(params.BeaconConfig().MinActivationBalance) { + return errors.New("too little available consolidation churn limit") + } + currentEpoch := slots.ToEpoch(st.Slot()) for _, c := range cs { @@ -179,10 +183,6 @@ func ProcessConsolidations(ctx context.Context, st state.BeaconState, cs []*ethp return errors.New("pending consolidations queue is full") } - if helpers.ConsolidationChurnLimit(math.Gwei(totalBalance)) <= math.Gwei(params.BeaconConfig().MinActivationBalance) { - return errors.New("too little available consolidation churn limit") - } - if c.Message.SourceIndex == c.Message.TargetIndex { return errors.New("source and target index are the same") } diff --git a/beacon-chain/core/electra/consolidations_test.go b/beacon-chain/core/electra/consolidations_test.go index c4da23d91367..1e3b22c0bd79 100644 --- a/beacon-chain/core/electra/consolidations_test.go +++ b/beacon-chain/core/electra/consolidations_test.go @@ -260,17 +260,14 @@ func TestProcessConsolidations(t *testing.T) { }, { name: "nil consolidation in slice", - state: func() state.BeaconState { - st, _ := util.DeterministicGenesisStateElectra(t, 1) - return st - }(), + state: stateWithActiveBalanceETH(t, 19_000_000), scs: []*eth.SignedConsolidation{nil, nil}, wantErr: "nil consolidation", }, { name: "state is 100% full of pending consolidations", state: func() state.BeaconState { - st, _ := util.DeterministicGenesisStateElectra(t, 1) + st := stateWithActiveBalanceETH(t, 19_000_000) pc := make([]*eth.PendingConsolidation, params.BeaconConfig().PendingConsolidationsLimit) require.NoError(t, st.SetPendingConsolidations(pc)) return st