From b0c844f234a3638f4703eb6e275725c55fbf015c Mon Sep 17 00:00:00 2001 From: terence tsao Date: Thu, 4 Sep 2025 09:20:15 -0700 Subject: [PATCH] Implement process pending payments --- beacon-chain/core/gloas/BUILD.bazel | 16 +++ beacon-chain/core/gloas/pending_payment.go | 90 ++++++++++++++ .../core/gloas/pending_payment_test.go | 110 ++++++++++++++++++ beacon-chain/state/BUILD.bazel | 1 + beacon-chain/state/interfaces.go | 3 + beacon-chain/state/interfaces_gloas.go | 20 ++++ beacon-chain/state/state-native/BUILD.bazel | 3 +- .../{gloas.go => getters_gloas.go} | 17 +++ .../state/state-native/getters_state.go | 10 ++ .../state/state-native/setters_gloas.go | 73 ++++++++++++ .../state/state-native/setters_gloas_test.go | 92 +++++++++++++++ config/params/config.go | 4 + config/params/mainnet_config.go | 5 + testing/spectest/mainnet/BUILD.bazel | 2 + ...__process_builder_pending_payments_test.go | 11 ++ testing/spectest/minimal/BUILD.bazel | 2 + ...__process_builder_pending_payments_test.go | 11 ++ .../shared/gloas/epoch_processing/BUILD.bazel | 26 +++++ .../builder_pending_payments.go | 28 +++++ .../shared/gloas/epoch_processing/helpers.go | 78 +++++++++++++ 20 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 beacon-chain/core/gloas/BUILD.bazel create mode 100644 beacon-chain/core/gloas/pending_payment.go create mode 100644 beacon-chain/core/gloas/pending_payment_test.go create mode 100644 beacon-chain/state/interfaces_gloas.go rename beacon-chain/state/state-native/{gloas.go => getters_gloas.go} (78%) create mode 100644 beacon-chain/state/state-native/setters_gloas.go create mode 100644 beacon-chain/state/state-native/setters_gloas_test.go create mode 100644 testing/spectest/mainnet/gloas__epoch_processing__process_builder_pending_payments_test.go create mode 100644 testing/spectest/minimal/gloas__epoch_processing__process_builder_pending_payments_test.go create mode 100644 testing/spectest/shared/gloas/epoch_processing/BUILD.bazel create mode 100644 testing/spectest/shared/gloas/epoch_processing/builder_pending_payments.go create mode 100644 testing/spectest/shared/gloas/epoch_processing/helpers.go diff --git a/beacon-chain/core/gloas/BUILD.bazel b/beacon-chain/core/gloas/BUILD.bazel new file mode 100644 index 0000000000..a8487e8ec0 --- /dev/null +++ b/beacon-chain/core/gloas/BUILD.bazel @@ -0,0 +1,16 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["pending_payment.go"], + importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas", + visibility = ["//visibility:public"], + deps = [ + "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/state:go_default_library", + "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", + "//runtime/version:go_default_library", + "@com_github_pkg_errors//:go_default_library", + ], +) diff --git a/beacon-chain/core/gloas/pending_payment.go b/beacon-chain/core/gloas/pending_payment.go new file mode 100644 index 0000000000..b7d07b2b5e --- /dev/null +++ b/beacon-chain/core/gloas/pending_payment.go @@ -0,0 +1,90 @@ +package gloas + +import ( + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers" + "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" +) + +// ProcessBuilderPendingPayments processes the builder pending payments from the previous epoch. +// Spec v1.6.1 (pseudocode): +// process_builder_pending_payments(state: BeaconState) -> None: +// +// quorum = get_builder_payment_quorum_threshold(state) +// for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]: +// if payment.weight >= quorum: +// exit_epoch = compute_exit_epoch_and_update_churn(state, payment.withdrawal.amount) +// payment.withdrawal.withdrawable_epoch = +// Epoch(exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +// state.builder_pending_withdrawals.append(payment.withdrawal) +// state.builder_pending_payments = +// state.builder_pending_payments[SLOTS_PER_EPOCH:] +// + [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)] +func ProcessBuilderPendingPayments(state state.BeaconState) error { + quorum, err := builderQuorumThreshold(state) + if err != nil { + return errors.Wrap(err, "could not compute builder payment quorum threshold") + } + + payments, err := state.BuilderPendingPayments() + if err != nil { + return errors.Wrap(err, "could not get builder pending payments") + } + + cfg := params.BeaconConfig() + slotsPerEpoch := uint64(cfg.SlotsPerEpoch) + minWithdrawDelay := uint64(cfg.MinValidatorWithdrawabilityDelay) + for i := uint64(0); i < slotsPerEpoch; i++ { + payment := payments[i] + if quorum > payment.Weight { + continue + } + + amount := payment.Withdrawal.Amount + exitEpoch, err := state.ExitEpochAndUpdateChurn(amount) + if err != nil { + return errors.Wrapf(err, "could not compute exit epoch for payment %d", i) + } + + withdrawableEpoch, err := exitEpoch.SafeAdd(minWithdrawDelay) + if err != nil { + return errors.Wrapf(err, "could not compute withdrawable epoch for payment %d", i) + } + payment.Withdrawal.WithdrawableEpoch = withdrawableEpoch + + if err := state.AppendBuilderPendingWithdrawal(payment.Withdrawal); err != nil { + return errors.Wrapf(err, "could not append builder pending withdrawal %d", i) + } + } + + if err := state.RotateBuilderPendingPayments(); err != nil { + return errors.Wrap(err, "could not rotate builder pending payments") + } + + return nil +} + +// builderQuorumThreshold calculates the quorum threshold for builder payments. +// Spec v1.6.1 (pseudocode): +// def get_builder_payment_quorum_threshold(state: BeaconState) -> uint64: +// +// per_slot_balance = get_total_active_balance(state) // SLOTS_PER_EPOCH +// quorum = per_slot_balance * BUILDER_PAYMENT_THRESHOLD_NUMERATOR +// return uint64(quorum // BUILDER_PAYMENT_THRESHOLD_DENOMINATOR) +func builderQuorumThreshold(state state.BeaconState) (primitives.Gwei, error) { + activeBalance, err := helpers.TotalActiveBalance(state) + if err != nil { + return 0, errors.Wrap(err, "could not get total active balance") + } + + cfg := params.BeaconConfig() + slotsPerEpoch := uint64(cfg.SlotsPerEpoch) + numerator := cfg.BuilderPaymentThresholdNumerator + denominator := cfg.BuilderPaymentThresholdDenominator + + activeBalancePerSlot := activeBalance / slotsPerEpoch + quorum := (activeBalancePerSlot * numerator) / denominator + return primitives.Gwei(quorum), nil +} diff --git a/beacon-chain/core/gloas/pending_payment_test.go b/beacon-chain/core/gloas/pending_payment_test.go new file mode 100644 index 0000000000..a59674e93e --- /dev/null +++ b/beacon-chain/core/gloas/pending_payment_test.go @@ -0,0 +1,110 @@ +package gloas + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestBuilderQuorumThreshold(t *testing.T) { + helpers.ClearCache() + cfg := params.BeaconConfig() + + validators := []*ethpb.Validator{ + {EffectiveBalance: cfg.MaxEffectiveBalance, ActivationEpoch: 0, ExitEpoch: 1}, + {EffectiveBalance: cfg.MaxEffectiveBalance, ActivationEpoch: 0, ExitEpoch: 1}, + } + st, err := state_native.InitializeFromProtoUnsafeGloas(ðpb.BeaconStateGloas{Validators: validators}) + require.NoError(t, err) + + got, err := builderQuorumThreshold(st) + require.NoError(t, err) + + total := uint64(len(validators)) * cfg.MaxEffectiveBalance + perSlot := total / uint64(cfg.SlotsPerEpoch) + want := (perSlot * cfg.BuilderPaymentThresholdNumerator) / cfg.BuilderPaymentThresholdDenominator + require.Equal(t, primitives.Gwei(want), got) +} + +func TestProcessBuilderPendingPayments(t *testing.T) { + helpers.ClearCache() + cfg := params.BeaconConfig() + + validators := []*ethpb.Validator{ + {EffectiveBalance: cfg.MaxEffectiveBalance, ActivationEpoch: 0, ExitEpoch: 1}, + {EffectiveBalance: cfg.MaxEffectiveBalance, ActivationEpoch: 0, ExitEpoch: 1}, + } + pbSt, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{Validators: validators}) + require.NoError(t, err) + + total := uint64(len(validators)) * cfg.MaxEffectiveBalance + perSlot := total / uint64(cfg.SlotsPerEpoch) + quorum := (perSlot * cfg.BuilderPaymentThresholdNumerator) / cfg.BuilderPaymentThresholdDenominator + + slotsPerEpoch := int(cfg.SlotsPerEpoch) + payments := make([]*ethpb.BuilderPendingPayment, 2*slotsPerEpoch) + for i := range payments { + payments[i] = ðpb.BuilderPendingPayment{ + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + }, + } + } + payments[0].Weight = primitives.Gwei(quorum + 1) + payments[0].Withdrawal.Amount = 1 + + st := &testProcessState{ + BeaconState: pbSt, + payments: payments, + exitEpoch: 7, + } + rotatedHead := st.payments[slotsPerEpoch] + + require.NoError(t, ProcessBuilderPendingPayments(st)) + require.Equal(t, 1, len(st.withdrawals)) + + wantEpoch, err := st.exitEpoch.SafeAdd(uint64(cfg.MinValidatorWithdrawabilityDelay)) + require.NoError(t, err) + require.Equal(t, wantEpoch, st.withdrawals[0].WithdrawableEpoch) + require.Equal(t, rotatedHead, st.payments[0]) +} + +type testProcessState struct { + state.BeaconState + payments []*ethpb.BuilderPendingPayment + withdrawals []*ethpb.BuilderPendingWithdrawal + exitEpoch primitives.Epoch +} + +func (t *testProcessState) BuilderPendingPayments() ([]*ethpb.BuilderPendingPayment, error) { + return t.payments, nil +} + +func (t *testProcessState) AppendBuilderPendingWithdrawal(withdrawal *ethpb.BuilderPendingWithdrawal) error { + t.withdrawals = append(t.withdrawals, withdrawal) + return nil +} + +func (t *testProcessState) RotateBuilderPendingPayments() error { + slotsPerEpoch := int(params.BeaconConfig().SlotsPerEpoch) + rotated := append([]*ethpb.BuilderPendingPayment{}, t.payments[slotsPerEpoch:]...) + for i := 0; i < slotsPerEpoch; i++ { + rotated = append(rotated, ðpb.BuilderPendingPayment{ + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + }, + }) + } + t.payments = rotated + return nil +} + +func (t *testProcessState) ExitEpochAndUpdateChurn(_ primitives.Gwei) (primitives.Epoch, error) { + return t.exitEpoch, nil +} diff --git a/beacon-chain/state/BUILD.bazel b/beacon-chain/state/BUILD.bazel index af0a4bc5ae..82b53b59b8 100644 --- a/beacon-chain/state/BUILD.bazel +++ b/beacon-chain/state/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "error.go", "interfaces.go", + "interfaces_gloas.go", "prometheus.go", ], importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/state", diff --git a/beacon-chain/state/interfaces.go b/beacon-chain/state/interfaces.go index c884a92d50..38b6785e68 100644 --- a/beacon-chain/state/interfaces.go +++ b/beacon-chain/state/interfaces.go @@ -63,6 +63,7 @@ type ReadOnlyBeaconState interface { ReadOnlyDeposits ReadOnlyConsolidations ReadOnlyProposerLookahead + ReadOnlyBuilderPendingPayments ToProtoUnsafe() any ToProto() any GenesisTime() time.Time @@ -98,6 +99,8 @@ type WriteOnlyBeaconState interface { WriteOnlyWithdrawals WriteOnlyDeposits WriteOnlyProposerLookahead + WriteOnlyBuilderPendingPayments + WriteOnlyBuilderPendingWithdrawals SetGenesisTime(val time.Time) error SetGenesisValidatorsRoot(val []byte) error SetSlot(val primitives.Slot) error diff --git a/beacon-chain/state/interfaces_gloas.go b/beacon-chain/state/interfaces_gloas.go new file mode 100644 index 0000000000..7bebb0e2f4 --- /dev/null +++ b/beacon-chain/state/interfaces_gloas.go @@ -0,0 +1,20 @@ +package state + +import ( + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +// ReadOnlyBuilderPendingPayments defines methods for reading builder pending payments from the beacon state. +type ReadOnlyBuilderPendingPayments interface { + BuilderPendingPayments() ([]*ethpb.BuilderPendingPayment, error) +} + +// WriteOnlyBuilderPendingPayments defines methods for writing builder pending payments to the beacon state. +type WriteOnlyBuilderPendingPayments interface { + RotateBuilderPendingPayments() error +} + +// WriteOnlyBuilderPendingWithdrawals defines methods for writing builder pending withdrawals to the beacon state. +type WriteOnlyBuilderPendingWithdrawals interface { + AppendBuilderPendingWithdrawal(*ethpb.BuilderPendingWithdrawal) error +} diff --git a/beacon-chain/state/state-native/BUILD.bazel b/beacon-chain/state/state-native/BUILD.bazel index 597d3e6496..6b3352087a 100644 --- a/beacon-chain/state/state-native/BUILD.bazel +++ b/beacon-chain/state/state-native/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "getters_deposits.go", "getters_eth1.go", "getters_exit.go", + "getters_gloas.go", "getters_misc.go", "getters_participation.go", "getters_payload_header.go", @@ -23,7 +24,6 @@ go_library( "getters_sync_committee.go", "getters_validator.go", "getters_withdrawal.go", - "gloas.go", "hasher.go", "multi_value_slices.go", "proofs.go", @@ -36,6 +36,7 @@ go_library( "setters_deposit_requests.go", "setters_deposits.go", "setters_eth1.go", + "setters_gloas.go", "setters_misc.go", "setters_participation.go", "setters_payload_header.go", diff --git a/beacon-chain/state/state-native/gloas.go b/beacon-chain/state/state-native/getters_gloas.go similarity index 78% rename from beacon-chain/state/state-native/gloas.go rename to beacon-chain/state/state-native/getters_gloas.go index b438de3dad..0b20595bb6 100644 --- a/beacon-chain/state/state-native/gloas.go +++ b/beacon-chain/state/state-native/getters_gloas.go @@ -17,6 +17,14 @@ func (b *BeaconState) executionPayloadAvailabilityVal() []byte { return availability } +// BuilderPendingPayments returns the builder pending payments in the beacon state. +func (b *BeaconState) BuilderPendingPayments() ([]*ethpb.BuilderPendingPayment, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + return b.builderPendingPaymentsVal(), nil +} + // builderPendingPaymentsVal returns a copy of the builder pending payments. // This assumes that a lock is already held on BeaconState. func (b *BeaconState) builderPendingPaymentsVal() []*ethpb.BuilderPendingPayment { @@ -47,6 +55,15 @@ func (b *BeaconState) builderPendingWithdrawalsVal() []*ethpb.BuilderPendingWith return withdrawals } +// executionPayloadHeaderGloas returns a copy of the beacon state execution payload header for Gloas. +// This assumes that a lock is already held on BeaconState. +func (b *BeaconState) executionPayloadHeaderGloas() *ethpb.ExecutionPayloadBid { + if b.latestExecutionPayloadBid == nil { + return nil + } + return b.latestExecutionPayloadBid.Copy() +} + // latestBlockHashVal returns a copy of the latest block hash. // This assumes that a lock is already held on BeaconState. func (b *BeaconState) latestBlockHashVal() []byte { diff --git a/beacon-chain/state/state-native/getters_state.go b/beacon-chain/state/state-native/getters_state.go index a6a3529f0c..2c2221dbd0 100644 --- a/beacon-chain/state/state-native/getters_state.go +++ b/beacon-chain/state/state-native/getters_state.go @@ -721,3 +721,13 @@ func ProtobufBeaconStateFulu(s any) (*ethpb.BeaconStateFulu, error) { } return pbState, nil } + +// ProtobufBeaconStateGloas transforms an input into beacon state Gloas in the form of protobuf. +// Error is returned if the input is not type protobuf beacon state. +func ProtobufBeaconStateGloas(s any) (*ethpb.BeaconStateGloas, error) { + pbState, ok := s.(*ethpb.BeaconStateGloas) + if !ok { + return nil, errors.New("input is not type pb.BeaconStateGloas") + } + return pbState, nil +} diff --git a/beacon-chain/state/state-native/setters_gloas.go b/beacon-chain/state/state-native/setters_gloas.go new file mode 100644 index 0000000000..0110c137dc --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas.go @@ -0,0 +1,73 @@ +package state_native + +import ( + "errors" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/stateutil" + "github.com/OffchainLabs/prysm/v7/config/params" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +// RotateBuilderPendingPayments rotates the queue by dropping slots per epoch payments from the +// front and appending slots per epoch empty payments to the end. +// This implements: state.builder_pending_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:] + [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)] +func (b *BeaconState) RotateBuilderPendingPayments() error { + b.lock.Lock() + defer b.lock.Unlock() + + oldPayments := b.builderPendingPayments + slotsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch) + newPayments := make([]*ethpb.BuilderPendingPayment, 2*slotsPerEpoch) + + copy(newPayments, oldPayments[slotsPerEpoch:]) + + for i := uint64(len(oldPayments)) - slotsPerEpoch; i < uint64(len(newPayments)); i++ { + newPayments[i] = emptyPayment() + } + + if b.sharedFieldReferences[types.BuilderPendingPayments].Refs() > 1 { + b.sharedFieldReferences[types.BuilderPendingPayments].MinusRef() + b.sharedFieldReferences[types.BuilderPendingPayments] = stateutil.NewRef(1) + } + + b.builderPendingPayments = newPayments + b.markFieldAsDirty(types.BuilderPendingPayments) + b.rebuildTrie[types.BuilderPendingPayments] = true + return nil +} + +// AppendBuilderPendingWithdrawal appends a builder pending withdrawal to the beacon state. +// If the withdrawals slice is shared, it copies the slice first to preserve references. +func (b *BeaconState) AppendBuilderPendingWithdrawal(withdrawal *ethpb.BuilderPendingWithdrawal) error { + if withdrawal == nil { + return errors.New("cannot append nil builder pending withdrawal") + } + + b.lock.Lock() + defer b.lock.Unlock() + + withdrawals := b.builderPendingWithdrawals + if b.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs() > 1 { + withdrawals = make([]*ethpb.BuilderPendingWithdrawal, len(b.builderPendingWithdrawals), len(b.builderPendingWithdrawals)+1) + copy(withdrawals, b.builderPendingWithdrawals) + b.sharedFieldReferences[types.BuilderPendingWithdrawals].MinusRef() + b.sharedFieldReferences[types.BuilderPendingWithdrawals] = stateutil.NewRef(1) + } + + b.builderPendingWithdrawals = append(withdrawals, withdrawal) + b.markFieldAsDirty(types.BuilderPendingWithdrawals) + return nil +} + +func emptyPayment() *ethpb.BuilderPendingPayment { + return ðpb.BuilderPendingPayment{ + Weight: 0, + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + Amount: 0, + BuilderIndex: 0, + WithdrawableEpoch: 0, + }, + } +} diff --git a/beacon-chain/state/state-native/setters_gloas_test.go b/beacon-chain/state/state-native/setters_gloas_test.go new file mode 100644 index 0000000000..d6dcc88440 --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas_test.go @@ -0,0 +1,92 @@ +package state_native + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/stateutil" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestRotateBuilderPendingPayments(t *testing.T) { + totalPayments := 2 * params.BeaconConfig().SlotsPerEpoch + payments := make([]*ethpb.BuilderPendingPayment, totalPayments) + for i := range payments { + idx := uint64(i) + payments[i] = ðpb.BuilderPendingPayment{ + Weight: primitives.Gwei(idx * 100e9), + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + Amount: primitives.Gwei(idx * 1e9), + BuilderIndex: primitives.ValidatorIndex(idx + 100), + WithdrawableEpoch: primitives.Epoch(idx + 1000), + }, + } + } + + statePb, err := InitializeFromProtoUnsafeGloas(ðpb.BeaconStateGloas{ + BuilderPendingPayments: payments, + }) + require.NoError(t, err) + st, ok := statePb.(*BeaconState) + require.Equal(t, true, ok) + st.sharedFieldReferences[types.BuilderPendingPayments] = stateutil.NewRef(1) + + oldPayments, err := st.BuilderPendingPayments() + require.NoError(t, err) + require.NoError(t, st.RotateBuilderPendingPayments()) + + newPayments, err := st.BuilderPendingPayments() + require.NoError(t, err) + slotsPerEpoch := int(params.BeaconConfig().SlotsPerEpoch) + for i := 0; i < slotsPerEpoch; i++ { + require.DeepEqual(t, oldPayments[slotsPerEpoch+i], newPayments[i]) + } + + for i := slotsPerEpoch; i < 2*slotsPerEpoch; i++ { + payment := newPayments[i] + require.Equal(t, primitives.Gwei(0), payment.Weight) + require.Equal(t, 20, len(payment.Withdrawal.FeeRecipient)) + require.Equal(t, primitives.Gwei(0), payment.Withdrawal.Amount) + require.Equal(t, primitives.ValidatorIndex(0), payment.Withdrawal.BuilderIndex) + require.Equal(t, primitives.Epoch(0), payment.Withdrawal.WithdrawableEpoch) + } +} + +func TestAppendBuilderPendingWithdrawal_CopyOnWrite(t *testing.T) { + wd := ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + Amount: 1, + BuilderIndex: 2, + WithdrawableEpoch: 3, + } + statePb, err := InitializeFromProtoUnsafeGloas(ðpb.BeaconStateGloas{ + BuilderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{wd}, + }) + require.NoError(t, err) + + st, ok := statePb.(*BeaconState) + require.Equal(t, true, ok) + + copied := st.Copy().(*BeaconState) + require.Equal(t, uint(2), st.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs()) + + appended := ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + Amount: 4, + BuilderIndex: 5, + WithdrawableEpoch: 6, + } + require.NoError(t, copied.AppendBuilderPendingWithdrawal(appended)) + + require.Equal(t, 1, len(st.builderPendingWithdrawals)) + require.Equal(t, 2, len(copied.builderPendingWithdrawals)) + require.DeepEqual(t, wd, copied.builderPendingWithdrawals[0]) + require.DeepEqual(t, appended, copied.builderPendingWithdrawals[1]) + require.DeepEqual(t, wd, st.builderPendingWithdrawals[0]) + require.Equal(t, uint(1), st.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs()) + require.Equal(t, uint(1), copied.sharedFieldReferences[types.BuilderPendingWithdrawals].Refs()) +} diff --git a/config/params/config.go b/config/params/config.go index 184aaac49a..de72a1e7db 100644 --- a/config/params/config.go +++ b/config/params/config.go @@ -290,6 +290,10 @@ type BeaconChainConfig struct { ValidatorCustodyRequirement uint64 `yaml:"VALIDATOR_CUSTODY_REQUIREMENT" spec:"true"` // ValidatorCustodyRequirement is the minimum number of custody groups an honest node with validators attached custodies and serves samples from BalancePerAdditionalCustodyGroup uint64 `yaml:"BALANCE_PER_ADDITIONAL_CUSTODY_GROUP" spec:"true"` // BalancePerAdditionalCustodyGroup is the balance increment corresponding to one additional group to custody. + // Values introduced in Gloas upgrade + BuilderPaymentThresholdNumerator uint64 `yaml:"BUILDER_PAYMENT_THRESHOLD_NUMERATOR" spec:"true"` // BuilderPaymentThresholdNumerator is the numerator for builder payment quorum threshold calculation. + BuilderPaymentThresholdDenominator uint64 `yaml:"BUILDER_PAYMENT_THRESHOLD_DENOMINATOR" spec:"true"` // BuilderPaymentThresholdDenominator is the denominator for builder payment quorum threshold calculation. + // Networking Specific Parameters MaxPayloadSize uint64 `yaml:"MAX_PAYLOAD_SIZE" spec:"true"` // MAX_PAYLOAD_SIZE is the maximum allowed size of uncompressed payload in gossip messages and rpc chunks. AttestationSubnetCount uint64 `yaml:"ATTESTATION_SUBNET_COUNT" spec:"true"` // AttestationSubnetCount is the number of attestation subnets used in the gossipsub protocol. diff --git a/config/params/mainnet_config.go b/config/params/mainnet_config.go index afd8173ae4..9c8442b367 100644 --- a/config/params/mainnet_config.go +++ b/config/params/mainnet_config.go @@ -328,6 +328,11 @@ var mainnetBeaconConfig = &BeaconChainConfig{ MinEpochsForDataColumnSidecarsRequest: 4096, ValidatorCustodyRequirement: 8, BalancePerAdditionalCustodyGroup: 32_000_000_000, + + // Values related to gloas + BuilderPaymentThresholdNumerator: 6, + BuilderPaymentThresholdDenominator: 10, + // Values related to networking parameters. MaxPayloadSize: 10 * 1 << 20, // 10 MiB AttestationSubnetCount: 64, diff --git a/testing/spectest/mainnet/BUILD.bazel b/testing/spectest/mainnet/BUILD.bazel index 75d3340e6a..4a6f8ea7cf 100644 --- a/testing/spectest/mainnet/BUILD.bazel +++ b/testing/spectest/mainnet/BUILD.bazel @@ -200,6 +200,7 @@ go_test( "fulu__sanity__blocks_test.go", "fulu__sanity__slots_test.go", "fulu__ssz_static__ssz_static_test.go", + "gloas__epoch_processing__process_builder_pending_payments_test.go", "gloas__ssz_static__ssz_static_test.go", "phase0__epoch_processing__effective_balance_updates_test.go", "phase0__epoch_processing__epoch_processing_test.go", @@ -278,6 +279,7 @@ go_test( "//testing/spectest/shared/fulu/rewards:go_default_library", "//testing/spectest/shared/fulu/sanity:go_default_library", "//testing/spectest/shared/fulu/ssz_static:go_default_library", + "//testing/spectest/shared/gloas/epoch_processing:go_default_library", "//testing/spectest/shared/gloas/ssz_static:go_default_library", "//testing/spectest/shared/phase0/epoch_processing:go_default_library", "//testing/spectest/shared/phase0/finality:go_default_library", diff --git a/testing/spectest/mainnet/gloas__epoch_processing__process_builder_pending_payments_test.go b/testing/spectest/mainnet/gloas__epoch_processing__process_builder_pending_payments_test.go new file mode 100644 index 0000000000..7679853ec9 --- /dev/null +++ b/testing/spectest/mainnet/gloas__epoch_processing__process_builder_pending_payments_test.go @@ -0,0 +1,11 @@ +package mainnet + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/epoch_processing" +) + +func TestMainnet_Gloas_EpochProcessing_ProcessBuilderPendingPayments(t *testing.T) { + epoch_processing.RunBuilderPendingPaymentsTests(t, "mainnet") +} diff --git a/testing/spectest/minimal/BUILD.bazel b/testing/spectest/minimal/BUILD.bazel index 5f80e7f82d..ce29a0a34a 100644 --- a/testing/spectest/minimal/BUILD.bazel +++ b/testing/spectest/minimal/BUILD.bazel @@ -206,6 +206,7 @@ go_test( "fulu__sanity__blocks_test.go", "fulu__sanity__slots_test.go", "fulu__ssz_static__ssz_static_test.go", + "gloas__epoch_processing__process_builder_pending_payments_test.go", "gloas__ssz_static__ssz_static_test.go", "phase0__epoch_processing__effective_balance_updates_test.go", "phase0__epoch_processing__epoch_processing_test.go", @@ -288,6 +289,7 @@ go_test( "//testing/spectest/shared/fulu/rewards:go_default_library", "//testing/spectest/shared/fulu/sanity:go_default_library", "//testing/spectest/shared/fulu/ssz_static:go_default_library", + "//testing/spectest/shared/gloas/epoch_processing:go_default_library", "//testing/spectest/shared/gloas/ssz_static:go_default_library", "//testing/spectest/shared/phase0/epoch_processing:go_default_library", "//testing/spectest/shared/phase0/finality:go_default_library", diff --git a/testing/spectest/minimal/gloas__epoch_processing__process_builder_pending_payments_test.go b/testing/spectest/minimal/gloas__epoch_processing__process_builder_pending_payments_test.go new file mode 100644 index 0000000000..536afae389 --- /dev/null +++ b/testing/spectest/minimal/gloas__epoch_processing__process_builder_pending_payments_test.go @@ -0,0 +1,11 @@ +package minimal + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/epoch_processing" +) + +func TestMinimal_Gloas_EpochProcessing_ProcessBuilderPendingPayments(t *testing.T) { + epoch_processing.RunBuilderPendingPaymentsTests(t, "minimal") +} diff --git a/testing/spectest/shared/gloas/epoch_processing/BUILD.bazel b/testing/spectest/shared/gloas/epoch_processing/BUILD.bazel new file mode 100644 index 0000000000..0dfeaee6e7 --- /dev/null +++ b/testing/spectest/shared/gloas/epoch_processing/BUILD.bazel @@ -0,0 +1,26 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + testonly = True, + srcs = [ + "builder_pending_payments.go", + "helpers.go", + ], + importpath = "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/epoch_processing", + visibility = ["//visibility:public"], + deps = [ + "//beacon-chain/core/gloas:go_default_library", + "//beacon-chain/state:go_default_library", + "//beacon-chain/state/state-native:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/require:go_default_library", + "//testing/spectest/utils:go_default_library", + "//testing/util:go_default_library", + "@com_github_golang_snappy//:go_default_library", + "@com_github_google_go_cmp//cmp:go_default_library", + "@io_bazel_rules_go//go/tools/bazel:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", + "@org_golang_google_protobuf//testing/protocmp:go_default_library", + ], +) diff --git a/testing/spectest/shared/gloas/epoch_processing/builder_pending_payments.go b/testing/spectest/shared/gloas/epoch_processing/builder_pending_payments.go new file mode 100644 index 0000000000..ac08089a70 --- /dev/null +++ b/testing/spectest/shared/gloas/epoch_processing/builder_pending_payments.go @@ -0,0 +1,28 @@ +package epoch_processing + +import ( + "fmt" + "path" + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/testing/spectest/utils" +) + +func RunBuilderPendingPaymentsTests(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + testFolders, testsFolderPath := utils.TestFolders(t, config, "gloas", "epoch_processing/builder_pending_payments/pyspec_tests") + fmt.Println(testsFolderPath, testFolders) + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + folderPath := path.Join(testsFolderPath, folder.Name()) + RunEpochOperationTest(t, folderPath, processBuilderPendingPayments) + }) + } +} + +func processBuilderPendingPayments(t *testing.T, st state.BeaconState) (state.BeaconState, error) { + return st, gloas.ProcessBuilderPendingPayments(st) +} diff --git a/testing/spectest/shared/gloas/epoch_processing/helpers.go b/testing/spectest/shared/gloas/epoch_processing/helpers.go new file mode 100644 index 0000000000..cab3a46972 --- /dev/null +++ b/testing/spectest/shared/gloas/epoch_processing/helpers.go @@ -0,0 +1,78 @@ +package epoch_processing + +import ( + "os" + "path" + "strings" + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/testing/util" + "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/golang/snappy" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +type epochOperation func(*testing.T, state.BeaconState) (state.BeaconState, error) + +// RunEpochOperationTest takes in the prestate and processes it through the +// passed in epoch operation function and checks the post state with the expected post state. +func RunEpochOperationTest( + t *testing.T, + testFolderPath string, + operationFn epochOperation, +) { + preBeaconStateFile, err := util.BazelFileBytes(path.Join(testFolderPath, "pre.ssz_snappy")) + require.NoError(t, err) + preBeaconStateSSZ, err := snappy.Decode(nil /* dst */, preBeaconStateFile) + require.NoError(t, err, "Failed to decompress") + preBeaconStateBase := ðpb.BeaconStateGloas{} + if err := preBeaconStateBase.UnmarshalSSZ(preBeaconStateSSZ); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + preBeaconState, err := state_native.InitializeFromProtoGloas(preBeaconStateBase) + require.NoError(t, err) + + // If the post.ssz is not present, it means the test should fail on our end. + postSSZFilepath, err := bazel.Runfile(path.Join(testFolderPath, "post.ssz_snappy")) + postSSZExists := true + if err != nil && strings.Contains(err.Error(), "could not locate file") { + postSSZExists = false + } else if err != nil { + t.Fatal(err) + } + + beaconState, err := operationFn(t, preBeaconState) + if postSSZExists { + require.NoError(t, err) + + postBeaconStateFile, err := os.ReadFile(postSSZFilepath) // #nosec G304 + require.NoError(t, err) + postBeaconStateSSZ, err := snappy.Decode(nil /* dst */, postBeaconStateFile) + require.NoError(t, err, "Failed to decompress") + postBeaconState := ðpb.BeaconStateGloas{} + if err := postBeaconState.UnmarshalSSZ(postBeaconStateSSZ); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + + pbState, err := state_native.ProtobufBeaconStateGloas(beaconState.ToProtoUnsafe()) + require.NoError(t, err) + if !proto.Equal(pbState, postBeaconState) { + t.Log(cmp.Diff(postBeaconState, pbState, protocmp.Transform())) + t.Fatal("Post state does not match expected") + } + } else { + // Note: This doesn't test anything worthwhile. It essentially tests + // that *any* error has occurred, not any specific error. + if err == nil { + t.Fatal("Did not fail when expected") + } + t.Logf("Expected failure; failure reason = %v", err) + return + } +}