diff --git a/beacon-chain/core/electra/deposits.go b/beacon-chain/core/electra/deposits.go index db18d5b575b7..80c16bea3668 100644 --- a/beacon-chain/core/electra/deposits.go +++ b/beacon-chain/core/electra/deposits.go @@ -2,6 +2,7 @@ package electra import ( "context" + "fmt" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" @@ -13,7 +14,9 @@ import ( "github.com/prysmaticlabs/prysm/v5/contracts/deposit" "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" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/time/slots" log "github.com/sirupsen/logrus" "go.opencensus.io/trace" ) @@ -190,12 +193,27 @@ func verifyDepositDataSigningRoot(obj *ethpb.Deposit_Data, domain []byte) error // available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit(state) // processed_amount = 0 // next_deposit_index = 0 +// deposits_to_postpone = [] // // for deposit in state.pending_balance_deposits: -// if processed_amount + deposit.amount > available_for_processing: -// break -// increase_balance(state, deposit.index, deposit.amount) -// processed_amount += deposit.amount +// validator = state.validators[deposit.index] +// # Validator is exiting, postpone the deposit until after withdrawable epoch +// if validator.exit_epoch < FAR_FUTURE_EPOCH: +// if get_current_epoch(state) <= validator.withdrawable_epoch: +// deposits_to_postpone.append(deposit) +// # Deposited balance will never become active. Increase balance but do not consume churn +// else: +// increase_balance(state, deposit.index, deposit.amount) +// # Validator is not exiting, attempt to process deposit +// else: +// # Deposit does not fit in the churn, no more deposit processing in this epoch. +// if processed_amount + deposit.amount > available_for_processing: +// break +// # Deposit fits in the churn, process it. Increase balance and consume churn. +// else: +// increase_balance(state, deposit.index, deposit.amount) +// processed_amount += deposit.amount +// # Regardless of how the deposit was handled, we move on in the queue. // next_deposit_index += 1 // // state.pending_balance_deposits = state.pending_balance_deposits[next_deposit_index:] @@ -204,6 +222,8 @@ func verifyDepositDataSigningRoot(obj *ethpb.Deposit_Data, domain []byte) error // state.deposit_balance_to_consume = Gwei(0) // else: // state.deposit_balance_to_consume = available_for_processing - processed_amount +// +// state.pending_balance_deposits += deposits_to_postpone func ProcessPendingBalanceDeposits(ctx context.Context, st state.BeaconState, activeBalance primitives.Gwei) error { _, span := trace.StartSpan(ctx, "electra.ProcessPendingBalanceDeposits") defer span.End() @@ -216,35 +236,69 @@ func ProcessPendingBalanceDeposits(ctx context.Context, st state.BeaconState, ac if err != nil { return err } - availableForProcessing := depBalToConsume + helpers.ActivationExitChurnLimit(activeBalance) + processedAmount := uint64(0) nextDepositIndex := 0 + var depositsToPostpone []*eth.PendingBalanceDeposit deposits, err := st.PendingBalanceDeposits() if err != nil { return err } + // constants + ffe := params.BeaconConfig().FarFutureEpoch + curEpoch := slots.ToEpoch(st.Slot()) + for _, balanceDeposit := range deposits { - if primitives.Gwei(balanceDeposit.Amount) > availableForProcessing { - break + v, err := st.ValidatorAtIndexReadOnly(balanceDeposit.Index) + if err != nil { + return fmt.Errorf("failed to fetch validator at index: %w", err) } - if err := helpers.IncreaseBalance(st, balanceDeposit.Index, balanceDeposit.Amount); err != nil { - return err + + // If the validator is currently exiting, postpone the deposit until after the withdrawable + // epoch. + if v.ExitEpoch() < ffe { + if curEpoch <= v.WithdrawableEpoch() { + depositsToPostpone = append(depositsToPostpone, balanceDeposit) + } else { + // The deposited balance will never become active. Therefore, we increase the balance but do + // not consume the churn. + if err := helpers.IncreaseBalance(st, balanceDeposit.Index, balanceDeposit.Amount); err != nil { + return err + } + } + } else { + // Validator is not exiting, attempt to process deposit. + if primitives.Gwei(processedAmount+balanceDeposit.Amount) > availableForProcessing { + break + } + // Deposit fits in churn, process it. Increase balance and consume churn. + if err := helpers.IncreaseBalance(st, balanceDeposit.Index, balanceDeposit.Amount); err != nil { + return err + } + processedAmount += balanceDeposit.Amount } - availableForProcessing -= primitives.Gwei(balanceDeposit.Amount) + + // Regardless of how the deposit was handled, we move on in the queue. nextDepositIndex++ } - deposits = deposits[nextDepositIndex:] + // Combined operation: + // - state.pending_balance_deposits = state.pending_balance_deposits[next_deposit_index:] + // - state.pending_balance_deposits += deposits_to_postpone + // However, the number of remaining deposits must be maintained to properly update the deposit + // balance to consume. + numRemainingDeposits := len(deposits[nextDepositIndex:]) + deposits = append(deposits[nextDepositIndex:], depositsToPostpone...) if err := st.SetPendingBalanceDeposits(deposits); err != nil { return err } - if len(deposits) == 0 { + if numRemainingDeposits == 0 { return st.SetDepositBalanceToConsume(0) } else { - return st.SetDepositBalanceToConsume(availableForProcessing) + return st.SetDepositBalanceToConsume(availableForProcessing - primitives.Gwei(processedAmount)) } } diff --git a/beacon-chain/core/electra/deposits_test.go b/beacon-chain/core/electra/deposits_test.go index fe160d55c4bc..e498ea613119 100644 --- a/beacon-chain/core/electra/deposits_test.go +++ b/beacon-chain/core/electra/deposits_test.go @@ -107,6 +107,80 @@ func TestProcessPendingBalanceDeposits(t *testing.T) { require.Equal(t, params.BeaconConfig().MinActivationBalance+uint64(amountAvailForProcessing)/5, b) } + // All of the balance deposits should have been processed. + remaining, err := st.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, 0, len(remaining)) + }, + }, + { + name: "exiting validator deposit postponed", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 1_000) + require.NoError(t, st.SetDepositBalanceToConsume(0)) + amountAvailForProcessing := helpers.ActivationExitChurnLimit(1_000 * 1e9) + deps := make([]*eth.PendingBalanceDeposit, 5) + for i := 0; i < len(deps); i += 1 { + deps[i] = ð.PendingBalanceDeposit{ + Amount: uint64(amountAvailForProcessing) / 5, + Index: primitives.ValidatorIndex(i), + } + } + require.NoError(t, st.SetPendingBalanceDeposits(deps)) + v, err := st.ValidatorAtIndex(0) + require.NoError(t, err) + v.ExitEpoch = 10 + v.WithdrawableEpoch = 20 + require.NoError(t, st.UpdateValidatorAtIndex(0, v)) + return st + }(), + check: func(t *testing.T, st state.BeaconState) { + amountAvailForProcessing := helpers.ActivationExitChurnLimit(1_000 * 1e9) + res, err := st.DepositBalanceToConsume() + require.NoError(t, err) + require.Equal(t, primitives.Gwei(0), res) + // Validators 1..4 should have their balance increased + for i := primitives.ValidatorIndex(1); i < 4; i++ { + b, err := st.BalanceAtIndex(i) + require.NoError(t, err) + require.Equal(t, params.BeaconConfig().MinActivationBalance+uint64(amountAvailForProcessing)/5, b) + } + + // All of the balance deposits should have been processed, except validator index 0 was + // added back to the pending deposits queue. + remaining, err := st.PendingBalanceDeposits() + require.NoError(t, err) + require.Equal(t, 1, len(remaining)) + }, + }, + { + name: "exited validator balance increased", + state: func() state.BeaconState { + st := stateWithActiveBalanceETH(t, 1_000) + deps := make([]*eth.PendingBalanceDeposit, 1) + for i := 0; i < len(deps); i += 1 { + deps[i] = ð.PendingBalanceDeposit{ + Amount: 1_000_000, + Index: primitives.ValidatorIndex(i), + } + } + require.NoError(t, st.SetPendingBalanceDeposits(deps)) + v, err := st.ValidatorAtIndex(0) + require.NoError(t, err) + v.ExitEpoch = 2 + v.WithdrawableEpoch = 8 + require.NoError(t, st.UpdateValidatorAtIndex(0, v)) + require.NoError(t, st.UpdateBalancesAtIndex(0, 100_000)) + return st + }(), + check: func(t *testing.T, st state.BeaconState) { + res, err := st.DepositBalanceToConsume() + require.NoError(t, err) + require.Equal(t, primitives.Gwei(0), res) + b, err := st.BalanceAtIndex(0) + require.NoError(t, err) + require.Equal(t, uint64(1_100_000), b) + // All of the balance deposits should have been processed. remaining, err := st.PendingBalanceDeposits() require.NoError(t, err) diff --git a/testing/spectest/shared/electra/epoch_processing/helpers.go b/testing/spectest/shared/electra/epoch_processing/helpers.go index 5ad4178f2cd3..6fe2e87c9534 100644 --- a/testing/spectest/shared/electra/epoch_processing/helpers.go +++ b/testing/spectest/shared/electra/epoch_processing/helpers.go @@ -27,7 +27,6 @@ func RunEpochOperationTest( testFolderPath string, operationFn epochOperation, ) { - t.Skip("Failing until spectests are updated to v1.5.0-alpha.3") preBeaconStateFile, err := util.BazelFileBytes(path.Join(testFolderPath, "pre.ssz_snappy")) require.NoError(t, err) preBeaconStateSSZ, err := snappy.Decode(nil /* dst */, preBeaconStateFile)