diff --git a/CHANGELOG.md b/CHANGELOG.md index d71b3efb074..59fc4a6b211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - Updated k8s-io/client-go to v0.30.4 and k8s-io/apimachinery to v0.30.4 - Migrated tracing library from opencensus to opentelemetry for both the beacon node and validator. - Refactored light client code to make it more readable and make future PRs easier. +- Switch to compounding when consolidating with source==target. ### Deprecated - `--disable-grpc-gateway` flag is deprecated due to grpc gateway removal. diff --git a/beacon-chain/core/electra/consolidations.go b/beacon-chain/core/electra/consolidations.go index 70d3ba4ee17..2faf1c839d0 100644 --- a/beacon-chain/core/electra/consolidations.go +++ b/beacon-chain/core/electra/consolidations.go @@ -32,8 +32,6 @@ import ( // 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) @@ -70,10 +68,6 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) err break } - if err := SwitchToCompoundingValidator(st, pc.TargetIndex); err != nil { - return err - } - activeBalance, err := st.ActiveBalanceAtIndex(pc.SourceIndex) if err != nil { return err @@ -97,69 +91,79 @@ func ProcessPendingConsolidations(ctx context.Context, st state.BeaconState) err // ProcessConsolidationRequests implements the spec definition below. This method makes mutating // calls to the beacon state. // -// def process_consolidation_request( -// state: BeaconState, -// consolidation_request: ConsolidationRequest -// ) -> None: -// # If the pending consolidations queue is full, consolidation requests are ignored -// if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT: -// return -// # If there is too little available consolidation churn limit, consolidation requests are ignored -// if get_consolidation_churn_limit(state) <= MIN_ACTIVATION_BALANCE: -// return -// -// validator_pubkeys = [v.pubkey for v in state.validators] -// # Verify pubkeys exists -// request_source_pubkey = consolidation_request.source_pubkey -// request_target_pubkey = consolidation_request.target_pubkey -// if request_source_pubkey not in validator_pubkeys: -// return -// if request_target_pubkey not in validator_pubkeys: -// return -// source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey)) -// target_index = ValidatorIndex(validator_pubkeys.index(request_target_pubkey)) -// source_validator = state.validators[source_index] -// target_validator = state.validators[target_index] -// -// # Verify that source != target, so a consolidation cannot be used as an exit. -// if source_index == target_index: -// return -// -// # Verify source withdrawal credentials -// has_correct_credential = has_execution_withdrawal_credential(source_validator) -// is_correct_source_address = ( -// source_validator.withdrawal_credentials[12:] == consolidation_request.source_address -// ) -// if not (has_correct_credential and is_correct_source_address): -// return -// -// # Verify that target has execution withdrawal credentials -// if not has_execution_withdrawal_credential(target_validator): -// return -// -// # Verify the source and the target are active -// current_epoch = get_current_epoch(state) -// if not is_active_validator(source_validator, current_epoch): -// return -// if not is_active_validator(target_validator, current_epoch): -// return -// # Verify exits for source and target have not been initiated -// if source_validator.exit_epoch != FAR_FUTURE_EPOCH: -// return -// if target_validator.exit_epoch != FAR_FUTURE_EPOCH: -// return -// -// # 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=source_index, -// target_index=target_index -// )) +// def process_consolidation_request( +// state: BeaconState, +// consolidation_request: ConsolidationRequest +// ) -> None: +// if is_valid_switch_to_compounding_request(state, consolidation_request): +// validator_pubkeys = [v.pubkey for v in state.validators] +// request_source_pubkey = consolidation_request.source_pubkey +// source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey)) +// switch_to_compounding_validator(state, source_index) +// return +// +// # Verify that source != target, so a consolidation cannot be used as an exit. +// if consolidation_request.source_pubkey == consolidation_request.target_pubkey: +// return +// # If the pending consolidations queue is full, consolidation requests are ignored +// if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT: +// return +// # If there is too little available consolidation churn limit, consolidation requests are ignored +// if get_consolidation_churn_limit(state) <= MIN_ACTIVATION_BALANCE: +// return +// +// validator_pubkeys = [v.pubkey for v in state.validators] +// # Verify pubkeys exists +// request_source_pubkey = consolidation_request.source_pubkey +// request_target_pubkey = consolidation_request.target_pubkey +// if request_source_pubkey not in validator_pubkeys: +// return +// if request_target_pubkey not in validator_pubkeys: +// return +// source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey)) +// target_index = ValidatorIndex(validator_pubkeys.index(request_target_pubkey)) +// source_validator = state.validators[source_index] +// target_validator = state.validators[target_index] +// +// # Verify source withdrawal credentials +// has_correct_credential = has_execution_withdrawal_credential(source_validator) +// is_correct_source_address = ( +// source_validator.withdrawal_credentials[12:] == consolidation_request.source_address +// ) +// if not (has_correct_credential and is_correct_source_address): +// return +// +// # Verify that target has execution withdrawal credentials +// if not has_execution_withdrawal_credential(target_validator): +// return +// +// # Verify the source and the target are active +// current_epoch = get_current_epoch(state) +// if not is_active_validator(source_validator, current_epoch): +// return +// if not is_active_validator(target_validator, current_epoch): +// return +// # Verify exits for source and target have not been initiated +// if source_validator.exit_epoch != FAR_FUTURE_EPOCH: +// return +// if target_validator.exit_epoch != FAR_FUTURE_EPOCH: +// return +// +// # 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=source_index, +// target_index=target_index +// )) +// +// # Churn any target excess active balance of target and raise its max +// if has_eth1_withdrawal_credential(target_validator): +// switch_to_compounding_validator(state, target_index) func ProcessConsolidationRequests(ctx context.Context, st state.BeaconState, reqs []*enginev1.ConsolidationRequest) error { if len(reqs) == 0 || st == nil { return nil @@ -182,25 +186,41 @@ func ProcessConsolidationRequests(ctx context.Context, st state.BeaconState, req if ctx.Err() != nil { return fmt.Errorf("cannot process consolidation requests: %w", ctx.Err()) } + canSwitch, err := IsValidSwitchToCompoundingRequest(ctx, st, cr) + if err != nil { + return fmt.Errorf("failed to validate consolidation request: %w", err) + } + if canSwitch { + srcIdx, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(cr.SourcePubkey)) + if !ok { + return errors.New("could not find validator in registry") + } + if err := SwitchToCompoundingValidator(st, srcIdx); err != nil { + return fmt.Errorf("failed to switch to compounding validator: %w", err) + } + return nil + } + sourcePubkey := bytesutil.ToBytes48(cr.SourcePubkey) + targetPubkey := bytesutil.ToBytes48(cr.TargetPubkey) + if sourcePubkey == targetPubkey { + continue + } + if npc, err := st.NumPendingConsolidations(); err != nil { return fmt.Errorf("failed to fetch number of pending consolidations: %w", err) // This should never happen. } else if npc >= pcLimit { return nil } - srcIdx, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(cr.SourcePubkey)) + srcIdx, ok := st.ValidatorIndexByPubkey(sourcePubkey) if !ok { continue } - tgtIdx, ok := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(cr.TargetPubkey)) + tgtIdx, ok := st.ValidatorIndexByPubkey(targetPubkey) if !ok { continue } - if srcIdx == tgtIdx { - continue - } - srcV, err := st.ValidatorAtIndex(srcIdx) if err != nil { return fmt.Errorf("failed to fetch source validator: %w", err) // This should never happen. @@ -248,7 +268,94 @@ func ProcessConsolidationRequests(ctx context.Context, st state.BeaconState, req if err := st.AppendPendingConsolidation(ð.PendingConsolidation{SourceIndex: srcIdx, TargetIndex: tgtIdx}); err != nil { return fmt.Errorf("failed to append pending consolidation: %w", err) // This should never happen. } + + if helpers.HasETH1WithdrawalCredential(tgtV) { + if err := SwitchToCompoundingValidator(st, tgtIdx); err != nil { + return fmt.Errorf("failed to switch to compounding validator: %w", err) + } + } } return nil } + +// IsValidSwitchToCompoundingRequest returns true if the given consolidation request is valid for switching to compounding. +// +// Spec code: +// +// def is_valid_switch_to_compounding_request( +// +// state: BeaconState, +// consolidation_request: ConsolidationRequest +// +// ) -> bool: +// +// # Switch to compounding requires source and target be equal +// if consolidation_request.source_pubkey != consolidation_request.target_pubkey: +// return False +// +// # Verify pubkey exists +// source_pubkey = consolidation_request.source_pubkey +// validator_pubkeys = [v.pubkey for v in state.validators] +// if source_pubkey not in validator_pubkeys: +// return False +// +// source_validator = state.validators[ValidatorIndex(validator_pubkeys.index(source_pubkey))] +// +// # Verify request has been authorized +// if source_validator.withdrawal_credentials[12:] != consolidation_request.source_address: +// return False +// +// # Verify source withdrawal credentials +// if not has_eth1_withdrawal_credential(source_validator): +// return False +// +// # Verify the source is active +// current_epoch = get_current_epoch(state) +// if not is_active_validator(source_validator, current_epoch): +// return False +// +// # Verify exit for source have not been initiated +// if source_validator.exit_epoch != FAR_FUTURE_EPOCH: +// return False +// +// return True +func IsValidSwitchToCompoundingRequest(ctx context.Context, st state.BeaconState, req *enginev1.ConsolidationRequest) (bool, error) { + if req.SourcePubkey == nil || req.TargetPubkey == nil { + return false, errors.New("nil source or target pubkey") + } + + sourcePubKey := bytesutil.ToBytes48(req.SourcePubkey) + targetPubKey := bytesutil.ToBytes48(req.TargetPubkey) + if sourcePubKey != targetPubKey { + return false, nil + } + + srcIdx, ok := st.ValidatorIndexByPubkey(sourcePubKey) + if !ok { + return false, nil + } + srcV, err := st.ValidatorAtIndex(srcIdx) + if err != nil { + return false, err + } + sourceAddress := req.SourceAddress + withdrawalCreds := srcV.WithdrawalCredentials + if len(withdrawalCreds) != 32 || len(sourceAddress) != 20 || !bytes.HasSuffix(withdrawalCreds, sourceAddress) { + return false, nil + } + + if !helpers.HasETH1WithdrawalCredential(srcV) { + return false, nil + } + + curEpoch := slots.ToEpoch(st.Slot()) + if !helpers.IsActiveValidator(srcV, curEpoch) { + return false, nil + } + + if srcV.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return false, nil + } + return true, nil +} diff --git a/beacon-chain/core/electra/deposits.go b/beacon-chain/core/electra/deposits.go index e9d753b5a3b..4f9fc8f9c51 100644 --- a/beacon-chain/core/electra/deposits.go +++ b/beacon-chain/core/electra/deposits.go @@ -109,13 +109,6 @@ func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, verif // # 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 @@ -139,24 +132,6 @@ func ApplyDeposit(beaconState state.BeaconState, data *ethpb.Deposit_Data, verif 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 } diff --git a/beacon-chain/core/electra/validator.go b/beacon-chain/core/electra/validator.go index 82b41fd8d7b..dfa7084a8ce 100644 --- a/beacon-chain/core/electra/validator.go +++ b/beacon-chain/core/electra/validator.go @@ -3,7 +3,6 @@ package electra import ( "errors" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -79,11 +78,10 @@ func ValidatorFromDeposit(pubKey []byte, withdrawalCredentials []byte) *ethpb.Va // // Spec definition: // -// def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None: -// validator = state.validators[index] -// if has_eth1_withdrawal_credential(validator): -// validator.withdrawal_credentials = COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] -// queue_excess_active_balance(state, index) +// def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None: +// validator = state.validators[index] +// validator.withdrawal_credentials = COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] +// queue_excess_active_balance(state, index) func SwitchToCompoundingValidator(s state.BeaconState, idx primitives.ValidatorIndex) error { v, err := s.ValidatorAtIndex(idx) if err != nil { @@ -92,14 +90,12 @@ func SwitchToCompoundingValidator(s state.BeaconState, idx primitives.ValidatorI if len(v.WithdrawalCredentials) == 0 { return errors.New("validator has no withdrawal credentials") } - if helpers.HasETH1WithdrawalCredential(v) { - v.WithdrawalCredentials[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte - if err := s.UpdateValidatorAtIndex(idx, v); err != nil { - return err - } - return QueueExcessActiveBalance(s, idx) + + v.WithdrawalCredentials[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte + if err := s.UpdateValidatorAtIndex(idx, v); err != nil { + return err } - return nil + return QueueExcessActiveBalance(s, idx) } // QueueExcessActiveBalance queues validators with balances above the min activation balance and adds to pending balance deposit.