From 3063f810b68c4c5392b3f314dd139c6d32f36a5e Mon Sep 17 00:00:00 2001 From: terence Date: Tue, 10 Sep 2024 07:55:05 -0700 Subject: [PATCH] Prysm rpc: Get payload attestation data (#14380) --- beacon-chain/blockchain/chain_info.go | 9 +-- .../blockchain/chain_info_forkchoice.go | 6 +- beacon-chain/blockchain/testing/mock.go | 15 +++-- .../forkchoice/doubly-linked-tree/store.go | 8 +-- .../doubly-linked-tree/store_test.go | 27 +++++--- beacon-chain/forkchoice/interfaces.go | 2 +- beacon-chain/forkchoice/ro.go | 6 +- beacon-chain/forkchoice/ro_test.go | 14 ++-- beacon-chain/rpc/eth/beacon/handlers.go | 3 +- .../v1alpha1/validator/proposer_builder.go | 2 +- .../prysm/v1alpha1/validator/ptc_attester.go | 22 ++++++- .../v1alpha1/validator/ptc_attester_test.go | 66 +++++++++++++++++++ 12 files changed, 138 insertions(+), 42 deletions(-) diff --git a/beacon-chain/blockchain/chain_info.go b/beacon-chain/blockchain/chain_info.go index d79d66897bbf..62d5366234bc 100644 --- a/beacon-chain/blockchain/chain_info.go +++ b/beacon-chain/blockchain/chain_info.go @@ -42,7 +42,7 @@ type ForkchoiceFetcher interface { GetProposerHead() [32]byte SetForkChoiceGenesisTime(uint64) UpdateHead(context.Context, primitives.Slot) - HighestReceivedBlockSlot() primitives.Slot + HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) ReceivedBlocksLastEpoch() (uint64, error) InsertNode(context.Context, state.BeaconState, [32]byte) error ForkChoiceDump(context.Context) (*forkchoice.Dump, error) @@ -50,6 +50,7 @@ type ForkchoiceFetcher interface { ProposerBoost() [32]byte RecentBlockSlot(root [32]byte) (primitives.Slot, error) IsCanonical(ctx context.Context, blockRoot [32]byte) (bool, error) + GetPTCVote(root [32]byte) primitives.PTCStatus } // TimeFetcher retrieves the Ethereum consensus data that's related to time. @@ -544,12 +545,6 @@ func (s *Service) recoverStateSummary(ctx context.Context, blockRoot [32]byte) ( return nil, errBlockDoesNotExist } -// PayloadBeingSynced returns whether the payload for the block with the given -// root is currently being synced and what is the withheld status in the payload -func (s *Service) PayloadBeingSynced(root [32]byte) (primitives.PTCStatus, bool) { - return s.payloadBeingSynced.isSyncing(root) -} - // BlockBeingSynced returns whether the block with the given root is currently being synced func (s *Service) BlockBeingSynced(root [32]byte) bool { return s.blockBeingSynced.isSyncing(root) diff --git a/beacon-chain/blockchain/chain_info_forkchoice.go b/beacon-chain/blockchain/chain_info_forkchoice.go index c3d382c4c37c..ee90f5612ec8 100644 --- a/beacon-chain/blockchain/chain_info_forkchoice.go +++ b/beacon-chain/blockchain/chain_info_forkchoice.go @@ -30,11 +30,11 @@ func (s *Service) SetForkChoiceGenesisTime(timestamp uint64) { s.cfg.ForkChoiceStore.SetGenesisTime(timestamp) } -// HighestReceivedBlockSlot returns the corresponding value from forkchoice -func (s *Service) HighestReceivedBlockSlot() primitives.Slot { +// HighestReceivedBlockSlotRoot returns the corresponding value from forkchoice +func (s *Service) HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) { s.cfg.ForkChoiceStore.RLock() defer s.cfg.ForkChoiceStore.RUnlock() - return s.cfg.ForkChoiceStore.HighestReceivedBlockSlot() + return s.cfg.ForkChoiceStore.HighestReceivedBlockSlotRoot() } // ReceivedBlocksLastEpoch returns the corresponding value from forkchoice diff --git a/beacon-chain/blockchain/testing/mock.go b/beacon-chain/blockchain/testing/mock.go index d38485fd5208..d2e4251ecd89 100644 --- a/beacon-chain/blockchain/testing/mock.go +++ b/beacon-chain/blockchain/testing/mock.go @@ -77,6 +77,9 @@ type ChainService struct { SyncingRoot [32]byte Blobs []blocks.VerifiedROBlob TargetRoot [32]byte + HighestReceivedSlot primitives.Slot + HighestReceivedRoot [32]byte + PayloadStatus primitives.PTCStatus ReceivePayloadAttestationMessageErr error } @@ -625,12 +628,12 @@ func (s *ChainService) ReceivedBlocksLastEpoch() (uint64, error) { return 0, nil } -// HighestReceivedBlockSlot mocks the same method in the chain service -func (s *ChainService) HighestReceivedBlockSlot() primitives.Slot { +// HighestReceivedBlockSlotRoot mocks the same method in the chain service +func (s *ChainService) HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) { if s.ForkChoiceStore != nil { - return s.ForkChoiceStore.HighestReceivedBlockSlot() + return s.ForkChoiceStore.HighestReceivedBlockSlotRoot() } - return 0 + return s.HighestReceivedSlot, s.HighestReceivedRoot } // InsertNode mocks the same method in the chain service @@ -700,3 +703,7 @@ func (c *ChainService) HashInForkchoice([32]byte) bool { func (c *ChainService) ReceivePayloadAttestationMessage(_ context.Context, _ *ethpb.PayloadAttestationMessage) error { return c.ReceivePayloadAttestationMessageErr } + +func (c *ChainService) GetPTCVote(root [32]byte) primitives.PTCStatus { + return c.PayloadStatus +} diff --git a/beacon-chain/forkchoice/doubly-linked-tree/store.go b/beacon-chain/forkchoice/doubly-linked-tree/store.go index f590e1e2729a..e79f36b28da9 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/store.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/store.go @@ -249,12 +249,12 @@ func (s *Store) tips() ([][32]byte, []primitives.Slot) { return roots, slots } -// HighestReceivedBlockSlot returns the highest slot received by the forkchoice -func (f *ForkChoice) HighestReceivedBlockSlot() primitives.Slot { +// HighestReceivedBlockSlotRoot returns the highest slot and root received by the forkchoice +func (f *ForkChoice) HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) { if f.store.highestReceivedNode == nil { - return 0 + return 0, [32]byte{} } - return f.store.highestReceivedNode.slot + return f.store.highestReceivedNode.slot, f.store.highestReceivedNode.root } // HighestReceivedBlockSlotDelay returns the number of slots that the highest diff --git a/beacon-chain/forkchoice/doubly-linked-tree/store_test.go b/beacon-chain/forkchoice/doubly-linked-tree/store_test.go index c345ba62a900..cbac5b9a725d 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/store_test.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/store_test.go @@ -332,7 +332,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err := f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(1), count) - require.Equal(t, primitives.Slot(1), f.HighestReceivedBlockSlot()) + slot, _ := f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(1), slot) require.Equal(t, primitives.Slot(0), f.HighestReceivedBlockDelay()) // 64 @@ -343,7 +344,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(1), count) - require.Equal(t, primitives.Slot(64), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(64), slot) require.Equal(t, primitives.Slot(0), f.HighestReceivedBlockDelay()) // 64 65 @@ -354,7 +356,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(2), count) - require.Equal(t, primitives.Slot(65), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(65), slot) require.Equal(t, primitives.Slot(1), f.HighestReceivedBlockDelay()) // 64 65 66 @@ -365,7 +368,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(3), count) - require.Equal(t, primitives.Slot(66), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(66), slot) // 64 65 66 // 98 @@ -376,7 +380,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(1), count) - require.Equal(t, primitives.Slot(98), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(98), slot) // 64 65 66 // 98 @@ -388,7 +393,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(1), count) - require.Equal(t, primitives.Slot(132), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(132), slot) // 64 65 66 // 98 @@ -401,7 +407,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(1), count) - require.Equal(t, primitives.Slot(132), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(132), slot) // 64 65 66 // 98 @@ -414,7 +421,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(1), count) - require.Equal(t, primitives.Slot(132), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(132), slot) // 64 65 66 // 98 @@ -427,7 +435,8 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) { count, err = f.ReceivedBlocksLastEpoch() require.NoError(t, err) require.Equal(t, uint64(2), count) - require.Equal(t, primitives.Slot(132), f.HighestReceivedBlockSlot()) + slot, _ = f.HighestReceivedBlockSlotRoot() + require.Equal(t, primitives.Slot(132), slot) s.genesisTime = uint64(time.Now().Add(time.Duration(-134*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second).Unix()) count, err = f.ReceivedBlocksLastEpoch() diff --git a/beacon-chain/forkchoice/interfaces.go b/beacon-chain/forkchoice/interfaces.go index f26496f0ebec..791b512bfe50 100644 --- a/beacon-chain/forkchoice/interfaces.go +++ b/beacon-chain/forkchoice/interfaces.go @@ -70,7 +70,7 @@ type FastGetter interface { FinalizedPayloadBlockHash() [32]byte HasNode([32]byte) bool HasHash([32]byte) bool - HighestReceivedBlockSlot() primitives.Slot + HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) HighestReceivedBlockDelay() primitives.Slot IsCanonical(root [32]byte) bool IsOptimistic(root [32]byte) (bool, error) diff --git a/beacon-chain/forkchoice/ro.go b/beacon-chain/forkchoice/ro.go index 7199372b6ed6..c59c6d4e6b36 100644 --- a/beacon-chain/forkchoice/ro.go +++ b/beacon-chain/forkchoice/ro.go @@ -114,11 +114,11 @@ func (ro *ROForkChoice) NodeCount() int { return ro.getter.NodeCount() } -// HighestReceivedBlockSlot delegates to the underlying forkchoice call, under a lock. -func (ro *ROForkChoice) HighestReceivedBlockSlot() primitives.Slot { +// HighestReceivedBlockSlotRoot delegates to the underlying forkchoice call, under a lock. +func (ro *ROForkChoice) HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) { ro.l.RLock() defer ro.l.RUnlock() - return ro.getter.HighestReceivedBlockSlot() + return ro.getter.HighestReceivedBlockSlotRoot() } // HighestReceivedBlockDelay delegates to the underlying forkchoice call, under a lock. diff --git a/beacon-chain/forkchoice/ro_test.go b/beacon-chain/forkchoice/ro_test.go index cc69905bc584..9dc0c367cca0 100644 --- a/beacon-chain/forkchoice/ro_test.go +++ b/beacon-chain/forkchoice/ro_test.go @@ -29,7 +29,7 @@ const ( justifiedPayloadBlockHashCalled unrealizedJustifiedPayloadBlockHashCalled nodeCountCalled - highestReceivedBlockSlotCalled + highestReceivedBlockSlotRootCalled highestReceivedBlockDelayCalled receivedBlocksLastEpochCalled weightCalled @@ -113,9 +113,9 @@ func TestROLocking(t *testing.T) { cb: func(g FastGetter) { g.NodeCount() }, }, { - name: "highestReceivedBlockSlotCalled", - call: highestReceivedBlockSlotCalled, - cb: func(g FastGetter) { g.HighestReceivedBlockSlot() }, + name: "highestReceivedBlockSlotRootCalled", + call: highestReceivedBlockSlotRootCalled, + cb: func(g FastGetter) { g.HighestReceivedBlockSlotRoot() }, }, { name: "highestReceivedBlockDelayCalled", @@ -254,9 +254,9 @@ func (ro *mockROForkchoice) NodeCount() int { return 0 } -func (ro *mockROForkchoice) HighestReceivedBlockSlot() primitives.Slot { - ro.calls = append(ro.calls, highestReceivedBlockSlotCalled) - return 0 +func (ro *mockROForkchoice) HighestReceivedBlockSlotRoot() (primitives.Slot, [32]byte) { + ro.calls = append(ro.calls, highestReceivedBlockSlotRootCalled) + return 0, [32]byte{} } func (ro *mockROForkchoice) HighestReceivedBlockDelay() primitives.Slot { diff --git a/beacon-chain/rpc/eth/beacon/handlers.go b/beacon-chain/rpc/eth/beacon/handlers.go index 33eeb9dbae24..eb0369bea6ca 100644 --- a/beacon-chain/rpc/eth/beacon/handlers.go +++ b/beacon-chain/rpc/eth/beacon/handlers.go @@ -1079,7 +1079,8 @@ func (s *Server) validateConsensus(ctx context.Context, blk interfaces.ReadOnlyS } func (s *Server) validateEquivocation(blk interfaces.ReadOnlyBeaconBlock) error { - if s.ForkchoiceFetcher.HighestReceivedBlockSlot() == blk.Slot() { + slot, _ := s.ForkchoiceFetcher.HighestReceivedBlockSlotRoot() + if slot == blk.Slot() { return errors.Wrapf(errEquivocatedBlock, "block for slot %d already exists in fork choice", blk.Slot()) } return nil diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_builder.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_builder.go index 660aea71425a..987b4cc961d1 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_builder.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_builder.go @@ -57,7 +57,7 @@ func (vs *Server) circuitBreakBuilder(s primitives.Slot) (bool, error) { } // Circuit breaker is active if the missing consecutive slots greater than `MaxBuilderConsecutiveMissedSlots`. - highestReceivedSlot := vs.ForkchoiceFetcher.HighestReceivedBlockSlot() + highestReceivedSlot, _ := vs.ForkchoiceFetcher.HighestReceivedBlockSlotRoot() maxConsecutiveSkipSlotsAllowed := params.BeaconConfig().MaxBuilderConsecutiveMissedSlots diff, err := s.SafeSubSlot(highestReceivedSlot) if err != nil { diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester.go b/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester.go index 2a5fd0913045..c8f985466459 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester.go @@ -4,14 +4,32 @@ import ( "context" "github.com/golang/protobuf/ptypes/empty" - "github.com/pkg/errors" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +// GetPayloadAttestationData returns the payload attestation data for a given slot. +// The request slot must be the current slot and there must exist a block from the current slot or the request will fail. func (vs *Server) GetPayloadAttestationData(ctx context.Context, req *ethpb.GetPayloadAttestationDataRequest) (*ethpb.PayloadAttestationData, error) { - return nil, errors.New("not implemented") + reqSlot := req.Slot + currentSlot := vs.TimeFetcher.CurrentSlot() + if reqSlot != currentSlot { + return nil, status.Errorf(codes.InvalidArgument, "Payload attestation request slot %d != current slot %d", reqSlot, currentSlot) + } + + highestSlot, root := vs.ForkchoiceFetcher.HighestReceivedBlockSlotRoot() + if reqSlot != highestSlot { + return nil, status.Errorf(codes.Unavailable, "Did not receive current slot %d block ", reqSlot) + } + + payloadStatus := vs.ForkchoiceFetcher.GetPTCVote(root) + + return ðpb.PayloadAttestationData{ + BeaconBlockRoot: root[:], + Slot: highestSlot, + PayloadStatus: payloadStatus, + }, nil } // SubmitPayloadAttestation broadcasts a payload attestation message to the network and saves the payload attestation to the cache. diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester_test.go b/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester_test.go index a2abc0e68955..97c87bae66f8 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester_test.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/ptc_attester_test.go @@ -3,10 +3,14 @@ package validator import ( "context" "testing" + "time" "github.com/pkg/errors" mock "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" p2ptest "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p/testing" + + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/require" ) @@ -39,3 +43,65 @@ func TestServer_SubmitPayloadAttestation(t *testing.T) { require.NoError(t, err) }) } + +func TestServer_GetPayloadAttestationData(t *testing.T) { + ctx := context.Background() + t.Run("Not current slot", func(t *testing.T) { + s := &Server{ + TimeFetcher: &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0)}, + } + _, err := s.GetPayloadAttestationData(ctx, ðpb.GetPayloadAttestationDataRequest{Slot: 2}) + require.ErrorContains(t, "Payload attestation request slot 2 != current slot 1", err) + }) + + t.Run("Last received block is not from current slot", func(t *testing.T) { + s := &Server{ + TimeFetcher: &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(2*params.BeaconConfig().SecondsPerSlot), 0)}, + ForkchoiceFetcher: &mock.ChainService{HighestReceivedSlot: 1}, + } + _, err := s.GetPayloadAttestationData(ctx, ðpb.GetPayloadAttestationDataRequest{Slot: 2}) + require.ErrorContains(t, "Did not receive current slot 2 block ", err) + }) + + t.Run("Payload is absent", func(t *testing.T) { + slot := primitives.Slot(2) + root := [32]byte{1} + s := &Server{ + TimeFetcher: &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(2*params.BeaconConfig().SecondsPerSlot), 0)}, + ForkchoiceFetcher: &mock.ChainService{HighestReceivedSlot: slot, HighestReceivedRoot: root, PayloadStatus: primitives.PAYLOAD_ABSENT}, + } + d, err := s.GetPayloadAttestationData(ctx, ðpb.GetPayloadAttestationDataRequest{Slot: slot}) + require.NoError(t, err) + require.DeepEqual(t, root[:], d.BeaconBlockRoot) + require.Equal(t, slot, d.Slot) + require.Equal(t, primitives.PAYLOAD_ABSENT, d.PayloadStatus) + }) + + t.Run("Payload is present", func(t *testing.T) { + slot := primitives.Slot(2) + root := [32]byte{1} + s := &Server{ + TimeFetcher: &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(2*params.BeaconConfig().SecondsPerSlot), 0)}, + ForkchoiceFetcher: &mock.ChainService{HighestReceivedSlot: slot, HighestReceivedRoot: root, PayloadStatus: primitives.PAYLOAD_PRESENT}, + } + d, err := s.GetPayloadAttestationData(ctx, ðpb.GetPayloadAttestationDataRequest{Slot: slot}) + require.NoError(t, err) + require.DeepEqual(t, root[:], d.BeaconBlockRoot) + require.Equal(t, slot, d.Slot) + require.Equal(t, primitives.PAYLOAD_PRESENT, d.PayloadStatus) + }) + + t.Run("Payload is withheld", func(t *testing.T) { + slot := primitives.Slot(2) + root := [32]byte{1} + s := &Server{ + TimeFetcher: &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(2*params.BeaconConfig().SecondsPerSlot), 0)}, + ForkchoiceFetcher: &mock.ChainService{HighestReceivedSlot: slot, HighestReceivedRoot: root, PayloadStatus: primitives.PAYLOAD_WITHHELD}, + } + d, err := s.GetPayloadAttestationData(ctx, ðpb.GetPayloadAttestationDataRequest{Slot: slot}) + require.NoError(t, err) + require.DeepEqual(t, root[:], d.BeaconBlockRoot) + require.Equal(t, slot, d.Slot) + require.Equal(t, primitives.PAYLOAD_WITHHELD, d.PayloadStatus) + }) +}