diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 11bf37a4bfc0..20b916264309 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1123,6 +1123,27 @@ "recordedRepoMappingEntries": [] } }, + "@@bazel_tools//tools/test:extensions.bzl%remote_coverage_tools_extension": { + "general": { + "bzlTransitiveDigest": "l5mcjH2gWmbmIycx97bzI2stD0Q0M5gpDc0aLOHKIm8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "remote_coverage_tools": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "7006375f6756819b7013ca875eab70a541cf7d89142d9c511ed78ea4fefa38af", + "urls": [ + "https://mirror.bazel.build/bazel_coverage_output_generator/releases/coverage_output_generator-v2.6.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [] + } + }, "@@rules_java~//java:extensions.bzl%toolchains": { "general": { "bzlTransitiveDigest": "tJHbmWnq7m+9eUBnUdv7jZziQ26FmcGL9C5/hU3Q9UQ=", diff --git a/beacon-chain/core/epoch/epoch_processing.go b/beacon-chain/core/epoch/epoch_processing.go index 41d3e748c143..48980c6d5eae 100644 --- a/beacon-chain/core/epoch/epoch_processing.go +++ b/beacon-chain/core/epoch/epoch_processing.go @@ -99,7 +99,7 @@ func ProcessRegistryUpdates(ctx context.Context, state state.BeaconState) (state activationEligibilityEpoch := time.CurrentEpoch(state) + 1 for idx, validator := range vals { // Process the validators for activation eligibility. - if helpers.IsEligibleForActivationQueue(validator) { + if helpers.IsEligibleForActivationQueue(validator, currentEpoch) { validator.ActivationEligibilityEpoch = activationEligibilityEpoch if err := state.UpdateValidatorAtIndex(primitives.ValidatorIndex(idx), validator); err != nil { return nil, err diff --git a/beacon-chain/core/helpers/BUILD.bazel b/beacon-chain/core/helpers/BUILD.bazel index f49a5ecaeeca..bbeb7964435f 100644 --- a/beacon-chain/core/helpers/BUILD.bazel +++ b/beacon-chain/core/helpers/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "rewards_penalties.go", "shuffle.go", "sync_committee.go", + "validator_churn.go", "validators.go", "weak_subjectivity.go", ], @@ -56,6 +57,7 @@ go_test( "rewards_penalties_test.go", "shuffle_test.go", "sync_committee_test.go", + "validator_churn_test.go", "validators_test.go", "weak_subjectivity_test.go", ], diff --git a/beacon-chain/core/helpers/validator_churn.go b/beacon-chain/core/helpers/validator_churn.go new file mode 100644 index 000000000000..b36fd81569f6 --- /dev/null +++ b/beacon-chain/core/helpers/validator_churn.go @@ -0,0 +1,52 @@ +package helpers + +import ( + "github.com/prysmaticlabs/prysm/v5/config/params" +) + +// BalanceChurnLimit for the current active balance, in gwei. +// New in Electra EIP-7251: https://eips.ethereum.org/EIPS/eip-7251 +// +// Spec definition: +// +// def get_balance_churn_limit(state: BeaconState) -> Gwei: +// """ +// Return the churn limit for the current epoch. +// """ +// churn = max( +// MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA, +// get_total_active_balance(state) // CHURN_LIMIT_QUOTIENT +// ) +// return churn - churn % EFFECTIVE_BALANCE_INCREMENT +func BalanceChurnLimit(activeBalanceGwei uint64) uint64 { + churn := max( + params.BeaconConfig().MinPerEpochChurnLimitElectra, + (activeBalanceGwei / params.BeaconConfig().ChurnLimitQuotient), + ) + return churn - churn%params.BeaconConfig().EffectiveBalanceIncrement +} + +// ActivationExitChurnLimit for the current active balance, in gwei. +// New in Electra EIP-7251: https://eips.ethereum.org/EIPS/eip-7251 +// +// Spec definition: +// +// def get_activation_exit_churn_limit(state: BeaconState) -> Gwei: +// """ +// Return the churn limit for the current epoch dedicated to activations and exits. +// """ +// return min(MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, get_balance_churn_limit(state)) +func ActivationExitChurnLimit(activeBalanceGwei uint64) uint64 { + return min(params.BeaconConfig().MaxPerEpochActivationExitChurnLimit, BalanceChurnLimit(activeBalanceGwei)) +} + +// ConsolidationChurnLimit for the current active balance, in gwei. +// New in EIP-7251: https://eips.ethereum.org/EIPS/eip-7251 +// +// Spec definition: +// +// def get_consolidation_churn_limit(state: BeaconState) -> Gwei: +// return get_balance_churn_limit(state) - get_activation_exit_churn_limit(state) +func ConsolidationChurnLimit(activeBalanceGwei uint64) uint64 { + return BalanceChurnLimit(activeBalanceGwei) - ActivationExitChurnLimit(activeBalanceGwei) +} diff --git a/beacon-chain/core/helpers/validator_churn_test.go b/beacon-chain/core/helpers/validator_churn_test.go new file mode 100644 index 000000000000..556505291a8a --- /dev/null +++ b/beacon-chain/core/helpers/validator_churn_test.go @@ -0,0 +1,71 @@ +package helpers_test + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/testing/assert" +) + +func TestBalanceChurnLimit(t *testing.T) { + tests := []struct { + name string + activeBalance uint64 + expected uint64 + }{ + { + name: "less than MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA", + activeBalance: 111, + expected: params.BeaconConfig().MinPerEpochChurnLimitElectra, + }, + { + name: "modulo EFFECTIVE_BALANCE_INCREMENT", + activeBalance: 111 + params.BeaconConfig().MinPerEpochChurnLimitElectra*params.BeaconConfig().ChurnLimitQuotient, + expected: params.BeaconConfig().MinPerEpochChurnLimitElectra, + }, + { + name: "more than MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA", + activeBalance: 2000 * params.BeaconConfig().EffectiveBalanceIncrement * params.BeaconConfig().ChurnLimitQuotient, + expected: 2000 * params.BeaconConfig().EffectiveBalanceIncrement, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, helpers.BalanceChurnLimit(tt.activeBalance)) + }) + } +} + +func TestActivationExitChurnLimit(t *testing.T) { + tests := []struct { + name string + activeBalance uint64 + expected uint64 + }{ + { + name: "less than MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT", + activeBalance: 1, + expected: params.BeaconConfig().MinPerEpochChurnLimitElectra, + }, + { + name: "more than MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT", + activeBalance: 2000 * params.BeaconConfig().EffectiveBalanceIncrement * params.BeaconConfig().ChurnLimitQuotient, + expected: params.BeaconConfig().MaxPerEpochActivationExitChurnLimit, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, helpers.ActivationExitChurnLimit(tt.activeBalance)) + }) + } +} + +// FuzzConsolidationChurnLimit exercises BalanceChurnLimit and ActivationExitChurnLimit +func FuzzConsolidationChurnLimit(f *testing.F) { + f.Fuzz(func(t *testing.T, activeBalance uint64) { + helpers.ConsolidationChurnLimit(activeBalance) + }) +} diff --git a/beacon-chain/core/helpers/validators.go b/beacon-chain/core/helpers/validators.go index aef32f10a9c3..d28aa39a3236 100644 --- a/beacon-chain/core/helpers/validators.go +++ b/beacon-chain/core/helpers/validators.go @@ -393,6 +393,24 @@ func ComputeProposerIndex(bState state.ReadOnlyValidators, activeIndices []primi // IsEligibleForActivationQueue checks if the validator is eligible to // be placed into the activation queue. // +// Spec definition: +// +// def is_eligible_for_activation_queue(validator: Validator) -> bool: +// """ +// Check if ``validator`` is eligible to be placed into the activation queue. +// """ +// return ( +// validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH +// and validator.effective_balance >= MIN_ACTIVATION_BALANCE # [Modified in Electra:EIP7251] +// ) +func IsEligibleForActivationQueue(validator *ethpb.Validator, currentEpoch primitives.Epoch) bool { + if currentEpoch >= params.BeaconConfig().ElectraForkEpoch { + return isEligibileForActivationQueueElectra(validator.ActivationEligibilityEpoch, validator.EffectiveBalance) + } + return isEligibileForActivationQueue(validator.ActivationEligibilityEpoch, validator.EffectiveBalance) +} + +// isEligibleForActivationQueue carries out the logic for IsEligibleForActivationQueue // Spec pseudocode definition: // // def is_eligible_for_activation_queue(validator: Validator) -> bool: @@ -403,20 +421,27 @@ func ComputeProposerIndex(bState state.ReadOnlyValidators, activeIndices []primi // validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH // and validator.effective_balance == MAX_EFFECTIVE_BALANCE // ) -func IsEligibleForActivationQueue(validator *ethpb.Validator) bool { - return isEligibileForActivationQueue(validator.ActivationEligibilityEpoch, validator.EffectiveBalance) +func isEligibileForActivationQueue(activationEligibilityEpoch primitives.Epoch, effectiveBalance uint64) bool { + return activationEligibilityEpoch == params.BeaconConfig().FarFutureEpoch && + effectiveBalance == params.BeaconConfig().MaxEffectiveBalance } -// IsEligibleForActivationQueueUsingTrie checks if the read-only validator is eligible to +// IsEligibleForActivationQueue checks if the validator is eligible to // be placed into the activation queue. -func IsEligibleForActivationQueueUsingTrie(validator state.ReadOnlyValidator) bool { - return isEligibileForActivationQueue(validator.ActivationEligibilityEpoch(), validator.EffectiveBalance()) -} - -// isEligibleForActivationQueue carries out the logic for IsEligibleForActivationQueue* -func isEligibileForActivationQueue(activationEligibilityEpoch primitives.Epoch, effectiveBalance uint64) bool { +// +// Spec definition: +// +// def is_eligible_for_activation_queue(validator: Validator) -> bool: +// """ +// Check if ``validator`` is eligible to be placed into the activation queue. +// """ +// return ( +// validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH +// and validator.effective_balance >= MIN_ACTIVATION_BALANCE # [Modified in Electra:EIP7251] +// ) +func isEligibileForActivationQueueElectra(activationEligibilityEpoch primitives.Epoch, effectiveBalance uint64) bool { return activationEligibilityEpoch == params.BeaconConfig().FarFutureEpoch && - effectiveBalance == params.BeaconConfig().MaxEffectiveBalance + effectiveBalance >= params.BeaconConfig().MinActivationBalance } // IsEligibleForActivation checks if the validator is eligible for activation. @@ -471,3 +496,180 @@ func LastActivatedValidatorIndex(ctx context.Context, st state.ReadOnlyBeaconSta } return lastActivatedvalidatorIndex, nil } + +// hasETH1WithdrawalCredential returns whether the validator has an ETH1 +// Withdrawal prefix. It assumes that the caller has a lock on the state +func HasETH1WithdrawalCredential(val *ethpb.Validator) bool { + if val == nil { + return false + } + return isETH1WithdrawalCredential(val.WithdrawalCredentials) +} + +func isETH1WithdrawalCredential(creds []byte) bool { + return bytes.HasPrefix(creds, []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte}) +} + +// HasCompoundingWithdrawalCredential checks if the validator has a compounding withdrawal credential. +// New in Electra EIP-7251: https://eips.ethereum.org/EIPS/eip-7251 +// +// Spec definition: +// +// def has_compounding_withdrawal_credential(validator: Validator) -> bool: +// """ +// Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. +// """ +// return is_compounding_withdrawal_credential(validator.withdrawal_credentials) +func HasCompoundingWithdrawalCredential(v *ethpb.Validator) bool { + if v == nil { + return false + } + return isCompoundingWithdrawalCredential(v.WithdrawalCredentials) +} + +// 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 { + return bytes.HasPrefix(creds, []byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte}) +} + +// HasExecutionWithdrawalCredentials checks if the validator has an execution withdrawal credential or compounding credential. +// New in Electra EIP-7251: https://eips.ethereum.org/EIPS/eip-7251 +// +// Spec definition: +// +// def has_execution_withdrawal_credential(validator: Validator) -> bool: +// """ +// Check if ``validator`` has a 0x01 or 0x02 prefixed withdrawal credential. +// """ +// return has_compounding_withdrawal_credential(validator) or has_eth1_withdrawal_credential(validator) +func HasExecutionWithdrawalCredentials(v *ethpb.Validator) bool { + if v == nil { + return false + } + return HasCompoundingWithdrawalCredential(v) || HasETH1WithdrawalCredential(v) +} + +// IsSameWithdrawalCredentials returns true if both validators have the same withdrawal credentials. +// +// return a.withdrawal_credentials[12:] == b.withdrawal_credentials[12:] +func IsSameWithdrawalCredentials(a, b *ethpb.Validator) bool { + if a == nil || b == nil { + return false + } + if len(a.WithdrawalCredentials) <= 12 || len(b.WithdrawalCredentials) <= 12 { + return false + } + return bytes.Equal(a.WithdrawalCredentials[12:], b.WithdrawalCredentials[12:]) +} + +// IsFullyWithdrawableValidator returns whether the validator is able to perform a full +// withdrawal. This function assumes that the caller holds a lock on the state. +// +// Spec definition: +// +// def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: +// """ +// Check if ``validator`` is fully withdrawable. +// """ +// return ( +// has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251] +// and validator.withdrawable_epoch <= epoch +// and balance > 0 +// ) +func IsFullyWithdrawableValidator(val *ethpb.Validator, balance uint64, epoch primitives.Epoch) bool { + if val == nil || balance <= 0 { + return false + } + + // Electra / EIP-7251 logic + if epoch >= params.BeaconConfig().ElectraForkEpoch { + return HasExecutionWithdrawalCredentials(val) && val.WithdrawableEpoch <= epoch + } + + return HasETH1WithdrawalCredential(val) && val.WithdrawableEpoch <= epoch +} + +// IsPartiallyWithdrawableValidator returns whether the validator is able to perform a +// partial withdrawal. This function assumes that the caller has a lock on the state. +// This method conditionally calls the fork appropriate implementation based on the epoch argument. +func IsPartiallyWithdrawableValidator(val *ethpb.Validator, balance uint64, epoch primitives.Epoch) bool { + if val == nil { + return false + } + + if epoch < params.BeaconConfig().ElectraForkEpoch { + return isPartiallyWithdrawableValidatorCapella(val, balance, epoch) + } + + return isPartiallyWithdrawableValidatorElectra(val, balance, epoch) +} + +// isPartiallyWithdrawableValidatorElectra implements is_partially_withdrawable_validator in the +// electra fork. +// +// Spec definition: +// +// def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: +// +// """ +// Check if ``validator`` is partially withdrawable. +// """ +// max_effective_balance = get_validator_max_effective_balance(validator) +// has_max_effective_balance = validator.effective_balance == max_effective_balance # [Modified in Electra:EIP7251] +// has_excess_balance = balance > max_effective_balance # [Modified in Electra:EIP7251] +// return ( +// has_execution_withdrawal_credential(validator) # [Modified in Electra:EIP7251] +// and has_max_effective_balance +// and has_excess_balance +// ) +func isPartiallyWithdrawableValidatorElectra(val *ethpb.Validator, balance uint64, epoch primitives.Epoch) bool { + maxEB := ValidatorMaxEffectiveBalance(val) + hasMaxBalance := val.EffectiveBalance == maxEB + hasExcessBalance := balance > maxEB + + return HasExecutionWithdrawalCredentials(val) && + hasMaxBalance && + hasExcessBalance +} + +// isPartiallyWithdrawableValidatorCapella implements is_partially_withdrawable_validator in the +// capella fork. +// +// Spec definition: +// +// def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: +// """ +// Check if ``validator`` is partially withdrawable. +// """ +// has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE +// has_excess_balance = balance > MAX_EFFECTIVE_BALANCE +// return has_eth1_withdrawal_credential(validator) and has_max_effective_balance and has_excess_balance +func isPartiallyWithdrawableValidatorCapella(val *ethpb.Validator, balance uint64, epoch primitives.Epoch) bool { + hasMaxBalance := val.EffectiveBalance == params.BeaconConfig().MaxEffectiveBalance + hasExcessBalance := balance > params.BeaconConfig().MaxEffectiveBalance + return HasETH1WithdrawalCredential(val) && hasExcessBalance && hasMaxBalance +} + +// ValidatorMaxEffectiveBalance returns the maximum effective balance for a validator. +// +// Spec definition: +// +// def get_validator_max_effective_balance(validator: Validator) -> Gwei: +// """ +// Get max effective balance for ``validator``. +// """ +// if has_compounding_withdrawal_credential(validator): +// return MAX_EFFECTIVE_BALANCE_ELECTRA +// else: +// return MIN_ACTIVATION_BALANCE +func ValidatorMaxEffectiveBalance(val *ethpb.Validator) uint64 { + if HasCompoundingWithdrawalCredential(val) { + return params.BeaconConfig().MaxEffectiveBalanceElectra + } + return params.BeaconConfig().MinActivationBalance // TODO: Add test that MinActivationBalance == (old) MaxEffectiveBalance +} diff --git a/beacon-chain/core/helpers/validators_test.go b/beacon-chain/core/helpers/validators_test.go index 0471e1740d64..efa21c75cb63 100644 --- a/beacon-chain/core/helpers/validators_test.go +++ b/beacon-chain/core/helpers/validators_test.go @@ -703,25 +703,47 @@ func TestComputeProposerIndex(t *testing.T) { func TestIsEligibleForActivationQueue(t *testing.T) { tests := []struct { - name string - validator *ethpb.Validator - want bool + name string + validator *ethpb.Validator + currentEpoch primitives.Epoch + want bool }{ - {"Eligible", - ðpb.Validator{ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance}, - true}, - {"Incorrect activation eligibility epoch", - ðpb.Validator{ActivationEligibilityEpoch: 1, EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance}, - false}, - {"Not enough balance", - ðpb.Validator{ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: 1}, - false}, + { + name: "Eligible", + validator: ðpb.Validator{ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance}, + currentEpoch: primitives.Epoch(params.BeaconConfig().ElectraForkEpoch - 1), + want: true, + }, + { + name: "Incorrect activation eligibility epoch", + validator: ðpb.Validator{ActivationEligibilityEpoch: 1, EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance}, + currentEpoch: primitives.Epoch(params.BeaconConfig().ElectraForkEpoch - 1), + want: false, + }, + { + name: "Not enough balance", + validator: ðpb.Validator{ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: 1}, + currentEpoch: primitives.Epoch(params.BeaconConfig().ElectraForkEpoch - 1), + want: false, + }, + { + name: "More than max effective balance before electra", + validator: ðpb.Validator{ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance + 1}, + currentEpoch: primitives.Epoch(params.BeaconConfig().ElectraForkEpoch - 1), + want: false, + }, + { + name: "More than min activation balance after electra", + validator: ðpb.Validator{ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch, EffectiveBalance: params.BeaconConfig().MinActivationBalance + 1}, + currentEpoch: primitives.Epoch(params.BeaconConfig().ElectraForkEpoch), + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { helpers.ClearCache() - assert.Equal(t, tt.want, helpers.IsEligibleForActivationQueue(tt.validator), "IsEligibleForActivationQueue()") + assert.Equal(t, tt.want, helpers.IsEligibleForActivationQueue(tt.validator, tt.currentEpoch), "IsEligibleForActivationQueue()") }) } } @@ -828,3 +850,272 @@ func TestProposerIndexFromCheckpoint(t *testing.T) { require.NoError(t, err) require.Equal(t, ids[5], id) } + +func TestHasETH1WithdrawalCredentials(t *testing.T) { + creds := []byte{0xFA, 0xCC} + v := ðpb.Validator{WithdrawalCredentials: creds} + require.Equal(t, false, helpers.HasETH1WithdrawalCredential(v)) + creds = []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC} + v = ðpb.Validator{WithdrawalCredentials: creds} + require.Equal(t, true, helpers.HasETH1WithdrawalCredential(v)) + // No Withdrawal cred + v = ðpb.Validator{} + require.Equal(t, false, helpers.HasETH1WithdrawalCredential(v)) +} + +func TestHasCompoundingWithdrawalCredential(t *testing.T) { + tests := []struct { + name string + validator *ethpb.Validator + want bool + }{ + {"Has compounding withdrawal credential", + ðpb.Validator{WithdrawalCredentials: bytesutil.PadTo([]byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte}, 32)}, + true}, + {"Does not have compounding withdrawal credential", + ðpb.Validator{WithdrawalCredentials: bytesutil.PadTo([]byte{0x00}, 32)}, + false}, + {"Handles nil case", nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, helpers.HasCompoundingWithdrawalCredential(tt.validator)) + }) + } +} + +func TestHasExecutionWithdrawalCredentials(t *testing.T) { + tests := []struct { + name string + validator *ethpb.Validator + want bool + }{ + {"Has compounding withdrawal credential", + ðpb.Validator{WithdrawalCredentials: bytesutil.PadTo([]byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte}, 32)}, + true}, + {"Has eth1 withdrawal credential", + ðpb.Validator{WithdrawalCredentials: bytesutil.PadTo([]byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte}, 32)}, + true}, + {"Does not have compounding withdrawal credential or eth1 withdrawal credential", + ðpb.Validator{WithdrawalCredentials: bytesutil.PadTo([]byte{0x00}, 32)}, + false}, + {"Handles nil case", nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, helpers.HasExecutionWithdrawalCredentials(tt.validator)) + }) + } +} + +func TestIsFullyWithdrawableValidator(t *testing.T) { + tests := []struct { + name string + validator *ethpb.Validator + balance uint64 + epoch primitives.Epoch + want bool + }{ + { + name: "Handles nil case", + validator: nil, + balance: 0, + epoch: 0, + want: false, + }, + { + name: "No ETH1 prefix", + validator: ðpb.Validator{ + WithdrawalCredentials: []byte{0xFA, 0xCC}, + WithdrawableEpoch: 2, + }, + balance: params.BeaconConfig().MaxEffectiveBalance, + epoch: 3, + want: false, + }, + { + name: "Wrong withdrawable epoch", + validator: ðpb.Validator{ + WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}, + WithdrawableEpoch: 2, + }, + balance: params.BeaconConfig().MaxEffectiveBalance, + epoch: 1, + want: false, + }, + { + name: "No balance", + validator: ðpb.Validator{ + WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}, + WithdrawableEpoch: 2, + }, + balance: 0, + epoch: 3, + want: false, + }, + { + name: "Fully withdrawable", + validator: ðpb.Validator{ + WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}, + WithdrawableEpoch: 2, + }, + balance: params.BeaconConfig().MaxEffectiveBalance, + epoch: 3, + want: true, + }, + { + name: "Fully withdrawable compounding validator electra", + validator: ðpb.Validator{ + WithdrawalCredentials: []byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte, 0xCC}, + WithdrawableEpoch: 2, + }, + balance: params.BeaconConfig().MaxEffectiveBalance, + epoch: params.BeaconConfig().ElectraForkEpoch, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, helpers.IsFullyWithdrawableValidator(tt.validator, tt.balance, tt.epoch)) + }) + } +} + +func TestIsPartiallyWithdrawableValidator(t *testing.T) { + tests := []struct { + name string + validator *ethpb.Validator + balance uint64 + epoch primitives.Epoch + want bool + }{ + { + name: "Handles nil case", + validator: nil, + balance: 0, + epoch: 0, + want: false, + }, + { + name: "No ETH1 prefix", + validator: ðpb.Validator{ + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + WithdrawalCredentials: []byte{0xFA, 0xCC}, + }, + balance: params.BeaconConfig().MaxEffectiveBalance, + epoch: 3, + want: false, + }, + { + name: "No balance", + validator: ðpb.Validator{ + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}, + }, + balance: 0, + epoch: 3, + want: false, + }, + { + name: "Partially withdrawable", + validator: ðpb.Validator{ + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}, + }, + balance: params.BeaconConfig().MaxEffectiveBalance * 2, + epoch: 3, + want: true, + }, + { + name: "Fully withdrawable vanilla validator electra", + validator: ðpb.Validator{ + EffectiveBalance: params.BeaconConfig().MinActivationBalance, + WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}, + }, + balance: params.BeaconConfig().MinActivationBalance * 2, + epoch: params.BeaconConfig().ElectraForkEpoch, + want: true, + }, + { + name: "Fully withdrawable compounding validator electra", + validator: ðpb.Validator{ + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalanceElectra, + WithdrawalCredentials: []byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte, 0xCC}, + }, + balance: params.BeaconConfig().MaxEffectiveBalanceElectra * 2, + epoch: params.BeaconConfig().ElectraForkEpoch, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, helpers.IsPartiallyWithdrawableValidator(tt.validator, tt.balance, tt.epoch)) + }) + } +} + +func TestIsSameWithdrawalCredentials(t *testing.T) { + makeWithdrawalCredentials := func(address []byte) []byte { + b := make([]byte, 12) + return append(b, address...) + } + + tests := []struct { + name string + a *ethpb.Validator + b *ethpb.Validator + want bool + }{ + { + "Same credentials", + ðpb.Validator{WithdrawalCredentials: makeWithdrawalCredentials([]byte("same"))}, + ðpb.Validator{WithdrawalCredentials: makeWithdrawalCredentials([]byte("same"))}, + true, + }, + { + "Different credentials", + ðpb.Validator{WithdrawalCredentials: makeWithdrawalCredentials([]byte("foo"))}, + ðpb.Validator{WithdrawalCredentials: makeWithdrawalCredentials([]byte("bar"))}, + false, + }, + {"Handles nil case", nil, nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, helpers.IsSameWithdrawalCredentials(tt.a, tt.b)) + }) + } +} + +func TestValidatorMaxEffectiveBalance(t *testing.T) { + tests := []struct { + name string + validator *ethpb.Validator + want uint64 + }{ + { + name: "Compounding withdrawal credential", + validator: ðpb.Validator{WithdrawalCredentials: []byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte, 0xCC}}, + want: params.BeaconConfig().MaxEffectiveBalanceElectra, + }, + { + name: "Vanilla credentials", + validator: ðpb.Validator{WithdrawalCredentials: []byte{params.BeaconConfig().ETH1AddressWithdrawalPrefixByte, 0xCC}}, + want: params.BeaconConfig().MinActivationBalance, + }, + { + "Handles nil case", + nil, + params.BeaconConfig().MinActivationBalance, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, helpers.ValidatorMaxEffectiveBalance(tt.validator)) + }) + } + // Sanity check that MinActivationBalance equals (pre-electra) MaxEffectiveBalance + assert.Equal(t, params.BeaconConfig().MinActivationBalance, params.BeaconConfig().MaxEffectiveBalance) +}