diff --git a/beacon-chain/core/electra/BUILD.bazel b/beacon-chain/core/electra/BUILD.bazel index 92d647c5de64..f703ff7aeb97 100644 --- a/beacon-chain/core/electra/BUILD.bazel +++ b/beacon-chain/core/electra/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "effective_balance_updates.go", "registry_updates.go", "transition.go", + "transition_no_verify_sig.go", "upgrade.go", "validator.go", "withdrawals.go", @@ -18,15 +19,19 @@ go_library( visibility = ["//visibility:public"], deps = [ "//beacon-chain/core/altair:go_default_library", + "//beacon-chain/core/blocks:go_default_library", "//beacon-chain/core/epoch:go_default_library", "//beacon-chain/core/epoch/precompute: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/core/validators:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/state-native:go_default_library", "//config/params:go_default_library", + "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", + "//contracts/deposit:go_default_library", "//encoding/bytesutil:go_default_library", "//math:go_default_library", "//proto/engine/v1:go_default_library", @@ -44,8 +49,10 @@ go_test( srcs = [ "churn_test.go", "consolidations_test.go", + "deposit_fuzz_test.go", "deposits_test.go", "effective_balance_updates_test.go", + "transition_test.go", "upgrade_test.go", "validator_test.go", "withdrawals_test.go", @@ -53,11 +60,15 @@ go_test( 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/blocks:go_default_library", "//consensus-types/primitives:go_default_library", + "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", @@ -65,7 +76,9 @@ go_test( "//testing/require:go_default_library", "//testing/util:go_default_library", "//time/slots:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", + "@com_github_google_gofuzz//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", ], diff --git a/beacon-chain/core/electra/deposit_fuzz_test.go b/beacon-chain/core/electra/deposit_fuzz_test.go new file mode 100644 index 000000000000..adf0d04dcd82 --- /dev/null +++ b/beacon-chain/core/electra/deposit_fuzz_test.go @@ -0,0 +1,48 @@ +package electra_test + +import ( + "context" + "testing" + + fuzz "github.com/google/gofuzz" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" +) + +func TestFuzzProcessDeposits_10000(t *testing.T) { + fuzzer := fuzz.NewWithSeed(0) + state := ðpb.BeaconStateElectra{} + deposits := make([]*ethpb.Deposit, 100) + ctx := context.Background() + for i := 0; i < 10000; i++ { + fuzzer.Fuzz(state) + for i := range deposits { + fuzzer.Fuzz(deposits[i]) + } + s, err := state_native.InitializeFromProtoUnsafeElectra(state) + require.NoError(t, err) + r, err := electra.ProcessDeposits(ctx, s, deposits) + if err != nil && r != nil { + t.Fatalf("return value should be nil on err. found: %v on error: %v for state: %v and block: %v", r, err, state, deposits) + } + } +} + +func TestFuzzProcessDeposit_10000(t *testing.T) { + fuzzer := fuzz.NewWithSeed(0) + state := ðpb.BeaconStateElectra{} + deposit := ðpb.Deposit{} + + for i := 0; i < 10000; i++ { + fuzzer.Fuzz(state) + fuzzer.Fuzz(deposit) + s, err := state_native.InitializeFromProtoUnsafeElectra(state) + require.NoError(t, err) + r, err := electra.ProcessDeposit(s, deposit, true) + if err != nil && r != nil { + t.Fatalf("return value should be nil on err. found: %v on error: %v for state: %v and block: %v", r, err, state, deposit) + } + } +} diff --git a/beacon-chain/core/electra/deposits.go b/beacon-chain/core/electra/deposits.go index 56916f26dea9..db18d5b575b7 100644 --- a/beacon-chain/core/electra/deposits.go +++ b/beacon-chain/core/electra/deposits.go @@ -2,15 +2,186 @@ package electra import ( "context" - "errors" + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" "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/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/contracts/deposit" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + log "github.com/sirupsen/logrus" "go.opencensus.io/trace" ) +// ProcessDeposits is one of the operations performed on each processed +// beacon block to verify queued validators from the Ethereum 1.0 Deposit Contract +// into the beacon chain. +// +// Spec pseudocode definition: +// +// For each deposit in block.body.deposits: +// process_deposit(state, deposit) +func ProcessDeposits( + ctx context.Context, + beaconState state.BeaconState, + deposits []*ethpb.Deposit, +) (state.BeaconState, error) { + ctx, span := trace.StartSpan(ctx, "electra.ProcessDeposits") + defer span.End() + // Attempt to verify all deposit signatures at once, if this fails then fall back to processing + // individual deposits with signature verification enabled. + batchVerified, err := blocks.BatchVerifyDepositsSignatures(ctx, deposits) + if err != nil { + return nil, errors.Wrap(err, "could not verify deposit signatures in batch") + } + + for _, d := range deposits { + if d == nil || d.Data == nil { + return nil, errors.New("got a nil deposit in block") + } + beaconState, err = ProcessDeposit(beaconState, d, batchVerified) + if err != nil { + return nil, errors.Wrapf(err, "could not process deposit from %#x", bytesutil.Trunc(d.Data.PublicKey)) + } + } + return beaconState, nil +} + +// ProcessDeposit takes in a deposit object and inserts it +// into the registry as a new validator or balance change. +// Returns the resulting state, a boolean to indicate whether or not the deposit +// resulted in a new validator entry into the beacon state, and any error. +// +// Spec pseudocode definition: +// def process_deposit(state: BeaconState, deposit: Deposit) -> None: +// +// # Verify the Merkle branch +// assert is_valid_merkle_branch( +// leaf=hash_tree_root(deposit.data), +// branch=deposit.proof, +// depth=DEPOSIT_CONTRACT_TREE_DEPTH + 1, # Add 1 for the List length mix-in +// index=state.eth1_deposit_index, +// root=state.eth1_data.deposit_root, +// ) +// +// # Deposits must be processed in order +// state.eth1_deposit_index += 1 +// +// apply_deposit( +// state=state, +// pubkey=deposit.data.pubkey, +// withdrawal_credentials=deposit.data.withdrawal_credentials, +// amount=deposit.data.amount, +// signature=deposit.data.signature, +// ) +func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, verifySignature bool) (state.BeaconState, error) { + if err := blocks.VerifyDeposit(beaconState, deposit); err != nil { + if deposit == nil || deposit.Data == nil { + return nil, err + } + return nil, errors.Wrapf(err, "could not verify deposit from %#x", bytesutil.Trunc(deposit.Data.PublicKey)) + } + if err := beaconState.SetEth1DepositIndex(beaconState.Eth1DepositIndex() + 1); err != nil { + return nil, err + } + return ApplyDeposit(beaconState, deposit.Data, verifySignature) +} + +// ApplyDeposit +// def apply_deposit(state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64, signature: BLSSignature) -> None: +// validator_pubkeys = [v.pubkey for v in state.validators] +// if pubkey not in validator_pubkeys: +// +// # Verify the deposit signature (proof of possession) which is not checked by the deposit contract +// if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): +// add_validator_to_registry(state, pubkey, withdrawal_credentials, amount) +// +// else: +// +// # Increase balance by deposit amount +// index = ValidatorIndex(validator_pubkeys.index(pubkey)) +// state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) # [Modified in Electra:EIP-7251] +// # Check if valid deposit switch to compounding credentials +// +// if ( is_compounding_withdrawal_credential(withdrawal_credentials) and has_eth1_withdrawal_credential(state.validators[index]) +// +// and is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature) +// ): +// switch_to_compounding_validator(state, index) +func ApplyDeposit(beaconState state.BeaconState, data *ethpb.Deposit_Data, verifySignature bool) (state.BeaconState, error) { + pubKey := data.PublicKey + amount := data.Amount + withdrawalCredentials := data.WithdrawalCredentials + index, ok := beaconState.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubKey)) + if !ok { + if verifySignature { + valid, err := IsValidDepositSignature(data) + if err != nil { + return nil, errors.Wrap(err, "could not verify deposit signature") + } + if !valid { + return beaconState, nil + } + } + if err := AddValidatorToRegistry(beaconState, pubKey, withdrawalCredentials, amount); err != nil { + return nil, errors.Wrap(err, "could not add validator to registry") + } + } else { + // no validation on top-ups (phase0 feature). no validation before state change + if err := beaconState.AppendPendingBalanceDeposit(index, amount); err != nil { + return nil, err + } + val, err := beaconState.ValidatorAtIndex(index) + if err != nil { + return nil, err + } + if helpers.IsCompoundingWithdrawalCredential(withdrawalCredentials) && helpers.HasETH1WithdrawalCredential(val) { + if verifySignature { + valid, err := IsValidDepositSignature(data) + if err != nil { + return nil, errors.Wrap(err, "could not verify deposit signature") + } + if !valid { + return beaconState, nil + } + } + if err := SwitchToCompoundingValidator(beaconState, index); err != nil { + return nil, errors.Wrap(err, "could not switch to compound validator") + } + } + } + return beaconState, nil +} + +// IsValidDepositSignature returns whether deposit_data is valid +// def is_valid_deposit_signature(pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64, signature: BLSSignature) -> bool: +// +// deposit_message = DepositMessage( pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount, ) +// domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks +// signing_root = compute_signing_root(deposit_message, domain) +// return bls.Verify(pubkey, signing_root, signature) +func IsValidDepositSignature(data *ethpb.Deposit_Data) (bool, error) { + domain, err := signing.ComputeDomain(params.BeaconConfig().DomainDeposit, nil, nil) + if err != nil { + return false, err + } + if err := verifyDepositDataSigningRoot(data, domain); err != nil { + // Ignore this error as in the spec pseudo code. + log.WithError(err).Debug("Skipping deposit: could not verify deposit data signature") + return false, nil + } + return true, nil +} + +func verifyDepositDataSigningRoot(obj *ethpb.Deposit_Data, domain []byte) error { + return deposit.VerifyDepositSignature(obj, domain) +} + // ProcessPendingBalanceDeposits implements the spec definition below. This method mutates the state. // // Spec definition: @@ -54,14 +225,14 @@ func ProcessPendingBalanceDeposits(ctx context.Context, st state.BeaconState, ac return err } - for _, deposit := range deposits { - if primitives.Gwei(deposit.Amount) > availableForProcessing { + for _, balanceDeposit := range deposits { + if primitives.Gwei(balanceDeposit.Amount) > availableForProcessing { break } - if err := helpers.IncreaseBalance(st, deposit.Index, deposit.Amount); err != nil { + if err := helpers.IncreaseBalance(st, balanceDeposit.Index, balanceDeposit.Amount); err != nil { return err } - availableForProcessing -= primitives.Gwei(deposit.Amount) + availableForProcessing -= primitives.Gwei(balanceDeposit.Amount) nextDepositIndex++ } @@ -79,9 +250,69 @@ func ProcessPendingBalanceDeposits(ctx context.Context, st state.BeaconState, ac // ProcessDepositRequests is a function as part of electra to process execution layer deposits func ProcessDepositRequests(ctx context.Context, beaconState state.BeaconState, requests []*enginev1.DepositRequest) (state.BeaconState, error) { - _, span := trace.StartSpan(ctx, "electra.ProcessDepositRequests") + ctx, span := trace.StartSpan(ctx, "electra.ProcessDepositRequests") defer span.End() - // TODO: replace with 6110 logic - // return b.ProcessDepositRequests(beaconState, requests) + + if len(requests) == 0 { + log.Debug("ProcessDepositRequests: no deposit requests found") + return beaconState, nil + } + + deposits := make([]*ethpb.Deposit, 0) + for _, req := range requests { + if req == nil { + return nil, errors.New("got a nil DepositRequest") + } + deposits = append(deposits, ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: req.Pubkey, + WithdrawalCredentials: req.WithdrawalCredentials, + Amount: req.Amount, + Signature: req.Signature, + }, + }) + } + batchVerified, err := blocks.BatchVerifyDepositsSignatures(ctx, deposits) + if err != nil { + return nil, errors.Wrap(err, "could not verify deposit signatures in batch") + } + for _, receipt := range requests { + beaconState, err = processDepositRequest(beaconState, receipt, batchVerified) + if err != nil { + return nil, errors.Wrap(err, "could not apply deposit request") + } + } return beaconState, nil } + +// processDepositRequest processes the specific deposit receipt +// def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: +// +// # Set deposit request start index +// if state.deposit_requests_start_index == UNSET_DEPOSIT_REQUEST_START_INDEX: +// state.deposit_requests_start_index = deposit_request.index +// +// apply_deposit( +// state=state, +// pubkey=deposit_request.pubkey, +// withdrawal_credentials=deposit_request.withdrawal_credentials, +// amount=deposit_request.amount, +// signature=deposit_request.signature, +// ) +func processDepositRequest(beaconState state.BeaconState, request *enginev1.DepositRequest, verifySignature bool) (state.BeaconState, error) { + requestsStartIndex, err := beaconState.DepositRequestsStartIndex() + if err != nil { + return nil, errors.Wrap(err, "could not get deposit requests start index") + } + if requestsStartIndex == params.BeaconConfig().UnsetDepositRequestsStartIndex { + if err := beaconState.SetDepositRequestsStartIndex(request.Index); err != nil { + return nil, errors.Wrap(err, "could not set deposit requests start index") + } + } + return ApplyDeposit(beaconState, ðpb.Deposit_Data{ + PublicKey: bytesutil.SafeCopyBytes(request.Pubkey), + Amount: request.Amount, + WithdrawalCredentials: bytesutil.SafeCopyBytes(request.WithdrawalCredentials), + Signature: bytesutil.SafeCopyBytes(request.Signature), + }, verifySignature) +} diff --git a/beacon-chain/core/electra/deposits_test.go b/beacon-chain/core/electra/deposits_test.go index 552ef548c54b..fe160d55c4bc 100644 --- a/beacon-chain/core/electra/deposits_test.go +++ b/beacon-chain/core/electra/deposits_test.go @@ -6,11 +6,18 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" "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" + 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" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" ) func TestProcessPendingBalanceDeposits(t *testing.T) { @@ -126,3 +133,194 @@ func TestProcessPendingBalanceDeposits(t *testing.T) { }) } } + +func TestProcessDepositRequests(t *testing.T) { + st, _ := util.DeterministicGenesisStateElectra(t, 1) + sk, err := bls.RandKey() + require.NoError(t, err) + + t.Run("empty requests continues", func(t *testing.T) { + newSt, err := electra.ProcessDepositRequests(context.Background(), st, []*enginev1.DepositRequest{}) + require.NoError(t, err) + require.DeepEqual(t, newSt, st) + }) + t.Run("nil request errors", func(t *testing.T) { + _, err = electra.ProcessDepositRequests(context.Background(), st, []*enginev1.DepositRequest{nil}) + require.ErrorContains(t, "got a nil DepositRequest", err) + }) + + vals := st.Validators() + vals[0].PublicKey = sk.PublicKey().Marshal() + vals[0].WithdrawalCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte + require.NoError(t, st.SetValidators(vals)) + bals := st.Balances() + bals[0] = params.BeaconConfig().MinActivationBalance + 2000 + require.NoError(t, st.SetBalances(bals)) + require.NoError(t, st.SetPendingBalanceDeposits(make([]*eth.PendingBalanceDeposit, 0))) // reset pbd as the determinitstic state populates this already + withdrawalCred := make([]byte, 32) + withdrawalCred[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte + depositMessage := ð.DepositMessage{ + PublicKey: sk.PublicKey().Marshal(), + Amount: 1000, + WithdrawalCredentials: withdrawalCred, + } + domain, err := signing.ComputeDomain(params.BeaconConfig().DomainDeposit, nil, nil) + require.NoError(t, err) + sr, err := signing.ComputeSigningRoot(depositMessage, domain) + require.NoError(t, err) + sig := sk.Sign(sr[:]) + requests := []*enginev1.DepositRequest{ + { + Pubkey: depositMessage.PublicKey, + Index: 0, + WithdrawalCredentials: depositMessage.WithdrawalCredentials, + Amount: depositMessage.Amount, + Signature: sig.Marshal(), + }, + } + st, err = electra.ProcessDepositRequests(context.Background(), st, requests) + require.NoError(t, err) + + pbd, err := st.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, 2, len(pbd)) + require.Equal(t, uint64(1000), pbd[0].Amount) + require.Equal(t, uint64(2000), pbd[1].Amount) +} + +func TestProcessDeposit_Electra_Simple(t *testing.T) { + deps, _, err := util.DeterministicDepositsAndKeysSameValidator(3) + require.NoError(t, err) + eth1Data, err := util.DeterministicEth1Data(len(deps)) + require.NoError(t, err) + registry := []*eth.Validator{ + { + PublicKey: []byte{1}, + WithdrawalCredentials: []byte{1, 2, 3}, + }, + } + balances := []uint64{0} + st, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ + Validators: registry, + Balances: balances, + Eth1Data: eth1Data, + Fork: ð.Fork{ + PreviousVersion: params.BeaconConfig().ElectraForkVersion, + CurrentVersion: params.BeaconConfig().ElectraForkVersion, + }, + }) + require.NoError(t, err) + pdSt, err := electra.ProcessDeposits(context.Background(), st, deps) + require.NoError(t, err) + pbd, err := pdSt.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().MinActivationBalance, pbd[2].Amount) + require.Equal(t, 3, len(pbd)) +} + +func TestProcessDeposit_SkipsInvalidDeposit(t *testing.T) { + // Same test settings as in TestProcessDeposit_AddsNewValidatorDeposit, except that we use an invalid signature + dep, _, err := util.DeterministicDepositsAndKeys(1) + require.NoError(t, err) + dep[0].Data.Signature = make([]byte, 96) + dt, _, err := util.DepositTrieFromDeposits(dep) + require.NoError(t, err) + root, err := dt.HashTreeRoot() + require.NoError(t, err) + eth1Data := ð.Eth1Data{ + DepositRoot: root[:], + DepositCount: 1, + } + registry := []*eth.Validator{ + { + PublicKey: []byte{1}, + WithdrawalCredentials: []byte{1, 2, 3}, + }, + } + balances := []uint64{0} + beaconState, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ + Validators: registry, + Balances: balances, + Eth1Data: eth1Data, + Fork: ð.Fork{ + PreviousVersion: params.BeaconConfig().GenesisForkVersion, + CurrentVersion: params.BeaconConfig().GenesisForkVersion, + }, + }) + require.NoError(t, err) + newState, err := electra.ProcessDeposit(beaconState, dep[0], true) + require.NoError(t, err, "Expected invalid block deposit to be ignored without error") + + if newState.Eth1DepositIndex() != 1 { + t.Errorf( + "Expected Eth1DepositIndex to be increased by 1 after processing an invalid deposit, received change: %v", + newState.Eth1DepositIndex(), + ) + } + if len(newState.Validators()) != 1 { + t.Errorf("Expected validator list to have length 1, received: %v", len(newState.Validators())) + } + if len(newState.Balances()) != 1 { + t.Errorf("Expected validator balances list to have length 1, received: %v", len(newState.Balances())) + } + if newState.Balances()[0] != 0 { + t.Errorf("Expected validator balance at index 0 to stay 0, received: %v", newState.Balances()[0]) + } +} + +func TestApplyDeposit_TopUps_WithBadSignature(t *testing.T) { + st, _ := util.DeterministicGenesisStateElectra(t, 3) + sk, err := bls.RandKey() + require.NoError(t, err) + withdrawalCred := make([]byte, 32) + withdrawalCred[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte + topUpAmount := uint64(1234) + depositData := ð.Deposit_Data{ + PublicKey: sk.PublicKey().Marshal(), + Amount: topUpAmount, + WithdrawalCredentials: withdrawalCred, + Signature: make([]byte, fieldparams.BLSSignatureLength), + } + vals := st.Validators() + vals[0].PublicKey = sk.PublicKey().Marshal() + vals[0].WithdrawalCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte + require.NoError(t, st.SetValidators(vals)) + adSt, err := electra.ApplyDeposit(st, depositData, true) + require.NoError(t, err) + pbd, err := adSt.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, 1, len(pbd)) + require.Equal(t, topUpAmount, pbd[0].Amount) +} + +func TestApplyDeposit_Electra_SwitchToCompoundingValidator(t *testing.T) { + st, _ := util.DeterministicGenesisStateElectra(t, 3) + sk, err := bls.RandKey() + require.NoError(t, err) + withdrawalCred := make([]byte, 32) + withdrawalCred[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte + depositData := ð.Deposit_Data{ + PublicKey: sk.PublicKey().Marshal(), + Amount: 1000, + WithdrawalCredentials: withdrawalCred, + Signature: make([]byte, fieldparams.BLSSignatureLength), + } + vals := st.Validators() + vals[0].PublicKey = sk.PublicKey().Marshal() + vals[0].WithdrawalCredentials[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte + require.NoError(t, st.SetValidators(vals)) + bals := st.Balances() + bals[0] = params.BeaconConfig().MinActivationBalance + 2000 + require.NoError(t, st.SetBalances(bals)) + sr, err := signing.ComputeSigningRoot(depositData, bytesutil.ToBytes(3, 32)) + require.NoError(t, err) + sig := sk.Sign(sr[:]) + depositData.Signature = sig.Marshal() + adSt, err := electra.ApplyDeposit(st, depositData, false) + require.NoError(t, err) + pbd, err := adSt.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, 2, len(pbd)) + require.Equal(t, uint64(1000), pbd[0].Amount) + require.Equal(t, uint64(2000), pbd[1].Amount) +} diff --git a/beacon-chain/core/electra/transition.go b/beacon-chain/core/electra/transition.go index 12accfccad78..4b2c62269cf6 100644 --- a/beacon-chain/core/electra/transition.go +++ b/beacon-chain/core/electra/transition.go @@ -2,12 +2,15 @@ package electra import ( "context" + "fmt" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/altair" e "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/epoch" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/epoch/precompute" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "go.opencensus.io/trace" ) @@ -127,3 +130,33 @@ func ProcessEpoch(ctx context.Context, state state.BeaconState) error { return nil } + +// VerifyBlockDepositLength +// +// Spec definition: +// +// # [Modified in Electra:EIP6110] +// # Disable former deposit mechanism once all prior deposits are processed +// eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_requests_start_index) +// if state.eth1_deposit_index < eth1_deposit_index_limit: +// assert len(body.deposits) == min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index) +// else: +// assert len(body.deposits) == 0 +func VerifyBlockDepositLength(body interfaces.ReadOnlyBeaconBlockBody, state state.BeaconState) error { + eth1Data := state.Eth1Data() + requestsStartIndex, err := state.DepositRequestsStartIndex() + if err != nil { + return errors.Wrap(err, "failed to get requests start index") + } + eth1DepositIndexLimit := min(eth1Data.DepositCount, requestsStartIndex) + if state.Eth1DepositIndex() < eth1DepositIndexLimit { + if uint64(len(body.Deposits())) != min(params.BeaconConfig().MaxDeposits, eth1DepositIndexLimit-state.Eth1DepositIndex()) { + return fmt.Errorf("incorrect outstanding deposits in block body, wanted: %d, got: %d", min(params.BeaconConfig().MaxDeposits, eth1DepositIndexLimit-state.Eth1DepositIndex()), len(body.Deposits())) + } + } else { + if len(body.Deposits()) != 0 { + return fmt.Errorf("incorrect outstanding deposits in block body, wanted: %d, got: %d", 0, len(body.Deposits())) + } + } + return nil +} diff --git a/beacon-chain/core/electra/transition_no_verify_sig.go b/beacon-chain/core/electra/transition_no_verify_sig.go new file mode 100644 index 000000000000..55ed84c2b3e6 --- /dev/null +++ b/beacon-chain/core/electra/transition_no_verify_sig.go @@ -0,0 +1,98 @@ +package electra + +import ( + "context" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" + v "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/validators" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" +) + +var ( + ProcessBLSToExecutionChanges = blocks.ProcessBLSToExecutionChanges + ProcessVoluntaryExits = blocks.ProcessVoluntaryExits + ProcessAttesterSlashings = blocks.ProcessAttesterSlashings + ProcessProposerSlashings = blocks.ProcessProposerSlashings +) + +// ProcessOperations +// +// Spec definition: +// +// def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: +// # [Modified in Electra:EIP6110] +// # Disable former deposit mechanism once all prior deposits are processed +// eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_requests_start_index) +// if state.eth1_deposit_index < eth1_deposit_index_limit: +// assert len(body.deposits) == min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index) +// else: +// assert len(body.deposits) == 0 +// +// def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: +// for operation in operations: +// fn(state, operation) +// +// for_ops(body.proposer_slashings, process_proposer_slashing) +// for_ops(body.attester_slashings, process_attester_slashing) +// for_ops(body.attestations, process_attestation) # [Modified in Electra:EIP7549] +// for_ops(body.deposits, process_deposit) # [Modified in Electra:EIP7251] +// for_ops(body.voluntary_exits, process_voluntary_exit) # [Modified in Electra:EIP7251] +// for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) +// # [New in Electra:EIP7002:EIP7251] +// for_ops(body.execution_payload.withdrawal_requests, process_execution_layer_withdrawal_request) +// for_ops(body.execution_payload.deposit_requests, process_deposit_requests) # [New in Electra:EIP6110] +// for_ops(body.consolidations, process_consolidation) # [New in Electra:EIP7251] +func ProcessOperations( + ctx context.Context, + st state.BeaconState, + block interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { + // 6110 validations are in VerifyOperationLengths + bb := block.Body() + // Electra extends the altair operations. + st, err := ProcessProposerSlashings(ctx, st, bb.ProposerSlashings(), v.SlashValidator) + if err != nil { + return nil, errors.Wrap(err, "could not process altair proposer slashing") + } + st, err = ProcessAttesterSlashings(ctx, st, bb.AttesterSlashings(), v.SlashValidator) + if err != nil { + return nil, errors.Wrap(err, "could not process altair attester slashing") + } + st, err = ProcessAttestationsNoVerifySignature(ctx, st, block) + if err != nil { + return nil, errors.Wrap(err, "could not process altair attestation") + } + if _, err := ProcessDeposits(ctx, st, bb.Deposits()); err != nil { // new in electra + return nil, errors.Wrap(err, "could not process altair deposit") + } + st, err = ProcessVoluntaryExits(ctx, st, bb.VoluntaryExits()) + if err != nil { + return nil, errors.Wrap(err, "could not process voluntary exits") + } + st, err = ProcessBLSToExecutionChanges(st, block) + if err != nil { + return nil, errors.Wrap(err, "could not process bls-to-execution changes") + } + // new in electra + e, err := bb.Execution() + if err != nil { + return nil, errors.Wrap(err, "could not get execution data from block") + } + exe, ok := e.(interfaces.ExecutionDataElectra) + if !ok { + return nil, errors.New("could not cast execution data to electra execution data") + } + st, err = ProcessWithdrawalRequests(ctx, st, exe.WithdrawalRequests()) + if err != nil { + return nil, errors.Wrap(err, "could not process execution layer withdrawal requests") + } + + st, err = ProcessDepositRequests(ctx, st, exe.DepositRequests()) // TODO: EIP-6110 deposit changes. + if err != nil { + return nil, errors.Wrap(err, "could not process deposit receipts") + } + + // TODO: Process consolidations from execution header. + return st, nil +} diff --git a/beacon-chain/core/electra/transition_test.go b/beacon-chain/core/electra/transition_test.go new file mode 100644 index 000000000000..5c111d2403dd --- /dev/null +++ b/beacon-chain/core/electra/transition_test.go @@ -0,0 +1,49 @@ +package electra_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + consensusblocks "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" +) + +func TestVerifyOperationLengths_Electra(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + s, _ := util.DeterministicGenesisStateElectra(t, 1) + sb, err := consensusblocks.NewSignedBeaconBlock(util.NewBeaconBlockElectra()) + require.NoError(t, err) + require.NoError(t, electra.VerifyBlockDepositLength(sb.Block().Body(), s)) + }) + t.Run("eth1depositIndex less than eth1depositIndexLimit & number of deposits incorrect", func(t *testing.T) { + s, _ := util.DeterministicGenesisStateElectra(t, 1) + sb, err := consensusblocks.NewSignedBeaconBlock(util.NewBeaconBlockElectra()) + require.NoError(t, err) + require.NoError(t, s.SetEth1DepositIndex(0)) + require.NoError(t, s.SetDepositRequestsStartIndex(1)) + err = electra.VerifyBlockDepositLength(sb.Block().Body(), s) + require.ErrorContains(t, "incorrect outstanding deposits in block body", err) + }) + t.Run("eth1depositIndex more than eth1depositIndexLimit & number of deposits is not 0", func(t *testing.T) { + s, _ := util.DeterministicGenesisStateElectra(t, 1) + sb, err := consensusblocks.NewSignedBeaconBlock(util.NewBeaconBlockElectra()) + require.NoError(t, err) + sb.SetDeposits([]*ethpb.Deposit{ + { + Data: ðpb.Deposit_Data{ + PublicKey: []byte{1, 2, 3}, + Amount: 1000, + WithdrawalCredentials: make([]byte, common.AddressLength), + Signature: []byte{4, 5, 6}, + }, + }, + }) + require.NoError(t, s.SetEth1DepositIndex(1)) + require.NoError(t, s.SetDepositRequestsStartIndex(1)) + err = electra.VerifyBlockDepositLength(sb.Block().Body(), s) + require.ErrorContains(t, "incorrect outstanding deposits in block body", err) + }) +} diff --git a/beacon-chain/core/electra/validator.go b/beacon-chain/core/electra/validator.go index 91fb0a87021a..82b41fd8d7b4 100644 --- a/beacon-chain/core/electra/validator.go +++ b/beacon-chain/core/electra/validator.go @@ -7,8 +7,74 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" ) +// AddValidatorToRegistry updates the beacon state with validator information +// def add_validator_to_registry(state: BeaconState, +// +// pubkey: BLSPubkey, +// withdrawal_credentials: Bytes32, +// amount: uint64) -> None: +// index = get_index_for_new_validator(state) +// validator = get_validator_from_deposit(pubkey, withdrawal_credentials) +// set_or_append_list(state.validators, index, validator) +// set_or_append_list(state.balances, index, 0) # [Modified in Electra:EIP7251] +// set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) +// set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) +// set_or_append_list(state.inactivity_scores, index, uint64(0)) +// state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) # [New in Electra:EIP7251] +func AddValidatorToRegistry(beaconState state.BeaconState, pubKey []byte, withdrawalCredentials []byte, amount uint64) error { + val := ValidatorFromDeposit(pubKey, withdrawalCredentials) + if err := beaconState.AppendValidator(val); err != nil { + return err + } + index, ok := beaconState.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubKey)) + if !ok { + return errors.New("could not find validator in registry") + } + if err := beaconState.AppendBalance(0); err != nil { + return err + } + if err := beaconState.AppendPendingBalanceDeposit(index, amount); err != nil { + return err + } + if err := beaconState.AppendInactivityScore(0); err != nil { + return err + } + if err := beaconState.AppendPreviousParticipationBits(0); err != nil { + return err + } + return beaconState.AppendCurrentParticipationBits(0) +} + +// ValidatorFromDeposit gets a new validator object with provided parameters +// +// def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32) -> Validator: +// +// return Validator( +// pubkey=pubkey, +// withdrawal_credentials=withdrawal_credentials, +// activation_eligibility_epoch=FAR_FUTURE_EPOCH, +// activation_epoch=FAR_FUTURE_EPOCH, +// exit_epoch=FAR_FUTURE_EPOCH, +// withdrawable_epoch=FAR_FUTURE_EPOCH, +// effective_balance=0, # [Modified in Electra:EIP7251] +// +// ) +func ValidatorFromDeposit(pubKey []byte, withdrawalCredentials []byte) *ethpb.Validator { + return ðpb.Validator{ + PublicKey: pubKey, + WithdrawalCredentials: withdrawalCredentials, + ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, + ActivationEpoch: params.BeaconConfig().FarFutureEpoch, + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + WithdrawableEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: 0, // [Modified in Electra:EIP7251] + } +} + // SwitchToCompoundingValidator // // Spec definition: @@ -36,7 +102,7 @@ func SwitchToCompoundingValidator(s state.BeaconState, idx primitives.ValidatorI return nil } -// QueueExcessActiveBalance +// QueueExcessActiveBalance queues validators with balances above the min activation balance and adds to pending balance deposit. // // Spec definition: // diff --git a/beacon-chain/core/electra/validator_test.go b/beacon-chain/core/electra/validator_test.go index b7d898f66420..a1fa8308ba22 100644 --- a/beacon-chain/core/electra/validator_test.go +++ b/beacon-chain/core/electra/validator_test.go @@ -6,6 +6,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" 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" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -13,6 +14,20 @@ import ( "github.com/prysmaticlabs/prysm/v5/testing/util" ) +func TestAddValidatorToRegistry(t *testing.T) { + st, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{}) + require.NoError(t, err) + require.NoError(t, electra.AddValidatorToRegistry(st, make([]byte, fieldparams.BLSPubkeyLength), make([]byte, fieldparams.RootLength), 100)) + balances := st.Balances() + require.Equal(t, 1, len(balances)) + require.Equal(t, uint64(0), balances[0]) + pbds, err := st.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, 1, len(pbds)) + require.Equal(t, uint64(100), pbds[0].Amount) + require.Equal(t, primitives.ValidatorIndex(0), pbds[0].Index) +} + func TestSwitchToCompoundingValidator(t *testing.T) { s, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ Validators: []*eth.Validator{ diff --git a/beacon-chain/core/electra/withdrawals_test.go b/beacon-chain/core/electra/withdrawals_test.go index 4f2634ebd6a2..da00cdd9cb77 100644 --- a/beacon-chain/core/electra/withdrawals_test.go +++ b/beacon-chain/core/electra/withdrawals_test.go @@ -81,7 +81,6 @@ func TestProcessWithdrawRequests(t *testing.T) { })) _, err = wantPostSt.ExitEpochAndUpdateChurn(primitives.Gwei(v.EffectiveBalance)) require.NoError(t, err) - require.DeepEqual(t, wantPostSt.Validators(), got.Validators()) webc, err := wantPostSt.ExitBalanceToConsume() require.NoError(t, err) gebc, err := got.ExitBalanceToConsume() @@ -110,10 +109,7 @@ func TestProcessWithdrawRequests(t *testing.T) { prefix[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte v.WithdrawalCredentials = append(prefix, source...) require.NoError(t, preSt.SetValidators([]*eth.Validator{v})) - bal, err := preSt.BalanceAtIndex(0) - require.NoError(t, err) - bal += 200 - require.NoError(t, preSt.SetBalances([]uint64{bal})) + require.NoError(t, preSt.SetBalances([]uint64{params.BeaconConfig().MinActivationBalance + 200})) require.NoError(t, preSt.AppendPendingPartialWithdrawal(ð.PendingPartialWithdrawal{ Index: 0, Amount: 100, @@ -168,7 +164,6 @@ func TestProcessWithdrawRequests(t *testing.T) { require.Equal(t, wece, gece) _, err = wantPostSt.ExitEpochAndUpdateChurn(primitives.Gwei(100)) require.NoError(t, err) - require.DeepEqual(t, wantPostSt.Validators(), got.Validators()) webc, err := wantPostSt.ExitBalanceToConsume() require.NoError(t, err) gebc, err := got.ExitBalanceToConsume() diff --git a/beacon-chain/core/helpers/validators.go b/beacon-chain/core/helpers/validators.go index ccee87add7c5..f1ea6376c066 100644 --- a/beacon-chain/core/helpers/validators.go +++ b/beacon-chain/core/helpers/validators.go @@ -525,16 +525,16 @@ func HasCompoundingWithdrawalCredential(v interfaces.WithWithdrawalCredentials) if v == nil { return false } - return isCompoundingWithdrawalCredential(v.GetWithdrawalCredentials()) + return IsCompoundingWithdrawalCredential(v.GetWithdrawalCredentials()) } -// isCompoundingWithdrawalCredential checks if the credentials are a compounding withdrawal credential. +// IsCompoundingWithdrawalCredential checks if the credentials are a compounding withdrawal credential. // // Spec definition: // // def is_compounding_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: // return withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX -func isCompoundingWithdrawalCredential(creds []byte) bool { +func IsCompoundingWithdrawalCredential(creds []byte) bool { return bytes.HasPrefix(creds, []byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte}) } diff --git a/beacon-chain/core/transition/BUILD.bazel b/beacon-chain/core/transition/BUILD.bazel index 9f2ea105cb2e..0f5bb260b4b8 100644 --- a/beacon-chain/core/transition/BUILD.bazel +++ b/beacon-chain/core/transition/BUILD.bazel @@ -40,7 +40,6 @@ go_library( "//crypto/bls:go_default_library", "//crypto/hash:go_default_library", "//encoding/bytesutil:go_default_library", - "//math:go_default_library", "//monitoring/tracing:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", diff --git a/beacon-chain/core/transition/transition.go b/beacon-chain/core/transition/transition.go index d8f4a3b24289..cc7a38341a06 100644 --- a/beacon-chain/core/transition/transition.go +++ b/beacon-chain/core/transition/transition.go @@ -24,7 +24,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" - "github.com/prysmaticlabs/prysm/v5/math" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" "github.com/prysmaticlabs/prysm/v5/runtime/version" "go.opencensus.io/trace" @@ -376,14 +375,25 @@ func VerifyOperationLengths(_ context.Context, state state.BeaconState, b interf if eth1Data == nil { return nil, errors.New("nil eth1data in state") } - if state.Eth1DepositIndex() > eth1Data.DepositCount { - return nil, fmt.Errorf("expected state.deposit_index %d <= eth1data.deposit_count %d", state.Eth1DepositIndex(), eth1Data.DepositCount) - } - maxDeposits := math.Min(params.BeaconConfig().MaxDeposits, eth1Data.DepositCount-state.Eth1DepositIndex()) - // Verify outstanding deposits are processed up to max number of deposits - if uint64(len(body.Deposits())) != maxDeposits { - return nil, fmt.Errorf("incorrect outstanding deposits in block body, wanted: %d, got: %d", - maxDeposits, len(body.Deposits())) + + if state.Version() < version.Electra { + // Deneb specs + // # Verify that outstanding deposits are processed up to the maximum number of deposits + // assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index) + if state.Eth1DepositIndex() > eth1Data.DepositCount { + return nil, fmt.Errorf("expected state.deposit_index %d <= eth1data.deposit_count %d", state.Eth1DepositIndex(), eth1Data.DepositCount) + } + maxDeposits := min(params.BeaconConfig().MaxDeposits, eth1Data.DepositCount-state.Eth1DepositIndex()) + // Verify outstanding deposits are processed up to max number of deposits + if uint64(len(body.Deposits())) != maxDeposits { + return nil, fmt.Errorf("incorrect outstanding deposits in block body, wanted: %d, got: %d", + maxDeposits, len(body.Deposits())) + } + } else { + // Electra + if err := electra.VerifyBlockDepositLength(body, state); err != nil { + return nil, errors.Wrap(err, "failed to verify block deposit length") + } } return state, nil diff --git a/beacon-chain/core/transition/transition_no_verify_sig.go b/beacon-chain/core/transition/transition_no_verify_sig.go index 5d85879d8142..17bc3b7d435d 100644 --- a/beacon-chain/core/transition/transition_no_verify_sig.go +++ b/beacon-chain/core/transition/transition_no_verify_sig.go @@ -262,24 +262,21 @@ func ProcessOperationsNoVerifyAttsSigs( } var err error - switch beaconBlock.Version() { - case version.Phase0: + if beaconBlock.Version() == version.Phase0 { state, err = phase0Operations(ctx, state, beaconBlock) if err != nil { return nil, err } - case version.Altair, version.Bellatrix, version.Capella, version.Deneb: + } else if beaconBlock.Version() < version.Electra { state, err = altairOperations(ctx, state, beaconBlock) if err != nil { return nil, err } - case version.Electra: - state, err = electraOperations(ctx, state, beaconBlock) + } else { + state, err = electra.ProcessOperations(ctx, state, beaconBlock) if err != nil { return nil, err } - default: - return nil, errors.New("block does not have correct version") } return state, nil @@ -394,67 +391,6 @@ func VerifyBlobCommitmentCount(blk interfaces.ReadOnlyBeaconBlock) error { return nil } -// electraOperations -// -// Spec definition: -// -// def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: -// # [Modified in Electra:EIP6110] -// # Disable former deposit mechanism once all prior deposits are processed -// eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_requests_start_index) -// if state.eth1_deposit_index < eth1_deposit_index_limit: -// assert len(body.deposits) == min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index) -// else: -// assert len(body.deposits) == 0 -// -// def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: -// for operation in operations: -// fn(state, operation) -// -// for_ops(body.proposer_slashings, process_proposer_slashing) -// for_ops(body.attester_slashings, process_attester_slashing) -// for_ops(body.attestations, process_attestation) # [Modified in Electra:EIP7549] -// for_ops(body.deposits, process_deposit) # [Modified in Electra:EIP7251] -// for_ops(body.voluntary_exits, process_voluntary_exit) # [Modified in Electra:EIP7251] -// for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) -// # [New in Electra:EIP7002:EIP7251] -// for_ops(body.execution_payload.withdrawal_requests, process_execution_layer_withdrawal_request) -// for_ops(body.execution_payload.deposit_requests, process_deposit_requests) # [New in Electra:EIP6110] -// for_ops(body.consolidations, process_consolidation) # [New in Electra:EIP7251] -func electraOperations( - ctx context.Context, - st state.BeaconState, - block interfaces.ReadOnlyBeaconBlock) (state.BeaconState, error) { - // 6110 validations are in VerifyOperationLengths - - // Electra extends the altair operations. - st, err := altairOperations(ctx, st, block) - if err != nil { - return nil, err - } - e, err := block.Body().Execution() - if err != nil { - return nil, errors.Wrap(err, "could not get execution data from block") - } - exe, ok := e.(interfaces.ExecutionDataElectra) - if !ok { - return nil, errors.New("could not cast execution data to electra execution data") - } - st, err = electra.ProcessWithdrawalRequests(ctx, st, exe.WithdrawalRequests()) - if err != nil { - return nil, errors.Wrap(err, "could not process execution layer withdrawal requests") - } - - st, err = electra.ProcessDepositRequests(ctx, st, exe.DepositRequests()) // TODO: EIP-6110 deposit changes. - if err != nil { - return nil, errors.Wrap(err, "could not process deposit receipts") - } - - // TODO: Process consolidations from execution header. - - return st, nil -} - // This calls altair block operations. func altairOperations( ctx context.Context, diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits.go index d2a6ecfb4bda..a32c68c42233 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits.go @@ -10,7 +10,9 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/container/trie" + "github.com/prysmaticlabs/prysm/v5/math" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/sirupsen/logrus" "go.opencensus.io/trace" "golang.org/x/sync/errgroup" @@ -107,9 +109,32 @@ func (vs *Server) deposits( // deposits are sorted from lowest to highest. var pendingDeps []*ethpb.DepositContainer for _, dep := range allPendingContainers { - if uint64(dep.Index) >= beaconState.Eth1DepositIndex() && uint64(dep.Index) < canonicalEth1Data.DepositCount { - pendingDeps = append(pendingDeps, dep) + if beaconState.Version() < version.Electra { + // Add deposits up to min(MAX_DEPOSITS, eth1_data.deposit_count - state.eth1_deposit_index) + if uint64(dep.Index) >= beaconState.Eth1DepositIndex() && uint64(dep.Index) < canonicalEth1Data.DepositCount { + pendingDeps = append(pendingDeps, dep) + } + } else { + // Electra change EIP6110 + // def get_eth1_pending_deposit_count(state: BeaconState) -> uint64: + // eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_requests_start_index) + // if state.eth1_deposit_index < eth1_deposit_index_limit: + // return min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index) + // else: + // return uint64(0) + requestsStartIndex, err := beaconState.DepositRequestsStartIndex() + if err != nil { + return nil, errors.Wrap(err, "could not retrieve requests start index") + } + eth1DepositIndexLimit := math.Min(canonicalEth1Data.DepositCount, requestsStartIndex) + if beaconState.Eth1DepositIndex() < eth1DepositIndexLimit { + if uint64(dep.Index) >= beaconState.Eth1DepositIndex() && uint64(dep.Index) < eth1DepositIndexLimit { + pendingDeps = append(pendingDeps, dep) + } + } + // just don't add any pending deps if it's not state.eth1_deposit_index < eth1_deposit_index_limit } + // Don't try to pack more than the max allowed in a block if uint64(len(pendingDeps)) == params.BeaconConfig().MaxDeposits { break diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits_test.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits_test.go index 00f96db41b55..3b58a88f4fe1 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits_test.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_deposits_test.go @@ -1,6 +1,22 @@ package validator -import "testing" +import ( + "context" + "math/big" + "testing" + + mock "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/cache/depositsnapshot" + mockExecution "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution/testing" + state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/container/trie" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/assert" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" +) func TestShouldFallback(t *testing.T) { tests := []struct { @@ -54,3 +70,145 @@ func TestShouldFallback(t *testing.T) { }) } } + +func TestProposer_PendingDeposits_Electra(t *testing.T) { + // Electra continues to pack deposits while the state eth1deposit index is less than the eth1depositIndexLimit + ctx := context.Background() + + height := big.NewInt(int64(params.BeaconConfig().Eth1FollowDistance)) + newHeight := big.NewInt(height.Int64() + 11000) + p := &mockExecution.Chain{ + LatestBlockNumber: height, + HashesByHeight: map[int][]byte{ + int(height.Int64()): []byte("0x0"), + int(newHeight.Int64()): []byte("0x1"), + }, + } + + var votes []*ethpb.Eth1Data + + vote := ðpb.Eth1Data{ + BlockHash: bytesutil.PadTo([]byte("0x1"), 32), + DepositRoot: make([]byte, 32), + DepositCount: 7, + } + period := uint64(params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().EpochsPerEth1VotingPeriod))) + for i := 0; i <= int(period/2); i++ { + votes = append(votes, vote) + } + + beaconState, err := state_native.InitializeFromProtoElectra(ðpb.BeaconStateElectra{ + Eth1Data: ðpb.Eth1Data{ + BlockHash: []byte("0x0"), + DepositRoot: make([]byte, 32), + DepositCount: 5, + }, + Eth1DepositIndex: 1, + DepositRequestsStartIndex: 7, + Eth1DataVotes: votes, + }) + require.NoError(t, err) + blk := util.NewBeaconBlockElectra() + blk.Block.Slot = beaconState.Slot() + + blkRoot, err := blk.HashTreeRoot() + require.NoError(t, err) + + var mockSig [96]byte + var mockCreds [32]byte + + // Using the merkleTreeIndex as the block number for this test... + readyDeposits := []*ethpb.DepositContainer{ + { + Index: 0, + Eth1BlockHeight: 8, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: bytesutil.PadTo([]byte("a"), 48), + Signature: mockSig[:], + WithdrawalCredentials: mockCreds[:], + }}, + }, + { + Index: 1, + Eth1BlockHeight: 14, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: bytesutil.PadTo([]byte("b"), 48), + Signature: mockSig[:], + WithdrawalCredentials: mockCreds[:], + }}, + }, + } + + recentDeposits := []*ethpb.DepositContainer{ + { + Index: 2, + Eth1BlockHeight: 5000, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: bytesutil.PadTo([]byte("c"), 48), + Signature: mockSig[:], + WithdrawalCredentials: mockCreds[:], + }}, + }, + { + Index: 3, + Eth1BlockHeight: 6000, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: bytesutil.PadTo([]byte("d"), 48), + Signature: mockSig[:], + WithdrawalCredentials: mockCreds[:], + }}, + }, + } + + depositCache, err := depositsnapshot.New() + require.NoError(t, err) + + depositTrie, err := trie.NewTrie(params.BeaconConfig().DepositContractTreeDepth) + require.NoError(t, err, "Could not setup deposit trie") + for _, dp := range append(readyDeposits, recentDeposits...) { + depositHash, err := dp.Deposit.Data.HashTreeRoot() + require.NoError(t, err, "Unable to determine hashed value of deposit") + + assert.NoError(t, depositTrie.Insert(depositHash[:], int(dp.Index))) + root, err := depositTrie.HashTreeRoot() + require.NoError(t, err) + assert.NoError(t, depositCache.InsertDeposit(ctx, dp.Deposit, dp.Eth1BlockHeight, dp.Index, root)) + } + for _, dp := range recentDeposits { + root, err := depositTrie.HashTreeRoot() + require.NoError(t, err) + depositCache.InsertPendingDeposit(ctx, dp.Deposit, dp.Eth1BlockHeight, dp.Index, root) + } + + bs := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + DepositFetcher: depositCache, + PendingDepositsFetcher: depositCache, + BlockReceiver: &mock.ChainService{State: beaconState, Root: blkRoot[:]}, + HeadFetcher: &mock.ChainService{State: beaconState, Root: blkRoot[:]}, + } + + deposits, err := bs.deposits(ctx, beaconState, ðpb.Eth1Data{}) + require.NoError(t, err) + assert.Equal(t, 0, len(deposits), "Received unexpected list of deposits") + + // It should also return the recent deposits after their follow window. + p.LatestBlockNumber = big.NewInt(0).Add(p.LatestBlockNumber, big.NewInt(10000)) + // we should get our pending deposits once this vote pushes the vote tally to include + // the updated eth1 data. + deposits, err = bs.deposits(ctx, beaconState, vote) + require.NoError(t, err) + assert.Equal(t, len(recentDeposits), len(deposits), "Received unexpected number of pending deposits") + + require.NoError(t, beaconState.SetDepositRequestsStartIndex(0)) // set it to 0 so it's less than Eth1DepositIndex + deposits, err = bs.deposits(ctx, beaconState, vote) + require.NoError(t, err) + assert.Equal(t, 0, len(deposits), "Received unexpected number of pending deposits") + +} diff --git a/runtime/interop/premine-state.go b/runtime/interop/premine-state.go index ab18e1cde35c..235f8a1968e3 100644 --- a/runtime/interop/premine-state.go +++ b/runtime/interop/premine-state.go @@ -66,7 +66,7 @@ func NewPreminedGenesis(ctx context.Context, t, nvals, pCreds uint64, version in func (s *PremineGenesisConfig) prepare(ctx context.Context) (state.BeaconState, error) { switch s.Version { - case version.Phase0, version.Altair, version.Bellatrix, version.Capella, version.Deneb: + case version.Phase0, version.Altair, version.Bellatrix, version.Capella, version.Deneb, version.Electra: default: return nil, errors.Wrapf(errUnsupportedVersion, "version=%s", version.String(s.Version)) } @@ -205,6 +205,8 @@ func (s *PremineGenesisConfig) processDeposits(ctx context.Context, g state.Beac if _, err = helpers.UpdateGenesisEth1Data(g, deposits, g.Eth1Data()); err != nil { return err } + + // TODO: should be updated when electra E2E is updated _, err = altair.ProcessPreGenesisDeposits(ctx, g, deposits) if err != nil { return errors.Wrap(err, "could not process validator deposits") diff --git a/testing/spectest/mainnet/electra/operations/BUILD.bazel b/testing/spectest/mainnet/electra/operations/BUILD.bazel index 21391d470691..82249f1b97e2 100644 --- a/testing/spectest/mainnet/electra/operations/BUILD.bazel +++ b/testing/spectest/mainnet/electra/operations/BUILD.bazel @@ -8,6 +8,8 @@ go_test( "block_header_test.go", "bls_to_execution_change_test.go", "consolidation_test.go", + "deposit_requests_test.go", + "deposit_test.go", "execution_layer_withdrawals_test.go", "execution_payload_test.go", "proposer_slashing_test.go", diff --git a/testing/spectest/mainnet/electra/operations/deposit_requests_test.go b/testing/spectest/mainnet/electra/operations/deposit_requests_test.go new file mode 100644 index 000000000000..45e2835fb3ab --- /dev/null +++ b/testing/spectest/mainnet/electra/operations/deposit_requests_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMainnet_Electra_Operations_DepositRequests(t *testing.T) { + operations.RunDepositRequestsTest(t, "mainnet") +} diff --git a/testing/spectest/mainnet/electra/operations/deposit_test.go b/testing/spectest/mainnet/electra/operations/deposit_test.go new file mode 100644 index 000000000000..c53dc32d3082 --- /dev/null +++ b/testing/spectest/mainnet/electra/operations/deposit_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMainnet_Electra_Operations_Deposit(t *testing.T) { + operations.RunDepositTest(t, "mainnet") +} diff --git a/testing/spectest/minimal/electra/operations/BUILD.bazel b/testing/spectest/minimal/electra/operations/BUILD.bazel index 51d59c038bfb..4d44c8b94666 100644 --- a/testing/spectest/minimal/electra/operations/BUILD.bazel +++ b/testing/spectest/minimal/electra/operations/BUILD.bazel @@ -8,6 +8,8 @@ go_test( "block_header_test.go", "bls_to_execution_change_test.go", "consolidation_test.go", + "deposit_requests_test.go", + "deposit_test.go", "execution_layer_withdrawals_test.go", "execution_payload_test.go", "proposer_slashing_test.go", diff --git a/testing/spectest/minimal/electra/operations/deposit_requests_test.go b/testing/spectest/minimal/electra/operations/deposit_requests_test.go new file mode 100644 index 000000000000..ba06e23e1e31 --- /dev/null +++ b/testing/spectest/minimal/electra/operations/deposit_requests_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMainnet_Electra_Operations_DepositRequests(t *testing.T) { + operations.RunDepositRequestsTest(t, "minimal") +} diff --git a/testing/spectest/minimal/electra/operations/deposit_test.go b/testing/spectest/minimal/electra/operations/deposit_test.go new file mode 100644 index 000000000000..98879f411d9f --- /dev/null +++ b/testing/spectest/minimal/electra/operations/deposit_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMinimal_Electra_Operations_Deposit(t *testing.T) { + operations.RunDepositTest(t, "minimal") +} diff --git a/testing/spectest/shared/electra/operations/BUILD.bazel b/testing/spectest/shared/electra/operations/BUILD.bazel index 01d0b339e9c6..87a8537b8ece 100644 --- a/testing/spectest/shared/electra/operations/BUILD.bazel +++ b/testing/spectest/shared/electra/operations/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "block_header.go", "bls_to_execution_changes.go", "consolidations.go", + "deposit.go", "deposit_request.go", "execution_payload.go", "helpers.go", diff --git a/testing/spectest/shared/electra/operations/deposit.go b/testing/spectest/shared/electra/operations/deposit.go new file mode 100644 index 000000000000..254b440a88d1 --- /dev/null +++ b/testing/spectest/shared/electra/operations/deposit.go @@ -0,0 +1,38 @@ +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 RunDepositTest(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "operations/deposit/pyspec_tests") + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + depositFile, err := util.BazelFileBytes(folderPath, "deposit.ssz_snappy") + require.NoError(t, err) + depositSSZ, err := snappy.Decode(nil /* dst */, depositFile) + require.NoError(t, err, "Failed to decompress") + deposit := ðpb.Deposit{} + require.NoError(t, deposit.UnmarshalSSZ(depositSSZ), "Failed to unmarshal") + + body := ðpb.BeaconBlockBodyElectra{Deposits: []*ethpb.Deposit{deposit}} + processDepositsFunc := func(ctx context.Context, s state.BeaconState, b interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) { + return electra.ProcessDeposits(ctx, s, b.Block().Body().Deposits()) + } + RunBlockOperationTest(t, folderPath, body, processDepositsFunc) + }) + } +} diff --git a/testing/spectest/shared/electra/operations/deposit_request.go b/testing/spectest/shared/electra/operations/deposit_request.go index 4cb008a835ca..627e09883bf8 100644 --- a/testing/spectest/shared/electra/operations/deposit_request.go +++ b/testing/spectest/shared/electra/operations/deposit_request.go @@ -1,3 +1,43 @@ package operations -// TODO: Add tests. +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" + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + 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 RunDepositRequestsTest(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "operations/deposit_receipt/pyspec_tests") // TODO: update this + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + depositRequestFile, err := util.BazelFileBytes(folderPath, "deposit_receipt.ssz_snappy") // TODO: #14112 should update this value + require.NoError(t, err) + depositRequestSSZ, err := snappy.Decode(nil /* dst */, depositRequestFile) + require.NoError(t, err, "Failed to decompress") + depositRequest := &enginev1.DepositRequest{} + require.NoError(t, depositRequest.UnmarshalSSZ(depositRequestSSZ), "failed to unmarshal") + requests := []*enginev1.DepositRequest{depositRequest} + body := ðpb.BeaconBlockBodyElectra{ExecutionPayload: &enginev1.ExecutionPayloadElectra{DepositRequests: requests}} + processDepositRequestsFunc := func(ctx context.Context, s state.BeaconState, signedBlock interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) { + e, err := signedBlock.Block().Body().Execution() + require.NoError(t, err, "Failed to get execution") + ee, ok := e.(interfaces.ExecutionDataElectra) + require.Equal(t, true, ok, "Invalid execution payload") + return electra.ProcessDepositRequests(ctx, s, ee.DepositRequests()) + } + RunBlockOperationTest(t, folderPath, body, processDepositRequestsFunc) + }) + } +} diff --git a/testing/util/electra_state.go b/testing/util/electra_state.go index 4b5d74b3aa97..437d2e62d6f5 100644 --- a/testing/util/electra_state.go +++ b/testing/util/electra_state.go @@ -32,10 +32,24 @@ func DeterministicGenesisStateElectra(t testing.TB, numValidators uint64) (state if err != nil { t.Fatal(errors.Wrapf(err, "failed to get genesis beacon state of %d validators", numValidators)) } + if err := setKeysToActive(beaconState); err != nil { + t.Fatal(errors.Wrapf(err, "failed to set keys to active")) + } resetCache() return beaconState, privKeys } +// setKeysToActive is a function to set the validators to active post electra, electra no longer processes deposits based on eth1data +func setKeysToActive(beaconState state.BeaconState) error { + vals := make([]*ethpb.Validator, len(beaconState.Validators())) + for i, val := range beaconState.Validators() { + val.ActivationEpoch = 0 + val.EffectiveBalance = params.BeaconConfig().MinActivationBalance + vals[i] = val + } + return beaconState.SetValidators(vals) +} + // genesisBeaconStateElectra returns the genesis beacon state. func genesisBeaconStateElectra(ctx context.Context, deposits []*ethpb.Deposit, genesisTime uint64, eth1Data *ethpb.Eth1Data) (state.BeaconState, error) { st, err := emptyGenesisStateElectra()