Skip to content

Commit

Permalink
[ePBS] implement UpdateVotesOnPayloadAttestation (#14308)
Browse files Browse the repository at this point in the history
  • Loading branch information
potuz committed Oct 7, 2024
1 parent 2c7d0f5 commit 9538bc3
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 1 deletion.
2 changes: 2 additions & 0 deletions beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
],
)
59 changes: 59 additions & 0 deletions beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,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{}
}
}
}
235 changes: 235 additions & 0 deletions beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,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"
Expand Down Expand Up @@ -62,6 +65,14 @@ func prepareForkchoiceState(
return st, blockRoot, 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()
Expand Down Expand Up @@ -888,3 +899,227 @@ func TestForkchoiceParentRoot(t *testing.T) {
require.NoError(t, err)
require.Equal(t, zeroHash, 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: &ethpb.PayloadAttestation{
Data: &ethpb.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: &ethpb.PayloadAttestation{
Data: &ethpb.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: &ethpb.PayloadAttestation{
Data: &ethpb.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: &ethpb.PayloadAttestation{
Data: &ethpb.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: &ethpb.PayloadAttestation{
Data: &ethpb.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")
})
}
}
4 changes: 4 additions & 0 deletions beacon-chain/forkchoice/doubly-linked-tree/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion beacon-chain/rpc/eth/config/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func TestGetSpec(t *testing.T) {
data, ok := resp.Data.(map[string]interface{})
require.Equal(t, true, ok)

assert.Equal(t, 157, len(data))
assert.Equal(t, 162, len(data))
for k, v := range data {
t.Run(k, func(t *testing.T) {
switch k {
Expand Down Expand Up @@ -527,6 +527,16 @@ func TestGetSpec(t *testing.T) {
assert.Equal(t, "92", v)
case "MAX_DEPOSIT_REQUESTS_PER_PAYLOAD":
assert.Equal(t, "93", 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:
Expand Down
1 change: 1 addition & 0 deletions config/fieldparams/mainnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 9538bc3

Please sign in to comment.