Implement process attestation

This commit is contained in:
terence tsao
2026-01-03 22:02:41 -08:00
parent 0db74365e0
commit 22aa87019d
23 changed files with 942 additions and 28 deletions

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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, &ethpb.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: &ethpb.AttestationData{
Slot: 3,
CommitteeIndex: 1, // invalid for same-slot
BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32),
Source: &ethpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)},
Target: &ethpb.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: &ethpb.AttestationData{
Slot: 3,
CommitteeIndex: 1,
BeaconBlockRoot: bytes.Repeat([]byte{0xBB}, 32),
Source: &ethpb.Checkpoint{Root: bytes.Repeat([]byte{0xDD}, 32)},
Target: &ethpb.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 := &ethpb.Checkpoint{Root: checkpointRoot}
stProto := &ethpb.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: &ethpb.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
}

View File

@@ -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")
}

View File

@@ -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 := &ethpb.Checkpoint{Epoch: 0, Root: checkpointRoot}
gloasStateProto := &ethpb.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: &ethpb.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 := &ethpb.AttestationElectra{
Data: &ethpb.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)

View File

@@ -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",
],
)

View File

@@ -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<<f) == 0 {
return true
}
}
return false
}
for _, idx := range indices {
if !hasNewFlag(idx) {
continue
}
validator, err := beaconState.ValidatorAtIndex(primitives.ValidatorIndex(idx))
if err != nil {
return nil, err
}
payment.Weight += primitives.Gwei(validator.EffectiveBalance)
}
if err := beaconState.SetBuilderPendingPayment(uint64(paymentSlot), payment); err != nil {
return nil, err
}
return beaconState, nil
}

View File

