From 22aa87019dce1cead149442f8533e1d57033d782 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Sat, 3 Jan 2026 22:02:41 -0800 Subject: [PATCH] Implement process attestation --- beacon-chain/core/altair/BUILD.bazel | 1 + beacon-chain/core/altair/attestation.go | 47 ++- beacon-chain/core/altair/attestation_test.go | 120 +++++++- beacon-chain/core/blocks/attestation.go | 11 +- beacon-chain/core/blocks/attestation_test.go | 70 +++++ beacon-chain/core/gloas/BUILD.bazel | 31 ++ beacon-chain/core/gloas/attestation.go | 144 +++++++++ beacon-chain/core/gloas/attestation_test.go | 288 ++++++++++++++++++ beacon-chain/state/BUILD.bazel | 1 + beacon-chain/state/interfaces.go | 2 + beacon-chain/state/interfaces_gloas.go | 15 + beacon-chain/state/state-native/BUILD.bazel | 2 + .../state/state-native/getters_gloas.go | 42 +++ .../state/state-native/setters_gloas.go | 22 ++ proto/prysm/v1alpha1/cloners.go | 25 ++ testing/spectest/mainnet/BUILD.bazel | 2 + .../gloas__operations__attestation_test.go | 11 + testing/spectest/minimal/BUILD.bazel | 2 + .../gloas__operations__attestation_test.go | 11 + .../shared/gloas/operations/BUILD.bazel | 22 ++ .../shared/gloas/operations/attestation.go | 27 ++ .../shared/gloas/operations/helpers.go | 33 ++ .../shared/gloas/ssz_static/ssz_static.go | 41 ++- 23 files changed, 942 insertions(+), 28 deletions(-) create mode 100644 beacon-chain/core/gloas/BUILD.bazel create mode 100644 beacon-chain/core/gloas/attestation.go create mode 100644 beacon-chain/core/gloas/attestation_test.go create mode 100644 beacon-chain/state/interfaces_gloas.go create mode 100644 beacon-chain/state/state-native/getters_gloas.go create mode 100644 beacon-chain/state/state-native/setters_gloas.go create mode 100644 testing/spectest/mainnet/gloas__operations__attestation_test.go create mode 100644 testing/spectest/minimal/gloas__operations__attestation_test.go create mode 100644 testing/spectest/shared/gloas/operations/BUILD.bazel create mode 100644 testing/spectest/shared/gloas/operations/attestation.go create mode 100644 testing/spectest/shared/gloas/operations/helpers.go diff --git a/beacon-chain/core/altair/BUILD.bazel b/beacon-chain/core/altair/BUILD.bazel index 5b87a7efa0..c5975cb4b8 100644 --- a/beacon-chain/core/altair/BUILD.bazel +++ b/beacon-chain/core/altair/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//beacon-chain/core/blocks:go_default_library", "//beacon-chain/core/epoch:go_default_library", "//beacon-chain/core/epoch/precompute:go_default_library", + "//beacon-chain/core/gloas:go_default_library", "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/core/signing:go_default_library", "//beacon-chain/core/time:go_default_library", diff --git a/beacon-chain/core/altair/attestation.go b/beacon-chain/core/altair/attestation.go index ec2ac3d537..d4eaf6a2bc 100644 --- a/beacon-chain/core/altair/attestation.go +++ b/beacon-chain/core/altair/attestation.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/blocks" + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/time" "github.com/OffchainLabs/prysm/v7/beacon-chain/state" @@ -16,6 +17,7 @@ import ( "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/attestation" + "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/pkg/errors" ) @@ -75,7 +77,12 @@ func ProcessAttestationNoVerifySignature( return nil, err } - return SetParticipationAndRewardProposer(ctx, beaconState, att.GetData().Target.Epoch, indices, participatedFlags, totalBalance) + beaconState, err = gloas.UpdatePendingPaymentWeight(beaconState, att, indices, participatedFlags) + if err != nil { + return nil, err + } + + return SetParticipationAndRewardProposer(ctx, beaconState, att.GetData().Target.Epoch, indices, participatedFlags, totalBalance, att) } // SetParticipationAndRewardProposer retrieves and sets the epoch participation bits in state. Based on the epoch participation, it rewards @@ -105,7 +112,9 @@ func SetParticipationAndRewardProposer( beaconState state.BeaconState, targetEpoch primitives.Epoch, indices []uint64, - participatedFlags map[uint8]bool, totalBalance uint64) (state.BeaconState, error) { + participatedFlags map[uint8]bool, + totalBalance uint64, + att ethpb.Att) (state.BeaconState, error) { var proposerRewardNumerator uint64 currentEpoch := time.CurrentEpoch(beaconState) var stateErr error @@ -299,6 +308,40 @@ func AttestationParticipationFlagIndices(beaconState state.ReadOnlyBeaconState, participatedFlags[targetFlagIndex] = true } matchedSrcTgtHead := matchedHead && matchedSrcTgt + + // Spec v1.6.1 (pseudocode excerpt): + // + // # [New in Gloas:EIP7732] + // if is_attestation_same_slot(state, data): + // assert data.index == 0 + // payload_matches = True + // else: + // slot_index = data.slot % SLOTS_PER_HISTORICAL_ROOT + // payload_index = state.execution_payload_availability[slot_index] + // payload_matches = data.index == payload_index + // + // # [Modified in Gloas:EIP7732] + // is_matching_head = is_matching_target and head_root_matches and payload_matches + var matchingPayload bool + if beaconState.Version() >= version.Gloas { + ok, err := gloas.SameSlotAttestation(beaconState, [32]byte(data.BeaconBlockRoot), data.Slot) + if err != nil { + return nil, err + } + if ok { + if data.CommitteeIndex != 0 { + return nil, fmt.Errorf("committee index %d for same slot attestation must be 0", data.CommitteeIndex) + } + matchingPayload = true + } else { + executionPayloadAvail, err := beaconState.ExecutionPayloadAvailability(data.Slot) + if err != nil { + return nil, err + } + matchingPayload = executionPayloadAvail == uint64(data.CommitteeIndex) + } + matchedSrcTgtHead = matchedSrcTgtHead && matchingPayload + } if matchedSrcTgtHead && delay == cfg.MinAttestationInclusionDelay { participatedFlags[headFlagIndex] = true } diff --git a/beacon-chain/core/altair/attestation_test.go b/beacon-chain/core/altair/attestation_test.go index 8ceb34810f..68e4babc7f 100644 --- a/beacon-chain/core/altair/attestation_test.go +++ b/beacon-chain/core/altair/attestation_test.go @@ -1,7 +1,9 @@ package altair_test import ( + "bytes" "fmt" + "reflect" "testing" "github.com/OffchainLabs/go-bitfield" @@ -556,7 +558,7 @@ func TestSetParticipationAndRewardProposer(t *testing.T) { b, err := helpers.TotalActiveBalance(beaconState) require.NoError(t, err) - st, err := altair.SetParticipationAndRewardProposer(t.Context(), beaconState, test.epoch, test.indices, test.participatedFlags, b) + st, err := altair.SetParticipationAndRewardProposer(t.Context(), beaconState, test.epoch, test.indices, test.participatedFlags, b, ðpb.Attestation{}) require.NoError(t, err) i, err := helpers.BeaconProposerIndex(t.Context(), st) @@ -775,11 +777,67 @@ func TestAttestationParticipationFlagIndices(t *testing.T) { headFlagIndex: true, }, }, + { + name: "gloas same-slot committee index non-zero errors", + inputState: func() state.BeaconState { + stateSlot := primitives.Slot(5) + slot := primitives.Slot(3) + targetRoot := bytes.Repeat([]byte{0xAA}, 32) + headRoot := bytes.Repeat([]byte{0xBB}, 32) + prevRoot := bytes.Repeat([]byte{0xCC}, 32) + return buildGloasStateForFlags(t, stateSlot, slot, targetRoot, headRoot, prevRoot, 0, 0) + }(), + inputData: ðpb.AttestationData{ + Slot: 3, + CommitteeIndex: 1, // invalid for same-slot + BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32), + Source: ðpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)}, + Target: ðpb.Checkpoint{ + Epoch: 0, + Root: bytes.Repeat([]byte{0xAA}, 32), + }, + }, + inputDelay: 1, + participationIndices: nil, + }, + { + name: "gloas payload availability matches committee index", + inputState: func() state.BeaconState { + stateSlot := primitives.Slot(5) + slot := primitives.Slot(3) + targetRoot := bytes.Repeat([]byte{0xAA}, 32) + headRoot := bytes.Repeat([]byte{0xBB}, 32) + // Same prev root to make SameSlotAttestation false and use payload availability. + return buildGloasStateForFlags(t, stateSlot, slot, targetRoot, headRoot, headRoot, 1, slot) + }(), + inputData: ðpb.AttestationData{ + Slot: 3, + CommitteeIndex: 1, + BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32), + Source: ðpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)}, + Target: ðpb.Checkpoint{ + Epoch: 0, + Root: bytes.Repeat([]byte{0xAA}, 32), + }, + }, + inputDelay: 1, + participationIndices: map[uint8]bool{ + sourceFlagIndex: true, + targetFlagIndex: true, + headFlagIndex: true, + }, + }, } for _, test := range tests { flagIndices, err := altair.AttestationParticipationFlagIndices(test.inputState, test.inputData, test.inputDelay) + if test.participationIndices == nil { + require.ErrorContains(t, "committee index", err) + continue + } require.NoError(t, err) - require.DeepEqual(t, test.participationIndices, flagIndices) + if !reflect.DeepEqual(test.participationIndices, flagIndices) { + t.Fatalf("unexpected participation indices: got %v want %v", flagIndices, test.participationIndices) + } } } @@ -858,3 +916,61 @@ func TestMatchingStatus(t *testing.T) { require.Equal(t, test.matchedHead, head) } } + +func buildGloasStateForFlags(t *testing.T, stateSlot, slot primitives.Slot, targetRoot, headRoot, prevRoot []byte, availabilityBit uint8, availabilitySlot primitives.Slot) state.BeaconState { + t.Helper() + + cfg := params.BeaconConfig() + blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot) + blockRoots[0] = targetRoot + blockRoots[slot%cfg.SlotsPerHistoricalRoot] = headRoot + blockRoots[(slot-1)%cfg.SlotsPerHistoricalRoot] = prevRoot + + stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot) + for i := range stateRoots { + stateRoots[i] = make([]byte, fieldparams.RootLength) + } + randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector) + for i := range randaoMixes { + randaoMixes[i] = make([]byte, fieldparams.RootLength) + } + + execPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8) + idx := availabilitySlot % cfg.SlotsPerHistoricalRoot + byteIndex := idx / 8 + bitIndex := idx % 8 + if availabilityBit == 1 { + execPayloadAvailability[byteIndex] |= 1 << bitIndex + } + + checkpointRoot := bytes.Repeat([]byte{0xDD}, fieldparams.RootLength) + justified := ðpb.Checkpoint{Root: checkpointRoot} + + stProto := ðpb.BeaconStateGloas{ + Slot: stateSlot, + GenesisValidatorsRoot: bytes.Repeat([]byte{0x11}, fieldparams.RootLength), + BlockRoots: blockRoots, + StateRoots: stateRoots, + RandaoMixes: randaoMixes, + ExecutionPayloadAvailability: execPayloadAvailability, + CurrentJustifiedCheckpoint: justified, + PreviousJustifiedCheckpoint: justified, + Validators: []*ethpb.Validator{ + { + EffectiveBalance: cfg.MinActivationBalance, + WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x01}, 31)...), + }, + }, + Balances: []uint64{cfg.MinActivationBalance}, + BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2), + Fork: ðpb.Fork{ + CurrentVersion: bytes.Repeat([]byte{0x01}, 4), + PreviousVersion: bytes.Repeat([]byte{0x01}, 4), + Epoch: 0, + }, + } + + beaconState, err := state_native.InitializeFromProtoGloas(stProto) + require.NoError(t, err) + return beaconState +} diff --git a/beacon-chain/core/blocks/attestation.go b/beacon-chain/core/blocks/attestation.go index 858464603d..99e9e28d83 100644 --- a/beacon-chain/core/blocks/attestation.go +++ b/beacon-chain/core/blocks/attestation.go @@ -111,7 +111,16 @@ func VerifyAttestationNoVerifySignature( var indexedAtt ethpb.IndexedAtt if att.Version() >= version.Electra { - if att.GetData().CommitteeIndex != 0 { + // Spec v1.6.1 (pseudocode excerpt): + // + // # [Modified in Gloas:EIP7732] + // assert data.index < 2 + // + if beaconState.Version() >= version.Gloas { + if att.GetData().CommitteeIndex >= 2 { + return fmt.Errorf("incorrect committee index %d", att.GetData().CommitteeIndex) + } + } else if att.GetData().CommitteeIndex != 0 { return errors.New("committee index must be 0 post-Electra") } diff --git a/beacon-chain/core/blocks/attestation_test.go b/beacon-chain/core/blocks/attestation_test.go index 24a068ef91..55e9eee210 100644 --- a/beacon-chain/core/blocks/attestation_test.go +++ b/beacon-chain/core/blocks/attestation_test.go @@ -1,6 +1,7 @@ package blocks_test import ( + "bytes" "context" "testing" @@ -314,6 +315,75 @@ func TestVerifyAttestationNoVerifySignature_Electra(t *testing.T) { }) } +func TestVerifyAttestationNoVerifySignature_GloasCommitteeIndexLimit(t *testing.T) { + cfg := params.BeaconConfig() + stateSlot := cfg.MinAttestationInclusionDelay + 1 + + blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot) + for i := range blockRoots { + blockRoots[i] = make([]byte, fieldparams.RootLength) + } + stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot) + for i := range stateRoots { + stateRoots[i] = make([]byte, fieldparams.RootLength) + } + randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector) + for i := range randaoMixes { + randaoMixes[i] = make([]byte, fieldparams.RootLength) + } + + checkpointRoot := bytes.Repeat([]byte{0xAA}, fieldparams.RootLength) + justified := ðpb.Checkpoint{Epoch: 0, Root: checkpointRoot} + + gloasStateProto := ðpb.BeaconStateGloas{ + Slot: stateSlot, + GenesisValidatorsRoot: bytes.Repeat([]byte{0x11}, fieldparams.RootLength), + BlockRoots: blockRoots, + StateRoots: stateRoots, + RandaoMixes: randaoMixes, + ExecutionPayloadAvailability: make([]byte, cfg.SlotsPerHistoricalRoot/8), + CurrentJustifiedCheckpoint: justified, + PreviousJustifiedCheckpoint: justified, + Validators: []*ethpb.Validator{ + { + EffectiveBalance: cfg.MinActivationBalance, + WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x01}, 31)...), + }, + }, + Balances: []uint64{cfg.MinActivationBalance}, + BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2), + Fork: ðpb.Fork{ + CurrentVersion: bytes.Repeat([]byte{0x01}, 4), + PreviousVersion: bytes.Repeat([]byte{0x01}, 4), + Epoch: 0, + }, + } + + beaconState, err := state_native.InitializeFromProtoGloas(gloasStateProto) + require.NoError(t, err) + + committeeBits := bitfield.NewBitvector64() + committeeBits.SetBitAt(0, true) + aggBits := bitfield.NewBitlist(1) + aggBits.SetBitAt(0, true) + + att := ðpb.AttestationElectra{ + Data: ðpb.AttestationData{ + Slot: 0, + CommitteeIndex: 2, // invalid for Gloas (must be <2) + BeaconBlockRoot: blockRoots[0], + Source: justified, + Target: justified, + }, + AggregationBits: aggBits, + CommitteeBits: committeeBits, + Signature: bytes.Repeat([]byte{0x00}, fieldparams.BLSSignatureLength), + } + + err = blocks.VerifyAttestationNoVerifySignature(context.TODO(), beaconState, att) + assert.ErrorContains(t, "incorrect committee index 2", err) +} + func TestConvertToIndexed_OK(t *testing.T) { helpers.ClearCache() validators := make([]*ethpb.Validator, 2*params.BeaconConfig().SlotsPerEpoch) diff --git a/beacon-chain/core/gloas/BUILD.bazel b/beacon-chain/core/gloas/BUILD.bazel new file mode 100644 index 0000000000..4e43e28a1e --- /dev/null +++ b/beacon-chain/core/gloas/BUILD.bazel @@ -0,0 +1,31 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["attestation.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", + "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", + "//time/slots:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["attestation_test.go"], + embed = [":go_default_library"], + deps = [ + "//beacon-chain/state/state-native:go_default_library", + "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/require:go_default_library", + "//time/slots:go_default_library", + ], +) diff --git a/beacon-chain/core/gloas/attestation.go b/beacon-chain/core/gloas/attestation.go new file mode 100644 index 0000000000..311edd345f --- /dev/null +++ b/beacon-chain/core/gloas/attestation.go @@ -0,0 +1,144 @@ +package gloas + +import ( + "bytes" + + "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" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/runtime/version" + "github.com/OffchainLabs/prysm/v7/time/slots" +) + +// SameSlotAttestation checks if the attestation is for the same slot as the block root in the state. +// Spec v1.6.1 (pseudocode excerpt): +// +// is_attestation_same_slot(state, data): +// block_root_at_slot = get_block_root_at_slot(state, data.slot) +// block_root_at_prev_slot = get_block_root_at_slot(state, data.slot - 1) +// return block_root_at_slot == data.beacon_block_root and block_root_at_prev_slot != data.beacon_block_root +func SameSlotAttestation(state state.ReadOnlyBeaconState, blockRoot [32]byte, slot primitives.Slot) (bool, error) { + if slot == 0 { + return true, nil + } + + blockRootAtSlot, err := helpers.BlockRootAtSlot(state, slot) + if err != nil { + return false, err + } + matchingBlockRoot := bytes.Equal(blockRoot[:], blockRootAtSlot) + + blockRootAtPrevSlot, err := helpers.BlockRootAtSlot(state, slot-1) + if err != nil { + return false, err + } + matchingPrevBlockRoot := bytes.Equal(blockRoot[:], blockRootAtPrevSlot) + + return matchingBlockRoot && !matchingPrevBlockRoot, nil +} + +// UpdatePendingPaymentWeight updates the builder pending payment weight based on attestation participation. +// Spec v1.6.1 (pseudocode excerpt): +// +// if data.target.epoch == get_current_epoch(state): +// epoch_participation = state.current_epoch_participation +// payment = state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] +// else: +// epoch_participation = state.previous_epoch_participation +// payment = state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] +// +// for index in get_attesting_indices(state, attestation): +// will_set_new_flag = False +// for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): +// if flag_index in participation_flag_indices and not has_flag(epoch_participation[index], flag_index): +// epoch_participation[index] = add_flag(epoch_participation[index], flag_index) +// proposer_reward_numerator += get_base_reward(state, index) * weight +// will_set_new_flag = True +// if will_set_new_flag and is_attestation_same_slot(state, data) and payment.withdrawal.amount > 0: +// payment.weight += state.validators[index].effective_balance +// +// if current_epoch_target: +// state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] = payment +// else: +// state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] = payment +func UpdatePendingPaymentWeight(beaconState state.BeaconState, att ethpb.Att, indices []uint64, participatedFlags map[uint8]bool) (state.BeaconState, error) { + if beaconState.Version() < version.Gloas { + return beaconState, nil + } + + data := att.GetData() + epoch := slots.ToEpoch(beaconState.Slot()) + + isSameSlot, err := SameSlotAttestation(beaconState, [32]byte(data.BeaconBlockRoot), data.Slot) + if err != nil { + return nil, err + } + if !isSameSlot { + return beaconState, nil + } + + slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch + var ( + paymentSlot primitives.Slot + payment *ethpb.BuilderPendingPayment + epochParticipation []byte + ) + if data.Target.Epoch == epoch { + paymentSlot = slotsPerEpoch + (data.Slot % slotsPerEpoch) + payment, err = beaconState.BuilderPendingPayment(uint64(paymentSlot)) + if err != nil { + return nil, err + } + epochParticipation, err = beaconState.CurrentEpochParticipation() + if err != nil { + return nil, err + } + } else { + paymentSlot = data.Slot % slotsPerEpoch + payment, err = beaconState.BuilderPendingPayment(uint64(paymentSlot)) + if err != nil { + return nil, err + } + epochParticipation, err = beaconState.PreviousEpochParticipation() + if err != nil { + return nil, err + } + } + if payment.Withdrawal.Amount == 0 { + return beaconState, nil + } + + cfg := params.BeaconConfig() + flagIndices := []uint8{cfg.TimelySourceFlagIndex, cfg.TimelyTargetFlagIndex, cfg.TimelyHeadFlagIndex} + hasNewFlag := func(idx uint64) bool { + participation := epochParticipation[idx] + for _, f := range flagIndices { + if !participatedFlags[f] { + continue + } + if participation&(1<> bitIndex) & 1 + + return uint64(bit), 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..c32a7bcc4c --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas.go @@ -0,0 +1,22 @@ +package state_native + +import ( + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/runtime/version" +) + +// SetBuilderPendingPayment sets a builder pending payment at the specified slot. +func (b *BeaconState) SetBuilderPendingPayment(index uint64, payment *ethpb.BuilderPendingPayment) error { + if b.version < version.Gloas { + return errNotSupported("SetBuilderPendingPayment", b.version) + } + + b.lock.Lock() + defer b.lock.Unlock() + + b.builderPendingPayments[index] = ethpb.CopyBuilderPendingPayment(payment) + + b.markFieldAsDirty(types.BuilderPendingPayments) + return nil +} diff --git a/proto/prysm/v1alpha1/cloners.go b/proto/prysm/v1alpha1/cloners.go index a8754f0400..0d07d689ab 100644 --- a/proto/prysm/v1alpha1/cloners.go +++ b/proto/prysm/v1alpha1/cloners.go @@ -174,3 +174,28 @@ func copyBeaconBlockBodyGloas(body *BeaconBlockBodyGloas) *BeaconBlockBodyGloas return copied } + +// CopyBuilderPendingPayment creates a deep copy of a builder pending payment. +func CopyBuilderPendingPayment(original *BuilderPendingPayment) *BuilderPendingPayment { + if original == nil { + return nil + } + + return &BuilderPendingPayment{ + Weight: original.Weight, + Withdrawal: copyBuilderPendingWithdrawal(original.Withdrawal), + } +} + +// copyBuilderPendingWithdrawal creates a deep copy of a builder pending withdrawal. +func copyBuilderPendingWithdrawal(original *BuilderPendingWithdrawal) *BuilderPendingWithdrawal { + if original == nil { + return nil + } + + return &BuilderPendingWithdrawal{ + FeeRecipient: bytesutil.SafeCopyBytes(original.FeeRecipient), + Amount: original.Amount, + BuilderIndex: original.BuilderIndex, + } +} diff --git a/testing/spectest/mainnet/BUILD.bazel b/testing/spectest/mainnet/BUILD.bazel index 75d3340e6a..ed0c2e7d9c 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__operations__attestation_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/operations: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__operations__attestation_test.go b/testing/spectest/mainnet/gloas__operations__attestation_test.go new file mode 100644 index 0000000000..418e7d0dc7 --- /dev/null +++ b/testing/spectest/mainnet/gloas__operations__attestation_test.go @@ -0,0 +1,11 @@ +package mainnet + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations" +) + +func TestMainnet_Gloas_Operations_Attestation(t *testing.T) { + operations.RunAttestationTest(t, "mainnet") +} diff --git a/testing/spectest/minimal/BUILD.bazel b/testing/spectest/minimal/BUILD.bazel index 5f80e7f82d..553ac04c05 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__operations__attestation_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/operations: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__operations__attestation_test.go b/testing/spectest/minimal/gloas__operations__attestation_test.go new file mode 100644 index 0000000000..a859b7be20 --- /dev/null +++ b/testing/spectest/minimal/gloas__operations__attestation_test.go @@ -0,0 +1,11 @@ +package minimal + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations" +) + +func TestMinimal_Gloas_Operations_Attestation(t *testing.T) { + operations.RunAttestationTest(t, "minimal") +} diff --git a/testing/spectest/shared/gloas/operations/BUILD.bazel b/testing/spectest/shared/gloas/operations/BUILD.bazel new file mode 100644 index 0000000000..7504984409 --- /dev/null +++ b/testing/spectest/shared/gloas/operations/BUILD.bazel @@ -0,0 +1,22 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "attestation.go", + "helpers.go", + ], + importpath = "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations", + visibility = ["//visibility:public"], + deps = [ + "//beacon-chain/core/altair:go_default_library", + "//beacon-chain/state:go_default_library", + "//beacon-chain/state/state-native:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/interfaces:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", + "//testing/spectest/shared/common/operations:go_default_library", + "//testing/util:go_default_library", + ], +) diff --git a/testing/spectest/shared/gloas/operations/attestation.go b/testing/spectest/shared/gloas/operations/attestation.go new file mode 100644 index 0000000000..a4045c6be5 --- /dev/null +++ b/testing/spectest/shared/gloas/operations/attestation.go @@ -0,0 +1,27 @@ +package operations + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/altair" + "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" + common "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/common/operations" + "github.com/OffchainLabs/prysm/v7/testing/util" +) + +func blockWithAttestation(attestationSSZ []byte) (interfaces.SignedBeaconBlock, error) { + att := ðpb.AttestationElectra{} + if err := att.UnmarshalSSZ(attestationSSZ); err != nil { + return nil, err + } + b := util.NewBeaconBlockGloas() + b.Block.Body = ðpb.BeaconBlockBodyGloas{Attestations: []*ethpb.AttestationElectra{att}} + return blocks.NewSignedBeaconBlock(b) +} + +func RunAttestationTest(t *testing.T, config string) { + common.RunAttestationTest(t, config, version.String(version.Gloas), blockWithAttestation, altair.ProcessAttestationsNoVerifySignature, sszToState) +} diff --git a/testing/spectest/shared/gloas/operations/helpers.go b/testing/spectest/shared/gloas/operations/helpers.go new file mode 100644 index 0000000000..2822701d96 --- /dev/null +++ b/testing/spectest/shared/gloas/operations/helpers.go @@ -0,0 +1,33 @@ +package operations + +import ( + "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/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +func sszToState(b []byte) (state.BeaconState, error) { + base := ðpb.BeaconStateGloas{} + if err := base.UnmarshalSSZ(b); err != nil { + return nil, err + } + return state_native.InitializeFromProtoGloas(base) +} + +func sszToBlock(b []byte) (interfaces.SignedBeaconBlock, error) { + base := ðpb.BeaconBlockGloas{} + if err := base.UnmarshalSSZ(b); err != nil { + return nil, err + } + return blocks.NewSignedBeaconBlock(ðpb.SignedBeaconBlockGloas{Block: base}) +} + +func sszToBlockBody(b []byte) (interfaces.ReadOnlyBeaconBlockBody, error) { + base := ðpb.BeaconBlockBodyGloas{} + if err := base.UnmarshalSSZ(b); err != nil { + return nil, err + } + return blocks.NewBeaconBlockBody(base) +} diff --git a/testing/spectest/shared/gloas/ssz_static/ssz_static.go b/testing/spectest/shared/gloas/ssz_static/ssz_static.go index 94693ca3f1..4c9c15a2d4 100644 --- a/testing/spectest/shared/gloas/ssz_static/ssz_static.go +++ b/testing/spectest/shared/gloas/ssz_static/ssz_static.go @@ -15,29 +15,27 @@ import ( // RunSSZStaticTests executes "ssz_static" tests. func RunSSZStaticTests(t *testing.T, config string) { - common.RunSSZStaticTests(t, config, "gloas", unmarshalledSSZ, customHtr) + common.RunSSZStaticTests(t, config, "gloas", UnmarshalledSSZ, customHtr) } -func customHtr(t *testing.T, htrs []common.HTR, object any) []common.HTR { +func customHtr(t *testing.T, htrs []common.HTR, object interface{}) []common.HTR { _, ok := object.(*ethpb.BeaconStateGloas) if !ok { return htrs } - htrs = append(htrs, func(s any) ([32]byte, error) { + htrs = append(htrs, func(s interface{}) ([32]byte, error) { beaconState, err := state_native.InitializeFromProtoGloas(s.(*ethpb.BeaconStateGloas)) require.NoError(t, err) - return beaconState.HashTreeRoot(context.Background()) }) return htrs } -// unmarshalledSSZ unmarshalls serialized input. -func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (any, error) { - var obj any - +// UnmarshalledSSZ unmarshalls serialized input. +func UnmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (interface{}, error) { + var obj interface{} switch folderName { // Gloas specific types case "ExecutionPayloadBid": @@ -54,28 +52,22 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a obj = ðpb.BeaconBlockGloas{} case "BeaconBlockBody": obj = ðpb.BeaconBlockBodyGloas{} - case "BeaconState": - obj = ðpb.BeaconStateGloas{} case "BuilderPendingPayment": obj = ðpb.BuilderPendingPayment{} case "BuilderPendingWithdrawal": obj = ðpb.BuilderPendingWithdrawal{} case "ExecutionPayloadEnvelope": - obj = ðpb.ExecutionPayloadEnvelope{} + obj = &enginev1.ExecutionPayloadEnvelope{} case "SignedExecutionPayloadEnvelope": - obj = ðpb.SignedExecutionPayloadEnvelope{} + obj = &enginev1.SignedExecutionPayloadEnvelope{} case "ForkChoiceNode": t.Skip("Not a consensus type") case "IndexedPayloadAttestation": t.Skip("Not a consensus type") - case "DataColumnSidecar": - obj = ðpb.DataColumnSidecarGloas{} // Standard types that also exist in gloas case "ExecutionPayload": obj = &enginev1.ExecutionPayloadDeneb{} - case "ExecutionPayloadHeader": - obj = &enginev1.ExecutionPayloadHeaderDeneb{} case "Attestation": obj = ðpb.AttestationElectra{} case "AttestationData": @@ -86,6 +78,8 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a obj = ðpb.AggregateAttestationAndProofElectra{} case "BeaconBlockHeader": obj = ðpb.BeaconBlockHeader{} + case "BeaconState": + obj = ðpb.BeaconStateGloas{} case "Checkpoint": obj = ðpb.Checkpoint{} case "Deposit": @@ -98,6 +92,7 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a obj = ðpb.Eth1Data{} case "Eth1Block": t.Skip("Unused type") + return nil, nil case "Fork": obj = ðpb.Fork{} case "ForkData": @@ -141,15 +136,15 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a case "SyncCommittee": obj = ðpb.SyncCommittee{} case "LightClientOptimisticUpdate": - obj = ðpb.LightClientOptimisticUpdateDeneb{} + t.Skip("Need to fix header type first") case "LightClientFinalityUpdate": - obj = ðpb.LightClientFinalityUpdateElectra{} + t.Skip("Need to fix header type first") case "LightClientBootstrap": - obj = ðpb.LightClientBootstrapElectra{} + t.Skip("Need to fix header type first") case "LightClientUpdate": - obj = ðpb.LightClientUpdateElectra{} + t.Skip("Need to fix header type first") case "LightClientHeader": - obj = ðpb.LightClientHeaderDeneb{} + t.Skip("Need to fix header type first") case "BlobIdentifier": obj = ðpb.BlobIdentifier{} case "BlobSidecar": @@ -178,6 +173,8 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a obj = &enginev1.ConsolidationRequest{} case "ExecutionRequests": obj = &enginev1.ExecutionRequests{} + case "DataColumnSidecar": + t.Skip("TODO: fix inclusion proof but not a priority") case "DataColumnsByRootIdentifier": obj = ðpb.DataColumnsByRootIdentifier{} case "MatrixEntry": @@ -185,13 +182,11 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a default: return nil, errors.New("type not found") } - var err error if o, ok := obj.(fssz.Unmarshaler); ok { err = o.UnmarshalSSZ(serializedBytes) } else { err = errors.New("could not unmarshal object, not a fastssz compatible object") } - return obj, err }