diff --git a/beacon-chain/core/gloas/deposit_request.go b/beacon-chain/core/gloas/deposit_request.go index affc25eef6f0..ee96eb08ead1 100644 --- a/beacon-chain/core/gloas/deposit_request.go +++ b/beacon-chain/core/gloas/deposit_request.go @@ -68,37 +68,27 @@ func processDepositRequests(ctx context.Context, beaconState state.BeaconState, // ) // func processDepositRequest(beaconState state.BeaconState, request *enginev1.DepositRequest) error { - var err error - defer func() { - if err == nil { - builderDepositsProcessedTotal.Inc() - } - }() - if request == nil { - err = errors.New("nil deposit request") - return err + return errors.New("nil deposit request") } - var applied bool - applied, err = applyBuilderDepositRequest(beaconState, request) + applied, err := applyBuilderDepositRequest(beaconState, request) if err != nil { - err = errors.Wrap(err, "could not apply builder deposit") - return err + return errors.Wrap(err, "could not apply builder deposit") } if applied { + builderDepositsProcessedTotal.Inc() return nil } - if err = beaconState.AppendPendingDeposit(ðpb.PendingDeposit{ + if err := beaconState.AppendPendingDeposit(ðpb.PendingDeposit{ PublicKey: request.Pubkey, WithdrawalCredentials: request.WithdrawalCredentials, Amount: request.Amount, Signature: request.Signature, Slot: beaconState.Slot(), }); err != nil { - err = errors.Wrap(err, "could not append deposit request") - return err + return errors.Wrap(err, "could not append deposit request") } return nil } @@ -132,13 +122,7 @@ func applyBuilderDepositRequest(beaconState state.BeaconState, request *enginev1 } pubkey := bytesutil.ToBytes48(request.Pubkey) - _, isValidator := beaconState.ValidatorIndexByPubkey(pubkey) idx, isBuilder := beaconState.BuilderIndexByPubkey(pubkey) - isBuilderPrefix := helpers.IsBuilderWithdrawalCredential(request.WithdrawalCredentials) - if !isBuilder && (!isBuilderPrefix || isValidator) { - return false, nil - } - if isBuilder { if err := beaconState.IncreaseBuilderBalance(idx, request.Amount); err != nil { return false, err @@ -146,6 +130,20 @@ func applyBuilderDepositRequest(beaconState state.BeaconState, request *enginev1 return true, nil } + isBuilderPrefix := helpers.IsBuilderWithdrawalCredential(request.WithdrawalCredentials) + _, isValidator := beaconState.ValidatorIndexByPubkey(pubkey) + if !isBuilderPrefix || isValidator { + return false, nil + } + + isPending, err := beaconState.IsPendingValidator(request.Pubkey) + if err != nil { + return false, err + } + if isPending { + return false, nil + } + if err := applyDepositForNewBuilder( beaconState, request.Pubkey, diff --git a/beacon-chain/core/gloas/deposit_request_test.go b/beacon-chain/core/gloas/deposit_request_test.go index 02fcf3de3c05..91b0e0f98986 100644 --- a/beacon-chain/core/gloas/deposit_request_test.go +++ b/beacon-chain/core/gloas/deposit_request_test.go @@ -91,6 +91,33 @@ func TestProcessDepositRequest_ExistingBuilderIncreasesBalance(t *testing.T) { require.Equal(t, 0, len(pending)) } +func TestProcessDepositRequest_BuilderDepositWithExistingPendingDepositStaysPending(t *testing.T) { + sk, err := bls.RandKey() + require.NoError(t, err) + + validatorCred := validatorWithdrawalCredentials() + builderCred := builderWithdrawalCredentials() + existingPending := stateTesting.GeneratePendingDeposit(t, sk, 1234, validatorCred, 0) + req := depositRequestFromPending(stateTesting.GeneratePendingDeposit(t, sk, 200, builderCred, 1), 9) + + st := newGloasState(t, nil, nil) + require.NoError(t, st.SetPendingDeposits([]*ethpb.PendingDeposit{existingPending})) + + err = processDepositRequest(st, req) + require.NoError(t, err) + + _, ok := st.BuilderIndexByPubkey(toBytes48(req.Pubkey)) + require.Equal(t, false, ok) + + pending, err := st.PendingDeposits() + require.NoError(t, err) + require.Equal(t, 2, len(pending)) + require.DeepEqual(t, existingPending.PublicKey, pending[0].PublicKey) + require.DeepEqual(t, req.Pubkey, pending[1].PublicKey) + require.DeepEqual(t, req.WithdrawalCredentials, pending[1].WithdrawalCredentials) + require.Equal(t, req.Amount, pending[1].Amount) +} + func TestApplyDepositForBuilder_InvalidSignatureIgnoresDeposit(t *testing.T) { sk, err := bls.RandKey() require.NoError(t, err) diff --git a/beacon-chain/state/interfaces.go b/beacon-chain/state/interfaces.go index 9a16b0f49793..47fdf3487c83 100644 --- a/beacon-chain/state/interfaces.go +++ b/beacon-chain/state/interfaces.go @@ -238,6 +238,7 @@ type ReadOnlyDeposits interface { DepositBalanceToConsume() (primitives.Gwei, error) DepositRequestsStartIndex() (uint64, error) PendingDeposits() ([]*ethpb.PendingDeposit, error) + IsPendingValidator(pubkey []byte) (bool, error) } type ReadOnlyConsolidations interface { diff --git a/beacon-chain/state/state-native/getters_deposits.go b/beacon-chain/state/state-native/getters_deposits.go index cca78de8e7d4..22a9a5688dd5 100644 --- a/beacon-chain/state/state-native/getters_deposits.go +++ b/beacon-chain/state/state-native/getters_deposits.go @@ -1,6 +1,10 @@ package state_native import ( + "bytes" + "fmt" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" @@ -30,6 +34,59 @@ func (b *BeaconState) PendingDeposits() ([]*ethpb.PendingDeposit, error) { return b.pendingDepositsVal(), nil } +// IsPendingValidator checks whether a pending deposit with a valid signature exists for the +// given pubkey. This method requires access to the RLock on the state and only applies in +// electra or later. +// +// +// def is_pending_validator(state: BeaconState, pubkey: BLSPubkey) -> bool: +// +// """ +// Check if a pending deposit with a valid signature is in the queue for the given pubkey. +// """ +// for pending_deposit in state.pending_deposits: +// if pending_deposit.pubkey != pubkey: +// continue +// if is_valid_deposit_signature( +// pending_deposit.pubkey, +// pending_deposit.withdrawal_credentials, +// pending_deposit.amount, +// pending_deposit.signature, +// ): +// return True +// return False +// +// +func (b *BeaconState) IsPendingValidator(pubkey []byte) (bool, error) { + if b.version < version.Electra { + return false, errNotSupported("IsPendingValidator", b.version) + } + b.lock.RLock() + defer b.lock.RUnlock() + for _, deposit := range b.pendingDeposits { + if deposit == nil { + continue + } + if !bytes.Equal(deposit.PublicKey, pubkey) { + continue + } + valid, err := helpers.IsValidDepositSignature(ðpb.Deposit_Data{ + PublicKey: deposit.PublicKey, + WithdrawalCredentials: deposit.WithdrawalCredentials, + Amount: deposit.Amount, + Signature: deposit.Signature, + }) + if err != nil { + log.WithField("pubkey", fmt.Sprintf("%x", deposit.PublicKey)).WithError(err).Warn("Could not verify pending deposit signature") + continue + } + if valid { + return true, nil + } + } + return false, nil +} + func (b *BeaconState) pendingDepositsVal() []*ethpb.PendingDeposit { if b.pendingDeposits == nil { return nil diff --git a/beacon-chain/state/state-native/getters_deposits_test.go b/beacon-chain/state/state-native/getters_deposits_test.go index b7047aae98fa..e36818e206bb 100644 --- a/beacon-chain/state/state-native/getters_deposits_test.go +++ b/beacon-chain/state/state-native/getters_deposits_test.go @@ -4,7 +4,9 @@ import ( "testing" state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + stateTesting "github.com/OffchainLabs/prysm/v7/beacon-chain/state/testing" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + "github.com/OffchainLabs/prysm/v7/crypto/bls" eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/testing/require" ) @@ -66,3 +68,66 @@ func TestPendingDeposits(t *testing.T) { _, err = s.DepositBalanceToConsume() require.ErrorContains(t, "not supported", err) } + +func TestIsPendingValidator(t *testing.T) { + sk, err := bls.RandKey() + require.NoError(t, err) + validDeposit := stateTesting.GeneratePendingDeposit(t, sk, 1000, [32]byte{0x01}, 0) + + t.Run("valid signature returns true", func(t *testing.T) { + s, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ + PendingDeposits: []*eth.PendingDeposit{validDeposit}, + }) + require.NoError(t, err) + + ok, err := s.IsPendingValidator(validDeposit.PublicKey) + require.NoError(t, err) + require.Equal(t, true, ok) + }) + + t.Run("invalid signature returns false", func(t *testing.T) { + invalidDeposit := ð.PendingDeposit{ + PublicKey: validDeposit.PublicKey, + WithdrawalCredentials: validDeposit.WithdrawalCredentials, + Amount: validDeposit.Amount, + Signature: make([]byte, 96), // invalid empty signature + } + s, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ + PendingDeposits: []*eth.PendingDeposit{invalidDeposit}, + }) + require.NoError(t, err) + + ok, err := s.IsPendingValidator(validDeposit.PublicKey) + require.NoError(t, err) + require.Equal(t, false, ok) + }) + + t.Run("unknown pubkey returns false", func(t *testing.T) { + s, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ + PendingDeposits: []*eth.PendingDeposit{validDeposit}, + }) + require.NoError(t, err) + + ok, err := s.IsPendingValidator([]byte{9, 9, 9}) + require.NoError(t, err) + require.Equal(t, false, ok) + }) + + t.Run("nil deposit skipped", func(t *testing.T) { + s, err := state_native.InitializeFromProtoElectra(ð.BeaconStateElectra{ + PendingDeposits: []*eth.PendingDeposit{nil, validDeposit}, + }) + require.NoError(t, err) + + ok, err := s.IsPendingValidator(validDeposit.PublicKey) + require.NoError(t, err) + require.Equal(t, true, ok) + }) + + t.Run("pre-electra not supported", func(t *testing.T) { + s, err := state_native.InitializeFromProtoDeneb(ð.BeaconStateDeneb{}) + require.NoError(t, err) + _, err = s.IsPendingValidator([]byte{1, 2, 3}) + require.ErrorContains(t, "not supported", err) + }) +} diff --git a/changelog/t_bump-consensus-spec-alpha3.md b/changelog/t_bump-consensus-spec-alpha3.md new file mode 100644 index 000000000000..0f59cd50e8eb --- /dev/null +++ b/changelog/t_bump-consensus-spec-alpha3.md @@ -0,0 +1,7 @@ +### Changed + +- Bump consensus spec version from v1.7.0-alpha.2 to v1.7.0-alpha.3 + +### Fixed + +- Check for pending validator deposits with valid signatures before applying builder deposits, preventing pubkey hijacking into the builder registry. diff --git a/config/params/loader_test.go b/config/params/loader_test.go index ea2e997698be..686304966b0e 100644 --- a/config/params/loader_test.go +++ b/config/params/loader_test.go @@ -35,11 +35,15 @@ var placeholderFields = []string{ "EIP7732_FORK_VERSION", "EIP7928_FORK_EPOCH", "EIP7928_FORK_VERSION", + "EIP8025_FORK_EPOCH", + "EIP8025_FORK_VERSION", + "EPOCHS_PER_SHUFFLING_PHASE", "FIELD_ELEMENTS_PER_CELL", // Configured as a constant in config/fieldparams/mainnet.go "FIELD_ELEMENTS_PER_EXT_BLOB", // Configured in proto/ssz_proto_library.bzl "HEZE_FORK_EPOCH", "HEZE_FORK_VERSION", "INCLUSION_LIST_COMMITTEE_SIZE", + "INCLUSION_LIST_SUBMISSION_DEADLINE", "INCLUSION_LIST_SUBMISSION_DUE_BPS", "KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH", // Configured in proto/ssz_proto_library.bzl "MAX_BYTES_PER_INCLUSION_LIST", diff --git a/specrefs/containers.yml b/specrefs/containers.yml index c9c69d8d55a2..b2ed9a1ea8c3 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -683,6 +683,60 @@ ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] +- name: BeaconState#heze + sources: [] + spec: | + + class BeaconState(Container): + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # [Modified in Heze:EIP7805] + latest_execution_payload_bid: ExecutionPayloadBid + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + deposit_requests_start_index: uint64 + deposit_balance_to_consume: Gwei + exit_balance_to_consume: Gwei + earliest_exit_epoch: Epoch + consolidation_balance_to_consume: Gwei + earliest_consolidation_epoch: Epoch + pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] + pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] + pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] + builders: List[Builder, BUILDER_REGISTRY_LIMIT] + next_withdrawal_builder_index: BuilderIndex + execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT] + builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH] + builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] + latest_block_hash: Hash32 + payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] + + - name: BlobIdentifier#deneb sources: - file: proto/prysm/v1alpha1/blobs.proto