@@ -0,0 +1,288 @@
package gloas
import (
"bytes"
"testing"
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"
"github.com/OffchainLabs/prysm/v7/time/slots"
)
func buildStateWithBlockRoots(t *testing.T, stateSlot primitives.Slot, roots map[primitives.Slot][]byte) *state_native.BeaconState {
t.Helper()
cfg := params.BeaconConfig()
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for slot, root := range roots {
blockRoots[slot%cfg.SlotsPerHistoricalRoot] = root
}
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for i := range stateRoots {
stateRoots[i] = bytes.Repeat([]byte{0x11}, 32)
}
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
for i := range randaoMixes {
randaoMixes[i] = bytes.Repeat([]byte{0x22}, 32)
}
execPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8)
stProto := &ethpb.BeaconStateGloas{
Slot: stateSlot,
GenesisValidatorsRoot: bytes.Repeat([]byte{0x33}, 32),
Fork: &ethpb.Fork{
CurrentVersion: bytes.Repeat([]byte{0x44}, 4),
PreviousVersion: bytes.Repeat([]byte{0x44}, 4),
Epoch: 0,
},
BlockRoots: blockRoots,
StateRoots: stateRoots,
RandaoMixes: randaoMixes,
ExecutionPayloadAvailability: execPayloadAvailability,
Validators: []*ethpb.Validator{},
Balances: []uint64{},
BuilderPendingPayments: make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2),
BuilderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{},
}
state, err := state_native.InitializeFromProtoGloas(stProto)
require.NoError(t, err)
return state.(*state_native.BeaconState)
}
func TestSameSlotAttestation(t *testing.T) {
rootA := bytes.Repeat([]byte{0xAA}, 32)
rootB := bytes.Repeat([]byte{0xBB}, 32)
rootC := bytes.Repeat([]byte{0xCC}, 32)
tests := []struct {
name string
stateSlot primitives.Slot
slot primitives.Slot
blockRoot []byte
roots map[primitives.Slot][]byte
want bool
}{
{
name: "slot zero always true",
stateSlot: 1,
slot: 0,
blockRoot: rootA,
roots: map[primitives.Slot][]byte{},
want: true,
},
{
name: "matching current different previous",
stateSlot: 6,
slot: 4,
blockRoot: rootA,
roots: map[primitives.Slot][]byte{
4: rootA,
3: rootB,
},
want: true,
},
{
name: "matching current same previous",
stateSlot: 6,
slot: 4,
blockRoot: rootA,
roots: map[primitives.Slot][]byte{
4: rootA,
3: rootA,
},
want: false,
},
{
name: "non matching current",
stateSlot: 6,
slot: 4,
blockRoot: rootC,
roots: map[primitives.Slot][]byte{
4: rootA,
3: rootB,
},
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
state := buildStateWithBlockRoots(t, tt.stateSlot, tt.roots)
var rootArr [32]byte
copy(rootArr[:], tt.blockRoot)
got, err := SameSlotAttestation(state, rootArr, tt.slot)
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}
func buildStateForPaymentTest(t *testing.T, stateSlot primitives.Slot, paymentIdx int, amount primitives.Gwei, weight primitives.Gwei, roots map[primitives.Slot][]byte) *state_native.BeaconState {
t.Helper()
cfg := params.BeaconConfig()
blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for slot, root := range roots {
blockRoots[slot%cfg.SlotsPerHistoricalRoot] = root
}
stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot)
for i := range stateRoots {
stateRoots[i] = bytes.Repeat([]byte{0x44}, 32)
}
randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector)
for i := range randaoMixes {
randaoMixes[i] = bytes.Repeat([]byte{0x55}, 32)
}
validator := &ethpb.Validator{
PublicKey: bytes.Repeat([]byte{0x01}, 48),
WithdrawalCredentials: append([]byte{cfg.ETH1AddressWithdrawalPrefixByte}, bytes.Repeat([]byte{0x02}, 31)...),
EffectiveBalance: cfg.MinActivationBalance,
}
payments := make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2)
for i := range payments {
payments[i] = &ethpb.BuilderPendingPayment{
Withdrawal: &ethpb.BuilderPendingWithdrawal{
FeeRecipient: make([]byte, 20),
},
}
}
payments[paymentIdx] = &ethpb.BuilderPendingPayment{
Weight: weight,
Withdrawal: &ethpb.BuilderPendingWithdrawal{
FeeRecipient: make([]byte, 20),
Amount: amount,
},
}
execPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8)
stProto := &ethpb.BeaconStateGloas{
Slot: stateSlot,
GenesisValidatorsRoot: bytes.Repeat([]byte{0x33}, 32),
BlockRoots: blockRoots,
StateRoots: stateRoots,
RandaoMixes: randaoMixes,
ExecutionPayloadAvailability: execPayloadAvailability,
Validators: []*ethpb.Validator{validator},
Balances: []uint64{cfg.MinActivationBalance},
CurrentEpochParticipation: []byte{0},
PreviousEpochParticipation: []byte{0},
BuilderPendingPayments: payments,
Fork: &ethpb.Fork{
CurrentVersion: bytes.Repeat([]byte{0x66}, 4),
PreviousVersion: bytes.Repeat([]byte{0x66}, 4),
Epoch: 0,
},
}
state, err := state_native.InitializeFromProtoGloas(stProto)
require.NoError(t, err)
return state.(*state_native.BeaconState)
}
func TestUpdatePendingPaymentWeight(t *testing.T) {
cfg := params.BeaconConfig()
slotsPerEpoch := cfg.SlotsPerEpoch
slot := primitives.Slot(4)
stateSlot := slot + 1
currentEpoch := slots.ToEpoch(stateSlot)
rootA := bytes.Repeat([]byte{0xAA}, 32)
rootB := bytes.Repeat([]byte{0xBB}, 32)
tests := []struct {
name string
targetEpoch primitives.Epoch
blockRoot []byte
initialAmount primitives.Gwei
initialWeight primitives.Gwei
wantWeight primitives.Gwei
}{
{
name: "same slot current epoch adds weight",
targetEpoch: currentEpoch,
blockRoot: rootA,
initialAmount: 10,
initialWeight: 0,
wantWeight: primitives.Gwei(cfg.MinActivationBalance),
},
{
name: "same slot zero amount no weight change",
targetEpoch: currentEpoch,
blockRoot: rootA,
initialAmount: 0,
initialWeight: 5,
wantWeight: 5,
},
{
name: "non matching block root no change",
targetEpoch: currentEpoch,
blockRoot: rootB,
initialAmount: 10,
initialWeight: 7,
wantWeight: 7,
},
{
name: "previous epoch target uses earlier slot",
targetEpoch: currentEpoch - 1,
blockRoot: rootA,
initialAmount: 20,
initialWeight: 0,
wantWeight: primitives.Gwei(cfg.MinActivationBalance),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var paymentIdx int
if tt.targetEpoch == currentEpoch {
paymentIdx = int(slotsPerEpoch + (slot % slotsPerEpoch))
} else {
paymentIdx = int(slot % slotsPerEpoch)
}
state := buildStateForPaymentTest(t, stateSlot, paymentIdx, tt.initialAmount, tt.initialWeight, map[primitives.Slot][]byte{
slot: tt.blockRoot,
slot - 1: rootB,
})
att := &ethpb.Attestation{
Data: &ethpb.AttestationData{
Slot: slot,
CommitteeIndex: 0,
BeaconBlockRoot: tt.blockRoot,
Source: &ethpb.Checkpoint{},
Target: &ethpb.Checkpoint{
Epoch: tt.targetEpoch,
},
},
}
participatedFlags := map[uint8]bool{
cfg.TimelySourceFlagIndex: true,
cfg.TimelyTargetFlagIndex: true,
cfg.TimelyHeadFlagIndex: true,
}
indices := []uint64{0}
gotState, err := UpdatePendingPaymentWeight(state, att, indices, participatedFlags)
require.NoError(t, err)
payment, err := gotState.BuilderPendingPayment(uint64(paymentIdx))
require.NoError(t, err)
require.Equal(t, tt.wantWeight, payment.Weight)
})
}
}

