Compare commits

..

5 Commits

Author SHA1 Message Date
Manu NALEPA
b49c35f37a Add TTL cache for aggregated attestations
Add a TTL cache that stores attestations when they are deleted from
the aggregatedAtt or blockAtt caches. This allows HasAggregatedAttestation
to still find recently pruned attestations, preventing duplicate processing.

The TTL cache is populated on delete (not save) and is pruned along with
the other attestation caches. A metric tracks when the TTL cache prevents
duplicate attestation processing.
2026-02-18 14:03:33 +01:00
Manu NALEPA
fc16bfb8f6 Refactor attestation cache lookup functions
Extract hasAggregatedAtt and hasBlockAtt helper functions from
HasAggregatedAttestation for better code organization and reuse.
Also improve code style with clearer variable names and error wrapping.
2026-02-18 13:30:21 +01:00
terence
8ee28394ab Add Gloas attestation committee index validation (#16359)
Adds Gloas fork attestation validation rules for gossip processing. This
implements the new committee index validation requirements introduced in
the Gloas fork.

## Changes
- Uses attestation epoch to determine if Gloas rules apply
- **Committee index validation**: 
  - Committee index must be < 2 (0 or 1 only)
- Same-slot attestations (where `attestation.data.slot == block.slot`)
must use committee index 0
  - Different-slot attestations can use either committee index 0 or 1
2026-02-17 18:00:32 +00:00
kasey
b31e2ffe51 avoid copying the finalized state when computing cgc (#16355)
Reviewing some (unrelated) sync code today I noticed that we are using a
stategen accessor for the finalized state which copies the entire state
object to look up validator balances to compute the custody_group_count.
This excess memory allocation is likely causing GC pressure and
increasing memory utilization.

This PR avoids state copying for this purpose by making the following
changes:
- Adds a new method to the `ReadOnlyBalances` state interface:
`EffectiveBalances([]primitives.ValidatorIndex) (uint64, []uint64,
error)`. This method computes returns the sum of the effective balances
of the given list of validator indices, a list with the individual
effective balance of each requested index (where the i-th element in the
return corresponds to the i-th element of the parameter), and an error -
which is necessary due to index bounds checks and quirks of multi-value
slice that can apparently result in the state being unusable for such
lookups if not correctly initialized.
- Adds a new method to the stategen interface
`FinalizedReadOnlyBalances`, which returns the finalized state asserted
to the `ReadOnlyBalances` interface.
- Switches the peerdas code to use the sum given by `EffectiveBalances`.

There was some existing nil checking code in the peerdas package that I
didn't want to modify, so I added a new compound interface in stategen
to allow the returned state to also expose the `IsNil` method.

fixes https://github.com/OffchainLabs/prysm/issues/16354

---------

Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
2026-02-17 08:50:58 +00:00
terence
22e77add54 Refactor function ProcessExecutionPayload with ApplyExecutionPayload (#16356)
This PR refactors `ProcessExecutionPayload` with `ApplyExecutionPayload`
so caller can use Apply method to calculate post state root. Note that
validations are not required for Apply function. We really need the
state mutation lines that's:
```
1. Ensure latest_block_header.state_root is set (if zero, set it to the pre‑payload HashTreeRoot)...
2. processExecutionRequests()
3. QueueBuilderPayment()
4. SetExecutionPayloadAvailability(state.Slot(), true)
5. SetLatestBlockHash(payload.BlockHash())
```
I decided to keep them there because a. it's cheap b. it makes refactor
cleaner to reason c. API/caller may want to validate envelope and bid
consistency (ex: beacon api has option to validate consensus)
2026-02-13 15:51:22 +00:00
46 changed files with 560 additions and 1624 deletions

View File

@@ -11,7 +11,6 @@ go_library(
"payload_attestation.go",
"pending_payment.go",
"proposer_slashing.go",
"withdrawal.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas",
visibility = ["//visibility:public"],
@@ -50,7 +49,6 @@ go_test(
"payload_test.go",
"pending_payment_test.go",
"proposer_slashing_test.go",
"withdrawal_test.go",
],
embed = [":go_default_library"],
deps = [

View File

@@ -93,22 +93,26 @@ func processDepositRequest(beaconState state.BeaconState, request *enginev1.Depo
// <spec fn="apply_deposit_for_builder" fork="gloas" hash="e4bc98c7">
// def apply_deposit_for_builder(
// state: BeaconState,
// pubkey: BLSPubkey,
// withdrawal_credentials: Bytes32,
// amount: uint64,
// signature: BLSSignature,
// slot: Slot,
//
// state: BeaconState,
// pubkey: BLSPubkey,
// withdrawal_credentials: Bytes32,
// amount: uint64,
// signature: BLSSignature,
// slot: Slot,
//
// ) -> None:
// builder_pubkeys = [b.pubkey for b in state.builders]
// if pubkey not in builder_pubkeys:
// # Verify the deposit signature (proof of possession) which is not checked by the deposit contract
// if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature):
// add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot)
// else:
// # Increase balance by deposit amount
// builder_index = builder_pubkeys.index(pubkey)
// state.builders[builder_index].balance += amount
//
// builder_pubkeys = [b.pubkey for b in state.builders]
// if pubkey not in builder_pubkeys:
// # Verify the deposit signature (proof of possession) which is not checked by the deposit contract
// if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature):
// add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot)
// else:
// # Increase balance by deposit amount
// builder_index = builder_pubkeys.index(pubkey)
// state.builders[builder_index].balance += amount
//
// </spec>
func applyBuilderDepositRequest(beaconState state.BeaconState, request *enginev1.DepositRequest) (bool, error) {
if beaconState.Version() < version.Gloas {

View File

@@ -112,6 +112,34 @@ func ProcessExecutionPayload(
return errors.Wrap(err, "signature verification failed")
}
envelope, err := signedEnvelope.Envelope()
if err != nil {
return errors.Wrap(err, "could not get envelope from signed envelope")
}
if err := ApplyExecutionPayload(ctx, st, envelope); err != nil {
return err
}
r, err := st.HashTreeRoot(ctx)
if err != nil {
return errors.Wrap(err, "could not get hash tree root")
}
if r != envelope.StateRoot() {
return fmt.Errorf("state root mismatch: expected %#x, got %#x", envelope.StateRoot(), r)
}
return nil
}
// ApplyExecutionPayload applies the execution payload envelope to the state and performs the same
// consistency checks as the full processing path. This keeps the post-payload state root computation
// on a shared code path, even though some bid/payload checks are not strictly required for the root itself.
func ApplyExecutionPayload(
ctx context.Context,
st state.BeaconState,
envelope interfaces.ROExecutionPayloadEnvelope,
) error {
latestHeader := st.LatestBlockHeader()
if len(latestHeader.StateRoot) == 0 || bytes.Equal(latestHeader.StateRoot, make([]byte, 32)) {
previousStateRoot, err := st.HashTreeRoot(ctx)
@@ -128,10 +156,6 @@ func ProcessExecutionPayload(
if err != nil {
return errors.Wrap(err, "could not compute block header root")
}
envelope, err := signedEnvelope.Envelope()
if err != nil {
return errors.Wrap(err, "could not get envelope from signed envelope")
}
beaconBlockRoot := envelope.BeaconBlockRoot()
if !bytes.Equal(beaconBlockRoot[:], blockHeaderRoot[:]) {
@@ -217,14 +241,6 @@ func ProcessExecutionPayload(
return errors.Wrap(err, "could not set latest block hash")
}
r, err := st.HashTreeRoot(ctx)
if err != nil {
return errors.Wrap(err, "could not get hash tree root")
}
if r != envelope.StateRoot() {
return fmt.Errorf("state root mismatch: expected %#x, got %#x", envelope.StateRoot(), r)
}
return nil
}

View File

@@ -1,100 +0,0 @@
package gloas
import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/pkg/errors"
)
// ProcessWithdrawals applies withdrawals to the state for Gloas.
//
// <spec fn="process_withdrawals" fork="gloas" hash="16d9ad2a">
// def process_withdrawals(
// state: BeaconState,
// # [Modified in Gloas:EIP7732]
// # Removed `payload`
// ) -> None:
// # [New in Gloas:EIP7732]
// # Return early if the parent block is empty
// if not is_parent_block_full(state):
// return
//
// # Get expected withdrawals
// expected = get_expected_withdrawals(state)
//
// # Apply expected withdrawals
// apply_withdrawals(state, expected.withdrawals)
//
// # Update withdrawals fields in the state
// update_next_withdrawal_index(state, expected.withdrawals)
// # [New in Gloas:EIP7732]
// update_payload_expected_withdrawals(state, expected.withdrawals)
// # [New in Gloas:EIP7732]
// update_builder_pending_withdrawals(state, expected.processed_builder_withdrawals_count)
// update_pending_partial_withdrawals(state, expected.processed_partial_withdrawals_count)
// # [New in Gloas:EIP7732]
// update_next_withdrawal_builder_index(state, expected.processed_builders_sweep_count)
// update_next_withdrawal_validator_index(state, expected.withdrawals)
// </spec>
func ProcessWithdrawals(st state.BeaconState) error {
full, err := st.IsParentBlockFull()
if err != nil {
return errors.Wrap(err, "could not get parent block full status")
}
if !full {
return nil
}
expected, err := st.ExpectedWithdrawalsGloas()
if err != nil {
return errors.Wrap(err, "could not get expected withdrawals")
}
if err := st.DecreaseWithdrawalBalances(expected.Withdrawals); err != nil {
return errors.Wrap(err, "could not decrease withdrawal balances")
}
if len(expected.Withdrawals) > 0 {
if err := st.SetNextWithdrawalIndex(expected.Withdrawals[len(expected.Withdrawals)-1].Index + 1); err != nil {
return errors.Wrap(err, "could not set next withdrawal index")
}
}
if err := st.SetPayloadExpectedWithdrawals(expected.Withdrawals); err != nil {
return errors.Wrap(err, "could not set payload expected withdrawals")
}
if err := st.DequeueBuilderPendingWithdrawals(expected.ProcessedBuilderWithdrawalsCount); err != nil {
return errors.Wrap(err, "unable to dequeue builder pending withdrawals from state")
}
if err := st.DequeuePendingPartialWithdrawals(expected.ProcessedPartialWithdrawalsCount); err != nil {
return errors.Wrap(err, "unable to dequeue partial withdrawals from state")
}
err = st.SetNextWithdrawalBuilderIndex(expected.NextWithdrawalBuilderIndex)
if err != nil {
return errors.Wrap(err, "could not set next withdrawal builder index")
}
var nextValidatorIndex primitives.ValidatorIndex
if uint64(len(expected.Withdrawals)) < params.BeaconConfig().MaxWithdrawalsPerPayload {
nextValidatorIndex, err = st.NextWithdrawalValidatorIndex()
if err != nil {
return errors.Wrap(err, "could not get next withdrawal validator index")
}
nextValidatorIndex += primitives.ValidatorIndex(params.BeaconConfig().MaxValidatorsPerWithdrawalsSweep)
nextValidatorIndex = nextValidatorIndex % primitives.ValidatorIndex(st.NumValidators())
} else {
nextValidatorIndex = expected.Withdrawals[len(expected.Withdrawals)-1].ValidatorIndex + 1
if nextValidatorIndex == primitives.ValidatorIndex(st.NumValidators()) {
nextValidatorIndex = 0
}
}
if err := st.SetNextWithdrawalValidatorIndex(nextValidatorIndex); err != nil {
return errors.Wrap(err, "could not set next withdrawal validator index")
}
return nil
}

View File

@@ -1,388 +0,0 @@
package gloas
import (
"errors"
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
"github.com/OffchainLabs/prysm/v7/testing/require"
)
func TestProcessWithdrawals(t *testing.T) {
cases := []struct {
name string
build func(t *testing.T) *withdrawalsState
check func(t *testing.T, st *withdrawalsState)
}{
{
name: "parent block not full",
build: func(t *testing.T) *withdrawalsState {
return &withdrawalsState{
BeaconState: newGloasState(t, nil, nil),
parentFull: false,
}
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, false, st.expectedCalled)
require.Equal(t, false, st.decreaseCalled)
require.Equal(t, false, st.setNextWithdrawalIndexCalled)
require.Equal(t, false, st.setPayloadExpectedWithdrawalsCalled)
require.Equal(t, false, st.dequeueBuilderCalled)
require.Equal(t, false, st.dequeuePartialCalled)
require.Equal(t, false, st.setNextBuilderIndexCalled)
require.Equal(t, false, st.nextValidatorIndexCalled)
require.Equal(t, false, st.setNextValidatorIndexCalled)
},
},
{
name: "updates indexes when not full payload",
build: func(t *testing.T) *withdrawalsState {
return &withdrawalsState{
BeaconState: newGloasState(t, nil, nil),
parentFull: true,
numValidators: 10,
nextValidatorIndex: 3,
expectedResult: state.ExpectedWithdrawalsGloasResult{
Withdrawals: []*enginev1.Withdrawal{
{Index: 7, ValidatorIndex: 2, Amount: 1, Address: []byte{0x01}},
{Index: 8, ValidatorIndex: 4, Amount: 2, Address: []byte{0x02}},
},
ProcessedBuilderWithdrawalsCount: 5,
ProcessedPartialWithdrawalsCount: 2,
NextWithdrawalBuilderIndex: 7,
},
}
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.expectedCalled)
require.Equal(t, true, st.decreaseCalled)
require.NotNil(t, st.setNextWithdrawalIndexArg)
require.Equal(t, uint64(9), *st.setNextWithdrawalIndexArg)
require.DeepEqual(t, st.expectedResult.Withdrawals, st.setPayloadExpectedWithdrawalsArg)
require.Equal(t, uint64(5), *st.dequeueBuilderArg)
require.Equal(t, uint64(2), *st.dequeuePartialArg)
require.Equal(t, primitives.BuilderIndex(7), *st.setNextBuilderIndexArg)
require.Equal(t, true, st.nextValidatorIndexCalled)
expectedNext := (uint64(st.nextValidatorIndex) + uint64(params.BeaconConfig().MaxValidatorsPerWithdrawalsSweep)) % st.numValidators
require.Equal(t, primitives.ValidatorIndex(expectedNext), *st.setNextValidatorIndexArg)
},
},
{
name: "full payload uses last validator index",
build: func(t *testing.T) *withdrawalsState {
max := int(params.BeaconConfig().MaxWithdrawalsPerPayload)
withdrawals := make([]*enginev1.Withdrawal, max)
for i := range max {
withdrawals[i] = &enginev1.Withdrawal{
Index: uint64(i),
ValidatorIndex: 0,
Amount: 1,
Address: []byte{0x03},
}
}
withdrawals[max-1].ValidatorIndex = 4
return &withdrawalsState{
BeaconState: newGloasState(t, nil, nil),
parentFull: true,
numValidators: 5,
expectedResult: state.ExpectedWithdrawalsGloasResult{
Withdrawals: withdrawals,
NextWithdrawalBuilderIndex: 1,
},
}
},
check: func(t *testing.T, st *withdrawalsState) {
max := int(params.BeaconConfig().MaxWithdrawalsPerPayload)
require.NotNil(t, st.setNextWithdrawalIndexArg)
require.Equal(t, uint64(max), *st.setNextWithdrawalIndexArg)
require.Equal(t, false, st.nextValidatorIndexCalled)
require.Equal(t, primitives.ValidatorIndex(0), *st.setNextValidatorIndexArg)
},
},
{
name: "empty withdrawals skips next index update",
build: func(t *testing.T) *withdrawalsState {
return &withdrawalsState{
BeaconState: newGloasState(t, nil, nil),
parentFull: true,
numValidators: 8,
expectedResult: state.ExpectedWithdrawalsGloasResult{
Withdrawals: []*enginev1.Withdrawal{},
ProcessedBuilderWithdrawalsCount: 1,
ProcessedPartialWithdrawalsCount: 2,
NextWithdrawalBuilderIndex: 4,
},
}
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, false, st.setNextWithdrawalIndexCalled)
require.Equal(t, true, st.setPayloadExpectedWithdrawalsCalled)
require.Equal(t, true, st.dequeueBuilderCalled)
require.Equal(t, true, st.dequeuePartialCalled)
require.Equal(t, true, st.setNextBuilderIndexCalled)
require.Equal(t, true, st.nextValidatorIndexCalled)
require.Equal(t, true, st.setNextValidatorIndexCalled)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
st := tc.build(t)
require.NoError(t, ProcessWithdrawals(st))
if tc.check != nil {
tc.check(t, st)
}
})
}
}
func TestProcessWithdrawals_ErrorPaths(t *testing.T) {
base := func(t *testing.T) *withdrawalsState {
return &withdrawalsState{
BeaconState: newGloasState(t, nil, nil),
parentFull: true,
numValidators: 16,
expectedResult: state.ExpectedWithdrawalsGloasResult{
Withdrawals: []*enginev1.Withdrawal{
{Index: 1, ValidatorIndex: 2, Amount: 1, Address: []byte{0x01}},
},
ProcessedBuilderWithdrawalsCount: 1,
ProcessedPartialWithdrawalsCount: 1,
NextWithdrawalBuilderIndex: 2,
},
nextValidatorIndex: 5,
}
}
cases := []struct {
name string
err error
set func(st *withdrawalsState, err error)
check func(t *testing.T, st *withdrawalsState)
}{
{
name: "parent block full error",
err: errors.New("parent err"),
set: func(st *withdrawalsState, err error) {
st.parentErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, false, st.expectedCalled)
},
},
{
name: "expected withdrawals error",
err: errors.New("expected err"),
set: func(st *withdrawalsState, err error) {
st.expectedErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.expectedCalled)
require.Equal(t, false, st.decreaseCalled)
},
},
{
name: "decrease balances error",
err: errors.New("decrease err"),
set: func(st *withdrawalsState, err error) {
st.decreaseErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.decreaseCalled)
require.Equal(t, false, st.setNextWithdrawalIndexCalled)
},
},
{
name: "set next withdrawal index error",
err: errors.New("next index err"),
set: func(st *withdrawalsState, err error) {
st.setNextWithdrawalIndexErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.setNextWithdrawalIndexCalled)
require.Equal(t, false, st.setPayloadExpectedWithdrawalsCalled)
},
},
{
name: "set payload expected withdrawals error",
err: errors.New("payload expected err"),
set: func(st *withdrawalsState, err error) {
st.setPayloadExpectedWithdrawalsErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.setPayloadExpectedWithdrawalsCalled)
require.Equal(t, false, st.dequeueBuilderCalled)
},
},
{
name: "dequeue builder pending withdrawals error",
err: errors.New("dequeue builder err"),
set: func(st *withdrawalsState, err error) {
st.dequeueBuilderErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.dequeueBuilderCalled)
require.Equal(t, false, st.dequeuePartialCalled)
},
},
{
name: "dequeue pending partial withdrawals error",
err: errors.New("dequeue partial err"),
set: func(st *withdrawalsState, err error) {
st.dequeuePartialErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.dequeuePartialCalled)
require.Equal(t, false, st.setNextBuilderIndexCalled)
},
},
{
name: "set next withdrawal builder index error",
err: errors.New("next builder err"),
set: func(st *withdrawalsState, err error) {
st.setNextBuilderIndexErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.setNextBuilderIndexCalled)
require.Equal(t, false, st.nextValidatorIndexCalled)
},
},
{
name: "next withdrawal validator index error",
err: errors.New("next validator err"),
set: func(st *withdrawalsState, err error) {
st.nextValidatorIndexErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.nextValidatorIndexCalled)
require.Equal(t, false, st.setNextValidatorIndexCalled)
},
},
{
name: "set next withdrawal validator index error",
err: errors.New("set next validator err"),
set: func(st *withdrawalsState, err error) {
st.setNextValidatorIndexErr = err
},
check: func(t *testing.T, st *withdrawalsState) {
require.Equal(t, true, st.setNextValidatorIndexCalled)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
st := base(t)
tc.set(st, tc.err)
err := ProcessWithdrawals(st)
require.ErrorIs(t, err, tc.err)
if tc.check != nil {
tc.check(t, st)
}
})
}
}
type withdrawalsState struct {
setNextValidatorIndexCalled bool
nextValidatorIndexCalled bool
setNextBuilderIndexCalled bool
dequeuePartialCalled bool
dequeueBuilderCalled bool
setPayloadExpectedWithdrawalsCalled bool
setNextWithdrawalIndexCalled bool
parentFull bool
expectedCalled bool
decreaseCalled bool
numValidators uint64
setNextWithdrawalIndexArg *uint64
nextValidatorIndex primitives.ValidatorIndex
setNextBuilderIndexArg *primitives.BuilderIndex
dequeuePartialArg *uint64
setNextValidatorIndexArg *primitives.ValidatorIndex
dequeueBuilderArg *uint64
state.BeaconState
setNextValidatorIndexErr error
setNextBuilderIndexErr error
dequeuePartialErr error
dequeueBuilderErr error
setPayloadExpectedWithdrawalsErr error
nextValidatorIndexErr error
decreaseErr error
expectedErr error
parentErr error
setNextWithdrawalIndexErr error
setPayloadExpectedWithdrawalsArg []*enginev1.Withdrawal
expectedResult state.ExpectedWithdrawalsGloasResult
}
func (w *withdrawalsState) IsParentBlockFull() (bool, error) {
return w.parentFull, w.parentErr
}
func (w *withdrawalsState) ExpectedWithdrawalsGloas() (state.ExpectedWithdrawalsGloasResult, error) {
w.expectedCalled = true
if w.expectedErr != nil {
return state.ExpectedWithdrawalsGloasResult{}, w.expectedErr
}
return w.expectedResult, nil
}
func (w *withdrawalsState) DecreaseWithdrawalBalances(_ []*enginev1.Withdrawal) error {
w.decreaseCalled = true
return w.decreaseErr
}
func (w *withdrawalsState) SetNextWithdrawalIndex(index uint64) error {
w.setNextWithdrawalIndexCalled = true
w.setNextWithdrawalIndexArg = &index
return w.setNextWithdrawalIndexErr
}
func (w *withdrawalsState) SetPayloadExpectedWithdrawals(withdrawals []*enginev1.Withdrawal) error {
w.setPayloadExpectedWithdrawalsCalled = true
w.setPayloadExpectedWithdrawalsArg = withdrawals
return w.setPayloadExpectedWithdrawalsErr
}
func (w *withdrawalsState) DequeueBuilderPendingWithdrawals(n uint64) error {
w.dequeueBuilderCalled = true
w.dequeueBuilderArg = &n
return w.dequeueBuilderErr
}
func (w *withdrawalsState) DequeuePendingPartialWithdrawals(n uint64) error {
w.dequeuePartialCalled = true
w.dequeuePartialArg = &n
return w.dequeuePartialErr
}
func (w *withdrawalsState) SetNextWithdrawalBuilderIndex(index primitives.BuilderIndex) error {
w.setNextBuilderIndexCalled = true
w.setNextBuilderIndexArg = &index
return w.setNextBuilderIndexErr
}
func (w *withdrawalsState) NextWithdrawalValidatorIndex() (primitives.ValidatorIndex, error) {
w.nextValidatorIndexCalled = true
if w.nextValidatorIndexErr != nil {
return 0, w.nextValidatorIndexErr
}
return w.nextValidatorIndex, nil
}
func (w *withdrawalsState) NumValidators() int {
return int(w.numValidators)
}
func (w *withdrawalsState) SetNextWithdrawalValidatorIndex(index primitives.ValidatorIndex) error {
w.setNextValidatorIndexCalled = true
w.setNextValidatorIndexArg = &index
return w.setNextValidatorIndexErr
}

