diff --git a/beacon-chain/builder/testing/mock.go b/beacon-chain/builder/testing/mock.go index 55b1aa05efb3..f22e21ca0d8a 100644 --- a/beacon-chain/builder/testing/mock.go +++ b/beacon-chain/builder/testing/mock.go @@ -29,6 +29,7 @@ type MockBuilderService struct { Payload *v1.ExecutionPayload PayloadCapella *v1.ExecutionPayloadCapella PayloadDeneb *v1.ExecutionPayloadDeneb + PayloadElectra *v1.ExecutionPayloadElectra BlobBundle *v1.BlobsBundle ErrSubmitBlindedBlock error Bid *ethpb.SignedBuilderBid @@ -66,6 +67,12 @@ func (s *MockBuilderService) SubmitBlindedBlock(_ context.Context, b interfaces. return nil, nil, errors.Wrap(err, "could not wrap deneb payload") } return w, s.BlobBundle, s.ErrSubmitBlindedBlock + case version.Electra: + w, err := blocks.WrappedExecutionPayloadElectra(s.PayloadElectra, big.NewInt(0)) + if err != nil { + return nil, nil, errors.Wrap(err, "could not wrap electra payload") + } + return w, s.BlobBundle, s.ErrSubmitBlindedBlock default: return nil, nil, errors.New("unknown block version for mocking") } diff --git a/beacon-chain/core/blocks/deposit.go b/beacon-chain/core/blocks/deposit.go index 8458115952e2..a67e08be6eba 100644 --- a/beacon-chain/core/blocks/deposit.go +++ b/beacon-chain/core/blocks/deposit.go @@ -9,6 +9,7 @@ import ( "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/container/trie" "github.com/prysmaticlabs/prysm/v5/contracts/deposit" "github.com/prysmaticlabs/prysm/v5/crypto/bls" @@ -136,26 +137,25 @@ func BatchVerifyDepositsSignatures(ctx context.Context, deposits []*ethpb.Deposi // // pubkey = deposit.data.pubkey // amount = deposit.data.amount -// 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 -// deposit_message = DepositMessage( -// pubkey=deposit.data.pubkey, -// withdrawal_credentials=deposit.data.withdrawal_credentials, -// amount=deposit.data.amount, -// ) -// domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks -// signing_root = compute_signing_root(deposit_message, domain) -// if not bls.Verify(pubkey, signing_root, deposit.data.signature): -// return // -// # Add validator and balance entries -// state.validators.append(get_validator_from_deposit(state, deposit)) -// state.balances.append(amount) -// else: -// # Increase balance by deposit amount -// index = ValidatorIndex(validator_pubkeys.index(pubkey)) -// increase_balance(state, index, amount) +// TODO: This is apply_deposit, refactor to another function for readability. +// +// 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 +// deposit_message = DepositMessage( +// pubkey=deposit.data.pubkey, +// withdrawal_credentials=deposit.data.withdrawal_credentials, +// amount=deposit.data.amount, +// ) +// domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks +// signing_root = compute_signing_root(deposit_message, domain) +// if bls.Verify(pubkey, signing_root, signature): +// add_validator_to_registry(state, pubkey, withdrawal_credentials, amount) // TODO: Missing spec def here +// else: +// # Increase balance by deposit amount +// index = ValidatorIndex(validator_pubkeys.index(pubkey)) +// state.pending_balance_deposits.append(PendingBalanceDeposit(index, amount)) # [Modified in EIP-7251] func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, verifySignature bool) (state.BeaconState, bool, error) { var newValidator bool if err := verifyDeposit(beaconState, deposit); err != nil { @@ -167,6 +167,7 @@ func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, verif if err := beaconState.SetEth1DepositIndex(beaconState.Eth1DepositIndex() + 1); err != nil { return nil, newValidator, err } + isElectraOrLater := beaconState.Fork() != nil && beaconState.Fork().Epoch >= params.BeaconConfig().ElectraForkEpoch pubKey := deposit.Data.PublicKey amount := deposit.Data.Amount index, ok := beaconState.ValidatorIndexByPubkey(bytesutil.ToBytes48(pubKey)) @@ -183,10 +184,16 @@ func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, verif } } + // NOTE: This is get_validator_from_deposit. There are changes in EIP-7251. effectiveBalance := amount - (amount % params.BeaconConfig().EffectiveBalanceIncrement) if params.BeaconConfig().MaxEffectiveBalance < effectiveBalance { effectiveBalance = params.BeaconConfig().MaxEffectiveBalance } + if isElectraOrLater { + // In EIP-7251, the balance updates happen in the process_pending_balance_deposits method of + // the epoch transition function. + effectiveBalance = 0 // New in EIP-7251 + } if err := beaconState.AppendValidator(ðpb.Validator{ PublicKey: pubKey, WithdrawalCredentials: deposit.Data.WithdrawalCredentials, @@ -202,10 +209,24 @@ func ProcessDeposit(beaconState state.BeaconState, deposit *ethpb.Deposit, verif if err := beaconState.AppendBalance(amount); err != nil { return nil, newValidator, err } - } else if err := helpers.IncreaseBalance(beaconState, index, amount); err != nil { - return nil, newValidator, err + numVals := beaconState.NumValidators() + if numVals == 0 { // Cautiously prevent impossible underflow + return nil, false, errors.New("underflow: zero validators") + } + index = primitives.ValidatorIndex(numVals - 1) + } else { + if !isElectraOrLater { + if err := helpers.IncreaseBalance(beaconState, index, amount); err != nil { + return nil, newValidator, err + } + } } + if isElectraOrLater { + if err := beaconState.AppendPendingBalanceDeposit(index, amount); err != nil { + return nil, newValidator, err + } + } return beaconState, newValidator, nil } diff --git a/beacon-chain/core/blocks/exit.go b/beacon-chain/core/blocks/exit.go index 046b9bbc1a49..c1f38ccacd22 100644 --- a/beacon-chain/core/blocks/exit.go +++ b/beacon-chain/core/blocks/exit.go @@ -40,6 +40,8 @@ var ValidatorCannotExitYetMsg = "validator has not been active long enough to ex // assert get_current_epoch(state) >= voluntary_exit.epoch // # Verify the validator has been active long enough // assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD +// # Only exit validator if it has no pending withdrawals in the queue +// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in EIP7251] // # Verify signature // domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) // signing_root = compute_signing_root(voluntary_exit, domain) @@ -98,6 +100,8 @@ func ProcessVoluntaryExits( // assert get_current_epoch(state) >= voluntary_exit.epoch // # Verify the validator has been active long enough // assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD +// # Only exit validator if it has no pending withdrawals in the queue +// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in EIP7251] // # Verify signature // domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) // signing_root = compute_signing_root(voluntary_exit, domain) @@ -128,7 +132,7 @@ func VerifyExitAndSignature( } exit := signed.Exit - if err := verifyExitConditions(validator, currentSlot, exit); err != nil { + if err := verifyExitConditions(state, validator, currentSlot, exit); err != nil { return err } domain, err := signing.Domain(fork, exit.Epoch, params.BeaconConfig().DomainVoluntaryExit, genesisRoot) @@ -157,13 +161,15 @@ func VerifyExitAndSignature( // assert get_current_epoch(state) >= voluntary_exit.epoch // # Verify the validator has been active long enough // assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD +// # Only exit validator if it has no pending withdrawals in the queue +// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in Electra:EIP7251] // # Verify signature // domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) // signing_root = compute_signing_root(voluntary_exit, domain) // assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) // # Initiate exit // initiate_validator_exit(state, voluntary_exit.validator_index) -func verifyExitConditions(validator state.ReadOnlyValidator, currentSlot primitives.Slot, exit *ethpb.VoluntaryExit) error { +func verifyExitConditions(st state.ReadOnlyBeaconState, validator state.ReadOnlyValidator, currentSlot primitives.Slot, exit *ethpb.VoluntaryExit) error { currentEpoch := slots.ToEpoch(currentSlot) // Verify the validator is active. if !helpers.IsActiveValidatorUsingTrie(validator, currentEpoch) { @@ -187,5 +193,14 @@ func verifyExitConditions(validator state.ReadOnlyValidator, currentSlot primiti validator.ActivationEpoch()+params.BeaconConfig().ShardCommitteePeriod, ) } + if st.Version() >= version.Electra { + pending, err := st.PendingBalanceToWithdraw(exit.ValidatorIndex) + if err != nil { + return err + } + if pending != 0 { + return fmt.Errorf("validator with index %d has pending balance to withdraw and cannot exit", exit.ValidatorIndex) + } + } return nil } diff --git a/beacon-chain/core/blocks/withdrawals.go b/beacon-chain/core/blocks/withdrawals.go index 5b234d1614a8..710ba7c0c534 100644 --- a/beacon-chain/core/blocks/withdrawals.go +++ b/beacon-chain/core/blocks/withdrawals.go @@ -118,56 +118,46 @@ func ValidateBLSToExecutionChange(st state.ReadOnlyBeaconState, signed *ethpb.Si // ProcessWithdrawals processes the validator withdrawals from the provided execution payload // into the beacon state. // -// Spec pseudocode definition: +// Spec definition: +// +// def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: +// expected_withdrawals, partial_withdrawals_count = get_expected_withdrawals(state) # [Modified in Electra:EIP7251] // -// def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: +// assert len(payload.withdrawals) == len(expected_withdrawals) // -// expected_withdrawals = get_expected_withdrawals(state) -// assert len(payload.withdrawals) == len(expected_withdrawals) +// for expected_withdrawal, withdrawal in zip(expected_withdrawals, payload.withdrawals): +// assert withdrawal == expected_withdrawal +// decrease_balance(state, withdrawal.validator_index, withdrawal.amount) // -// for expected_withdrawal, withdrawal in zip(expected_withdrawals, payload.withdrawals): -// assert withdrawal == expected_withdrawal -// decrease_balance(state, withdrawal.validator_index, withdrawal.amount) +// # Update pending partial withdrawals [New in Electra:EIP7251] +// state.pending_partial_withdrawals = state.pending_partial_withdrawals[partial_withdrawals_count:] // -// # Update the next withdrawal index if this block contained withdrawals -// if len(expected_withdrawals) != 0: -// latest_withdrawal = expected_withdrawals[-1] -// state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) +// # Update the next withdrawal index if this block contained withdrawals +// if len(expected_withdrawals) != 0: +// latest_withdrawal = expected_withdrawals[-1] +// state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) // -// # Update the next validator index to start the next withdrawal sweep -// if len(expected_withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: -// # Next sweep starts after the latest withdrawal's validator index -// next_validator_index = ValidatorIndex((expected_withdrawals[-1].validator_index + 1) % len(state.validators)) -// state.next_withdrawal_validator_index = next_validator_index -// else: -// # Advance sweep by the max length of the sweep if there was not a full set of withdrawals -// next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP -// next_validator_index = ValidatorIndex(next_index % len(state.validators)) -// state.next_withdrawal_validator_index = next_validator_index +// # Update the next validator index to start the next withdrawal sweep +// if len(expected_withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: +// # Next sweep starts after the latest withdrawal's validator index +// next_validator_index = ValidatorIndex((expected_withdrawals[-1].validator_index + 1) % len(state.validators)) +// state.next_withdrawal_validator_index = next_validator_index +// else: +// # Advance sweep by the max length of the sweep if there was not a full set of withdrawals +// next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP +// next_validator_index = ValidatorIndex(next_index % len(state.validators)) +// state.next_withdrawal_validator_index = next_validator_index func ProcessWithdrawals(st state.BeaconState, executionData interfaces.ExecutionData) (state.BeaconState, error) { - expectedWithdrawals, _, err := st.ExpectedWithdrawals() + expectedWithdrawals, partials, err := st.ExpectedWithdrawals() if err != nil { return nil, errors.Wrap(err, "could not get expected withdrawals") } - var wdRoot [32]byte - if executionData.IsBlinded() { - r, err := executionData.WithdrawalsRoot() - if err != nil { - return nil, errors.Wrap(err, "could not get withdrawals root") - } - wdRoot = bytesutil.ToBytes32(r) - } else { - wds, err := executionData.Withdrawals() - if err != nil { - return nil, errors.Wrap(err, "could not get withdrawals") - } - wdRoot, err = ssz.WithdrawalSliceRoot(wds, fieldparams.MaxWithdrawalsPerPayload) - if err != nil { - return nil, errors.Wrap(err, "could not get withdrawals root") - } + // Instead of checking "assert withdrawal == expected_withdrawal", we check the withdrawals root. + wdRoot, err := withdrawalsRootFromMaybeBlindedExecutionData(executionData) + if err != nil { + return nil, errors.Wrap(err, "could not get withdrawals root") } - expectedRoot, err := ssz.WithdrawalSliceRoot(expectedWithdrawals, fieldparams.MaxWithdrawalsPerPayload) if err != nil { return nil, errors.Wrap(err, "could not get expected withdrawals root") @@ -182,6 +172,13 @@ func ProcessWithdrawals(st state.BeaconState, executionData interfaces.Execution return nil, errors.Wrap(err, "could not decrease balance") } } + + if partials > 0 { + if err := st.DequeuePartialWithdrawals(partials); err != nil { + return nil, fmt.Errorf("could not dequeue partial withdrawals: %w", err) + } + } + if len(expectedWithdrawals) > 0 { if err := st.SetNextWithdrawalIndex(expectedWithdrawals[len(expectedWithdrawals)-1].Index + 1); err != nil { return nil, errors.Wrap(err, "could not set next withdrawal index") @@ -207,6 +204,21 @@ func ProcessWithdrawals(st state.BeaconState, executionData interfaces.Execution return st, nil } +func withdrawalsRootFromMaybeBlindedExecutionData(ed interfaces.ExecutionData) ([32]byte, error) { + if ed.IsBlinded() { + r, err := ed.WithdrawalsRoot() + if err != nil { + return [32]byte{}, errors.Wrap(err, "could not get withdrawals root from blinded execution data") + } + return bytesutil.ToBytes32(r), nil + } + wds, err := ed.Withdrawals() + if err != nil { + return [32]byte{}, errors.Wrap(err, "could not get withdrawals") + } + return ssz.WithdrawalSliceRoot(wds, fieldparams.MaxWithdrawalsPerPayload) +} + // BLSChangesSignatureBatch extracts the relevant signatures from the provided execution change // messages and transforms them into a signature batch object. func BLSChangesSignatureBatch( diff --git a/beacon-chain/core/electra/BUILD.bazel b/beacon-chain/core/electra/BUILD.bazel index acb1f7dceded..1acd92852cf8 100644 --- a/beacon-chain/core/electra/BUILD.bazel +++ b/beacon-chain/core/electra/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "churn.go", "consolidations.go", + "epoch_processing.go", "transition.go", "upgrade.go", "validator.go", @@ -14,9 +15,11 @@ go_library( deps = [ "//beacon-chain/core/altair: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", diff --git a/beacon-chain/core/electra/epoch_processing.go b/beacon-chain/core/electra/epoch_processing.go new file mode 100644 index 000000000000..c774518b1381 --- /dev/null +++ b/beacon-chain/core/electra/epoch_processing.go @@ -0,0 +1,149 @@ +package electra + +import ( + "context" + "fmt" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/validators" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" +) + +// sortableIndices implements the Sort interface to sort newly activated validator indices +// by activation epoch and by index number. +type sortableIndices struct { + indices []primitives.ValidatorIndex + validators []*ethpb.Validator +} + +// Len is the number of elements in the collection. +func (s sortableIndices) Len() int { return len(s.indices) } + +// Swap swaps the elements with indexes i and j. +func (s sortableIndices) Swap(i, j int) { s.indices[i], s.indices[j] = s.indices[j], s.indices[i] } + +// Less reports whether the element with index i must sort before the element with index j. +func (s sortableIndices) Less(i, j int) bool { + if s.validators[s.indices[i]].ActivationEligibilityEpoch == s.validators[s.indices[j]].ActivationEligibilityEpoch { + return s.indices[i] < s.indices[j] + } + return s.validators[s.indices[i]].ActivationEligibilityEpoch < s.validators[s.indices[j]].ActivationEligibilityEpoch +} + +// ProcessRegistryUpdates rotates validators in and out of active pool. +// the amount to rotate is determined churn limit. +// +// Spec pseudocode definition: +// +// def process_registry_updates(state: BeaconState) -> None: +// # Process activation eligibility and ejections +// for index, validator in enumerate(state.validators): +// if is_eligible_for_activation_queue(validator): +// validator.activation_eligibility_epoch = get_current_epoch(state) + 1 +// +// if ( +// is_active_validator(validator, get_current_epoch(state)) +// and validator.effective_balance <= EJECTION_BALANCE +// ): +// initiate_validator_exit(state, ValidatorIndex(index)) +// +// # Activate all eligible validators +// activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) +// for validator in state.validators: +// if is_eligible_for_activation(state, validator): +// validator.activation_epoch = activation_epoch +func ProcessRegistryUpdates(ctx context.Context, state state.BeaconState) (state.BeaconState, error) { + currentEpoch := time.CurrentEpoch(state) + ejectionBal := params.BeaconConfig().EjectionBalance + activationEpoch := helpers.ActivationExitEpoch(currentEpoch) + vals := state.Validators() + for idx, val := range vals { + if helpers.IsEligibleForActivationQueue(val, currentEpoch) { + val.ActivationEligibilityEpoch = currentEpoch + 1 + } + if helpers.IsActiveValidator(val, currentEpoch) && val.EffectiveBalance <= ejectionBal { + var err error + maxExitEpoch, churn := validators.MaxExitEpochAndChurn(state) + state, _, err = validators.InitiateValidatorExit(ctx, state, primitives.ValidatorIndex(idx), maxExitEpoch, churn) + if err != nil { + return nil, err + } + } + + if helpers.IsEligibleForActivation(state, val) { + val.ActivationEpoch = activationEpoch + if err := state.UpdateValidatorAtIndex(primitives.ValidatorIndex(idx), val); err != nil { + return nil, err + } + } + } + + return state, nil +} + +// ProcessEffectiveBalanceUpdates processes effective balance updates during epoch processing. +// +// Spec pseudocode definition: +// +// def process_effective_balance_updates(state: BeaconState) -> None: +// # Update effective balances with hysteresis +// for index, validator in enumerate(state.validators): +// balance = state.balances[index] +// HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) +// DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER +// UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER +// EFFECTIVE_BALANCE_LIMIT = ( +// MAX_EFFECTIVE_BALANCE_EIP7251 if has_compounding_withdrawal_credential(validator) +// else MIN_ACTIVATION_BALANCE +// ) +// +// if ( +// balance + DOWNWARD_THRESHOLD < validator.effective_balance +// or validator.effective_balance + UPWARD_THRESHOLD < balance +// ): +// validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, EFFECTIVE_BALANCE_LIMIT) +func ProcessEffectiveBalanceUpdates(state state.BeaconState) (state.BeaconState, error) { + effBalanceInc := params.BeaconConfig().EffectiveBalanceIncrement + hysteresisInc := effBalanceInc / params.BeaconConfig().HysteresisQuotient + downwardThreshold := hysteresisInc * params.BeaconConfig().HysteresisDownwardMultiplier + upwardThreshold := hysteresisInc * params.BeaconConfig().HysteresisUpwardMultiplier + + bals := state.Balances() + + // Update effective balances with hysteresis. + validatorFunc := func(idx int, val *ethpb.Validator) (bool, *ethpb.Validator, error) { + if val == nil { + return false, nil, fmt.Errorf("validator %d is nil in state", idx) + } + if idx >= len(bals) { + return false, nil, fmt.Errorf("validator index exceeds validator length in state %d >= %d", idx, len(state.Balances())) + } + balance := bals[idx] + + effectiveBalanceLimit := params.BeaconConfig().MinActivationBalance + if helpers.HasCompoundingWithdrawalCredential(val) { + effectiveBalanceLimit = params.BeaconConfig().MaxEffectiveBalanceElectra + } + + if balance+downwardThreshold < val.EffectiveBalance || val.EffectiveBalance+upwardThreshold < balance { + effectiveBal := min(balance-balance%effBalanceInc, effectiveBalanceLimit) + if effectiveBal != val.EffectiveBalance { + newVal := ethpb.CopyValidator(val) + newVal.EffectiveBalance = effectiveBal + return true, newVal, nil + } + return false, val, nil + } + return false, val, nil + } + + if err := state.ApplyToEveryValidator(validatorFunc); err != nil { + return nil, err + } + + return state, nil +} diff --git a/beacon-chain/core/electra/transition.go b/beacon-chain/core/electra/transition.go index 878b2e67c96c..364c8eec2533 100644 --- a/beacon-chain/core/electra/transition.go +++ b/beacon-chain/core/electra/transition.go @@ -1,8 +1,24 @@ package electra import ( + "bytes" + "context" + "slices" + + "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/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/validators" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/math" + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/time/slots" + "go.opencensus.io/trace" ) // Re-exports for methods that haven't changed in Electra. @@ -20,4 +36,330 @@ var ( ProcessSyncCommitteeUpdates = altair.ProcessSyncCommitteeUpdates AttestationsDelta = altair.AttestationsDelta ProcessSyncAggregate = altair.ProcessSyncAggregate + + // These need to be rewritten for electra. + ProcessDeposits = altair.ProcessDeposits + ProcessAttestationsNoVerifySignature = altair.ProcessAttestationsNoVerifySignature ) + +// ProcessEpoch describes the per epoch operations that are performed on the beacon state. +// It's optimized by pre computing validator attested info and epoch total/attested balances upfront. +// +// Spec definition: +// +// def process_epoch(state: BeaconState) -> None: +// process_justification_and_finalization(state) +// process_inactivity_updates(state) +// process_rewards_and_penalties(state) +// process_registry_updates(state) +// process_slashings(state) +// process_eth1_data_reset(state) +// process_pending_balance_deposits(state) # New in EIP7251 +// process_pending_consolidations(state) # New in EIP7251 +// process_effective_balance_updates(state) +// process_slashings_reset(state) +// process_randao_mixes_reset(state) +func ProcessEpoch(ctx context.Context, state state.BeaconState) (state.BeaconState, error) { + ctx, span := trace.StartSpan(ctx, "electra.ProcessEpoch") + defer span.End() + + if state == nil || state.IsNil() { + return nil, errors.New("nil state") + } + vp, bp, err := InitializePrecomputeValidators(ctx, state) + if err != nil { + return nil, err + } + vp, bp, err = ProcessEpochParticipation(ctx, state, bp, vp) + if err != nil { + return nil, err + } + state, err = precompute.ProcessJustificationAndFinalizationPreCompute(state, bp) + if err != nil { + return nil, errors.Wrap(err, "could not process justification") + } + state, vp, err = ProcessInactivityScores(ctx, state, vp) + if err != nil { + return nil, errors.Wrap(err, "could not process inactivity updates") + } + state, err = ProcessRewardsAndPenaltiesPrecompute(state, bp, vp) + if err != nil { + return nil, errors.Wrap(err, "could not process rewards and penalties") + } + state, err = ProcessRegistryUpdates(ctx, state) + if err != nil { + return nil, errors.Wrap(err, "could not process registry updates") + } + proportionalSlashingMultiplier, err := state.ProportionalSlashingMultiplier() + if err != nil { + return nil, err + } + state, err = ProcessSlashings(state, proportionalSlashingMultiplier) + if err != nil { + return nil, err + } + state, err = ProcessEth1DataReset(state) + if err != nil { + return nil, err + } + state, err = ProcessPendingBalanceDeposits(ctx, state, math.Gwei(bp.ActiveCurrentEpoch)) + if err != nil { + return nil, err + } + state, err = ProcessPendingConsolidations(ctx, state) + if err != nil { + return nil, err + } + state, err = ProcessEffectiveBalanceUpdates(state) + if err != nil { + return nil, err + } + state, err = ProcessSlashingsReset(state) + if err != nil { + return nil, err + } + state, err = ProcessRandaoMixesReset(state) + if err != nil { + return nil, err + } + state, err = ProcessHistoricalDataUpdate(state) + if err != nil { + return nil, err + } + + state, err = ProcessParticipationFlagUpdates(state) + if err != nil { + return nil, err + } + + state, err = ProcessSyncCommitteeUpdates(ctx, state) + if err != nil { + return nil, err + } + + return state, nil +} + +// ProcessPendingBalanceUpdates -- +// +// Spec definition: +// +// def process_pending_balance_deposits(state: BeaconState) -> None: +// available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit(state) +// processed_amount = 0 +// next_deposit_index = 0 +// +// 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 +// next_deposit_index += 1 +// +// state.pending_balance_deposits = state.pending_balance_deposits[next_deposit_index:] +// +// if len(state.pending_balance_deposits) == 0: +// state.deposit_balance_to_consume = Gwei(0) +// else: +// state.deposit_balance_to_consume = available_for_processing - processed_amount +func ProcessPendingBalanceDeposits(ctx context.Context, st state.BeaconState, activeBalance math.Gwei) (state.BeaconState, error) { + _, span := trace.StartSpan(ctx, "electra.ProcessPendingBalanceDeposits") + defer span.End() + + if st == nil || st.IsNil() { + return nil, errors.New("nil state") + } + + depBalToConsume, err := st.DepositBalanceToConsume() + if err != nil { + return nil, err + } + var activeBalGwei math.Gwei // TODO: get_active_balance(state) + + availableForProcessing := depBalToConsume + helpers.ActivationExitChurnLimit(activeBalGwei) + processedAmount := math.Gwei(0) + nextDepositIndex := 0 + + deposits, err := st.PendingBalanceDeposits() + if err != nil { + return nil, err + } + + for _, deposit := range deposits { + if processedAmount+math.Gwei(deposit.Amount) > availableForProcessing { + break + } + if err := helpers.IncreaseBalance(st, deposit.Index, deposit.Amount); err != nil { + return nil, err + } + processedAmount += math.Gwei(deposit.Amount) + nextDepositIndex++ + } + + deposits = slices.Clip(deposits[nextDepositIndex:]) // TODO: Does clip make sense here or can it clip on copy? + if err := st.SetPendingBalanceDeposits(deposits); err != nil { + return nil, err + } + + if len(deposits) == 0 { + if err := st.SetDepositBalanceToConsume(0); err != nil { + return nil, err + } + } else { + if err := st.SetDepositBalanceToConsume(availableForProcessing - processedAmount); err != nil { + return nil, err + } + } + + return st, nil +} + +// ProcessExecutionLayerWithdrawRequests +// +// Spec definition: +// +// def process_execution_layer_withdrawal_request( +// state: BeaconState, +// execution_layer_withdrawal_request: ExecutionLayerWithdrawalRequest +// ) -> None: +// amount = execution_layer_withdrawal_request.amount +// is_full_exit_request = amount == FULL_EXIT_REQUEST_AMOUNT +// +// # If partial withdrawal queue is full, only full exits are processed +// if len(state.pending_partial_withdrawals) == PENDING_PARTIAL_WITHDRAWALS_LIMIT and not is_full_exit_request: +// return +// +// validator_pubkeys = [v.pubkey for v in state.validators] +// # Verify pubkey exists +// request_pubkey = execution_layer_withdrawal_request.validator_pubkey +// if request_pubkey not in validator_pubkeys: +// return +// index = ValidatorIndex(validator_pubkeys.index(request_pubkey)) +// validator = state.validators[index] +// +// # Verify withdrawal credentials +// has_correct_credential = has_execution_withdrawal_credential(validator) +// is_correct_source_address = ( +// validator.withdrawal_credentials[12:] == execution_layer_withdrawal_request.source_address +// ) +// if not (has_correct_credential and is_correct_source_address): +// return +// # Verify the validator is active +// if not is_active_validator(validator, get_current_epoch(state)): +// return +// # Verify exit has not been initiated +// if validator.exit_epoch != FAR_FUTURE_EPOCH: +// return +// # Verify the validator has been active long enough +// if get_current_epoch(state) < validator.activation_epoch + SHARD_COMMITTEE_PERIOD: +// return +// +// pending_balance_to_withdraw = get_pending_balance_to_withdraw(state, index) +// +// if is_full_exit_request: +// # Only exit validator if it has no pending withdrawals in the queue +// if pending_balance_to_withdraw == 0: +// initiate_validator_exit(state, index) +// return +// +// has_sufficient_effective_balance = validator.effective_balance >= MIN_ACTIVATION_BALANCE +// has_excess_balance = state.balances[index] > MIN_ACTIVATION_BALANCE + pending_balance_to_withdraw +// +// # Only allow partial withdrawals with compounding withdrawal credentials +// if has_compounding_withdrawal_credential(validator) and has_sufficient_effective_balance and has_excess_balance: +// to_withdraw = min( +// state.balances[index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw, +// amount +// ) +// exit_queue_epoch = compute_exit_epoch_and_update_churn(state, to_withdraw) +// withdrawable_epoch = Epoch(exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +// state.pending_partial_withdrawals.append(PendingPartialWithdrawal( +// index=index, +// amount=to_withdraw, +// withdrawable_epoch=withdrawable_epoch, +// )) +func ProcessExecutionLayerWithdrawRequests(ctx context.Context, st state.BeaconState, wrs []*enginev1.ExecutionLayerWithdrawalRequest) (state.BeaconState, error) { + // TODO: Use PR 13888 + for _, wr := range wrs { + if wr == nil { + return nil, errors.New("nil execution layer withdrawal request") + } + amount := wr.Amount + isFullExitRequest := amount == params.BeaconConfig().FullExitRequestAmount + // If partial withdrawal queue is full, only full exits are processed + if n, err := st.NumPendingPartialWithdrawals(); err != nil { + return nil, err + } else if n == params.BeaconConfig().PendingPartialWithdrawalsLimit && !isFullExitRequest { + continue + } + + vIdx, exists := st.ValidatorIndexByPubkey(bytesutil.ToBytes48(wr.ValidatorPubkey)) + if !exists { + continue + } + validator, err := st.ValidatorAtIndex(vIdx) + if err != nil { + return nil, err + } + hasCorrectCredential := helpers.HasExecutionWithdrawalCredentials(validator) + isCorrectSourceAddress := bytes.Equal(validator.WithdrawalCredentials[12:], wr.SourceAddress) // TODO: Is the correct way to perform this check safely? + if !hasCorrectCredential || !isCorrectSourceAddress { + continue + } + if !helpers.IsActiveValidator(validator, slots.ToEpoch(st.Slot())) { + continue + } + if validator.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + continue + } + if slots.ToEpoch(st.Slot()) < validator.ActivationEpoch+params.BeaconConfig().ShardCommitteePeriod { + continue + } + + pendingBalanceToWithdraw, err := st.PendingBalanceToWithdraw(vIdx) + if err != nil { + return nil, err + } + if isFullExitRequest { + if pendingBalanceToWithdraw == 0 { + maxExitEpoch, churn := validators.MaxExitEpochAndChurn(st) + var err error + st, _, err = validators.InitiateValidatorExit(ctx, st, vIdx, maxExitEpoch, churn) + if err != nil { + return nil, err + } + } + continue + } + + hasSufficientEffectiveBalance := validator.EffectiveBalance >= params.BeaconConfig().MinActivationBalance + vBal, err := st.BalanceAtIndex(vIdx) + if err != nil { + return nil, err + } + hasExcessBalance := vBal > params.BeaconConfig().MinActivationBalance+pendingBalanceToWithdraw + + // Only allow partial withdrawals with compounding withdrawal credentials + if helpers.HasCompoundingWithdrawalCredential(validator) && hasSufficientEffectiveBalance && hasExcessBalance { + toWithdraw := min(vBal-params.BeaconConfig().MinActivationBalance-pendingBalanceToWithdraw, amount) + exitQueueEpoch, err := st.ExitEpochAndUpdateChurn(math.Gwei(toWithdraw)) + if err != nil { + return nil, err + } + withdrawableEpoch := exitQueueEpoch + params.BeaconConfig().MinValidatorWithdrawabilityDelay + if err := st.AppendPendingPartialWithdrawal(ðpb.PendingPartialWithdrawal{ + Index: vIdx, + Amount: toWithdraw, + WithdrawableEpoch: withdrawableEpoch, + }); err != nil { + return nil, err + } + } + } + + return st, nil +} + +func ProcessDepositReceipts(ctx context.Context, st state.BeaconState, drs []*enginev1.DepositReceipt) (state.BeaconState, error) { + return st, nil // TODO: EIP-6110 +} diff --git a/beacon-chain/core/time/slot_epoch.go b/beacon-chain/core/time/slot_epoch.go index 9ffa1561a3bf..00a3536856e0 100644 --- a/beacon-chain/core/time/slot_epoch.go +++ b/beacon-chain/core/time/slot_epoch.go @@ -95,8 +95,8 @@ func CanUpgradeToDeneb(slot primitives.Slot) bool { // If state.slot % SLOTS_PER_EPOCH == 0 and compute_epoch_at_slot(state.slot) == ELECTRA_FORK_EPOCH func CanUpgradeToElectra(slot primitives.Slot) bool { epochStart := slots.IsEpochStart(slot) - electraEpoch := slots.ToEpoch(slot) == params.BeaconConfig().ElectraForkEpoch - return epochStart && electraEpoch + ElectraEpoch := slots.ToEpoch(slot) == params.BeaconConfig().ElectraForkEpoch + return epochStart && ElectraEpoch } // CanProcessEpoch checks the eligibility to process epoch. diff --git a/beacon-chain/core/transition/transition_no_verify_sig.go b/beacon-chain/core/transition/transition_no_verify_sig.go index c4c357dfea2c..d71994698563 100644 --- a/beacon-chain/core/transition/transition_no_verify_sig.go +++ b/beacon-chain/core/transition/transition_no_verify_sig.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/altair" b "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/transition/interop" v "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/validators" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" @@ -223,19 +224,29 @@ func ProcessBlockNoVerifyAnySig( // // Spec pseudocode definition: // -// def process_operations(state: BeaconState, body: ReadOnlyBeaconBlockBody) -> None: -// # 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) +// 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_receipts_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) +// 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) -// for_ops(body.deposits, process_deposit) -// for_ops(body.voluntary_exits, process_voluntary_exit) +// 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_receipts, process_deposit_receipt) # [New in Electra:EIP6110] +// for_ops(body.consolidations, process_consolidation) # [New in Electra:EIP7251] func ProcessOperationsNoVerifyAttsSigs( ctx context.Context, state state.BeaconState, @@ -257,11 +268,16 @@ func ProcessOperationsNoVerifyAttsSigs( if err != nil { return nil, err } - case version.Altair, version.Bellatrix, version.Capella, version.Deneb, version.Electra: + case version.Altair, version.Bellatrix, version.Capella, version.Deneb: state, err = altairOperations(ctx, state, beaconBlock) if err != nil { return nil, err } + case version.Electra: + state, err = electraOperations(ctx, state, beaconBlock) + if err != nil { + return nil, err + } default: return nil, errors.New("block does not have correct version") } @@ -378,6 +394,74 @@ 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_receipts_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_receipts, process_deposit_receipt) # [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) { + + // TODO: EIP-6110 deposit changes. + + // Electra extends the altair operations. + st, err := altairOperations(ctx, st, block) + if err != nil { + return nil, err + } + b := block.Body() + bod, ok := b.(interfaces.ROBlockBodyElectra) + if !ok { + return nil, errors.New("could not cast block body to electra block body") + } + e, err := bod.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.ProcessExecutionLayerWithdrawRequests(ctx, st, exe.WithdrawalRequests()) + if err != nil { + return nil, errors.Wrap(err, "could not process execution layer withdrawal requests") + } + st, err = electra.ProcessDepositReceipts(ctx, st, exe.DepositReceipts()) + if err != nil { + return nil, errors.Wrap(err, "could not process deposit receipts") + } + st, err = electra.ProcessConsolidations(ctx, st, bod.Consolidations()) + if err != nil { + return nil, errors.Wrap(err, "could not process consolidations") + } + + return st, nil +} + // This calls altair block operations. func altairOperations( ctx context.Context, diff --git a/beacon-chain/execution/engine_client.go b/beacon-chain/execution/engine_client.go index 5a6eb39e6fa8..162a38d4b947 100644 --- a/beacon-chain/execution/engine_client.go +++ b/beacon-chain/execution/engine_client.go @@ -222,7 +222,7 @@ func (s *Service) ForkchoiceUpdated( if err != nil { return nil, nil, handleRPCError(err) } - case version.Deneb: + case version.Deneb, version.Electra: // TODO: Confirm no changes in electra here. a, err := attrs.PbV3() if err != nil { return nil, nil, err @@ -709,14 +709,31 @@ func (s *Service) retrievePayloadsFromExecutionHashes( return fullBlocks, nil } -func fullPayloadFromPayloadBody( - header interfaces.ExecutionData, body *pb.ExecutionPayloadBodyV1, bVersion int, +func fullPayloadFromExecutionBlock( + blockVersion int, header interfaces.ExecutionData, block *pb.ExecutionBlock, ) (interfaces.ExecutionData, error) { - if header.IsNil() || body == nil { + if header.IsNil() || block == nil { return nil, errors.New("execution block and header cannot be nil") } + blockHash := block.Hash + if !bytes.Equal(header.BlockHash(), blockHash[:]) { + return nil, fmt.Errorf( + "block hash field in execution header %#x does not match execution block hash %#x", + header.BlockHash(), + blockHash, + ) + } + blockTransactions := block.Transactions + txs := make([][]byte, len(blockTransactions)) + for i, tx := range blockTransactions { + txBin, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + txs[i] = txBin + } - switch bVersion { + switch blockVersion { case version.Bellatrix: return blocks.WrappedExecutionPayload(&pb.ExecutionPayload{ ParentHash: header.ParentHash(), @@ -731,8 +748,8 @@ func fullPayloadFromPayloadBody( Timestamp: header.Timestamp(), ExtraData: header.ExtraData(), BaseFeePerGas: header.BaseFeePerGas(), - BlockHash: header.BlockHash(), - Transactions: body.Transactions, + BlockHash: blockHash[:], + Transactions: txs, }) case version.Capella: return blocks.WrappedExecutionPayloadCapella(&pb.ExecutionPayloadCapella{ @@ -748,9 +765,9 @@ func fullPayloadFromPayloadBody( Timestamp: header.Timestamp(), ExtraData: header.ExtraData(), BaseFeePerGas: header.BaseFeePerGas(), - BlockHash: header.BlockHash(), - Transactions: body.Transactions, - Withdrawals: body.Withdrawals, + BlockHash: blockHash[:], + Transactions: txs, + Withdrawals: block.Withdrawals, }, big.NewInt(0)) // We can't get the block value and don't care about the block value for this instance case version.Deneb: ebg, err := header.ExcessBlobGas() @@ -775,23 +792,106 @@ func fullPayloadFromPayloadBody( Timestamp: header.Timestamp(), ExtraData: header.ExtraData(), BaseFeePerGas: header.BaseFeePerGas(), - BlockHash: header.BlockHash(), - Transactions: body.Transactions, - Withdrawals: body.Withdrawals, - ExcessBlobGas: ebg, + BlockHash: blockHash[:], + Transactions: txs, + Withdrawals: block.Withdrawals, BlobGasUsed: bgu, + ExcessBlobGas: ebg, }, big.NewInt(0)) // We can't get the block value and don't care about the block value for this instance case version.Electra: - ebg, err := header.ExcessBlobGas() + h, ok := header.(interfaces.ExecutionDataElectra) + if !ok { + return nil, errors.New("header is not an Electra execution data") + } + ebg, err := h.ExcessBlobGas() if err != nil { return nil, errors.Wrap(err, "unable to extract ExcessBlobGas attribute from execution payload header") } - bgu, err := header.BlobGasUsed() + bgu, err := h.BlobGasUsed() if err != nil { return nil, errors.Wrap(err, "unable to extract BlobGasUsed attribute from execution payload header") } return blocks.WrappedExecutionPayloadElectra( &pb.ExecutionPayloadElectra{ + ParentHash: h.ParentHash(), + FeeRecipient: h.FeeRecipient(), + StateRoot: h.StateRoot(), + ReceiptsRoot: h.ReceiptsRoot(), + LogsBloom: h.LogsBloom(), + PrevRandao: h.PrevRandao(), + BlockNumber: h.BlockNumber(), + GasLimit: h.GasLimit(), + GasUsed: h.GasUsed(), + Timestamp: h.Timestamp(), + ExtraData: h.ExtraData(), + BaseFeePerGas: h.BaseFeePerGas(), + BlockHash: blockHash[:], + Transactions: txs, + Withdrawals: block.Withdrawals, + BlobGasUsed: bgu, + ExcessBlobGas: ebg, + DepositReceipts: h.DepositReceipts(), + WithdrawalRequests: h.WithdrawalRequests(), + }, big.NewInt(0)) // We can't get the block value and don't care about the block value for this instance + default: + return nil, fmt.Errorf("unknown execution block version %d", block.Version) + } +} + +func fullPayloadFromPayloadBody( + header interfaces.ExecutionData, body *pb.ExecutionPayloadBodyV1, bVersion int, +) (interfaces.ExecutionData, error) { + if header.IsNil() || body == nil { + return nil, errors.New("execution block and header cannot be nil") + } + + switch bVersion { + case version.Bellatrix: + return blocks.WrappedExecutionPayload(&pb.ExecutionPayload{ + ParentHash: header.ParentHash(), + FeeRecipient: header.FeeRecipient(), + StateRoot: header.StateRoot(), + ReceiptsRoot: header.ReceiptsRoot(), + LogsBloom: header.LogsBloom(), + PrevRandao: header.PrevRandao(), + BlockNumber: header.BlockNumber(), + GasLimit: header.GasLimit(), + GasUsed: header.GasUsed(), + Timestamp: header.Timestamp(), + ExtraData: header.ExtraData(), + BaseFeePerGas: header.BaseFeePerGas(), + BlockHash: header.BlockHash(), + Transactions: body.Transactions, + }) + case version.Capella: + return blocks.WrappedExecutionPayloadCapella(&pb.ExecutionPayloadCapella{ + ParentHash: header.ParentHash(), + FeeRecipient: header.FeeRecipient(), + StateRoot: header.StateRoot(), + ReceiptsRoot: header.ReceiptsRoot(), + LogsBloom: header.LogsBloom(), + PrevRandao: header.PrevRandao(), + BlockNumber: header.BlockNumber(), + GasLimit: header.GasLimit(), + GasUsed: header.GasUsed(), + Timestamp: header.Timestamp(), + ExtraData: header.ExtraData(), + BaseFeePerGas: header.BaseFeePerGas(), + BlockHash: header.BlockHash(), + Transactions: body.Transactions, + Withdrawals: body.Withdrawals, + }, big.NewInt(0)) // We can't get the block value and don't care about the block value for this instance + case version.Deneb: + ebg, err := header.ExcessBlobGas() + if err != nil { + return nil, errors.Wrap(err, "unable to extract ExcessBlobGas attribute from execution payload header") + } + bgu, err := header.BlobGasUsed() + if err != nil { + return nil, errors.Wrap(err, "unable to extract BlobGasUsed attribute from execution payload header") + } + return blocks.WrappedExecutionPayloadDeneb( + &pb.ExecutionPayloadDeneb{ ParentHash: header.ParentHash(), FeeRecipient: header.FeeRecipient(), StateRoot: header.StateRoot(), @@ -810,6 +910,41 @@ func fullPayloadFromPayloadBody( ExcessBlobGas: ebg, BlobGasUsed: bgu, }, big.NewInt(0)) // We can't get the block value and don't care about the block value for this instance + case version.Electra: + h, ok := header.(interfaces.ExecutionDataElectra) + if !ok { + return nil, errors.New("header is not an Electra execution data") + } + ebg, err := h.ExcessBlobGas() + if err != nil { + return nil, errors.Wrap(err, "unable to extract ExcessBlobGas attribute from execution payload header") + } + bgu, err := h.BlobGasUsed() + if err != nil { + return nil, errors.Wrap(err, "unable to extract BlobGasUsed attribute from execution payload header") + } + return blocks.WrappedExecutionPayloadElectra( + &pb.ExecutionPayloadElectra{ + ParentHash: h.ParentHash(), + FeeRecipient: h.FeeRecipient(), + StateRoot: h.StateRoot(), + ReceiptsRoot: h.ReceiptsRoot(), + LogsBloom: h.LogsBloom(), + PrevRandao: h.PrevRandao(), + BlockNumber: h.BlockNumber(), + GasLimit: h.GasLimit(), + GasUsed: h.GasUsed(), + Timestamp: h.Timestamp(), + ExtraData: h.ExtraData(), + BaseFeePerGas: h.BaseFeePerGas(), + BlockHash: h.BlockHash(), + Transactions: body.Transactions, + Withdrawals: body.Withdrawals, + ExcessBlobGas: ebg, + BlobGasUsed: bgu, + DepositReceipts: h.DepositReceipts(), + WithdrawalRequests: h.WithdrawalRequests(), + }, big.NewInt(0)) // We can't get the block value and don't care about the block value for this instance default: return nil, fmt.Errorf("unknown execution block version for payload %d", bVersion) } @@ -943,6 +1078,8 @@ func buildEmptyExecutionPayload(v int) (proto.Message, error) { Transactions: make([][]byte, 0), Withdrawals: make([]*pb.Withdrawal, 0), }, nil + case version.Electra: + panic("implement me") default: return nil, errors.Wrapf(ErrUnsupportedVersion, "version=%s", version.String(v)) } diff --git a/beacon-chain/rpc/eth/debug/handlers.go b/beacon-chain/rpc/eth/debug/handlers.go index 49741eb67439..1d705348fbab 100644 --- a/beacon-chain/rpc/eth/debug/handlers.go +++ b/beacon-chain/rpc/eth/debug/handlers.go @@ -89,6 +89,8 @@ func (s *Server) getBeaconStateV2(ctx context.Context, w http.ResponseWriter, id httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } + case version.Electra: + panic("implement me") default: httputil.HandleError(w, "Unsupported state version", http.StatusInternalServerError) return diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index 942b02545a8b..6bd486e3c68d 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -475,6 +475,8 @@ func (s *Server) sendPayloadAttributes(ctx context.Context, w http.ResponseWrite Withdrawals: structs.WithdrawalsFromConsensus(withdrawals), ParentBeaconBlockRoot: hexutil.Encode(parentRoot[:]), } + case version.Electra: + panic("implement me") default: return write(w, flusher, "Payload version %s is not supported", version.String(headState.Version())) } diff --git a/beacon-chain/sync/rpc_chunked_response.go b/beacon-chain/sync/rpc_chunked_response.go index a8b8e74aec06..87668882eec2 100644 --- a/beacon-chain/sync/rpc_chunked_response.go +++ b/beacon-chain/sync/rpc_chunked_response.go @@ -65,6 +65,8 @@ func WriteBlockChunk(stream libp2pcore.Stream, tor blockchain.TemporalOracle, en return err } obtainedCtx = digest[:] + case version.Electra: + panic("implement me") default: return errors.Wrapf(ErrUnrecognizedVersion, "block version %d is not recognized", blk.Version()) } diff --git a/consensus-types/blocks/execution.go b/consensus-types/blocks/execution.go index 08cc5aeabf6d..b8cdecef548b 100644 --- a/consensus-types/blocks/execution.go +++ b/consensus-types/blocks/execution.go @@ -1490,7 +1490,7 @@ type executionPayloadElectra struct { } // WrappedExecutionPayloadElectra is a constructor which wraps a protobuf execution payload into an interface. -func WrappedExecutionPayloadElectra(p *enginev1.ExecutionPayloadElectra, value math.Wei) (interfaces.ExecutionData, error) { +func WrappedExecutionPayloadElectra(p *enginev1.ExecutionPayloadElectra, value math.Wei) (interfaces.ExecutionDataElectra, error) { w := executionPayloadElectra{p: p, weiValue: value, gweiValue: uint64(math.WeiToGwei(value))} if w.IsNil() { return nil, consensus_types.ErrNilObjectWrapped diff --git a/proto/eth/v1/beacon_block.proto b/proto/eth/v1/beacon_block.proto index 2cb1f213b10c..cebf9f11a6b9 100644 --- a/proto/eth/v1/beacon_block.proto +++ b/proto/eth/v1/beacon_block.proto @@ -209,3 +209,4 @@ message SyncAggregate { // BLS aggregated signature of the sync committee for the ones that voted. bytes sync_committee_signature = 2 [(ethereum.eth.ext.ssz_size) = "96"]; } + diff --git a/runtime/interop/premine-state.go b/runtime/interop/premine-state.go index 7a9640739101..cdd979cfacd3 100644 --- a/runtime/interop/premine-state.go +++ b/runtime/interop/premine-state.go @@ -157,6 +157,11 @@ func (s *PremineGenesisConfig) empty() (state.BeaconState, error) { if err != nil { return nil, err } + case version.Electra: + e, err = state_native.InitializeFromProtoElectra(ðpb.BeaconStateElectra{}) + if err != nil { + return nil, err + } default: return nil, errUnsupportedVersion } @@ -336,6 +341,8 @@ func (s *PremineGenesisConfig) setFork(g state.BeaconState) error { pv, cv = params.BeaconConfig().BellatrixForkVersion, params.BeaconConfig().CapellaForkVersion case version.Deneb: pv, cv = params.BeaconConfig().CapellaForkVersion, params.BeaconConfig().DenebForkVersion + case version.Electra: + pv, cv = params.BeaconConfig().DenebForkVersion, params.BeaconConfig().ElectraForkVersion default: return errUnsupportedVersion } @@ -524,6 +531,37 @@ func (s *PremineGenesisConfig) setLatestBlockHeader(g state.BeaconState) error { BlsToExecutionChanges: make([]*ethpb.SignedBLSToExecutionChange, 0), BlobKzgCommitments: make([][]byte, 0), } + case version.Electra: + body = ðpb.BeaconBlockBodyElectra{ + RandaoReveal: make([]byte, 96), + Eth1Data: ðpb.Eth1Data{ + DepositRoot: make([]byte, 32), + BlockHash: make([]byte, 32), + }, + Graffiti: make([]byte, 32), + SyncAggregate: ðpb.SyncAggregate{ + SyncCommitteeBits: make([]byte, fieldparams.SyncCommitteeLength/8), + SyncCommitteeSignature: make([]byte, fieldparams.BLSSignatureLength), + }, + ExecutionPayload: &enginev1.ExecutionPayloadElectra{ + ParentHash: make([]byte, 32), + FeeRecipient: make([]byte, 20), + StateRoot: make([]byte, 32), + ReceiptsRoot: make([]byte, 32), + LogsBloom: make([]byte, 256), + PrevRandao: make([]byte, 32), + ExtraData: make([]byte, 0), + BaseFeePerGas: make([]byte, 32), + BlockHash: make([]byte, 32), + Transactions: make([][]byte, 0), + Withdrawals: make([]*enginev1.Withdrawal, 0), + DepositReceipts: make([]*enginev1.DepositReceipt, 0), + WithdrawalRequests: make([]*enginev1.ExecutionLayerWithdrawalRequest, 0), + }, + BlsToExecutionChanges: make([]*ethpb.SignedBLSToExecutionChange, 0), + BlobKzgCommitments: make([][]byte, 0), + Consolidations: make([]*ethpb.SignedConsolidation, 0), + } default: return errUnsupportedVersion } @@ -640,6 +678,40 @@ func (s *PremineGenesisConfig) setExecutionPayload(g state.BeaconState) error { if err != nil { return err } + case version.Electra: + payload := &enginev1.ExecutionPayloadElectra{ + ParentHash: gb.ParentHash().Bytes(), + FeeRecipient: gb.Coinbase().Bytes(), + StateRoot: gb.Root().Bytes(), + ReceiptsRoot: gb.ReceiptHash().Bytes(), + LogsBloom: gb.Bloom().Bytes(), + PrevRandao: params.BeaconConfig().ZeroHash[:], + BlockNumber: gb.NumberU64(), + GasLimit: gb.GasLimit(), + GasUsed: gb.GasUsed(), + Timestamp: gb.Time(), + ExtraData: gb.Extra()[:32], + BaseFeePerGas: bytesutil.PadTo(bytesutil.ReverseByteOrder(gb.BaseFee().Bytes()), fieldparams.RootLength), + BlockHash: gb.Hash().Bytes(), + Transactions: make([][]byte, 0), + Withdrawals: make([]*enginev1.Withdrawal, 0), + ExcessBlobGas: *gb.ExcessBlobGas(), + BlobGasUsed: *gb.BlobGasUsed(), + DepositReceipts: make([]*enginev1.DepositReceipt, 0), + WithdrawalRequests: make([]*enginev1.ExecutionLayerWithdrawalRequest, 0), + } + wep, err := blocks.WrappedExecutionPayloadElectra(payload, big.NewInt(0)) + if err != nil { + return err + } + eph, err := blocks.PayloadToHeaderElectra(wep) + if err != nil { + return err + } + ed, err = blocks.WrappedExecutionPayloadHeaderElectra(eph, big.NewInt(0)) + if err != nil { + return err + } default: return errUnsupportedVersion } diff --git a/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel b/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel index c256d5da8516..09c62a9a0c31 100644 --- a/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel +++ b/testing/spectest/mainnet/electra/epoch_processing/BUILD.bazel @@ -3,6 +3,7 @@ load("@prysm//tools/go:def.bzl", "go_test") go_test( name = "go_default_test", srcs = [ + "effective_balance_updates_test.go", "eth1_data_reset_test.go", "historical_summaries_update_test.go", "inactivity_updates_test.go", @@ -10,6 +11,7 @@ go_test( "participation_flag_updates_test.go", "pending_consolidations_test.go", "randao_mixes_reset_test.go", + "registry_updates_test.go", "rewards_and_penalties_test.go", "slashings_reset_test.go", "slashings_test.go", diff --git a/testing/spectest/mainnet/electra/epoch_processing/effective_balance_updates_test.go b/testing/spectest/mainnet/electra/epoch_processing/effective_balance_updates_test.go new file mode 100644 index 000000000000..22532aed6f0d --- /dev/null +++ b/testing/spectest/mainnet/electra/epoch_processing/effective_balance_updates_test.go @@ -0,0 +1,11 @@ +package epoch_processing + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing" +) + +func TestMainnet_electra_EpochProcessing_EffectiveBalanceUpdates(t *testing.T) { + epoch_processing.RunEffectiveBalanceUpdatesTests(t, "mainnet") +} diff --git a/testing/spectest/mainnet/electra/epoch_processing/registry_updates_test.go b/testing/spectest/mainnet/electra/epoch_processing/registry_updates_test.go new file mode 100644 index 000000000000..3d21e3eec967 --- /dev/null +++ b/testing/spectest/mainnet/electra/epoch_processing/registry_updates_test.go @@ -0,0 +1,11 @@ +package epoch_processing + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing" +) + +func TestMainnet_Electra_EpochProcessing_ResetRegistryUpdates(t *testing.T) { + epoch_processing.RunRegistryUpdatesTests(t, "mainnet") +} diff --git a/testing/spectest/mainnet/electra/finality/finality_test.go b/testing/spectest/mainnet/electra/finality/finality_test.go index 6b290ca7cc3c..7cebe2685027 100644 --- a/testing/spectest/mainnet/electra/finality/finality_test.go +++ b/testing/spectest/mainnet/electra/finality/finality_test.go @@ -7,6 +7,5 @@ import ( ) func TestMainnet_Electra_Finality(t *testing.T) { - t.Skip("TODO: Electra") finality.RunFinalityTest(t, "mainnet") } diff --git a/testing/spectest/mainnet/electra/fork_transition/transition_test.go b/testing/spectest/mainnet/electra/fork_transition/transition_test.go index bfde60ba213e..0c0622b0aaed 100644 --- a/testing/spectest/mainnet/electra/fork_transition/transition_test.go +++ b/testing/spectest/mainnet/electra/fork_transition/transition_test.go @@ -7,6 +7,5 @@ import ( ) func TestMainnet_Electra_Transition(t *testing.T) { - t.Skip("TODO: Electra") fork.RunForkTransitionTest(t, "mainnet") } diff --git a/testing/spectest/mainnet/electra/forkchoice/forkchoice_test.go b/testing/spectest/mainnet/electra/forkchoice/forkchoice_test.go index e46b5557a8a3..50f73e858f6f 100644 --- a/testing/spectest/mainnet/electra/forkchoice/forkchoice_test.go +++ b/testing/spectest/mainnet/electra/forkchoice/forkchoice_test.go @@ -8,6 +8,5 @@ import ( ) func TestMainnet_Electra_Forkchoice(t *testing.T) { - t.Skip("TODO: Electra") forkchoice.Run(t, "mainnet", version.Electra) } diff --git a/testing/spectest/mainnet/electra/operations/BUILD.bazel b/testing/spectest/mainnet/electra/operations/BUILD.bazel index a8a3ba231ec0..991c6042d625 100644 --- a/testing/spectest/mainnet/electra/operations/BUILD.bazel +++ b/testing/spectest/mainnet/electra/operations/BUILD.bazel @@ -3,10 +3,12 @@ load("@prysm//tools/go:def.bzl", "go_test") go_test( name = "go_default_test", srcs = [ + "attestation_test.go", "attester_slashing_test.go", "block_header_test.go", "bls_to_execution_change_test.go", "consolidation_test.go", + "deposit_test.go", "execution_payload_test.go", "proposer_slashing_test.go", "sync_committee_test.go", diff --git a/testing/spectest/mainnet/electra/operations/attestation_test.go b/testing/spectest/mainnet/electra/operations/attestation_test.go new file mode 100644 index 000000000000..ac693540b138 --- /dev/null +++ b/testing/spectest/mainnet/electra/operations/attestation_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMainnet_Electra_Operations_Attestation(t *testing.T) { + operations.RunAttestationTest(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/mainnet/electra/operations/voluntary_exit_test.go b/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go index ed87d6df8b87..c0f18adecf2f 100644 --- a/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go +++ b/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go @@ -7,6 +7,5 @@ import ( ) func TestMainnet_Electra_Operations_VoluntaryExit(t *testing.T) { - t.Skip("TODO: Electra") operations.RunVoluntaryExitTest(t, "mainnet") } diff --git a/testing/spectest/mainnet/electra/random/random_test.go b/testing/spectest/mainnet/electra/random/random_test.go index 02c8c474f083..c7404380a1d7 100644 --- a/testing/spectest/mainnet/electra/random/random_test.go +++ b/testing/spectest/mainnet/electra/random/random_test.go @@ -7,6 +7,5 @@ import ( ) func TestMainnet_Electra_Random(t *testing.T) { - t.Skip("TODO: Electra") sanity.RunBlockProcessingTest(t, "mainnet", "random/random/pyspec_tests") } diff --git a/testing/spectest/mainnet/electra/sanity/blocks_test.go b/testing/spectest/mainnet/electra/sanity/blocks_test.go index 27cef36e34ba..8e3fc1bcb93e 100644 --- a/testing/spectest/mainnet/electra/sanity/blocks_test.go +++ b/testing/spectest/mainnet/electra/sanity/blocks_test.go @@ -7,6 +7,5 @@ import ( ) func TestMainnet_Electra_Sanity_Blocks(t *testing.T) { - t.Skip("TODO: Electra") sanity.RunBlockProcessingTest(t, "mainnet", "sanity/blocks/pyspec_tests") } diff --git a/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel b/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel index 02bf1f72aba4..87ef5fb4631f 100644 --- a/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel +++ b/testing/spectest/minimal/electra/epoch_processing/BUILD.bazel @@ -3,6 +3,7 @@ load("@prysm//tools/go:def.bzl", "go_test") go_test( name = "go_default_test", srcs = [ + "effective_balance_updates_test.go", "eth1_data_reset_test.go", "historical_summaries_update_test.go", "inactivity_updates_test.go", @@ -10,6 +11,7 @@ go_test( "participation_flag_updates_test.go", "pending_consolidations_test.go", "randao_mixes_reset_test.go", + "registry_updates_test.go", "rewards_and_penalties_test.go", "slashings_reset_test.go", "slashings_test.go", diff --git a/testing/spectest/minimal/electra/epoch_processing/effective_balance_updates_test.go b/testing/spectest/minimal/electra/epoch_processing/effective_balance_updates_test.go new file mode 100644 index 000000000000..0f14281f917e --- /dev/null +++ b/testing/spectest/minimal/electra/epoch_processing/effective_balance_updates_test.go @@ -0,0 +1,11 @@ +package epoch_processing + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing" +) + +func TestMinimal_electra_EpochProcessing_EffectiveBalanceUpdates(t *testing.T) { + epoch_processing.RunEffectiveBalanceUpdatesTests(t, "minimal") +} diff --git a/testing/spectest/minimal/electra/epoch_processing/registry_updates_test.go b/testing/spectest/minimal/electra/epoch_processing/registry_updates_test.go new file mode 100644 index 000000000000..4b113a1fa121 --- /dev/null +++ b/testing/spectest/minimal/electra/epoch_processing/registry_updates_test.go @@ -0,0 +1,11 @@ +package epoch_processing + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing" +) + +func TestMinimal_Electra_EpochProcessing_ResetRegistryUpdates(t *testing.T) { + epoch_processing.RunRegistryUpdatesTests(t, "minimal") +} diff --git a/testing/spectest/minimal/electra/finality/finality_test.go b/testing/spectest/minimal/electra/finality/finality_test.go index 05b498488e09..8e53a2f926f3 100644 --- a/testing/spectest/minimal/electra/finality/finality_test.go +++ b/testing/spectest/minimal/electra/finality/finality_test.go @@ -7,6 +7,5 @@ import ( ) func TestMinimal_Electra_Finality(t *testing.T) { - t.Skip("TODO: Electra") finality.RunFinalityTest(t, "minimal") } diff --git a/testing/spectest/minimal/electra/forkchoice/forkchoice_test.go b/testing/spectest/minimal/electra/forkchoice/forkchoice_test.go index aa04917b5ae6..19c0acd78ad4 100644 --- a/testing/spectest/minimal/electra/forkchoice/forkchoice_test.go +++ b/testing/spectest/minimal/electra/forkchoice/forkchoice_test.go @@ -8,6 +8,5 @@ import ( ) func TestMinimal_Electra_Forkchoice(t *testing.T) { - t.Skip("TODO: Electra") forkchoice.Run(t, "minimal", version.Electra) } diff --git a/testing/spectest/minimal/electra/operations/BUILD.bazel b/testing/spectest/minimal/electra/operations/BUILD.bazel index d95b1cb0eb0a..8f43ec9df16a 100644 --- a/testing/spectest/minimal/electra/operations/BUILD.bazel +++ b/testing/spectest/minimal/electra/operations/BUILD.bazel @@ -3,10 +3,12 @@ load("@prysm//tools/go:def.bzl", "go_test") go_test( name = "go_default_test", srcs = [ + "attestation_test.go", "attester_slashing_test.go", "block_header_test.go", "bls_to_execution_change_test.go", "consolidation_test.go", + "deposit_test.go", "execution_payload_test.go", "proposer_slashing_test.go", "sync_committee_test.go", diff --git a/testing/spectest/minimal/electra/operations/attestation_test.go b/testing/spectest/minimal/electra/operations/attestation_test.go new file mode 100644 index 000000000000..aafc399b68c8 --- /dev/null +++ b/testing/spectest/minimal/electra/operations/attestation_test.go @@ -0,0 +1,11 @@ +package operations + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/operations" +) + +func TestMinimal_Electra_Operations_Attestation(t *testing.T) { + operations.RunAttestationTest(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/minimal/electra/operations/voluntary_exit_test.go b/testing/spectest/minimal/electra/operations/voluntary_exit_test.go index ab3098ed6b0c..ef54818ada01 100644 --- a/testing/spectest/minimal/electra/operations/voluntary_exit_test.go +++ b/testing/spectest/minimal/electra/operations/voluntary_exit_test.go @@ -7,6 +7,5 @@ import ( ) func TestMinimal_Electra_Operations_VoluntaryExit(t *testing.T) { - t.Skip("TODO: Electra") operations.RunVoluntaryExitTest(t, "minimal") } diff --git a/testing/spectest/minimal/electra/random/random_test.go b/testing/spectest/minimal/electra/random/random_test.go index d454d894bcd0..8ac53e297f2d 100644 --- a/testing/spectest/minimal/electra/random/random_test.go +++ b/testing/spectest/minimal/electra/random/random_test.go @@ -7,6 +7,5 @@ import ( ) func TestMinimal_Electra_Random(t *testing.T) { - t.Skip("TODO: Electra") sanity.RunBlockProcessingTest(t, "minimal", "random/random/pyspec_tests") } diff --git a/testing/spectest/minimal/electra/sanity/blocks_test.go b/testing/spectest/minimal/electra/sanity/blocks_test.go index a6a7b8188764..5d2e27fcd24e 100644 --- a/testing/spectest/minimal/electra/sanity/blocks_test.go +++ b/testing/spectest/minimal/electra/sanity/blocks_test.go @@ -7,6 +7,5 @@ import ( ) func TestMinimal_Electra_Sanity_Blocks(t *testing.T) { - t.Skip("TODO: Electra") sanity.RunBlockProcessingTest(t, "minimal", "sanity/blocks/pyspec_tests") } diff --git a/testing/spectest/shared/electra/epoch_processing/BUILD.bazel b/testing/spectest/shared/electra/epoch_processing/BUILD.bazel index 7140a280dab4..da445078b3b5 100644 --- a/testing/spectest/shared/electra/epoch_processing/BUILD.bazel +++ b/testing/spectest/shared/electra/epoch_processing/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", testonly = True, srcs = [ + "effective_balance_updates.go", "eth1_data_reset.go", "helpers.go", "historical_summaries_update.go", @@ -12,6 +13,7 @@ go_library( "participation_flag_updates.go", "pending_consolidations.go", "randao_mixes_reset.go", + "registry_updates.go", "rewards_and_penalties.go", "slashings.go", "slashings_reset.go", diff --git a/testing/spectest/shared/electra/epoch_processing/effective_balance_updates.go b/testing/spectest/shared/electra/epoch_processing/effective_balance_updates.go new file mode 100644 index 000000000000..fcc60e3b013a --- /dev/null +++ b/testing/spectest/shared/electra/epoch_processing/effective_balance_updates.go @@ -0,0 +1,30 @@ +package epoch_processing + +import ( + "path" + "testing" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/spectest/utils" +) + +// RunEffectiveBalanceUpdatesTests executes "epoch_processing/effective_balance_updates" tests. +func RunEffectiveBalanceUpdatesTests(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "epoch_processing/effective_balance_updates/pyspec_tests") + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + RunEpochOperationTest(t, folderPath, processEffectiveBalanceUpdatesWrapper) + }) + } +} + +func processEffectiveBalanceUpdatesWrapper(t *testing.T, st state.BeaconState) (state.BeaconState, error) { + st, err := electra.ProcessEffectiveBalanceUpdates(st) + require.NoError(t, err, "Could not process final updates") + return st, nil +} diff --git a/testing/spectest/shared/electra/epoch_processing/registry_updates.go b/testing/spectest/shared/electra/epoch_processing/registry_updates.go new file mode 100644 index 000000000000..0d646db8eabf --- /dev/null +++ b/testing/spectest/shared/electra/epoch_processing/registry_updates.go @@ -0,0 +1,32 @@ +package epoch_processing + +import ( + "context" + "path" + "testing" + + "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/state" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/spectest/utils" +) + +// RunRegistryUpdatesTests executes "epoch_processing/registry_updates" tests. +func RunRegistryUpdatesTests(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "epoch_processing/registry_updates/pyspec_tests") + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + // Important to clear cache for every test or else the old value of active validator count gets reused. + helpers.ClearCache() + folderPath := path.Join(testsFolderPath, folder.Name()) + RunEpochOperationTest(t, folderPath, processRegistryUpdatesWrapper) + }) + } +} + +func processRegistryUpdatesWrapper(_ *testing.T, state state.BeaconState) (state.BeaconState, error) { + return electra.ProcessRegistryUpdates(context.Background(), state) +} diff --git a/testing/spectest/shared/electra/operations/BUILD.bazel b/testing/spectest/shared/electra/operations/BUILD.bazel index 9596456ef58d..9b5fe5b52ac0 100644 --- a/testing/spectest/shared/electra/operations/BUILD.bazel +++ b/testing/spectest/shared/electra/operations/BUILD.bazel @@ -4,10 +4,12 @@ go_library( name = "go_default_library", testonly = True, srcs = [ + "attestation.go", "attester_slashing.go", "block_header.go", "bls_to_execution_changes.go", "consolidations.go", + "deposit.go", "deposit_receipt.go", "execution_layer_withdrawal_request.go", "execution_payload.go", diff --git a/testing/spectest/shared/electra/operations/attestation.go b/testing/spectest/shared/electra/operations/attestation.go new file mode 100644 index 000000000000..64b8d5525a43 --- /dev/null +++ b/testing/spectest/shared/electra/operations/attestation.go @@ -0,0 +1,56 @@ +package operations + +import ( + "context" + "errors" + "path" + "testing" + + "github.com/golang/snappy" + b "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" + "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 RunAttestationTest(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "operations/attestation/pyspec_tests") + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + attestationFile, err := util.BazelFileBytes(folderPath, "attestation.ssz_snappy") + require.NoError(t, err) + attestationSSZ, err := snappy.Decode(nil /* dst */, attestationFile) + require.NoError(t, err, "Failed to decompress") + att := ðpb.AttestationElectra{} + require.NoError(t, att.UnmarshalSSZ(attestationSSZ), "Failed to unmarshal") + + body := ðpb.BeaconBlockBodyElectra{Attestations: []*ethpb.AttestationElectra{att}} + processAtt := func(ctx context.Context, st state.BeaconState, blk interfaces.SignedBeaconBlock) (state.BeaconState, error) { + st, err = electra.ProcessAttestationsNoVerifySignature(ctx, st, blk.Block()) + if err != nil { + return nil, err + } + aSet, err := b.AttestationSignatureBatch(ctx, st, blk.Block().Body().Attestations()) + if err != nil { + return nil, err + } + verified, err := aSet.Verify() + if err != nil { + return nil, err + } + if !verified { + return nil, errors.New("could not batch verify attestation signature") + } + return st, nil + } + + RunBlockOperationTest(t, folderPath, body, processAtt) + }) + } +} diff --git a/testing/spectest/shared/electra/operations/deposit.go b/testing/spectest/shared/electra/operations/deposit.go new file mode 100644 index 000000000000..ffb6c5133b04 --- /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.SignedBeaconBlock) (state.BeaconState, error) { + return electra.ProcessDeposits(ctx, s, b.Block().Body().Deposits()) + } + RunBlockOperationTest(t, folderPath, body, processDepositsFunc) + }) + } +}