View File

@@ -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",

View File

@@ -63,6 +63,7 @@ type ReadOnlyBeaconState interface {
ReadOnlyDeposits
ReadOnlyConsolidations
ReadOnlyProposerLookahead
readOnlyGloasFields
ToProtoUnsafe() any
ToProto() any
GenesisTime() time.Time
@@ -98,6 +99,7 @@ type WriteOnlyBeaconState interface {
WriteOnlyWithdrawals
WriteOnlyDeposits
WriteOnlyProposerLookahead
writeOnlyGloasFields
SetGenesisTime(val time.Time) error
SetGenesisValidatorsRoot(val []byte) error
SetSlot(val primitives.Slot) error

View File

@@ -0,0 +1,15 @@
package state
import (
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
)
type writeOnlyGloasFields interface {
SetBuilderPendingPayment(index uint64, payment *ethpb.BuilderPendingPayment) error
}
type readOnlyGloasFields interface {
BuilderPendingPayment(index uint64) (*ethpb.BuilderPendingPayment, error)
ExecutionPayloadAvailability(slot primitives.Slot) (uint64, error)
}

View File

@@ -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",
@@ -36,6 +37,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",

View File

@@ -0,0 +1,42 @@
package state_native
import (
"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"
)
// BuilderPendingPayment returns the builder pending payment for the given index.
func (b *BeaconState) BuilderPendingPayment(index uint64) (*ethpb.BuilderPendingPayment, error) {
if b.version < version.Gloas {
return nil, errNotSupported("BuilderPendingPayment", b.version)
}
b.lock.RLock()
defer b.lock.RUnlock()
return b.builderPendingPayments[index], 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 {
return 0, errNotSupported("ExecutionPayloadAvailability", b.version)
}
b.lock.RLock()
defer b.lock.RUnlock()
if b.executionPayloadAvailability == nil {
return 0, nil
}
slotIndex := slot % params.BeaconConfig().SlotsPerHistoricalRoot
byteIndex := slotIndex / 8
bitIndex := slotIndex % 8
bit := (b.executionPayloadAvailability[byteIndex] >> bitIndex) & 1
return uint64(bit), nil
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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",

View File

@@ -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")
}

View File

@@ -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",

View File

@@ -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")
}

View File

@@ -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",
],
)

View File