View File

@@ -73,23 +73,22 @@ func PopulateFromSidecar(sidecar blocks.VerifiedRODataColumn) *SidecarReconstruc
// ValidatorsCustodyRequirement returns the number of custody groups regarding the validator indices attached to the beacon node.
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/validator.md#validator-custody
func ValidatorsCustodyRequirement(state beaconState.ReadOnlyBeaconState, validatorsIndex map[primitives.ValidatorIndex]bool) (uint64, error) {
totalNodeBalance := uint64(0)
func ValidatorsCustodyRequirement(st beaconState.ReadOnlyBalances, validatorsIndex map[primitives.ValidatorIndex]bool) (uint64, error) {
cfg := params.BeaconConfig()
idxs := make([]primitives.ValidatorIndex, 0, len(validatorsIndex))
for index := range validatorsIndex {
validator, err := state.ValidatorAtIndexReadOnly(index)
if err != nil {
return 0, errors.Wrapf(err, "validator at index %v", index)
}
totalNodeBalance += validator.EffectiveBalance()
idxs = append(idxs, index)
}
totalBalance, err := st.EffectiveBalanceSum(idxs)
if err != nil {
return 0, errors.Wrap(err, "effective balances")
}
cfg := params.BeaconConfig()
numberOfCustodyGroups := cfg.NumberOfCustodyGroups
validatorCustodyRequirement := cfg.ValidatorCustodyRequirement
balancePerAdditionalCustodyGroup := cfg.BalancePerAdditionalCustodyGroup
count := totalNodeBalance / balancePerAdditionalCustodyGroup
count := totalBalance / balancePerAdditionalCustodyGroup
return min(max(count, validatorCustodyRequirement), numberOfCustodyGroups), nil
}

View File

@@ -7,6 +7,7 @@ go_library(
"block.go",
"kv.go",
"log.go",
"metrics.go",
"seen_bits.go",
"unaggregated.go",
],
@@ -24,6 +25,8 @@ go_library(
"//runtime/version:go_default_library",
"@com_github_patrickmn_go_cache//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prometheus_client_golang//prometheus:go_default_library",
"@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],

View File

@@ -2,6 +2,7 @@ package kv
import (
"context"
"fmt"
"runtime"
"sync"
@@ -273,18 +274,30 @@ func (c *AttCaches) DeleteAggregatedAttestation(att ethpb.Att) error {
filtered := make([]ethpb.Att, 0)
for _, a := range attList {
if c, err := att.GetAggregationBits().Contains(a.GetAggregationBits()); err != nil {
return err
} else if !c {
filtered = append(filtered, a)
contains, err := att.GetAggregationBits().Contains(a.GetAggregationBits())
if err != nil {
return fmt.Errorf("aggregation bits contain: %w", err)
}
}
if len(filtered) == 0 {
delete(c.aggregatedAtt, id)
} else {
c.aggregatedAtt[id] = filtered
if contains {
// Insert into TTL cache before removing from aggregated cache
if err := c.insertAggregatedAttTTL(a); err != nil {
return errors.Wrap(err, "could not insert into TTL cache")
}
continue
}
// If the attestation in the cache doesn't contain the bits of the attestation to delete, we keep it in the cache.
filtered = append(filtered, a)
}
if len(filtered) == 0 {
delete(c.aggregatedAtt, id)
return nil
}
c.aggregatedAtt[id] = filtered
return nil
}
@@ -294,32 +307,118 @@ func (c *AttCaches) HasAggregatedAttestation(att ethpb.Att) (bool, error) {
return false, err
}
has, err := c.hasAggregatedAtt(att)
if err != nil {
return false, fmt.Errorf("has aggregated att: %w", err)
}
if has {
return true, nil
}
has, err = c.hasBlockAtt(att)
if err != nil {
return false, fmt.Errorf("has block att: %w", err)
}
if has {
return true, nil
}
has, err = c.hasAggregatedAttTTL(att)
if err != nil {
return false, fmt.Errorf("has aggregated att TTL: %w", err)
}
if has {
savedByTTLCache.Inc()
return true, nil
}
return false, nil
}
// hasAggregatedAtt checks if the attestation bits are contained in the aggregated attestation cache.
func (c *AttCaches) hasAggregatedAtt(att ethpb.Att) (bool, error) {
id, err := attestation.NewId(att, attestation.Data)
if err != nil {
return false, errors.Wrap(err, "could not create attestation ID")
return false, fmt.Errorf("could not create attestation ID: %w", err)
}
c.aggregatedAttLock.RLock()
defer c.aggregatedAttLock.RUnlock()
if atts, ok := c.aggregatedAtt[id]; ok {
for _, a := range atts {
if c, err := a.GetAggregationBits().Contains(att.GetAggregationBits()); err != nil {
return false, err
} else if c {
return true, nil
}
cacheAtts, ok := c.aggregatedAtt[id]
if !ok {
return false, nil
}
for _, cacheAtt := range cacheAtts {
contains, err := cacheAtt.GetAggregationBits().Contains(att.GetAggregationBits())
if err != nil {
return false, fmt.Errorf("aggregation bits contains: %w", err)
}
if contains {
return true, nil
}
}
return false, nil
}
// hasBlockAtt checks if the attestation bits are contained in the block attestation cache.
func (c *AttCaches) hasBlockAtt(att ethpb.Att) (bool, error) {
id, err := attestation.NewId(att, attestation.Data)
if err != nil {
return false, fmt.Errorf("could not create attestation ID: %w", err)
}
c.blockAttLock.RLock()
defer c.blockAttLock.RUnlock()
if atts, ok := c.blockAtt[id]; ok {
for _, a := range atts {
if c, err := a.GetAggregationBits().Contains(att.GetAggregationBits()); err != nil {
return false, err
} else if c {
return true, nil
}
cacheAtts, ok := c.blockAtt[id]
if !ok {
return false, nil
}
for _, cacheAtt := range cacheAtts {
contains, err := cacheAtt.GetAggregationBits().Contains(att.GetAggregationBits())
if err != nil {
return false, fmt.Errorf("aggregation bits contains: %w", err)
}
if contains {
return true, nil
}
}
return false, nil
}
// hasAggregatedAttTTL checks if the attestation bits are contained in the TTL cache.
func (c *AttCaches) hasAggregatedAttTTL(att ethpb.Att) (bool, error) {
id, err := attestation.NewId(att, attestation.Data)
if err != nil {
return false, fmt.Errorf("could not create attestation ID: %w", err)
}
c.aggregatedAttTTLLock.RLock()
defer c.aggregatedAttTTLLock.RUnlock()
cacheAtts, ok := c.aggregatedAttTTL[id]
if !ok {
return false, nil
}
for _, cacheAtt := range cacheAtts {
contains, err := cacheAtt.GetAggregationBits().Contains(att.GetAggregationBits())
if err != nil {
return false, fmt.Errorf("aggregation bits contains: %w", err)
}
if contains {
return true, nil
}
}
@@ -332,3 +431,93 @@ func (c *AttCaches) AggregatedAttestationCount() int {
defer c.aggregatedAttLock.RUnlock()
return len(c.aggregatedAtt)
}
// insertAggregatedAttTTL inserts an attestation into the TTL cache.
func (c *AttCaches) insertAggregatedAttTTL(att ethpb.Att) error {
id, err := attestation.NewId(att, attestation.Data)
if err != nil {
return fmt.Errorf("new ID: %w", err)
}
c.aggregatedAttTTLLock.Lock()
defer c.aggregatedAttTTLLock.Unlock()
cacheAtts, ok := c.aggregatedAttTTL[id]
if !ok {
c.aggregatedAttTTL[id] = []ethpb.Att{att.Clone()}
return nil
}
// Check if attestation is already contained
for _, cacheAtt := range cacheAtts {
contains, err := cacheAtt.GetAggregationBits().Contains(att.GetAggregationBits())
if err != nil {
return fmt.Errorf("aggregation bits contains: %w", err)
}
if contains {
return nil
}
}
c.aggregatedAttTTL[id] = append(cacheAtts, att.Clone())
return nil
}
// AggregatedAttestationTTLCount returns the number of keys in the TTL cache.
func (c *AttCaches) AggregatedAttestationTTLCount() int {
c.aggregatedAttTTLLock.RLock()
defer c.aggregatedAttTTLLock.RUnlock()
return len(c.aggregatedAttTTL)
}
// AggregatedAttestationsTTL returns all attestations from the TTL cache.
func (c *AttCaches) AggregatedAttestationsTTL() []ethpb.Att {
c.aggregatedAttTTLLock.RLock()
defer c.aggregatedAttTTLLock.RUnlock()
atts := make([]ethpb.Att, 0)
for _, a := range c.aggregatedAttTTL {
atts = append(atts, a...)
}
return atts
}
// DeleteAggregatedAttestationTTL deletes an attestation from the TTL cache.
func (c *AttCaches) DeleteAggregatedAttestationTTL(att ethpb.Att) error {
if att == nil || att.IsNil() {
return nil
}
id, err := attestation.NewId(att, attestation.Data)
if err != nil {
return fmt.Errorf("could not create attestation ID: %w", err)
}
c.aggregatedAttTTLLock.Lock()
defer c.aggregatedAttTTLLock.Unlock()
cacheAtts, ok := c.aggregatedAttTTL[id]
if !ok {
return nil
}
filtered := make([]ethpb.Att, 0)
for _, cacheAtt := range cacheAtts {
contains, err := att.GetAggregationBits().Contains(cacheAtt.GetAggregationBits())
if err != nil {
return fmt.Errorf("aggregation bits contains: %w", err)
}
if !contains {
filtered = append(filtered, cacheAtt)
}
}
if len(filtered) == 0 {
delete(c.aggregatedAttTTL, id)
return nil
}
c.aggregatedAttTTL[id] = filtered
return nil
}

View File

@@ -1,6 +1,8 @@
package kv
import (
"fmt"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/attestation"
"github.com/pkg/errors"
@@ -63,6 +65,16 @@ func (c *AttCaches) DeleteBlockAttestation(att ethpb.Att) error {
c.blockAttLock.Lock()
defer c.blockAttLock.Unlock()
// Insert all attestations into TTL cache before deleting
if cacheAtts, ok := c.blockAtt[id]; ok {
for _, cacheAtt := range cacheAtts {
if err := c.insertAggregatedAttTTL(cacheAtt); err != nil {
return fmt.Errorf("insert aggregated att TTL: %w", err)
}
}
}
delete(c.blockAtt, id)
return nil

View File

@@ -25,7 +25,11 @@ type AttCaches struct {
forkchoiceAtt *attmap.Attestations
blockAttLock sync.RWMutex
blockAtt map[attestation.Id][]ethpb.Att
seenAtt *cache.Cache
seenAtt *cache.Cache
// TTL cache for aggregated attestations, used for fast lookup in HasAggregatedAttestation.
// Entries expire after 1 epoch.
aggregatedAttTTLLock sync.RWMutex
aggregatedAttTTL map[attestation.Id][]ethpb.Att
}
// NewAttCaches initializes a new attestation pool consists of multiple KV store in cache for
@@ -34,11 +38,12 @@ func NewAttCaches() *AttCaches {
secsInEpoch := time.Duration(params.BeaconConfig().SlotsPerEpoch.Mul(params.BeaconConfig().SecondsPerSlot))
c := cache.New(2*secsInEpoch*time.Second, 2*secsInEpoch*time.Second)
pool := &AttCaches{
unAggregatedAtt: make(map[attestation.Id]ethpb.Att),
aggregatedAtt: make(map[attestation.Id][]ethpb.Att),
forkchoiceAtt: attmap.New(),
blockAtt: make(map[attestation.Id][]ethpb.Att),
seenAtt: c,
unAggregatedAtt: make(map[attestation.Id]ethpb.Att),
aggregatedAtt: make(map[attestation.Id][]ethpb.Att),
forkchoiceAtt: attmap.New(),
blockAtt: make(map[attestation.Id][]ethpb.Att),
seenAtt: c,
aggregatedAttTTL: make(map[attestation.Id][]ethpb.Att),
}
return pool

View File

@@ -0,0 +1,11 @@
package kv
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var savedByTTLCache = promauto.NewCounter(prometheus.CounterOpts{
Name: "attestation_saved_by_ttl_cache_total",
Help: "The number of times an attestation was found only in the TTL cache and not in the regular caches.",
})

View File

@@ -30,6 +30,16 @@ var (
Name: "expired_block_atts_total",
Help: "The number of expired and deleted block attestations in the pool.",
})
aggregatedAttsTTLCount = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "aggregated_attestations_ttl_in_pool_total",
Help: "The number of aggregated attestations in the TTL cache.",
},
)
expiredAggregatedAttsTTL = promauto.NewCounter(prometheus.CounterOpts{
Name: "expired_aggregated_atts_ttl_total",
Help: "The number of expired and deleted aggregated attestations from the TTL cache.",
})
attCount = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "attestations_in_pool_total",
@@ -59,6 +69,7 @@ var (
func (s *Service) updateMetrics() {
aggregatedAttsCount.Set(float64(s.cfg.Pool.AggregatedAttestationCount()))
unaggregatedAttsCount.Set(float64(s.cfg.Pool.UnaggregatedAttestationCount()))
aggregatedAttsTTLCount.Set(float64(s.cfg.Pool.AggregatedAttestationTTLCount()))
}
func (s *Service) updateMetricsExperimental(numExpired uint64) {

View File

@@ -68,6 +68,21 @@ func (*PoolMock) AggregatedAttestationCount() int {
panic("implement me")
}
// AggregatedAttestationsTTL --
func (*PoolMock) AggregatedAttestationsTTL() []ethpb.Att {
panic("implement me")
}
// DeleteAggregatedAttestationTTL --
func (*PoolMock) DeleteAggregatedAttestationTTL(_ ethpb.Att) error {
panic("implement me")
}
// AggregatedAttestationTTLCount --
func (*PoolMock) AggregatedAttestationTTLCount() int {
panic("implement me")
}
// SaveUnaggregatedAttestation --
func (*PoolMock) SaveUnaggregatedAttestation(_ ethpb.Att) error {
panic("implement me")

View File

@@ -23,6 +23,10 @@ type Pool interface {
DeleteAggregatedAttestation(att ethpb.Att) error
HasAggregatedAttestation(att ethpb.Att) (bool, error)
AggregatedAttestationCount() int
// TTL cache methods for aggregated attestations
AggregatedAttestationsTTL() []ethpb.Att
DeleteAggregatedAttestationTTL(att ethpb.Att) error
AggregatedAttestationTTLCount() int
// For unaggregated attestations.
SaveUnaggregatedAttestation(att ethpb.Att) error
SaveUnaggregatedAttestations(atts []ethpb.Att) error

View File

@@ -81,6 +81,16 @@ func (s *Service) pruneExpiredAtts() {
expiredBlockAtts.Inc()
}
}
ttlAtts := s.cfg.Pool.AggregatedAttestationsTTL()
for _, att := range ttlAtts {
if s.expired(att.GetData().Slot) {
if err := s.cfg.Pool.DeleteAggregatedAttestationTTL(att); err != nil {
log.WithError(err).Error("Could not delete expired TTL attestation")
}
expiredAggregatedAttsTTL.Inc()
}
}
}
// Return true if the input slot has been expired.

View File

@@ -84,7 +84,6 @@ func TestGetSpec(t *testing.T) {
config.FuluForkVersion = []byte("FuluForkVersion")
config.FuluForkEpoch = 109
config.GloasForkEpoch = 110
config.MaxBuildersPerWithdrawalsSweep = 112
config.BLSWithdrawalPrefixByte = byte('b')
config.ETH1AddressWithdrawalPrefixByte = byte('c')
config.BuilderWithdrawalPrefixByte = byte('e')
@@ -222,7 +221,7 @@ func TestGetSpec(t *testing.T) {
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp))
data, ok := resp.Data.(map[string]any)
require.Equal(t, true, ok)
assert.Equal(t, 193, len(data))
assert.Equal(t, 192, len(data))
for k, v := range data {
t.Run(k, func(t *testing.T) {
switch k {
@@ -304,8 +303,6 @@ func TestGetSpec(t *testing.T) {
assert.Equal(t, "109", v)
case "GLOAS_FORK_EPOCH":
assert.Equal(t, "110", v)
case "MAX_BUILDERS_PER_WITHDRAWALS_SWEEP":
assert.Equal(t, "112", v)
case "MIN_ANCHOR_POW_BLOCK_DIFFICULTY":
assert.Equal(t, "1000", v)
case "BLS_WITHDRAWAL_PREFIX":

View File

@@ -152,6 +152,7 @@ type ReadOnlyBalances interface {
Balances() []uint64
BalanceAtIndex(idx primitives.ValidatorIndex) (uint64, error)
BalancesLength() int
EffectiveBalanceSum([]primitives.ValidatorIndex) (uint64, error)
}
// ReadOnlyCheckpoint defines a struct which only has read access to checkpoint methods.

View File

@@ -30,12 +30,6 @@ type writeOnlyGloasFields interface {
IncreaseBuilderBalance(index primitives.BuilderIndex, amount uint64) error
AddBuilderFromDeposit(pubkey [fieldparams.BLSPubkeyLength]byte, withdrawalCredentials [fieldparams.RootLength]byte, amount uint64) error
UpdatePendingPaymentWeight(att ethpb.Att, indices []uint64, participatedFlags map[uint8]bool) error
// Withdrawals.
SetPayloadExpectedWithdrawals(withdrawals []*enginev1.Withdrawal) error
DecreaseWithdrawalBalances(withdrawals []*enginev1.Withdrawal) error
DequeueBuilderPendingWithdrawals(num uint64) error
SetNextWithdrawalBuilderIndex(idx primitives.BuilderIndex) error
}
type readOnlyGloasFields interface {
@@ -58,17 +52,4 @@ type readOnlyGloasFields interface {
IsAttestationSameSlot(blockRoot [32]byte, slot primitives.Slot) (bool, error)
BuilderPendingPayment(index uint64) (*ethpb.BuilderPendingPayment, error)
ExecutionPayloadAvailability(slot primitives.Slot) (uint64, error)
// Withdrawals
IsParentBlockFull() (bool, error)
ExpectedWithdrawalsGloas() (ExpectedWithdrawalsGloasResult, error)
}
// ExpectedWithdrawalsGloasResult bundles the expected withdrawals and related counters
// for the Gloas fork to avoid positional return mistakes.
type ExpectedWithdrawalsGloasResult struct {
Withdrawals []*enginev1.Withdrawal
ProcessedBuilderWithdrawalsCount uint64
ProcessedPartialWithdrawalsCount uint64
NextWithdrawalBuilderIndex primitives.BuilderIndex
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
@@ -14,7 +13,6 @@ import (
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
@@ -265,29 +263,6 @@ func withdrawalsEqual(a, b []*enginev1.Withdrawal) bool {
return true
}
// IsParentBlockFull returns true if the last committed payload bid was fulfilled with a payload,
// which can only happen when both beacon block and payload were present.
// This function must be called on a beacon state before processing the bid in the block.
//
// <spec fn="is_parent_block_full" fork="gloas" hash="b59640c9">
// def is_parent_block_full(state: BeaconState) -> bool:
// return state.latest_execution_payload_bid.block_hash == state.latest_block_hash
// </spec>
func (b *BeaconState) IsParentBlockFull() (bool, error) {
if b.version < version.Gloas {
return false, errNotSupported("IsParentBlockFull", b.version)
}
b.lock.RLock()
defer b.lock.RUnlock()
if b.latestExecutionPayloadBid == nil {
return false, nil
}
return bytes.Equal(b.latestExecutionPayloadBid.BlockHash, b.latestBlockHash), nil
}
// ExecutionPayloadAvailability returns the execution payload availability bit for the given slot.
func (b *BeaconState) ExecutionPayloadAvailability(slot primitives.Slot) (uint64, error) {
if b.version < version.Gloas {
@@ -339,205 +314,3 @@ func (b *BeaconState) BuilderIndexByPubkey(pubkey [fieldparams.BLSPubkeyLength]b
}
return 0, false
}
// ExpectedWithdrawalsGloas returns the withdrawals that a proposer will need to pack in the next block
// applied to the current state. It is also used by validators to check that the execution payload carried
// the right number of withdrawals.
//
// <spec fn="get_expected_withdrawals" fork="gloas" hash="8d0675cb">
// def get_expected_withdrawals(state: BeaconState) -> ExpectedWithdrawals:
// withdrawal_index = state.next_withdrawal_index
// withdrawals: List[Withdrawal] = []
//
// # [New in Gloas:EIP7732]
// # Get builder withdrawals
// builder_withdrawals, withdrawal_index, processed_builder_withdrawals_count = (
// get_builder_withdrawals(state, withdrawal_index, withdrawals)
// )
// withdrawals.extend(builder_withdrawals)
//
// # Get partial withdrawals
// partial_withdrawals, withdrawal_index, processed_partial_withdrawals_count = (
// get_pending_partial_withdrawals(state, withdrawal_index, withdrawals)
// )
// withdrawals.extend(partial_withdrawals)
//
// # [New in Gloas:EIP7732]
// # Get builders sweep withdrawals
// builders_sweep_withdrawals, withdrawal_index, processed_builders_sweep_count = (
// get_builders_sweep_withdrawals(state, withdrawal_index, withdrawals)
// )
// withdrawals.extend(builders_sweep_withdrawals)
//
// # Get validators sweep withdrawals
// validators_sweep_withdrawals, withdrawal_index, processed_validators_sweep_count = (
// get_validators_sweep_withdrawals(state, withdrawal_index, withdrawals)
// )
// withdrawals.extend(validators_sweep_withdrawals)
//
// return ExpectedWithdrawals(
// withdrawals,
// # [New in Gloas:EIP7732]
// processed_builder_withdrawals_count,
// processed_partial_withdrawals_count,
// # [New in Gloas:EIP7732]
// processed_builders_sweep_count,
// processed_validators_sweep_count,
// )
// </spec>
func (b *BeaconState) ExpectedWithdrawalsGloas() (state.ExpectedWithdrawalsGloasResult, error) {
if b.version < version.Gloas {
return state.ExpectedWithdrawalsGloasResult{}, errNotSupported("ExpectedWithdrawalsGloas", b.version)
}
b.lock.RLock()
defer b.lock.RUnlock()
cfg := params.BeaconConfig()
withdrawals := make([]*enginev1.Withdrawal, 0, cfg.MaxWithdrawalsPerPayload)
withdrawalIndex := b.nextWithdrawalIndex
withdrawalIndex, processedBuilderWithdrawalsCount, err := b.appendBuilderWithdrawals(withdrawalIndex, &withdrawals)
if err != nil {
return state.ExpectedWithdrawalsGloasResult{}, err
}
withdrawalIndex, processedPartialWithdrawalsCount, err := b.appendPendingPartialWithdrawals(withdrawalIndex, &withdrawals)
if err != nil {
return state.ExpectedWithdrawalsGloasResult{}, err
}
withdrawalIndex, nextBuilderIndex, err := b.appendBuildersSweepWithdrawals(withdrawalIndex, &withdrawals)
if err != nil {
return state.ExpectedWithdrawalsGloasResult{}, err
}
err = b.appendValidatorsSweepWithdrawals(withdrawalIndex, &withdrawals)
if err != nil {
return state.ExpectedWithdrawalsGloasResult{}, err
}
return state.ExpectedWithdrawalsGloasResult{
Withdrawals: withdrawals,
ProcessedBuilderWithdrawalsCount: processedBuilderWithdrawalsCount,
ProcessedPartialWithdrawalsCount: processedPartialWithdrawalsCount,
NextWithdrawalBuilderIndex: nextBuilderIndex,
}, nil
}
// appendBuilderWithdrawals returns builder pending withdrawals, the updated withdrawal index,
// and the processed count, following spec v1.7.0-alpha.2:
//
// def get_builder_withdrawals(state, withdrawal_index, prior_withdrawals):
// withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1
// assert len(prior_withdrawals) <= withdrawals_limit
// processed_count = 0
// withdrawals = []
// for withdrawal in state.builder_pending_withdrawals:
// if len(prior_withdrawals + withdrawals) >= withdrawals_limit:
// break
// withdrawals.append(Withdrawal(
// index=withdrawal_index,
// validator_index=convert_builder_index_to_validator_index(withdrawal.builder_index),
// address=withdrawal.fee_recipient,
// amount=withdrawal.amount,
// ))
// withdrawal_index += 1
// processed_count += 1
// return withdrawals, withdrawal_index, processed_count
func (b *BeaconState) appendBuilderWithdrawals(withdrawalIndex uint64, withdrawals *[]*enginev1.Withdrawal) (uint64, uint64, error) {
cfg := params.BeaconConfig()
withdrawalsLimit := int(cfg.MaxWithdrawalsPerPayload - 1)
ws := *withdrawals
if len(ws) > withdrawalsLimit {
return withdrawalIndex, 0, fmt.Errorf("prior withdrawals length %d exceeds limit %d", len(ws), withdrawalsLimit)
}
var processedCount uint64
for _, w := range b.builderPendingWithdrawals {
if len(ws) >= withdrawalsLimit {
break
}
ws = append(ws, &enginev1.Withdrawal{
Index: withdrawalIndex,
ValidatorIndex: w.BuilderIndex.ToValidatorIndex(),
Address: w.FeeRecipient,
Amount: uint64(w.Amount),
})
withdrawalIndex++
processedCount++
}
*withdrawals = ws
return withdrawalIndex, processedCount, nil
}
// appendBuildersSweepWithdrawals returns builder sweep withdrawals, the updated withdrawal index,
// and the processed count, following spec v1.7.0-alpha.2:
//
// def get_builders_sweep_withdrawals(state, withdrawal_index, prior_withdrawals):
// epoch = get_current_epoch(state)
// builders_limit = min(len(state.builders), MAX_BUILDERS_PER_WITHDRAWALS_SWEEP)
// withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1
// assert len(prior_withdrawals) <= withdrawals_limit
// processed_count = 0
// withdrawals = []
// builder_index = state.next_withdrawal_builder_index
// for _ in range(builders_limit):
// if len(prior_withdrawals + withdrawals) >= withdrawals_limit:
// break
// builder = state.builders[builder_index]
// if builder.withdrawable_epoch <= epoch and builder.balance > 0:
// withdrawals.append(Withdrawal(
// index=withdrawal_index,
// validator_index=convert_builder_index_to_validator_index(builder_index),
// address=builder.execution_address,
// amount=builder.balance,
// ))
// withdrawal_index += 1
// builder_index = BuilderIndex((builder_index + 1) % len(state.builders))
// processed_count += 1
// return withdrawals, withdrawal_index, processed_count
func (b *BeaconState) appendBuildersSweepWithdrawals(withdrawalIndex uint64, withdrawals *[]*enginev1.Withdrawal) (uint64, primitives.BuilderIndex, error) {
cfg := params.BeaconConfig()
withdrawalsLimit := int(cfg.MaxWithdrawalsPerPayload - 1)
if len(*withdrawals) > withdrawalsLimit {
return withdrawalIndex, 0, fmt.Errorf("prior withdrawals length %d exceeds limit %d", len(*withdrawals), withdrawalsLimit)
}
ws := *withdrawals
if b.builders == nil {
return withdrawalIndex, 0, errors.New("builders list is nil")
}
buildersLimit := min(len(b.builders), int(cfg.MaxBuildersPerWithdrawalsSweep))
builderIndex := b.nextWithdrawalBuilderIndex
epoch := slots.ToEpoch(b.slot)
for range buildersLimit {
if len(ws) >= withdrawalsLimit {
break
}
builder := b.builders[builderIndex]
if builder == nil {
return withdrawalIndex, 0, fmt.Errorf("builder at index %d is nil", builderIndex)
}
if builder.WithdrawableEpoch <= epoch && builder.Balance > 0 {
ws = append(ws, &enginev1.Withdrawal{
Index: withdrawalIndex,
ValidatorIndex: builderIndex.ToValidatorIndex(),
Address: builder.ExecutionAddress,
Amount: uint64(builder.Balance),
})
withdrawalIndex++
}
builderIndex = primitives.BuilderIndex((uint64(builderIndex) + 1) % uint64(len(b.builders)))
}
*withdrawals = ws
return withdrawalIndex, builderIndex, nil
}

View File

@@ -1,28 +1,28 @@
package state_native
package state_native_test
import (
"bytes"
"testing"
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/OffchainLabs/prysm/v7/testing/util"
)
func TestLatestBlockHash(t *testing.T) {
t.Run("returns error before gloas", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
st, _ := util.DeterministicGenesisState(t, 1)
_, err := st.LatestBlockHash()
require.ErrorContains(t, "is not supported", err)
})
t.Run("returns zero hash when unset", func(t *testing.T) {
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{})
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{})
require.NoError(t, err)
got, err := st.LatestBlockHash()
@@ -35,7 +35,7 @@ func TestLatestBlockHash(t *testing.T) {
var want [32]byte
copy(want[:], hashBytes)
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
LatestBlockHash: hashBytes,
})
require.NoError(t, err)
@@ -48,14 +48,17 @@ func TestLatestBlockHash(t *testing.T) {
func TestLatestExecutionPayloadBid(t *testing.T) {
t.Run("returns error before gloas", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
_, err := st.LatestExecutionPayloadBid()
stIface, _ := util.DeterministicGenesisState(t, 1)
native, ok := stIface.(*state_native.BeaconState)
require.Equal(t, true, ok)
_, err := native.LatestExecutionPayloadBid()
require.ErrorContains(t, "is not supported", err)
})
}
func TestIsAttestationSameSlot(t *testing.T) {
buildStateWithBlockRoots := func(t *testing.T, stateSlot primitives.Slot, roots map[primitives.Slot][]byte) *BeaconState {
buildStateWithBlockRoots := func(t *testing.T, stateSlot primitives.Slot, roots map[primitives.Slot][]byte) *state_native.BeaconState {
t.Helper()
cfg := params.BeaconConfig()
@@ -64,12 +67,12 @@ func TestIsAttestationSameSlot(t *testing.T) {
blockRoots[slot%cfg.SlotsPerHistoricalRoot] = root
}
stIface, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
stIface, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Slot: stateSlot,
BlockRoots: blockRoots,
})
require.NoError(t, err)
return stIface.(*BeaconState)
return stIface.(*state_native.BeaconState)
}
rootA := bytes.Repeat([]byte{0xAA}, 32)
@@ -142,14 +145,17 @@ func TestIsAttestationSameSlot(t *testing.T) {
func TestBuilderPubkey(t *testing.T) {
t.Run("returns error before gloas", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
_, err := st.BuilderPubkey(0)
stIface, _ := util.DeterministicGenesisState(t, 1)
native, ok := stIface.(*state_native.BeaconState)
require.Equal(t, true, ok)
_, err := native.BuilderPubkey(0)
require.ErrorContains(t, "is not supported", err)
})
t.Run("returns pubkey copy", func(t *testing.T) {
pubkey := bytes.Repeat([]byte{0xAA}, 48)
stIface, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
stIface, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{
{
Pubkey: pubkey,
@@ -173,12 +179,12 @@ func TestBuilderPubkey(t *testing.T) {
})
t.Run("out of range returns error", func(t *testing.T) {
stIface, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
stIface, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{},
})
require.NoError(t, err)
st := stIface.(*BeaconState)
st := stIface.(*state_native.BeaconState)
_, err = st.BuilderPubkey(1)
require.ErrorContains(t, "out of range", err)
})
@@ -186,7 +192,7 @@ func TestBuilderPubkey(t *testing.T) {
func TestBuilderHelpers(t *testing.T) {
t.Run("is active builder", func(t *testing.T) {
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{
{
Balance: 10,
@@ -213,7 +219,7 @@ func TestBuilderHelpers(t *testing.T) {
},
FinalizedCheckpoint: &ethpb.Checkpoint{Epoch: 2},
}
stInactive, err := InitializeFromProtoGloas(stProto)
stInactive, err := state_native.InitializeFromProtoGloas(stProto)
require.NoError(t, err)
active, err = stInactive.IsActiveBuilder(0)
@@ -222,7 +228,7 @@ func TestBuilderHelpers(t *testing.T) {
})
t.Run("can builder cover bid", func(t *testing.T) {
stIface, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
stIface, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{
{
Balance: primitives.Gwei(params.BeaconConfig().MinDepositAmount + 50),
@@ -240,7 +246,7 @@ func TestBuilderHelpers(t *testing.T) {
})
require.NoError(t, err)
st := stIface.(*BeaconState)
st := stIface.(*state_native.BeaconState)
ok, err := st.CanBuilderCoverBid(0, 20)
require.NoError(t, err)
require.Equal(t, true, ok)
@@ -252,9 +258,9 @@ func TestBuilderHelpers(t *testing.T) {
}
func TestBuilderPendingPayments_UnsupportedVersion(t *testing.T) {
stIface, err := InitializeFromProtoElectra(&ethpb.BeaconStateElectra{})
stIface, err := state_native.InitializeFromProtoElectra(&ethpb.BeaconStateElectra{})
require.NoError(t, err)
st := stIface.(*BeaconState)
st := stIface.(*state_native.BeaconState)
_, err = st.BuilderPendingPayments()
require.ErrorContains(t, "BuilderPendingPayments", err)
@@ -262,8 +268,11 @@ func TestBuilderPendingPayments_UnsupportedVersion(t *testing.T) {
func TestWithdrawalsMatchPayloadExpected(t *testing.T) {
t.Run("returns error before gloas", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
_, err := st.WithdrawalsMatchPayloadExpected(nil)
stIface, _ := util.DeterministicGenesisState(t, 1)
native, ok := stIface.(*state_native.BeaconState)
require.Equal(t, true, ok)
_, err := native.WithdrawalsMatchPayloadExpected(nil)
require.ErrorContains(t, "is not supported", err)
})
@@ -271,7 +280,7 @@ func TestWithdrawalsMatchPayloadExpected(t *testing.T) {
withdrawals := []*enginev1.Withdrawal{
{Index: 0, ValidatorIndex: 1, Address: bytes.Repeat([]byte{0x01}, 20), Amount: 10},
}
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
PayloadExpectedWithdrawals: withdrawals,
})
require.NoError(t, err)
@@ -289,7 +298,7 @@ func TestWithdrawalsMatchPayloadExpected(t *testing.T) {
{Index: 0, ValidatorIndex: 1, Address: bytes.Repeat([]byte{0x01}, 20), Amount: 11},
}
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
PayloadExpectedWithdrawals: expected,
})
require.NoError(t, err)
@@ -302,7 +311,7 @@ func TestWithdrawalsMatchPayloadExpected(t *testing.T) {
func TestBuilder(t *testing.T) {
t.Run("nil builders returns nil", func(t *testing.T) {
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: nil,
})
require.NoError(t, err)
@@ -313,7 +322,7 @@ func TestBuilder(t *testing.T) {
})
t.Run("out of bounds returns error", func(t *testing.T) {
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{{}},
})
require.NoError(t, err)
@@ -324,7 +333,7 @@ func TestBuilder(t *testing.T) {
t.Run("returns copy", func(t *testing.T) {
pubkey := bytes.Repeat([]byte{0xAA}, fieldparams.BLSPubkeyLength)
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{
{
Pubkey: pubkey,
@@ -352,7 +361,7 @@ func TestBuilder(t *testing.T) {
func TestBuilderIndexByPubkey(t *testing.T) {
t.Run("not found returns false", func(t *testing.T) {
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{
{Pubkey: bytes.Repeat([]byte{0x11}, fieldparams.BLSPubkeyLength)},
},
@@ -370,7 +379,7 @@ func TestBuilderIndexByPubkey(t *testing.T) {
wantIdx := primitives.BuilderIndex(1)
wantPkBytes := bytes.Repeat([]byte{0xAB}, fieldparams.BLSPubkeyLength)
st, err := InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoGloas(&ethpb.BeaconStateGloas{
Builders: []*ethpb.Builder{
nil,
{Pubkey: wantPkBytes},
@@ -393,7 +402,7 @@ func TestBuilderPendingPayment(t *testing.T) {
target := uint64(slotsPerEpoch + 1)
payments[target] = &ethpb.BuilderPendingPayment{Weight: 10}
st, err := InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
st, err := state_native.InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
BuilderPendingPayments: payments,
})
require.NoError(t, err)
@@ -410,16 +419,16 @@ func TestBuilderPendingPayment(t *testing.T) {
})
t.Run("unsupported version", func(t *testing.T) {
stIface, err := InitializeFromProtoElectra(&ethpb.BeaconStateElectra{})
stIface, err := state_native.InitializeFromProtoElectra(&ethpb.BeaconStateElectra{})
require.NoError(t, err)
st := stIface.(*BeaconState)
st := stIface.(*state_native.BeaconState)
_, err = st.BuilderPendingPayment(0)
require.ErrorContains(t, "BuilderPendingPayment", err)
})
t.Run("out of range", func(t *testing.T) {
stIface, err := InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
stIface, err := state_native.InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
BuilderPendingPayments: []*ethpb.BuilderPendingPayment{},
})
require.NoError(t, err)
@@ -431,9 +440,9 @@ func TestBuilderPendingPayment(t *testing.T) {
func TestExecutionPayloadAvailability(t *testing.T) {
t.Run("unsupported version", func(t *testing.T) {
stIface, err := InitializeFromProtoElectra(&ethpb.BeaconStateElectra{})
stIface, err := state_native.InitializeFromProtoElectra(&ethpb.BeaconStateElectra{})
require.NoError(t, err)
st := stIface.(*BeaconState)
st := stIface.(*state_native.BeaconState)
_, err = st.ExecutionPayloadAvailability(0)
require.ErrorContains(t, "ExecutionPayloadAvailability", err)
@@ -447,7 +456,7 @@ func TestExecutionPayloadAvailability(t *testing.T) {
slot := primitives.Slot(9) // byteIndex=1, bitIndex=1
availability[1] = 0b00000010
stIface, err := InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
stIface, err := state_native.InitializeFromProtoUnsafeGloas(&ethpb.BeaconStateGloas{
ExecutionPayloadAvailability: availability,
})
require.NoError(t, err)
@@ -461,238 +470,3 @@ func TestExecutionPayloadAvailability(t *testing.T) {
require.Equal(t, uint64(0), otherBit)
})
}
func TestIsParentBlockFull(t *testing.T) {
t.Run("returns error before gloas", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
_, err := st.IsParentBlockFull()
require.ErrorContains(t, "is not supported", err)
})
t.Run("returns false when bid is nil", func(t *testing.T) {
st := &BeaconState{version: version.Gloas}
got, err := st.IsParentBlockFull()
require.NoError(t, err)
require.Equal(t, false, got)
})
t.Run("returns true when hashes match", func(t *testing.T) {
hash := bytes.Repeat([]byte{0xAB}, 32)
st := &BeaconState{
version: version.Gloas,
latestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{
BlockHash: hash,
},
latestBlockHash: hash,
}
got, err := st.IsParentBlockFull()
require.NoError(t, err)
require.Equal(t, true, got)
})
t.Run("returns false when hashes differ", func(t *testing.T) {
hash := bytes.Repeat([]byte{0xAB}, 32)
other := bytes.Repeat([]byte{0xCD}, 32)
st := &BeaconState{
version: version.Gloas,
latestExecutionPayloadBid: &ethpb.ExecutionPayloadBid{
BlockHash: hash,
},
latestBlockHash: other,
}
got, err := st.IsParentBlockFull()
require.NoError(t, err)
require.Equal(t, false, got)
})
}
func TestAppendBuilderWithdrawals(t *testing.T) {
t.Run("errors when prior withdrawals exceed limit", func(t *testing.T) {
st := &BeaconState{}
limit := params.BeaconConfig().MaxWithdrawalsPerPayload - 1
withdrawals := make([]*enginev1.Withdrawal, limit+1)
nextIndex, processed, err := st.appendBuilderWithdrawals(5, &withdrawals)
require.ErrorContains(t, "exceeds limit", err)
require.Equal(t, uint64(5), nextIndex)
require.Equal(t, uint64(0), processed)
require.Equal(t, int(limit+1), len(withdrawals))
})
t.Run("appends builder withdrawals and increments index", func(t *testing.T) {
st := &BeaconState{
builderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{
{BuilderIndex: 1, FeeRecipient: []byte{0x01}, Amount: 11},
{BuilderIndex: 2, FeeRecipient: []byte{0x02}, Amount: 22},
{BuilderIndex: 3, FeeRecipient: []byte{0x03}, Amount: 33},
},
}
withdrawals := []*enginev1.Withdrawal{
{Index: 7, ValidatorIndex: 9, Address: []byte{0xAA}, Amount: 99},
}
nextIndex, processed, err := st.appendBuilderWithdrawals(10, &withdrawals)
require.NoError(t, err)
require.Equal(t, uint64(13), nextIndex)
require.Equal(t, uint64(3), processed)
require.Equal(t, 4, len(withdrawals))
require.DeepEqual(t, &enginev1.Withdrawal{
Index: 10,
ValidatorIndex: primitives.BuilderIndex(1).ToValidatorIndex(),
Address: []byte{0x01},
Amount: 11,
}, withdrawals[1])
require.DeepEqual(t, &enginev1.Withdrawal{
Index: 11,
ValidatorIndex: primitives.BuilderIndex(2).ToValidatorIndex(),
Address: []byte{0x02},
Amount: 22,
}, withdrawals[2])
require.DeepEqual(t, &enginev1.Withdrawal{
Index: 12,
ValidatorIndex: primitives.BuilderIndex(3).ToValidatorIndex(),
Address: []byte{0x03},
Amount: 33,
}, withdrawals[3])
})
t.Run("respects per-payload limit", func(t *testing.T) {
limit := params.BeaconConfig().MaxWithdrawalsPerPayload - 1
st := &BeaconState{
builderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{
{BuilderIndex: 4, FeeRecipient: []byte{0x04}, Amount: 44},
{BuilderIndex: 5, FeeRecipient: []byte{0x05}, Amount: 55},
},
}
withdrawals := make([]*enginev1.Withdrawal, limit-1)
nextIndex, processed, err := st.appendBuilderWithdrawals(20, &withdrawals)
require.NoError(t, err)
require.Equal(t, uint64(21), nextIndex)
require.Equal(t, uint64(1), processed)
require.Equal(t, int(limit), len(withdrawals))
require.DeepEqual(t, &enginev1.Withdrawal{
Index: 20,
ValidatorIndex: primitives.BuilderIndex(4).ToValidatorIndex(),
Address: []byte{0x04},
Amount: 44,
}, withdrawals[len(withdrawals)-1])
})
t.Run("does not append when already at limit", func(t *testing.T) {
limit := params.BeaconConfig().MaxWithdrawalsPerPayload - 1
if limit == 0 {
t.Skip("withdrawals limit too small")
}
st := &BeaconState{
builderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{
{BuilderIndex: 6, FeeRecipient: []byte{0x06}, Amount: 66},
},
}
withdrawals := make([]*enginev1.Withdrawal, limit)
nextIndex, processed, err := st.appendBuilderWithdrawals(30, &withdrawals)
require.NoError(t, err)
require.Equal(t, uint64(30), nextIndex)
require.Equal(t, uint64(0), processed)
require.Equal(t, int(limit), len(withdrawals))
})
}
func TestAppendBuildersSweepWithdrawals(t *testing.T) {
t.Run("errors when prior withdrawals exceed limit", func(t *testing.T) {
st := &BeaconState{}
limit := params.BeaconConfig().MaxWithdrawalsPerPayload - 1
withdrawals := make([]*enginev1.Withdrawal, limit+1)
nextIndex, nextBuilderIndex, err := st.appendBuildersSweepWithdrawals(5, &withdrawals)
require.ErrorContains(t, "exceeds limit", err)
require.Equal(t, uint64(5), nextIndex)
require.Equal(t, primitives.BuilderIndex(0), nextBuilderIndex)
require.Equal(t, int(limit+1), len(withdrawals))
})
t.Run("appends eligible builders, skips ineligible", func(t *testing.T) {
epoch := primitives.Epoch(3)
st := &BeaconState{
slot: slots.UnsafeEpochStart(epoch),
nextWithdrawalBuilderIndex: 2,
builders: []*ethpb.Builder{
{ExecutionAddress: []byte{0x01}, Balance: 0, WithdrawableEpoch: epoch},
{ExecutionAddress: []byte{0x02}, Balance: 10, WithdrawableEpoch: epoch + 1},
{ExecutionAddress: []byte{0x03}, Balance: 20, WithdrawableEpoch: epoch},
},
}
withdrawals := []*enginev1.Withdrawal{}
nextIndex, nextBuilderIndex, err := st.appendBuildersSweepWithdrawals(100, &withdrawals)
require.NoError(t, err)
require.Equal(t, uint64(101), nextIndex)
require.Equal(t, primitives.BuilderIndex(2), nextBuilderIndex)
require.Equal(t, 1, len(withdrawals))
require.DeepEqual(t, &enginev1.Withdrawal{
Index: 100,
ValidatorIndex: primitives.BuilderIndex(2).ToValidatorIndex(),
Address: []byte{0x03},
Amount: 20,
}, withdrawals[0])
})
t.Run("respects max builders per sweep", func(t *testing.T) {
cfg := params.BeaconConfig()
max := int(cfg.MaxBuildersPerWithdrawalsSweep)
epoch := primitives.Epoch(1)
builders := make([]*ethpb.Builder, max+2)
for i := range builders {
builders[i] = &ethpb.Builder{
ExecutionAddress: []byte{byte(i + 1)},
Balance: 1,
WithdrawableEpoch: epoch,
}
}
start := len(builders) - 1
st := &BeaconState{
slot: slots.UnsafeEpochStart(epoch),
nextWithdrawalBuilderIndex: primitives.BuilderIndex(start),
builders: builders,
}
withdrawals := []*enginev1.Withdrawal{}
nextIndex, nextBuilderIndex, err := st.appendBuildersSweepWithdrawals(7, &withdrawals)
require.NoError(t, err)
limit := int(cfg.MaxWithdrawalsPerPayload - 1)
expectedCount := min(max, limit)
require.Equal(t, uint64(7)+uint64(expectedCount), nextIndex)
require.Equal(t, expectedCount, len(withdrawals))
expectedNext := primitives.BuilderIndex((uint64(start) + uint64(expectedCount)) % uint64(len(builders)))
require.Equal(t, expectedNext, nextBuilderIndex)
})
t.Run("stops when payload limit reached", func(t *testing.T) {
cfg := params.BeaconConfig()
limit := cfg.MaxWithdrawalsPerPayload - 1
if limit < 1 {
t.Skip("withdrawals limit too small")
}
epoch := primitives.Epoch(2)
builders := []*ethpb.Builder{
{ExecutionAddress: []byte{0x0A}, Balance: 3, WithdrawableEpoch: epoch},
{ExecutionAddress: []byte{0x0B}, Balance: 4, WithdrawableEpoch: epoch},
}
st := &BeaconState{
slot: slots.UnsafeEpochStart(epoch),
nextWithdrawalBuilderIndex: 0,
builders: builders,
}
withdrawals := make([]*enginev1.Withdrawal, limit)
nextIndex, nextBuilderIndex, err := st.appendBuildersSweepWithdrawals(20, &withdrawals)
require.NoError(t, err)
require.Equal(t, uint64(20), nextIndex)
require.Equal(t, int(limit), len(withdrawals))
require.Equal(t, primitives.BuilderIndex(0), nextBuilderIndex)
})
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/pkg/errors"
)
// Validators participating in consensus on the beacon chain.
@@ -80,6 +81,25 @@ func (b *BeaconState) ValidatorAtIndex(idx primitives.ValidatorIndex) (*ethpb.Va
return b.validatorAtIndex(idx)
}
// EffectiveBalances returns the sum of the effective balances of the given list of validator indices, the eb of each given validator, or an
// error if one of the indices is out of bounds, or the state wasn't correctly initialized.
func (b *BeaconState) EffectiveBalanceSum(idxs []primitives.ValidatorIndex) (uint64, error) {
b.lock.RLock()
defer b.lock.RUnlock()
var sum uint64
for i := range idxs {
if b.validatorsMultiValue == nil {
return 0, errors.Wrap(state.ErrNilValidatorsInState, "nil validators multi-value slice")
}
v, err := b.validatorsMultiValue.At(b, uint64(idxs[i]))
if err != nil {
return 0, errors.Wrap(err, "validators multi value at index")
}
sum += v.EffectiveBalance
}
return sum, nil
}
func (b *BeaconState) validatorAtIndex(idx primitives.ValidatorIndex) (*ethpb.Validator, error) {
if b.validatorsMultiValue == nil {
return &ethpb.Validator{}, nil

View File

@@ -133,22 +133,11 @@ func (b *BeaconState) appendPendingPartialWithdrawals(withdrawalIndex uint64, wi
return withdrawalIndex, 0, nil
}
cfg := params.BeaconConfig()
var withdrawalsLimit int
if b.version >= version.Gloas {
withdrawalsLimit = min(
len(*withdrawals)+int(cfg.MaxPendingPartialsPerWithdrawalsSweep),
int(cfg.MaxWithdrawalsPerPayload-1),
)
} else {
withdrawalsLimit = int(cfg.MaxPendingPartialsPerWithdrawalsSweep)
}
ws := *withdrawals
epoch := slots.ToEpoch(b.slot)
var processedPartialWithdrawalsCount uint64
for _, w := range b.pendingPartialWithdrawals {
if w.WithdrawableEpoch > epoch || len(ws) >= withdrawalsLimit {
if w.WithdrawableEpoch > epoch || len(ws) >= int(params.BeaconConfig().MaxPendingPartialsPerWithdrawalsSweep) {
break
}
@@ -160,7 +149,7 @@ func (b *BeaconState) appendPendingPartialWithdrawals(withdrawalIndex uint64, wi
if err != nil {
return withdrawalIndex, 0, fmt.Errorf("could not retrieve balance at index %d: %w", w.Index, err)
}
hasSufficientEffectiveBalance := v.EffectiveBalance() >= cfg.MinActivationBalance
hasSufficientEffectiveBalance := v.EffectiveBalance() >= params.BeaconConfig().MinActivationBalance
var totalWithdrawn uint64
for _, wi := range ws {
if wi.ValidatorIndex == w.Index {
@@ -171,9 +160,9 @@ func (b *BeaconState) appendPendingPartialWithdrawals(withdrawalIndex uint64, wi
if err != nil {
return withdrawalIndex, 0, errors.Wrapf(err, "failed to subtract balance %d with total withdrawn %d", vBal, totalWithdrawn)
}
hasExcessBalance := balance > cfg.MinActivationBalance
if v.ExitEpoch() == cfg.FarFutureEpoch && hasSufficientEffectiveBalance && hasExcessBalance {
amount := min(balance-cfg.MinActivationBalance, w.Amount)
hasExcessBalance := balance > params.BeaconConfig().MinActivationBalance
if v.ExitEpoch() == params.BeaconConfig().FarFutureEpoch && hasSufficientEffectiveBalance && hasExcessBalance {
amount := min(balance-params.BeaconConfig().MinActivationBalance, w.Amount)
ws = append(ws, &enginev1.Withdrawal{
Index: withdrawalIndex,
ValidatorIndex: w.Index,

View File

@@ -1,7 +1,6 @@
package state_native
import (
"errors"
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types"
@@ -11,11 +10,9 @@ import (
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/time/slots"
pkgerrors "github.com/pkg/errors"
)
// RotateBuilderPendingPayments rotates the queue by dropping slots per epoch payments from the
@@ -217,21 +214,6 @@ func (b *BeaconState) SetLatestBlockHash(hash [32]byte) error {
return nil
}
// SetPayloadExpectedWithdrawals stores the expected withdrawals for the next payload.
func (b *BeaconState) SetPayloadExpectedWithdrawals(withdrawals []*enginev1.Withdrawal) error {
if b.version < version.Gloas {
return errNotSupported("SetPayloadExpectedWithdrawals", b.version)
}
b.lock.Lock()
defer b.lock.Unlock()
b.payloadExpectedWithdrawals = withdrawals
b.markFieldAsDirty(types.PayloadExpectedWithdrawals)
return nil
}
// SetExecutionPayloadAvailability sets the execution payload availability bit for a specific slot.
func (b *BeaconState) SetExecutionPayloadAvailability(index primitives.Slot, available bool) error {
if b.version < version.Gloas {
@@ -463,136 +445,3 @@ func (b *BeaconState) UpdatePendingPaymentWeight(att ethpb.Att, indices []uint64
return nil
}
// DequeueBuilderPendingWithdrawals removes processed builder withdrawals from the front of the queue.
func (b *BeaconState) DequeueBuilderPendingWithdrawals(n uint64) error {
if b.version < version.Gloas {
return errNotSupported("DequeueBuilderPendingWithdrawals", b.version)
}
if n == 0 {
return nil
}
b.lock.Lock()
defer b.lock.Unlock()
if n > uint64(len(b.builderPendingWithdrawals)) {
return errors.New("cannot dequeue more builder withdrawals than are in the queue")
}
if b.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs() > 1 {
withdrawals := make([]*ethpb.BuilderPendingWithdrawal, len(b.builderPendingWithdrawals))
copy(withdrawals, b.builderPendingWithdrawals)
b.builderPendingWithdrawals = withdrawals
b.sharedFieldReferences[types.BuilderPendingWithdrawals].MinusRef()
b.sharedFieldReferences[types.BuilderPendingWithdrawals] = stateutil.NewRef(1)
}
b.builderPendingWithdrawals = b.builderPendingWithdrawals[n:]
b.markFieldAsDirty(types.BuilderPendingWithdrawals)
b.rebuildTrie[types.BuilderPendingWithdrawals] = true
return nil
}
// SetNextWithdrawalBuilderIndex sets the next builder index for the withdrawals sweep.
func (b *BeaconState) SetNextWithdrawalBuilderIndex(index primitives.BuilderIndex) error {
if b.version < version.Gloas {
return errNotSupported("SetNextWithdrawalBuilderIndex", b.version)
}
b.lock.Lock()
defer b.lock.Unlock()
b.nextWithdrawalBuilderIndex = index
b.markFieldAsDirty(types.NextWithdrawalBuilderIndex)
return nil
}
// DecreaseWithdrawalBalances applies withdrawal balance decreases for validators and builders.
// This method holds the state lock for the full batch to avoid lock churn.
func (b *BeaconState) DecreaseWithdrawalBalances(withdrawals []*enginev1.Withdrawal) error {
if b.version < version.Gloas {
return errNotSupported("DecreaseWithdrawalBalances", b.version)
}
if len(withdrawals) == 0 {
return nil
}
b.lock.Lock()
defer b.lock.Unlock()
var (
balanceIndices []uint64
builderIndices []uint64
)
for _, withdrawal := range withdrawals {
if withdrawal == nil {
return errors.New("withdrawal is nil")
}
if withdrawal.Amount == 0 {
continue
}
if withdrawal.ValidatorIndex.IsBuilderIndex() {
builderIndex := withdrawal.ValidatorIndex.ToBuilderIndex()
if err := b.decreaseBuilderBalanceLockFree(builderIndex, withdrawal.Amount); err != nil {
return err
}
builderIndices = append(builderIndices, uint64(builderIndex))
continue
}
balAtIdx, err := b.balanceAtIndex(withdrawal.ValidatorIndex)
if err != nil {
return err
}
newBal := decreaseBalanceWithVal(primitives.Gwei(balAtIdx), primitives.Gwei(withdrawal.Amount))
if err := b.balancesMultiValue.UpdateAt(b, uint64(withdrawal.ValidatorIndex), uint64(newBal)); err != nil {
return pkgerrors.Wrap(err, "could not update balances")
}
balanceIndices = append(balanceIndices, uint64(withdrawal.ValidatorIndex))
}
if len(balanceIndices) > 0 {
b.markFieldAsDirty(types.Balances)
b.addDirtyIndices(types.Balances, balanceIndices)
}
if len(builderIndices) > 0 {
b.markFieldAsDirty(types.Builders)
b.addDirtyIndices(types.Builders, builderIndices)
}
return nil
}
func (b *BeaconState) decreaseBuilderBalanceLockFree(builderIndex primitives.BuilderIndex, amount uint64) error {
idx := uint64(builderIndex)
if idx >= uint64(len(b.builders)) {
return fmt.Errorf("builder index %d out of range (len=%d)", builderIndex, len(b.builders))
}
if b.sharedFieldReferences[types.Builders].Refs() > 1 {
builders := make([]*ethpb.Builder, len(b.builders))
copy(builders, b.builders)
builder := ethpb.CopyBuilder(builders[idx])
builders[idx] = builder
b.builders = builders
b.sharedFieldReferences[types.Builders].MinusRef()
b.sharedFieldReferences[types.Builders] = stateutil.NewRef(1)
}
builder := b.builders[idx]
builder.Balance = decreaseBalanceWithVal(builder.Balance, primitives.Gwei(amount))
return nil
}
func decreaseBalanceWithVal(currBalance, delta primitives.Gwei) primitives.Gwei {
if delta > currBalance {
return 0
}
return currBalance - delta
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stateutil"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/testing/require"
@@ -401,235 +400,6 @@ func TestRotateBuilderPendingPayments_UnsupportedVersion(t *testing.T) {
require.ErrorContains(t, "RotateBuilderPendingPayments", err)
}
func TestSetPayloadExpectedWithdrawals(t *testing.T) {
t.Run("previous fork returns expected error", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
err := st.SetPayloadExpectedWithdrawals([]*enginev1.Withdrawal{})
require.ErrorContains(t, "SetPayloadExpectedWithdrawals", err)
})
t.Run("allows nil input and marks dirty", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
}
require.NoError(t, st.SetPayloadExpectedWithdrawals(nil))
require.Equal(t, true, st.payloadExpectedWithdrawals == nil)
require.Equal(t, true, st.dirtyFields[types.PayloadExpectedWithdrawals])
})
t.Run("sets and marks dirty", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
payloadExpectedWithdrawals: []*enginev1.Withdrawal{{Index: 1}, {Index: 2}},
}
withdrawals := []*enginev1.Withdrawal{{Index: 3}}
require.NoError(t, st.SetPayloadExpectedWithdrawals(withdrawals))
require.DeepEqual(t, withdrawals, st.payloadExpectedWithdrawals)
require.Equal(t, true, st.dirtyFields[types.PayloadExpectedWithdrawals])
})
}
func TestDecreaseWithdrawalBalances(t *testing.T) {
t.Run("previous fork returns expected error", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
err := st.DecreaseWithdrawalBalances([]*enginev1.Withdrawal{{}})
require.ErrorContains(t, "DecreaseWithdrawalBalances", err)
})
t.Run("rejects nil withdrawal", func(t *testing.T) {
st := &BeaconState{version: version.Gloas}
err := st.DecreaseWithdrawalBalances([]*enginev1.Withdrawal{nil})
require.ErrorContains(t, "withdrawal is nil", err)
})
t.Run("no-op on empty input", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
dirtyIndices: make(map[types.FieldIndex][]uint64),
rebuildTrie: make(map[types.FieldIndex]bool),
}
require.NoError(t, st.DecreaseWithdrawalBalances(nil))
require.Equal(t, 0, len(st.dirtyFields))
require.Equal(t, 0, len(st.dirtyIndices))
})
t.Run("updates validator and builder balances and tracks dirty indices", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
dirtyIndices: make(map[types.FieldIndex][]uint64),
rebuildTrie: make(map[types.FieldIndex]bool),
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.Builders: stateutil.NewRef(1),
},
balancesMultiValue: NewMultiValueBalances([]uint64{100, 200, 300}),
builders: []*ethpb.Builder{
{Balance: 1000},
{Balance: 50},
},
}
withdrawals := []*enginev1.Withdrawal{
{ValidatorIndex: primitives.ValidatorIndex(1), Amount: 20},
{ValidatorIndex: primitives.BuilderIndex(1).ToValidatorIndex(), Amount: 30},
{ValidatorIndex: primitives.ValidatorIndex(2), Amount: 400},
{ValidatorIndex: primitives.BuilderIndex(0).ToValidatorIndex(), Amount: 2000},
{ValidatorIndex: primitives.ValidatorIndex(0), Amount: 0},
}
require.NoError(t, st.DecreaseWithdrawalBalances(withdrawals))
require.DeepEqual(t, []uint64{100, 180, 0}, st.Balances())
require.Equal(t, primitives.Gwei(0), st.builders[0].Balance)
require.Equal(t, primitives.Gwei(20), st.builders[1].Balance)
require.Equal(t, true, st.dirtyFields[types.Balances])
require.Equal(t, true, st.dirtyFields[types.Builders])
require.DeepEqual(t, []uint64{1, 2}, st.dirtyIndices[types.Balances])
require.DeepEqual(t, []uint64{1, 0}, st.dirtyIndices[types.Builders])
})
t.Run("returns error on builder index out of range", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
dirtyIndices: make(map[types.FieldIndex][]uint64),
rebuildTrie: make(map[types.FieldIndex]bool),
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.Builders: stateutil.NewRef(1),
},
builders: []*ethpb.Builder{{Balance: 5}},
}
err := st.DecreaseWithdrawalBalances([]*enginev1.Withdrawal{
{ValidatorIndex: primitives.BuilderIndex(2).ToValidatorIndex(), Amount: 1},
})
require.ErrorContains(t, "out of range", err)
require.Equal(t, false, st.dirtyFields[types.Builders])
require.Equal(t, 0, len(st.dirtyIndices[types.Builders]))
})
}
func TestDequeueBuilderPendingWithdrawals(t *testing.T) {
t.Run("previous fork returns expected error", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
err := st.DequeueBuilderPendingWithdrawals(1)
require.ErrorContains(t, "DequeueBuilderPendingWithdrawals", err)
})
t.Run("returns error when dequeueing more than length", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.BuilderPendingWithdrawals: stateutil.NewRef(1),
},
builderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{{Amount: 1}},
}
err := st.DequeueBuilderPendingWithdrawals(2)
require.ErrorContains(t, "cannot dequeue more builder withdrawals", err)
require.Equal(t, 1, len(st.builderPendingWithdrawals))
require.Equal(t, false, st.dirtyFields[types.BuilderPendingWithdrawals])
})
t.Run("no-op on zero", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.BuilderPendingWithdrawals: stateutil.NewRef(1),
},
builderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{{Amount: 1}},
}
require.NoError(t, st.DequeueBuilderPendingWithdrawals(0))
require.Equal(t, 1, len(st.builderPendingWithdrawals))
require.Equal(t, false, st.dirtyFields[types.BuilderPendingWithdrawals])
require.Equal(t, false, st.rebuildTrie[types.BuilderPendingWithdrawals])
})
t.Run("dequeues and marks dirty", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.BuilderPendingWithdrawals: stateutil.NewRef(1),
},
builderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{
{Amount: 1},
{Amount: 2},
{Amount: 3},
},
rebuildTrie: make(map[types.FieldIndex]bool),
}
require.NoError(t, st.DequeueBuilderPendingWithdrawals(2))
require.Equal(t, 1, len(st.builderPendingWithdrawals))
require.Equal(t, primitives.Gwei(3), st.builderPendingWithdrawals[0].Amount)
require.Equal(t, true, st.dirtyFields[types.BuilderPendingWithdrawals])
require.Equal(t, true, st.rebuildTrie[types.BuilderPendingWithdrawals])
})
t.Run("copy-on-write preserves shared state", func(t *testing.T) {
sharedRef := stateutil.NewRef(2)
sharedWithdrawals := []*ethpb.BuilderPendingWithdrawal{
{Amount: 1},
{Amount: 2},
{Amount: 3},
}
st1 := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.BuilderPendingWithdrawals: sharedRef,
},
builderPendingWithdrawals: sharedWithdrawals,
rebuildTrie: make(map[types.FieldIndex]bool),
}
st2 := &BeaconState{
sharedFieldReferences: map[types.FieldIndex]*stateutil.Reference{
types.BuilderPendingWithdrawals: sharedRef,
},
builderPendingWithdrawals: sharedWithdrawals,
}
require.NoError(t, st1.DequeueBuilderPendingWithdrawals(2))
require.Equal(t, primitives.Gwei(3), st1.builderPendingWithdrawals[0].Amount)
require.Equal(t, 3, len(st2.builderPendingWithdrawals))
require.Equal(t, primitives.Gwei(1), st2.builderPendingWithdrawals[0].Amount)
require.Equal(t, uint(1), st1.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs())
require.Equal(t, uint(1), st2.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs())
})
}
func TestSetNextWithdrawalBuilderIndex(t *testing.T) {
t.Run("previous fork returns expected error", func(t *testing.T) {
st := &BeaconState{version: version.Fulu}
err := st.SetNextWithdrawalBuilderIndex(1)
require.ErrorContains(t, "SetNextWithdrawalBuilderIndex", err)
})
t.Run("sets and marks dirty", func(t *testing.T) {
st := &BeaconState{
version: version.Gloas,
dirtyFields: make(map[types.FieldIndex]bool),
}
require.NoError(t, st.SetNextWithdrawalBuilderIndex(7))
require.Equal(t, primitives.BuilderIndex(7), st.nextWithdrawalBuilderIndex)
require.Equal(t, true, st.dirtyFields[types.NextWithdrawalBuilderIndex])
})
}
func TestAppendBuilderPendingWithdrawal_CopyOnWrite(t *testing.T) {
wd := &ethpb.BuilderPendingWithdrawal{
FeeRecipient: make([]byte, 20),

View File

@@ -5,6 +5,7 @@ import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
)
@@ -14,6 +15,8 @@ type StateManager struct {
StatesBySlot map[primitives.Slot]state.BeaconState
}
var _ stategen.StateManager = (*StateManager)(nil)
// NewService --
func NewService() *StateManager {
return &StateManager{
@@ -101,3 +104,8 @@ func (m *StateManager) AddStateForSlot(state state.BeaconState, slot primitives.
func (m *StateManager) DeleteStateFromCaches(context.Context, [32]byte) error {
return nil
}
// FinalizedReadOnlyBalances --
func (m *StateManager) FinalizedReadOnlyBalances() stategen.NilCheckableReadOnlyBalances {
panic("unimplemented")
}

View File

@@ -27,6 +27,13 @@ var defaultHotStateDBInterval primitives.Slot = 128
var populatePubkeyCacheOnce sync.Once
// NilCheckableReadOnlyBalances adds the IsNil method to ReadOnlyBalances
// to allow checking if the underlying state value is nil.
type NilCheckableReadOnlyBalances interface {
state.ReadOnlyBalances
IsNil() bool
}
// StateManager represents a management object that handles the internal
// logic of maintaining both hot and cold states in DB.
type StateManager interface {
@@ -43,6 +50,7 @@ type StateManager interface {
ActiveNonSlashedBalancesByRoot(context.Context, [32]byte) ([]uint64, error)
StateByRootIfCachedNoCopy(blockRoot [32]byte) state.BeaconState
StateByRootInitialSync(ctx context.Context, blockRoot [32]byte) (state.BeaconState, error)
FinalizedReadOnlyBalances() NilCheckableReadOnlyBalances
}
// State is a concrete implementation of StateManager.
@@ -201,3 +209,8 @@ func (s *State) FinalizedState() state.BeaconState {
defer s.finalizedInfo.lock.RUnlock()
return s.finalizedInfo.state.Copy()
}
// Returns the finalized state as a ReadOnlyBalances so that it can be used read-only without copying.
func (s *State) FinalizedReadOnlyBalances() NilCheckableReadOnlyBalances {
return s.finalizedInfo.state
}

View File

@@ -185,7 +185,7 @@ func (s *Service) validatorsCustodyRequirement() (uint64, error) {
}
// Retrieve the finalized state.
finalizedState := s.cfg.stateGen.FinalizedState()
finalizedState := s.cfg.stateGen.FinalizedReadOnlyBalances()
if finalizedState == nil || finalizedState.IsNil() {
return 0, nilFinalizedStateError
}

View File

@@ -268,10 +268,23 @@ func (s *Service) validateCommitteeIndexAndCount(
a eth.Att,
bs state.ReadOnlyBeaconState,
) (primitives.CommitteeIndex, uint64, pubsub.ValidationResult, error) {
// - [REJECT] attestation.data.index == 0
if a.Version() >= version.Electra && a.GetData().CommitteeIndex != 0 {
return 0, 0, pubsub.ValidationReject, errors.New("attestation data's committee index must be 0")
// Validate committee index based on fork.
if a.Version() >= version.Electra {
data := a.GetData()
attEpoch := slots.ToEpoch(data.Slot)
postGloas := attEpoch >= params.BeaconConfig().GloasForkEpoch
if postGloas {
if result, err := s.validateGloasCommitteeIndex(data); result != pubsub.ValidationAccept {
return 0, 0, result, err
}
} else {
// [REJECT] attestation.data.index == 0 (New in Electra, removed in Gloas)
if data.CommitteeIndex != 0 {
return 0, 0, pubsub.ValidationReject, errors.New("attestation data's committee index must be 0")
}
}
}
valCount, err := helpers.ActiveValidatorCount(ctx, bs, slots.ToEpoch(a.GetData().Slot))
if err != nil {
return 0, 0, pubsub.ValidationIgnore, err
@@ -356,6 +369,29 @@ func validateAttestingIndex(
return pubsub.ValidationAccept, nil
}
// validateGloasCommitteeIndex validates committee index rules for Gloas fork.
// [REJECT] attestation.data.index < 2. (New in Gloas)
// [REJECT] attestation.data.index == 0 if block.slot == attestation.data.slot. (New in Gloas)
func (s *Service) validateGloasCommitteeIndex(data *eth.AttestationData) (pubsub.ValidationResult, error) {
if data.CommitteeIndex >= 2 {
return pubsub.ValidationReject, errors.New("attestation data's committee index must be < 2")
}
// Same-slot attestations must use committee index 0
if data.CommitteeIndex != 0 {
blockRoot := bytesutil.ToBytes32(data.BeaconBlockRoot)
slot, err := s.cfg.chain.RecentBlockSlot(blockRoot)
if err != nil {
return pubsub.ValidationIgnore, err
}
if slot == data.Slot {
return pubsub.ValidationReject, errors.New("same slot attestations must use committee index 0")
}
}
return pubsub.ValidationAccept, nil
}
// generateUnaggregatedAttCacheKey generates the cache key for unaggregated attestation tracking.
func generateUnaggregatedAttCacheKey(att eth.Att) (string, error) {
var attester uint64

View File

@@ -684,3 +684,75 @@ func Test_validateCommitteeIndexAndCount_Boundary(t *testing.T) {
require.ErrorContains(t, "committee index", err)
require.Equal(t, pubsub.ValidationReject, res)
}
func Test_validateGloasCommitteeIndex(t *testing.T) {
tests := []struct {
name string
committeeIndex primitives.CommitteeIndex
attestationSlot primitives.Slot
blockSlot primitives.Slot
wantResult pubsub.ValidationResult
wantErr string
}{
{
name: "committee index >= 2 should reject",
committeeIndex: 2,
attestationSlot: 10,
blockSlot: 10,
wantResult: pubsub.ValidationReject,
wantErr: "committee index must be < 2",
},
{
name: "committee index 0 should accept",
committeeIndex: 0,
attestationSlot: 10,
blockSlot: 10,
wantResult: pubsub.ValidationAccept,
wantErr: "",
},
{
name: "committee index 1 different-slot should accept",
committeeIndex: 1,
attestationSlot: 10,
blockSlot: 9,
wantResult: pubsub.ValidationAccept,
wantErr: "",
},
{
name: "committee index 1 same-slot should reject",
committeeIndex: 1,
attestationSlot: 10,
blockSlot: 10,
wantResult: pubsub.ValidationReject,
wantErr: "same slot attestations must use committee index 0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockChain := &mockChain.ChainService{
BlockSlot: tt.blockSlot,
}
s := &Service{
cfg: &config{
chain: mockChain,
},
}
data := &ethpb.AttestationData{
Slot: tt.attestationSlot,
CommitteeIndex: tt.committeeIndex,
BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot"), 32),
}
result, err := s.validateGloasCommitteeIndex(data)
require.Equal(t, tt.wantResult, result)
if tt.wantErr != "" {
require.ErrorContains(t, tt.wantErr, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,2 @@
### Fixed
- Avoid copying the full finalized state every time we compute cgc.

View File

@@ -0,0 +1,3 @@
### Added
- Add gossip beacon attestation validation conditions for Gloas fork

View File

@@ -0,0 +1,2 @@
### Ignored
- Refactor ProcessExecutionPayload to ApplyExecutionPayload

View File

@@ -1,2 +0,0 @@
### Added
- add modified process withdrawals for gloas

View File

@@ -130,7 +130,6 @@ type BeaconChainConfig struct {
MaxWithdrawalsPerPayload uint64 `yaml:"MAX_WITHDRAWALS_PER_PAYLOAD" spec:"true"` // MaxWithdrawalsPerPayload defines the maximum number of withdrawals in a block.
MaxBlsToExecutionChanges uint64 `yaml:"MAX_BLS_TO_EXECUTION_CHANGES" spec:"true"` // MaxBlsToExecutionChanges defines the maximum number of BLS-to-execution-change objects in a block.
MaxValidatorsPerWithdrawalsSweep uint64 `yaml:"MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP" spec:"true"` // MaxValidatorsPerWithdrawalsSweep bounds the size of the sweep searching for withdrawals per slot.
MaxBuildersPerWithdrawalsSweep uint64 `yaml:"MAX_BUILDERS_PER_WITHDRAWALS_SWEEP" spec:"true"` // MaxBuildersPerWithdrawalsSweep bounds the size of the builder withdrawals sweep per slot.
// BLS domain values.
DomainBeaconProposer [4]byte `yaml:"DOMAIN_BEACON_PROPOSER" spec:"true"` // DomainBeaconProposer defines the BLS signature domain for beacon proposal verification.

View File

@@ -194,7 +194,6 @@ func ConfigToYaml(cfg *BeaconChainConfig) []byte {
fmt.Sprintf("SHARD_COMMITTEE_PERIOD: %d", cfg.ShardCommitteePeriod),
fmt.Sprintf("MIN_VALIDATOR_WITHDRAWABILITY_DELAY: %d", cfg.MinValidatorWithdrawabilityDelay),
fmt.Sprintf("MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP: %d", cfg.MaxValidatorsPerWithdrawalsSweep),
fmt.Sprintf("MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: %d", cfg.MaxBuildersPerWithdrawalsSweep),
fmt.Sprintf("MAX_SEED_LOOKAHEAD: %d", cfg.MaxSeedLookahead),
fmt.Sprintf("EJECTION_BALANCE: %d", cfg.EjectionBalance),
fmt.Sprintf("MIN_PER_EPOCH_CHURN_LIMIT: %d", cfg.MinPerEpochChurnLimit),

View File

@@ -175,7 +175,6 @@ var mainnetBeaconConfig = &BeaconChainConfig{
MaxWithdrawalsPerPayload: 16,
MaxBlsToExecutionChanges: 16,
MaxValidatorsPerWithdrawalsSweep: 16384,
MaxBuildersPerWithdrawalsSweep: 16384,
// BLS domain values.
DomainBeaconProposer: bytesutil.Uint32ToBytes4(0x00000000),

View File

@@ -95,7 +95,6 @@ func compareConfigs(t *testing.T, expected, actual *params.BeaconChainConfig) {
require.DeepEqual(t, expected.MaxDeposits, actual.MaxDeposits)
require.DeepEqual(t, expected.MaxVoluntaryExits, actual.MaxVoluntaryExits)
require.DeepEqual(t, expected.MaxWithdrawalsPerPayload, actual.MaxWithdrawalsPerPayload)
require.DeepEqual(t, expected.MaxBuildersPerWithdrawalsSweep, actual.MaxBuildersPerWithdrawalsSweep)
require.DeepEqual(t, expected.DomainBeaconProposer, actual.DomainBeaconProposer)
require.DeepEqual(t, expected.DomainRandao, actual.DomainRandao)
require.DeepEqual(t, expected.DomainBeaconAttester, actual.DomainBeaconAttester)

View File

@@ -13,16 +13,6 @@ var _ fssz.Unmarshaler = (*BuilderIndex)(nil)
// BuilderIndex is an index into the builder registry.
type BuilderIndex uint64
// ToValidatorIndex sets the builder flag on a builder index.
//
// Spec v1.6.1 (pseudocode):
// def convert_builder_index_to_validator_index(builder_index: BuilderIndex) -> ValidatorIndex:
//
// return ValidatorIndex(builder_index | BUILDER_INDEX_FLAG)
func (b BuilderIndex) ToValidatorIndex() ValidatorIndex {
return ValidatorIndex(uint64(b) | BuilderIndexFlag)
}
// HashTreeRoot returns the SSZ hash tree root of the index.
func (b BuilderIndex) HashTreeRoot() ([32]byte, error) {
return fssz.HashWithDefaultHasher(b)

View File

@@ -10,34 +10,9 @@ var _ fssz.HashRoot = (ValidatorIndex)(0)
var _ fssz.Marshaler = (*ValidatorIndex)(nil)
var _ fssz.Unmarshaler = (*ValidatorIndex)(nil)
// BuilderIndexFlag marks a ValidatorIndex as a BuilderIndex when the bit is set.
//
// Spec v1.6.1: BUILDER_INDEX_FLAG.
const BuilderIndexFlag uint64 = 1 << 40
// ValidatorIndex in eth2.
type ValidatorIndex uint64
// IsBuilderIndex returns true when the BuilderIndex flag is set on the validator index.
//
// <spec fn="is_builder_index" fork="gloas" hash="2fbd46e9">
// def is_builder_index(validator_index: ValidatorIndex) -> bool:
// return (validator_index & BUILDER_INDEX_FLAG) != 0
// </spec>
func (v ValidatorIndex) IsBuilderIndex() bool {
return uint64(v)&BuilderIndexFlag != 0
}
// ToBuilderIndex strips the builder flag from a validator index.
//
// <spec fn="convert_validator_index_to_builder_index" fork="gloas" hash="2fea5b47">
// def convert_validator_index_to_builder_index(validator_index: ValidatorIndex) -> BuilderIndex:
// return BuilderIndex(validator_index & ~BUILDER_INDEX_FLAG)
// </spec>
func (v ValidatorIndex) ToBuilderIndex() BuilderIndex {
return BuilderIndex(uint64(v) & ^BuilderIndexFlag)
}
// Div divides validator index by x.
// This method panics if dividing by zero!
func (v ValidatorIndex) Div(x uint64) ValidatorIndex {

View File

@@ -33,32 +33,3 @@ func TestValidatorIndex_Casting(t *testing.T) {
}
})
}
func TestValidatorIndex_BuilderIndexFlagConversions(t *testing.T) {
base := uint64(42)
unflagged := ValidatorIndex(base)
if unflagged.IsBuilderIndex() {
t.Fatalf("expected unflagged validator index to not be a builder index")
}
if got, want := unflagged.ToBuilderIndex(), BuilderIndex(base); got != want {
t.Fatalf("unexpected builder index: got %d want %d", got, want)
}
flagged := ValidatorIndex(base | BuilderIndexFlag)
if !flagged.IsBuilderIndex() {
t.Fatalf("expected flagged validator index to be a builder index")
}
if got, want := flagged.ToBuilderIndex(), BuilderIndex(base); got != want {
t.Fatalf("unexpected builder index: got %d want %d", got, want)
}
builder := BuilderIndex(base)
roundTrip := builder.ToValidatorIndex()
if !roundTrip.IsBuilderIndex() {
t.Fatalf("expected round-tripped validator index to be a builder index")
}
if got, want := roundTrip.ToBuilderIndex(), builder; got != want {
t.Fatalf("unexpected round-trip builder index: got %d want %d", got, want)
}
}

View File

@@ -206,7 +206,6 @@ go_test(
"gloas__operations__execution_payload_test.go",
"gloas__operations__payload_attestation_test.go",
"gloas__operations__proposer_slashing_test.go",
"gloas__operations__withdrawals_test.go",
"gloas__sanity__slots_test.go",
"gloas__ssz_static__ssz_static_test.go",
"phase0__epoch_processing__effective_balance_updates_test.go",

View File

@@ -1,11 +0,0 @@
package mainnet
import (
"testing"
"github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations"
)
func TestMainnet_Gloas_Operations_Withdrawals(t *testing.T) {
operations.RunWithdrawalsTest(t, "mainnet")
}

View File

@@ -212,7 +212,6 @@ go_test(
"gloas__operations__execution_payload_test.go",
"gloas__operations__payload_attestation_test.go",
"gloas__operations__proposer_slashing_test.go",
"gloas__operations__withdrawals_test.go",
"gloas__sanity__slots_test.go",
"gloas__ssz_static__ssz_static_test.go",
"phase0__epoch_processing__effective_balance_updates_test.go",

View File

@@ -1,11 +0,0 @@
package minimal
import (
"testing"
"github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations"
)
func TestMinimal_Gloas_Operations_Withdrawals(t *testing.T) {
operations.RunWithdrawalsTest(t, "minimal")
}

View File

@@ -10,7 +10,6 @@ go_library(
"helpers.go",
"payload_attestation.go",
"proposer_slashing.go",
"withdrawals.go",
],
importpath = "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations",
visibility = ["//visibility:public"],

View File

@@ -1,48 +0,0 @@
package operations
import (
"context"
"path"
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/testing/require"
common "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/common/operations"
"github.com/OffchainLabs/prysm/v7/testing/spectest/utils"
)
func emptyBlockGloas() (interfaces.SignedBeaconBlock, error) {
b := &ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Body: &ethpb.BeaconBlockBodyGloas{},
},
}
return blocks.NewSignedBeaconBlock(b)
}
func RunWithdrawalsTest(t *testing.T, config string) {
require.NoError(t, utils.SetConfig(t, config))
testFolders, testsFolderPath := utils.TestFolders(t, config, version.String(version.Gloas), "operations/withdrawals/pyspec_tests")
if len(testFolders) == 0 {
t.Fatalf("No test folders found for %s/%s/%s", config, version.String(version.Gloas), "operations/withdrawals/pyspec_tests")
}
for _, folder := range testFolders {
t.Run(folder.Name(), func(t *testing.T) {
folderPath := path.Join(testsFolderPath, folder.Name())
blk, err := emptyBlockGloas()
require.NoError(t, err)
common.RunBlockOperationTest(t, folderPath, blk, sszToState, func(_ context.Context, s state.BeaconState, _ interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) {
if err := gloas.ProcessWithdrawals(s); err != nil {
return nil, err
}
return s, nil
})
})
}
}