From fcbe19445a8e2260763dcea513306cbb550c3541 Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Wed, 15 May 2024 08:50:33 -0500 Subject: [PATCH] eip-7251: process consolidations (#13983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eip-7251: process_pending_consolidations and process_consolidations * Consolidate unit tests + spectests Fix failing spectest //testing/spectest/minimal/electra/operations:go_default_test * Unskip consolidation processing for minimal spectests * PR feedback * Update beacon-chain/core/electra/consolidations_test.go Co-authored-by: Radosław Kapka * Update beacon-chain/core/electra/consolidations_test.go Co-authored-by: Radosław Kapka * Move consolidation limit check outside of the loop --------- Co-authored-by: Radosław Kapka --- beacon-chain/core/electra/BUILD.bazel | 10 + beacon-chain/core/electra/consolidations.go | 258 ++++++++++ .../core/electra/consolidations_test.go | 441 ++++++++++++++++++ .../electra/epoch_processing/BUILD.bazel | 1 + .../pending_consolidations_test.go | 11 + .../mainnet/electra/operations/BUILD.bazel | 1 + .../electra/operations/consolidation_test.go | 12 + .../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 | 47 ++ 15 files changed, 835 insertions(+) create mode 100644 beacon-chain/core/electra/consolidations.go 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 697b9df7291b..acb1f7dceded 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", ], ) @@ -32,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 new file mode 100644 index 000000000000..603e40d893e0 --- /dev/null +++ b/beacon-chain/core/electra/consolidations.go @@ -0,0 +1,258 @@ +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" +) + +// ProcessPendingConsolidations implements the spec definition below. This method makes mutating +// calls to the beacon state. +// +// 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) error { + ctx, span := trace.StartSpan(ctx, "electra.ProcessPendingConsolidations") + defer span.End() + + if st == nil || st.IsNil() { + return errors.New("nil state") + } + + currentEpoch := slots.ToEpoch(st.Slot()) + + var nextPendingConsolidation uint64 + pendingConsolidations, err := st.PendingConsolidations() + if err != nil { + return err + } + + for _, pc := range pendingConsolidations { + sourceValidator, err := st.ValidatorAtIndex(pc.SourceIndex) + if err != nil { + return err + } + if sourceValidator.Slashed { + nextPendingConsolidation++ + continue + } + if sourceValidator.WithdrawableEpoch > currentEpoch { + break + } + + if err := SwitchToCompoundingValidator(ctx, st, pc.TargetIndex); err != nil { + return err + } + + activeBalance, err := st.ActiveBalanceAtIndex(pc.SourceIndex) + if err != nil { + return err + } + if err := helpers.DecreaseBalance(st, pc.SourceIndex, activeBalance); err != nil { + return err + } + if err := helpers.IncreaseBalance(st, pc.TargetIndex, activeBalance); err != nil { + return err + } + nextPendingConsolidation++ + } + + if nextPendingConsolidation > 0 { + return st.SetPendingConsolidations(pendingConsolidations[nextPendingConsolidation:]) + } + + return nil +} + +// ProcessConsolidations implements the spec definition below. This method makes mutating calls to +// the beacon state. +// +// 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) error { + _, span := trace.StartSpan(ctx, "electra.ProcessConsolidations") + defer span.End() + + if st == nil || st.IsNil() { + return errors.New("nil state") + } + + if len(cs) == 0 { + return nil // Nothing to process. + } + + domain, err := signing.ComputeDomain( + params.BeaconConfig().DomainConsolidation, + nil, // Use genesis fork version + st.GenesisValidatorsRoot(), + ) + if err != nil { + return err + } + + totalBalance, err := helpers.TotalActiveBalance(st) + if err != nil { + 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 { + if c == nil || c.Message == nil { + return errors.New("nil consolidation") + } + + if n, err := st.NumPendingConsolidations(); err != nil { + return err + } else if n >= params.BeaconConfig().PendingConsolidationsLimit { + return errors.New("pending consolidations queue is full") + } + + if c.Message.SourceIndex == c.Message.TargetIndex { + return errors.New("source and target index are the same") + } + source, err := st.ValidatorAtIndex(c.Message.SourceIndex) + if err != nil { + return err + } + target, err := st.ValidatorAtIndex(c.Message.TargetIndex) + if err != nil { + return err + } + if !helpers.IsActiveValidator(source, currentEpoch) { + return errors.New("source is not active") + } + if !helpers.IsActiveValidator(target, currentEpoch) { + return errors.New("target is not active") + } + if source.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return errors.New("source exit epoch has been initiated") + } + if target.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return errors.New("target exit epoch has been initiated") + } + if currentEpoch < c.Message.Epoch { + return errors.New("consolidation is not valid yet") + } + + if !helpers.HasExecutionWithdrawalCredentials(source) { + return errors.New("source does not have execution withdrawal credentials") + } + if !helpers.HasExecutionWithdrawalCredentials(target) { + return errors.New("target does not have execution withdrawal credentials") + } + if !helpers.IsSameWithdrawalCredentials(source, target) { + return errors.New("source and target have different withdrawal credentials") + } + + sr, err := signing.ComputeSigningRoot(c.Message, domain) + if err != nil { + return err + } + sourcePk, err := bls.PublicKeyFromBytes(source.PublicKey) + if err != nil { + 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 errors.Wrap(err, "could not convert target public key bytes to bls public key") + } + sig, err := bls.SignatureFromBytes(c.Signature) + if err != nil { + return errors.Wrap(err, "could not convert bytes to signature") + } + if !sig.FastAggregateVerify([]bls.PublicKey{sourcePk, targetPk}, sr) { + return errors.New("consolidation signature verification failed") + } + + sEE, err := ComputeConsolidationEpochAndUpdateChurn(ctx, st, math.Gwei(source.EffectiveBalance)) + if err != nil { + return err + } + source.ExitEpoch = sEE + source.WithdrawableEpoch = sEE + params.BeaconConfig().MinValidatorWithdrawabilityDelay + if err := st.UpdateValidatorAtIndex(c.Message.SourceIndex, source); err != nil { + return err + } + if err := st.AppendPendingConsolidation(c.Message.ToPendingConsolidation()); err != nil { + return err + } + } + + return nil +} diff --git a/beacon-chain/core/electra/consolidations_test.go b/beacon-chain/core/electra/consolidations_test.go new file mode 100644 index 000000000000..1e3b22c0bd79 --- /dev/null +++ b/beacon-chain/core/electra/consolidations_test.go @@ -0,0 +1,441 @@ +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) { + err := electra.ProcessPendingConsolidations(context.TODO(), tt.state) + require.Equal(t, tt.wantErr, err != nil) + if tt.check != nil { + tt.check(t, tt.state) + } + }) + } +} + +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: 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 := stateWithActiveBalanceETH(t, 19_000_000) + 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 is rejected", + 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 is rejected", + 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) { + 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, tt.state) + } + }) + } +} 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..9f6b0208d8cf --- /dev/null +++ b/testing/spectest/mainnet/electra/operations/consolidation_test.go @@ -0,0 +1,12 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +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/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..06e7f8161b9d --- /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 st, 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..967ddafa2400 --- /dev/null +++ b/testing/spectest/shared/electra/operations/consolidations.go @@ -0,0 +1,47 @@ +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) { + 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 s, electra.ProcessConsolidations(ctx, s, cs) + } + RunBlockOperationTest(t, folderPath, body, processConsolidationFunc) + }) + } +}