@@ -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 := &ethpb.AttestationElectra{}
if err := att.UnmarshalSSZ(attestationSSZ); err != nil {
return nil, err
}
b := util.NewBeaconBlockGloas()
b.Block.Body = &ethpb.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)
}

View File

@@ -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 := &ethpb.BeaconStateGloas{}
if err := base.UnmarshalSSZ(b); err != nil {
return nil, err
}
return state_native.InitializeFromProtoGloas(base)
}
func sszToBlock(b []byte) (interfaces.SignedBeaconBlock, error) {
base := &ethpb.BeaconBlockGloas{}
if err := base.UnmarshalSSZ(b); err != nil {
return nil, err
}
return blocks.NewSignedBeaconBlock(&ethpb.SignedBeaconBlockGloas{Block: base})
}
func sszToBlockBody(b []byte) (interfaces.ReadOnlyBeaconBlockBody, error) {
base := &ethpb.BeaconBlockBodyGloas{}
if err := base.UnmarshalSSZ(b); err != nil {
return nil, err
}
return blocks.NewBeaconBlockBody(base)
}

View File

@@ -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 = &ethpb.BeaconBlockGloas{}
case "BeaconBlockBody":
obj = &ethpb.BeaconBlockBodyGloas{}
case "BeaconState":
obj = &ethpb.BeaconStateGloas{}
case "BuilderPendingPayment":
obj = &ethpb.BuilderPendingPayment{}
case "BuilderPendingWithdrawal":
obj = &ethpb.BuilderPendingWithdrawal{}
case "ExecutionPayloadEnvelope":
obj = &ethpb.ExecutionPayloadEnvelope{}
obj = &enginev1.ExecutionPayloadEnvelope{}
case "SignedExecutionPayloadEnvelope":
obj = &ethpb.SignedExecutionPayloadEnvelope{}
obj = &enginev1.SignedExecutionPayloadEnvelope{}
case "ForkChoiceNode":
t.Skip("Not a consensus type")
case "IndexedPayloadAttestation":
t.Skip("Not a consensus type")
case "DataColumnSidecar":
obj = &ethpb.DataColumnSidecarGloas{}
// Standard types that also exist in gloas
case "ExecutionPayload":
obj = &enginev1.ExecutionPayloadDeneb{}
case "ExecutionPayloadHeader":
obj = &enginev1.ExecutionPayloadHeaderDeneb{}
case "Attestation":
obj = &ethpb.AttestationElectra{}
case "AttestationData":
@@ -86,6 +78,8 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a
obj = &ethpb.AggregateAttestationAndProofElectra{}
case "BeaconBlockHeader":
obj = &ethpb.BeaconBlockHeader{}
case "BeaconState":
obj = &ethpb.BeaconStateGloas{}
case "Checkpoint":
obj = &ethpb.Checkpoint{}
case "Deposit":
@@ -98,6 +92,7 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a
obj = &ethpb.Eth1Data{}
case "Eth1Block":
t.Skip("Unused type")
return nil, nil
case "Fork":
obj = &ethpb.Fork{}
case "ForkData":
@@ -141,15 +136,15 @@ func unmarshalledSSZ(t *testing.T, serializedBytes []byte, folderName string) (a
case "SyncCommittee":
obj = &ethpb.SyncCommittee{}
case "LightClientOptimisticUpdate":
obj = &ethpb.LightClientOptimisticUpdateDeneb{}
t.Skip("Need to fix header type first")
case "LightClientFinalityUpdate":
obj = &ethpb.LightClientFinalityUpdateElectra{}
t.Skip("Need to fix header type first")
case "LightClientBootstrap":
obj = &ethpb.LightClientBootstrapElectra{}
t.Skip("Need to fix header type first")
case "LightClientUpdate":
obj = &ethpb.LightClientUpdateElectra{}
t.Skip("Need to fix header type first")
case "LightClientHeader":
obj = &ethpb.LightClientHeaderDeneb{}
t.Skip("Need to fix header type first")
case "BlobIdentifier":
obj = &ethpb.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 = &ethpb.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
}