diff --git a/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel b/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel index 60492ec2645d..8ec870c04e37 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel +++ b/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel @@ -68,6 +68,7 @@ go_test( "//beacon-chain/forkchoice/types:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/state-native:go_default_library", + "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", "//consensus-types/forkchoice:go_default_library", @@ -80,5 +81,6 @@ go_test( "//testing/require:go_default_library", "//testing/util:go_default_library", "//time/slots:go_default_library", + "@com_github_prysmaticlabs_go_bitfield//:go_default_library", ], ) diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go index 7318751ef8ec..4c14bb8c92d8 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go @@ -681,3 +681,62 @@ func (f *ForkChoice) ParentRoot(root [32]byte) ([32]byte, error) { } return n.parent.root, nil } + +// UpdateVotesOnPayloadAttestation processes a new aggregated +// payload attestation message and updates +// the Payload Timeliness Committee (PTC) votes for the corresponding block. +func (s *Store) updateVotesOnPayloadAttestation( + payloadAttestation *ethpb.PayloadAttestation) error { + // Extract the attestation data and convert the beacon block root to a 32-byte array + data := payloadAttestation.Data + blockRoot := bytesutil.ToBytes32(data.BeaconBlockRoot) + + // Check if the block exists in the store + node, ok := s.nodeByRoot[blockRoot] + if !ok || node == nil { + return ErrNilNode + } + + // Update the PTC votes based on the attestation + // We only set the vote if it hasn't been set before + // to handle potential equivocations + for i := uint64(0); i < fieldparams.PTCSize; i++ { + if payloadAttestation.AggregationBits.BitAt(i) && node.ptcVote[i] == primitives.PAYLOAD_ABSENT { + node.ptcVote[i] = data.PayloadStatus + } + } + + return nil +} + +// updatePayloadBoosts checks the PTC votes for a given node and updates +// the payload reveal and withhold boost roots if the necessary thresholds are met. +func (s *Store) updatePayloadBoosts(node *Node) { + presentCount := 0 + withheldCount := 0 + + // Count the number of PRESENT and WITHHELD votes + for _, vote := range node.ptcVote { + if vote == primitives.PAYLOAD_PRESENT { + presentCount++ + } else if vote == primitives.PAYLOAD_WITHHELD { + withheldCount++ + } + } + + // If the number of PRESENT votes exceeds the threshold, + // update the payload reveal boost root + if presentCount > int(params.BeaconConfig().PayloadTimelyThreshold) { + s.payloadRevealBoostRoot = node.root + return + } + // If the number of WITHHELD votes exceeds the threshold, + // update the payload reveal boost root + if withheldCount > int(params.BeaconConfig().PayloadTimelyThreshold) { + if node.parent != nil { + s.payloadWithholdBoostRoot = node.parent.root + // A node is considered "full" if it has a non-zero payload hash + s.payloadWithholdBoostFull = node.parent.payloadHash != [32]byte{} + } + } +} diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go index ec771cc101c1..5fd1b6780a82 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go @@ -7,14 +7,17 @@ import ( "testing" "time" + "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice" forkchoicetypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/forkchoice/types" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" + fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/crypto/hash" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/assert" @@ -82,6 +85,14 @@ func prepareForkchoiceState( return st, roblock, err } +// Helper function to set all bits in a Bitvector512 +func setAllBits(bv bitfield.Bitvector512) bitfield.Bitvector512 { + for i := 0; i < len(bv); i++ { + bv[i] = 0xFF + } + return bv +} + func TestForkChoice_UpdateBalancesPositiveChange(t *testing.T) { f := setup(0, 0) ctx := context.Background() @@ -925,3 +936,227 @@ func TestForkChoice_CleanupInserting(t *testing.T) { require.NotNil(t, f.InsertNode(ctx, st, roblock)) require.Equal(t, false, f.HasNode(roblock.Root())) } + +func TestStore_UpdateVotesOnPayloadAttestation(t *testing.T) { + tests := []struct { + name string + setupStore func(*Store) + payloadAttestation *ethpb.PayloadAttestation + wantErr bool + expectedPTCVotes []primitives.PTCStatus + expectedBoosts func(*Store) bool + }{ + { + name: "Unknown block root", + setupStore: func(_ *Store) {}, + payloadAttestation: ðpb.PayloadAttestation{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: []byte{1, 2, 3}, + }, + }, + wantErr: true, + }, + { + name: "Update PTC votes - all present", + setupStore: func(s *Store) { + root := [32]byte{1, 2, 3} + s.nodeByRoot[root] = &Node{root: root, ptcVote: make([]primitives.PTCStatus, fieldparams.PTCSize)} + }, + payloadAttestation: ðpb.PayloadAttestation{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: []byte{1, 2, 3}, + PayloadStatus: primitives.PAYLOAD_PRESENT, + }, + AggregationBits: setAllBits(bitfield.NewBitvector512()), + }, + expectedPTCVotes: func() []primitives.PTCStatus { + return makeVotes(fieldparams.PTCSize, primitives.PAYLOAD_PRESENT) + }(), + expectedBoosts: noBoosts, + }, + { + name: "Update PTC votes - all withheld", + setupStore: func(s *Store) { + root := [32]byte{4, 5, 6} + s.nodeByRoot[root] = &Node{root: root, ptcVote: make([]primitives.PTCStatus, fieldparams.PTCSize)} + }, + payloadAttestation: ðpb.PayloadAttestation{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: []byte{4, 5, 6}, + PayloadStatus: primitives.PAYLOAD_WITHHELD, + }, + AggregationBits: setAllBits(bitfield.NewBitvector512()), + }, + expectedPTCVotes: func() []primitives.PTCStatus { + return makeVotes(fieldparams.PTCSize, primitives.PAYLOAD_WITHHELD) + }(), + expectedBoosts: noBoosts, + }, + { + name: "Update PTC votes - partial attestation", + setupStore: func(s *Store) { + root := [32]byte{7, 8, 9} + s.nodeByRoot[root] = &Node{root: root, ptcVote: make([]primitives.PTCStatus, fieldparams.PTCSize)} + }, + payloadAttestation: ðpb.PayloadAttestation{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: []byte{7, 8, 9}, + PayloadStatus: primitives.PAYLOAD_PRESENT, + }, + AggregationBits: func() bitfield.Bitvector512 { + bits := bitfield.NewBitvector512() + for i := 0; i < fieldparams.PTCSize/2; i++ { + bits.SetBitAt(uint64(i), true) + } + return bits + }(), + }, + expectedPTCVotes: func() []primitives.PTCStatus { + votes := make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := 0; i < fieldparams.PTCSize/2; i++ { + votes[i] = primitives.PAYLOAD_PRESENT + } + return votes + }(), + expectedBoosts: noBoosts, + }, + { + name: "Update PTC votes - no change for already set votes", + setupStore: func(s *Store) { + root := [32]byte{10, 11, 12} + votes := make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := range votes { + if i%2 == 0 { + votes[i] = primitives.PAYLOAD_PRESENT + } + } + s.nodeByRoot[root] = &Node{root: root, ptcVote: votes} + }, + payloadAttestation: ðpb.PayloadAttestation{ + Data: ðpb.PayloadAttestationData{ + BeaconBlockRoot: []byte{10, 11, 12}, + PayloadStatus: primitives.PAYLOAD_WITHHELD, + }, + AggregationBits: setAllBits(bitfield.NewBitvector512()), + }, + expectedPTCVotes: func() []primitives.PTCStatus { + votes := make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := range votes { + if i%2 == 0 { + votes[i] = primitives.PAYLOAD_PRESENT + } else { + votes[i] = primitives.PAYLOAD_WITHHELD + } + } + return votes + }(), + expectedBoosts: noBoosts, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Store{ + nodeByRoot: make(map[[32]byte]*Node), + } + tt.setupStore(s) + + err := s.updateVotesOnPayloadAttestation(tt.payloadAttestation) + + if tt.wantErr { + require.ErrorIs(t, err, ErrNilNode, "Expected ErrNilNode") + } else { + require.NoError(t, err) + node := s.nodeByRoot[bytesutil.ToBytes32(tt.payloadAttestation.Data.BeaconBlockRoot)] + assert.DeepEqual(t, tt.expectedPTCVotes, node.ptcVote, "Unexpected PTC votes") + assert.Equal(t, tt.expectedBoosts(s), true, "Unexpected boost values") + } + }) + } +} + +func makeVotes(count int, status primitives.PTCStatus) []primitives.PTCStatus { + votes := make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := 0; i < count; i++ { + votes[i] = status + } + return votes +} + +func noBoosts(s *Store) bool { + return s.payloadRevealBoostRoot == [32]byte{} && + s.payloadWithholdBoostRoot == [32]byte{} && + !s.payloadWithholdBoostFull +} + +func TestStore_UpdatePayloadBoosts(t *testing.T) { + tests := []struct { + name string + setupNode func(*Node) + expectedRevealBoost [32]byte + expectedWithholdBoost [32]byte + expectedWithholdFull bool + }{ + { + name: "No boost", + setupNode: func(n *Node) { + n.ptcVote = make([]primitives.PTCStatus, fieldparams.PTCSize) + }, + }, + { + name: "Reveal boost", + setupNode: func(n *Node) { + n.root = [32]byte{1, 2, 3} + n.ptcVote = make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := 0; i < int(params.BeaconConfig().PayloadTimelyThreshold)+1; i++ { + n.ptcVote[i] = primitives.PAYLOAD_PRESENT + } + }, + expectedRevealBoost: [32]byte{1, 2, 3}, + }, + { + name: "Withhold boost", + setupNode: func(n *Node) { + n.ptcVote = make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := 0; i < int(params.BeaconConfig().PayloadTimelyThreshold)+1; i++ { + n.ptcVote[i] = primitives.PAYLOAD_WITHHELD + } + n.parent = &Node{root: [32]byte{4, 5, 6}, payloadHash: [32]byte{7, 8, 9}} + }, + expectedWithholdBoost: [32]byte{4, 5, 6}, + expectedWithholdFull: true, + }, + { + name: "Reveal boost with early return", + setupNode: func(n *Node) { + n.root = [32]byte{1, 2, 3} + n.ptcVote = make([]primitives.PTCStatus, fieldparams.PTCSize) + for i := 0; i < int(params.BeaconConfig().PayloadTimelyThreshold)+1; i++ { + n.ptcVote[i] = primitives.PAYLOAD_PRESENT + } + // Set up conditions for withhold boost, which should not be applied due to early return + n.parent = &Node{root: [32]byte{4, 5, 6}, payloadHash: [32]byte{7, 8, 9}} + for i := int(params.BeaconConfig().PayloadTimelyThreshold) + 1; i < fieldparams.PTCSize; i++ { + n.ptcVote[i] = primitives.PAYLOAD_WITHHELD + } + }, + expectedRevealBoost: [32]byte{1, 2, 3}, + expectedWithholdBoost: [32]byte{}, // Should remain zero due to early return + expectedWithholdFull: false, // Should remain false due to early return + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Store{} + n := &Node{} + tt.setupNode(n) + + s.updatePayloadBoosts(n) + + assert.Equal(t, tt.expectedRevealBoost, s.payloadRevealBoostRoot, "Unexpected reveal boost root") + assert.Equal(t, tt.expectedWithholdBoost, s.payloadWithholdBoostRoot, "Unexpected withhold boost root") + assert.Equal(t, tt.expectedWithholdFull, s.payloadWithholdBoostFull, "Unexpected withhold full status") + }) + } +} diff --git a/beacon-chain/forkchoice/doubly-linked-tree/types.go b/beacon-chain/forkchoice/doubly-linked-tree/types.go index a8877834a60f..0039496810c7 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/types.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/types.go @@ -41,6 +41,9 @@ type Store struct { highestReceivedNode *Node // The highest slot node. receivedBlocksLastEpoch [fieldparams.SlotsPerEpoch]primitives.Slot // Using `highestReceivedSlot`. The slot of blocks received in the last epoch. allTipsAreInvalid bool // tracks if all tips are not viable for head + payloadWithholdBoostRoot [fieldparams.RootLength]byte // the root of the block that receives the withhold boost + payloadWithholdBoostFull bool // Indicator of whether the block receiving the withhold boost is full or empty + payloadRevealBoostRoot [fieldparams.RootLength]byte // the root of the block that receives the reveal boost } // Node defines the individual block which includes its block parent, ancestor and how much weight accounted for it. @@ -61,6 +64,7 @@ type Node struct { bestDescendant *Node // bestDescendant node of this node. optimistic bool // whether the block has been fully validated or not timestamp uint64 // The timestamp when the node was inserted. + ptcVote []primitives.PTCStatus // tracks the Payload Timeliness Committee (PTC) votes for the node } // Vote defines an individual validator's vote. diff --git a/beacon-chain/rpc/eth/config/handlers_test.go b/beacon-chain/rpc/eth/config/handlers_test.go index ecd409953770..29ada163a03e 100644 --- a/beacon-chain/rpc/eth/config/handlers_test.go +++ b/beacon-chain/rpc/eth/config/handlers_test.go @@ -193,7 +193,7 @@ func TestGetSpec(t *testing.T) { data, ok := resp.Data.(map[string]interface{}) require.Equal(t, true, ok) - assert.Equal(t, 158, len(data)) + assert.Equal(t, 163, len(data)) for k, v := range data { t.Run(k, func(t *testing.T) { switch k { @@ -530,6 +530,16 @@ func TestGetSpec(t *testing.T) { assert.Equal(t, "93", v) case "MAX_PENDING_DEPOSITS_PER_EPOCH": assert.Equal(t, "94", v) + case "PROPOSER_SCORE_BOOST_EPBS": + assert.Equal(t, "20", v) + case "PAYLOAD_REVEAL_BOOST": + assert.Equal(t, "40", v) + case "PAYLOAD_WITHHOLD_BOOST": + assert.Equal(t, "40", v) + case "PAYLOAD_TIMELY_THRESHOLD": + assert.Equal(t, "256", v) + case "INTERVALS_PER_SLOT_EPBS": + assert.Equal(t, "4", v) case "MIN_BUILDER_BALANCE": assert.Equal(t, "0", v) default: diff --git a/config/fieldparams/mainnet.go b/config/fieldparams/mainnet.go index 92d5c66d6002..c47d3187b25a 100644 --- a/config/fieldparams/mainnet.go +++ b/config/fieldparams/mainnet.go @@ -15,6 +15,7 @@ const ( SlashingsLength = 8192 // EPOCHS_PER_SLASHINGS_VECTOR SyncCommitteeLength = 512 // SYNC_COMMITTEE_SIZE PTCSize = 512 // PTC_SIZE [New in ePBS] + PayloadTimelyThreshold = 256 // PTC_SIZE / 2 [New in ePBS] RootLength = 32 // RootLength defines the byte length of a Merkle root. BLSSignatureLength = 96 // BLSSignatureLength defines the byte length of a BLSSignature. BLSPubkeyLength = 48 // BLSPubkeyLength defines the byte length of a BLSSignature. diff --git a/config/params/config.go b/config/params/config.go index 3c5ff9590562..f173b6f651ec 100644 --- a/config/params/config.go +++ b/config/params/config.go @@ -70,10 +70,15 @@ type BeaconChainConfig struct { // Fork choice algorithm constants. ProposerScoreBoost uint64 `yaml:"PROPOSER_SCORE_BOOST" spec:"true"` // ProposerScoreBoost defines a value that is a % of the committee weight for fork-choice boosting. + ProposerScoreBoostEPBS uint64 `yaml:"PROPOSER_SCORE_BOOST_EPBS" spec:"true"` // ProposerScoreBoostEPBS defines a value that is a % of the committee weight for fork-choice boosting. ReorgWeightThreshold uint64 `yaml:"REORG_WEIGHT_THRESHOLD" spec:"true"` // ReorgWeightThreshold defines a value that is a % of the committee weight to consider a block weak and subject to being orphaned. ReorgParentWeightThreshold uint64 `yaml:"REORG_PARENT_WEIGHT_THRESHOLD" spec:"true"` // ReorgParentWeightThreshold defines a value that is a % of the committee weight to consider a parent block strong and subject its child to being orphaned. ReorgMaxEpochsSinceFinalization primitives.Epoch `yaml:"REORG_MAX_EPOCHS_SINCE_FINALIZATION" spec:"true"` // This defines a limit to consider safe to orphan a block if the network is finalizing IntervalsPerSlot uint64 `yaml:"INTERVALS_PER_SLOT" spec:"true"` // IntervalsPerSlot defines the number of fork choice intervals in a slot defined in the fork choice spec. + IntervalsPerSlotEPBS uint64 `yaml:"INTERVALS_PER_SLOT_EPBS" spec:"true"` // IntervalsPerSlotEPBS defines the number of fork choice intervals in a slot defined in the fork choice spec from EIP-7732. + PayloadWithholdBoost uint64 `yaml:"PAYLOAD_WITHHOLD_BOOST" spec:"true"` // PayloadWithholdBoost define a value that is the score boost given when a payload is withheld by the builder + PayloadRevealBoost uint64 `yaml:"PAYLOAD_REVEAL_BOOST" spec:"true"` // PayloadRevealBoost a value that is the score boost given when a payload is revealed by the builder + PayloadTimelyThreshold uint64 `yaml:"PAYLOAD_TIMELY_THRESHOLD" spec:"true"` // PayloadTimelyThreshold is the threshold for considering a payload timely. // Ethereum PoW parameters. DepositChainID uint64 `yaml:"DEPOSIT_CHAIN_ID" spec:"true"` // DepositChainID of the eth1 network. This used for replay protection. diff --git a/config/params/mainnet_config.go b/config/params/mainnet_config.go index fadb07aafc4e..1b3cf0303837 100644 --- a/config/params/mainnet_config.go +++ b/config/params/mainnet_config.go @@ -111,10 +111,15 @@ var mainnetBeaconConfig = &BeaconChainConfig{ // Fork choice algorithm constants. ProposerScoreBoost: 40, + ProposerScoreBoostEPBS: 20, ReorgWeightThreshold: 20, ReorgParentWeightThreshold: 160, ReorgMaxEpochsSinceFinalization: 2, IntervalsPerSlot: 3, + IntervalsPerSlotEPBS: 4, + PayloadWithholdBoost: 40, + PayloadRevealBoost: 40, + PayloadTimelyThreshold: 256, // PTC_SIZE / 2. // Ethereum PoW parameters. DepositChainID: 1, // Chain ID of eth1 mainnet.