diff --git a/beacon-chain/blockchain/BUILD.bazel b/beacon-chain/blockchain/BUILD.bazel index b60a763d0fde..2d5dd05f386d 100644 --- a/beacon-chain/blockchain/BUILD.bazel +++ b/beacon-chain/blockchain/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "chain_info.go", "chain_info_forkchoice.go", "currently_syncing_block.go", + "currently_syncing_execution_payload_envelope.go", "defragment.go", "error.go", "execution_engine.go", @@ -26,6 +27,7 @@ go_library( "receive_attestation.go", "receive_blob.go", "receive_block.go", + "receive_execution_payload_envelope.go", "service.go", "tracked_proposer.go", "weak_subjectivity_checks.go", @@ -44,6 +46,7 @@ go_library( "//beacon-chain/cache:go_default_library", "//beacon-chain/core/altair:go_default_library", "//beacon-chain/core/blocks:go_default_library", + "//beacon-chain/core/electra:go_default_library", "//beacon-chain/core/epoch/precompute:go_default_library", "//beacon-chain/core/feed:go_default_library", "//beacon-chain/core/feed/state:go_default_library", diff --git a/beacon-chain/blockchain/chain_info.go b/beacon-chain/blockchain/chain_info.go index f73652add1a8..b7745b8676f6 100644 --- a/beacon-chain/blockchain/chain_info.go +++ b/beacon-chain/blockchain/chain_info.go @@ -530,6 +530,11 @@ 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 +func (s *Service) PayloadBeingSynced(root [32]byte) 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/currently_syncing_execution_payload_envelope.go b/beacon-chain/blockchain/currently_syncing_execution_payload_envelope.go new file mode 100644 index 000000000000..df15abcb1c58 --- /dev/null +++ b/beacon-chain/blockchain/currently_syncing_execution_payload_envelope.go @@ -0,0 +1,27 @@ +package blockchain + +import "sync" + +type currentlySyncingPayload struct { + sync.Mutex + roots map[[32]byte]struct{} +} + +func (b *currentlySyncingPayload) set(root [32]byte) { + b.Lock() + defer b.Unlock() + b.roots[root] = struct{}{} +} + +func (b *currentlySyncingPayload) unset(root [32]byte) { + b.Lock() + defer b.Unlock() + delete(b.roots, root) +} + +func (b *currentlySyncingPayload) isSyncing(root [32]byte) bool { + b.Lock() + defer b.Unlock() + _, ok := b.roots[root] + return ok +} diff --git a/beacon-chain/blockchain/execution_engine.go b/beacon-chain/blockchain/execution_engine.go index db911b31ab4e..fc7365c82912 100644 --- a/beacon-chain/blockchain/execution_engine.go +++ b/beacon-chain/blockchain/execution_engine.go @@ -2,7 +2,6 @@ package blockchain import ( "context" - "crypto/sha256" "fmt" "github.com/ethereum/go-ethereum/common" @@ -28,8 +27,6 @@ import ( "go.opencensus.io/trace" ) -const blobCommitmentVersionKZG uint8 = 0x01 - var defaultLatestValidHash = bytesutil.PadTo([]byte{0xff}, 32) // notifyForkchoiceUpdate signals execution engine the fork choice updates. Execution engine should: @@ -402,13 +399,7 @@ func kzgCommitmentsToVersionedHashes(body interfaces.ReadOnlyBeaconBlockBody) ([ versionedHashes := make([]common.Hash, len(commitments)) for i, commitment := range commitments { - versionedHashes[i] = ConvertKzgCommitmentToVersionedHash(commitment) + versionedHashes[i] = primitives.ConvertKzgCommitmentToVersionedHash(commitment) } return versionedHashes, nil } - -func ConvertKzgCommitmentToVersionedHash(commitment []byte) common.Hash { - versionedHash := sha256.Sum256(commitment) - versionedHash[0] = blobCommitmentVersionKZG - return versionedHash -} diff --git a/beacon-chain/blockchain/execution_engine_test.go b/beacon-chain/blockchain/execution_engine_test.go index be5d700d52ea..e2695f579524 100644 --- a/beacon-chain/blockchain/execution_engine_test.go +++ b/beacon-chain/blockchain/execution_engine_test.go @@ -1056,8 +1056,8 @@ func TestService_removeInvalidBlockAndState(t *testing.T) { require.NoError(t, service.removeInvalidBlockAndState(ctx, [][32]byte{r1, r2})) - require.Equal(t, false, service.hasBlock(ctx, r1)) - require.Equal(t, false, service.hasBlock(ctx, r2)) + require.Equal(t, false, service.chainHasBlock(ctx, r1)) + require.Equal(t, false, service.chainHasBlock(ctx, r2)) require.Equal(t, false, service.cfg.BeaconDB.HasStateSummary(ctx, r1)) require.Equal(t, false, service.cfg.BeaconDB.HasStateSummary(ctx, r2)) has, err := service.cfg.StateGen.HasState(ctx, r1) diff --git a/beacon-chain/blockchain/log.go b/beacon-chain/blockchain/log.go index b8f4e0d94168..8e56ac5c5e05 100644 --- a/beacon-chain/blockchain/log.go +++ b/beacon-chain/blockchain/log.go @@ -6,7 +6,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/blocks" "github.com/prysmaticlabs/prysm/v5/config/params" consensus_types "github.com/prysmaticlabs/prysm/v5/consensus-types" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" @@ -111,18 +110,7 @@ func logBlockSyncStatus(block interfaces.ReadOnlyBeaconBlock, blockRoot [32]byte } // logs payload related data every slot. -func logPayload(block interfaces.ReadOnlyBeaconBlock) error { - isExecutionBlk, err := blocks.IsExecutionBlock(block.Body()) - if err != nil { - return errors.Wrap(err, "could not determine if block is execution block") - } - if !isExecutionBlk { - return nil - } - payload, err := block.Body().Execution() - if err != nil { - return err - } +func logPayload(ver int, payload interfaces.ExecutionData) error { if payload.GasLimit() == 0 { return errors.New("gas limit should not be 0") } @@ -133,17 +121,12 @@ func logPayload(block interfaces.ReadOnlyBeaconBlock) error { "blockNumber": payload.BlockNumber(), "gasUtilized": fmt.Sprintf("%.2f", gasUtilized), } - if block.Version() >= version.Capella { + if ver >= version.Capella { withdrawals, err := payload.Withdrawals() if err != nil { return errors.Wrap(err, "could not get withdrawals") } fields["withdrawals"] = len(withdrawals) - changes, err := block.Body().BLSToExecutionChanges() - if err != nil { - return errors.Wrap(err, "could not get BLSToExecutionChanges") - } - fields["blsToExecutionChanges"] = len(changes) } log.WithFields(fields).Debug("Synced new payload") return nil diff --git a/beacon-chain/blockchain/metrics.go b/beacon-chain/blockchain/metrics.go index 8abcce3a2c52..551a48b8a4c4 100644 --- a/beacon-chain/blockchain/metrics.go +++ b/beacon-chain/blockchain/metrics.go @@ -182,6 +182,10 @@ var ( Name: "chain_service_processing_milliseconds", Help: "Total time to call a chain service in ReceiveBlock()", }) + executionEngineProcessingTime = promauto.NewSummary(prometheus.SummaryOpts{ + Name: "execution_engine_processing_milliseconds", + Help: "Total time to process an execution payload envelope in ReceiveExecutionPayloadEnvelope()", + }) dataAvailWaitedTime = promauto.NewSummary(prometheus.SummaryOpts{ Name: "da_waited_time_milliseconds", Help: "Total time spent waiting for a data availability check in ReceiveBlock()", diff --git a/beacon-chain/blockchain/receive_attestation.go b/beacon-chain/blockchain/receive_attestation.go index 89f344383cf6..05b233e2420f 100644 --- a/beacon-chain/blockchain/receive_attestation.go +++ b/beacon-chain/blockchain/receive_attestation.go @@ -176,7 +176,7 @@ func (s *Service) processAttestations(ctx context.Context, disparity time.Durati } hasState := s.cfg.BeaconDB.HasStateSummary(ctx, bytesutil.ToBytes32(a.GetData().BeaconBlockRoot)) - hasBlock := s.hasBlock(ctx, bytesutil.ToBytes32(a.GetData().BeaconBlockRoot)) + hasBlock := s.chainHasBlock(ctx, bytesutil.ToBytes32(a.GetData().BeaconBlockRoot)) if !(hasState && hasBlock) { continue } diff --git a/beacon-chain/blockchain/receive_block.go b/beacon-chain/blockchain/receive_block.go index 42f9a9542f55..d212bb702c98 100644 --- a/beacon-chain/blockchain/receive_block.go +++ b/beacon-chain/blockchain/receive_block.go @@ -206,8 +206,14 @@ func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySig log.WithError(err).Error("Unable to log block sync status") } // Log payload data - if err := logPayload(blockCopy.Block()); err != nil { - log.WithError(err).Error("Unable to log debug block payload data") + ver := blockCopy.Version() + if ver >= version.Bellatrix { + payload, err := blockCopy.Block().Body().Execution() + if err != nil { + log.WithError(err).Error("unable to get execution payload") + } else if err := logPayload(ver, payload); err != nil { + log.WithError(err).Error("Unable to log debug block payload data") + } } // Log state transition data. if err := logStateTransitionData(blockCopy.Block()); err != nil { diff --git a/beacon-chain/blockchain/receive_execution_payload_envelope.go b/beacon-chain/blockchain/receive_execution_payload_envelope.go new file mode 100644 index 000000000000..30a163c544be --- /dev/null +++ b/beacon-chain/blockchain/receive_execution_payload_envelope.go @@ -0,0 +1,251 @@ +package blockchain + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/das" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/sirupsen/logrus" + "go.opencensus.io/trace" + "golang.org/x/sync/errgroup" +) + +// ReceiveExecutionPayloadEnvelope is a function that defines the operations (minus pubsub) +// that are performed on a received execution payload envelope. The operations consist of: +// 1. Validate the payload, apply state transition. +// 2. Apply fork choice to the processed payload +// 3. Save latest head info +func (s *Service) ReceiveExecutionPayloadEnvelope(ctx context.Context, envelope interfaces.ROExecutionPayloadEnvelope, _ das.AvailabilityStore) error { + receivedTime := time.Now() + root, err := envelope.BeaconBlockRoot() + if err != nil { + return errors.Wrap(err, "could not get beacon block root") + } + s.payloadBeingSynced.set(root) + defer s.payloadBeingSynced.unset(root) + + preState, err := s.getPayloadEnvelopePrestate(ctx, envelope) + if err != nil { + return errors.Wrap(err, "could not get prestate") + } + + eg, _ := errgroup.WithContext(ctx) + var postState state.BeaconState + eg.Go(func() error { + var err error + postState, err = validatePayloadStateTransition(ctx, preState, envelope) + if err != nil { + return errors.Wrap(err, "failed to validate consensus state transition function") + } + return nil + }) + var isValidPayload bool + eg.Go(func() error { + var err error + isValidPayload, err = s.validateExecutionOnEnvelope(ctx, envelope, root) + if err != nil { + return errors.Wrap(err, "could not notify the engine of the new payload") + } + return nil + }) + + if err := eg.Wait(); err != nil { + return err + } + _ = isValidPayload + _ = postState + daStartTime := time.Now() + // TODO: Add DA check + daWaitedTime := time.Since(daStartTime) + dataAvailWaitedTime.Observe(float64(daWaitedTime.Milliseconds())) + // TODO: Add Head update, cache handling, postProcessing + timeWithoutDaWait := time.Since(receivedTime) - daWaitedTime + executionEngineProcessingTime.Observe(float64(timeWithoutDaWait.Milliseconds())) + return nil +} + +func validatePayloadStateTransition( + ctx context.Context, + preState state.BeaconState, + envelope interfaces.ROExecutionPayloadEnvelope, +) (state.BeaconState, error) { + blockHeader := preState.LatestBlockHeader() + if blockHeader == nil { + return nil, errors.New("invalid nil latest block header") + } + if blockHeader.StateRoot == nil || [32]byte(blockHeader.StateRoot) == [32]byte{} { + prevStateRoot, err := preState.HashTreeRoot(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not compute previous state root") + } + blockHeader.StateRoot = prevStateRoot[:] + if err := preState.SetLatestBlockHeader(blockHeader); err != nil { + return nil, errors.Wrap(err, "could not set latest block header") + } + } + blockHeaderRoot, err := blockHeader.HashTreeRoot() + if err != nil { + return nil, err + } + beaconBlockRoot, err := envelope.BeaconBlockRoot() + if err != nil { + return nil, err + } + if blockHeaderRoot != beaconBlockRoot { + return nil, errors.New("beacon block root does not match previous header") + } + committedHeader, err := preState.LatestExecutionPayloadHeaderEPBS() + if err != nil { + return nil, err + } + builderIndex, err := envelope.BuilderIndex() + if err != nil { + return nil, err + } + if committedHeader.BuilderIndex != builderIndex { + return nil, errors.New("builder index does not match committed header") + } + kzgRoot, err := envelope.BlobKzgCommitmentsRoot() + if err != nil { + return nil, err + } + if [32]byte(committedHeader.BlobKzgCommitmentsRoot) != kzgRoot { + return nil, errors.New("blob KZG commitments root does not match committed header") + } + payload, err := envelope.Execution() + if err != nil { + return nil, err + } + exe, ok := payload.(interfaces.ExecutionDataElectra) + if !ok { + return nil, errors.New("could not cast execution data to electra execution data") + } + preState, err = electra.ProcessDepositRequests(ctx, preState, exe.DepositRequests()) + if err != nil { + return nil, errors.Wrap(err, "could not process deposit receipts") + } + preState, err = electra.ProcessWithdrawalRequests(ctx, preState, exe.WithdrawalRequests()) + if err != nil { + return nil, errors.Wrap(err, "could not process execution layer withdrawal requests") + } + if err := electra.ProcessConsolidationRequests(ctx, preState, exe.ConsolidationRequests()); err != nil { + return nil, errors.Wrap(err, "could not process consolidation requests") + } + if err := preState.SetLatestBlockHash(payload.BlockHash()); err != nil { + return nil, err + } + if err := preState.SetLatestFullSlot(preState.Slot()); err != nil { + return nil, err + } + stateRoot, err := preState.HashTreeRoot(ctx) + if err != nil { + return nil, err + } + envelopeStateRoot, err := envelope.StateRoot() + if err != nil { + return nil, err + } + if stateRoot != envelopeStateRoot { + return nil, errors.New("state root mismatch") + } + return preState, nil +} + +// notifyNewPayload signals execution engine on a new payload. +// It returns true if the EL has returned VALID for the block +func (s *Service) notifyNewEnvelope(ctx context.Context, envelope interfaces.ROExecutionPayloadEnvelope) (bool, error) { + ctx, span := trace.StartSpan(ctx, "blockChain.notifyNewPayload") + defer span.End() + + payload, err := envelope.Execution() + if err != nil { + return false, errors.Wrap(invalidBlock{error: err}, "could not get execution payload") + } + + var lastValidHash []byte + var versionedHashes []common.Hash + versionedHashes, err = envelope.VersionedHashes() + if err != nil { + return false, errors.Wrap(err, "could not get versioned hashes to feed the engine") + } + root, err := envelope.BeaconBlockRoot() + if err != nil { + return false, errors.Wrap(err, "could not get beacon block root") + } + parentRoot, err := s.ParentRoot(root) + if err != nil { + return false, errors.Wrap(err, "could not get parent block root") + } + pr := common.Hash(parentRoot) + lastValidHash, err = s.cfg.ExecutionEngineCaller.NewPayload(ctx, payload, versionedHashes, &pr) + switch { + case err == nil: + newPayloadValidNodeCount.Inc() + return true, nil + case errors.Is(err, execution.ErrAcceptedSyncingPayloadStatus): + newPayloadOptimisticNodeCount.Inc() + log.WithFields(logrus.Fields{ + "payloadBlockHash": fmt.Sprintf("%#x", bytesutil.Trunc(payload.BlockHash())), + }).Info("Called new payload with optimistic block") + return false, nil + case errors.Is(err, execution.ErrInvalidPayloadStatus): + lvh := bytesutil.ToBytes32(lastValidHash) + return false, invalidBlock{ + error: ErrInvalidPayload, + lastValidHash: lvh, + } + default: + return false, errors.WithMessage(ErrUndefinedExecutionEngineError, err.Error()) + } +} + +// validateExecutionOnEnvelope notifies the engine of the incoming execution payload and returns true if the payload is valid +func (s *Service) validateExecutionOnEnvelope(ctx context.Context, e interfaces.ROExecutionPayloadEnvelope, parentRoot [32]byte) (bool, error) { + isValidPayload, err := s.notifyNewEnvelope(ctx, e) + if err != nil { + blockRoot, err := e.BeaconBlockRoot() + if err != nil { + return false, err + } + s.cfg.ForkChoiceStore.Lock() + err = s.handleInvalidExecutionError(ctx, err, blockRoot, parentRoot) + s.cfg.ForkChoiceStore.Unlock() + return false, err + } + return isValidPayload, nil +} + +func (s *Service) getPayloadEnvelopePrestate(ctx context.Context, e interfaces.ROExecutionPayloadEnvelope) (state.BeaconState, error) { + ctx, span := trace.StartSpan(ctx, "blockChain.getPayloadEnvelopePreState") + defer span.End() + + // Verify incoming payload has a valid pre state. + root, err := e.BeaconBlockRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get beacon block root") + } + // Verify the referred block is known to forkchoice + if !s.InForkchoice(root) { + return nil, errors.New("Cannot import execution payload envelope for unknown block") + } + if err := s.verifyBlkPreState(ctx, root); err != nil { + return nil, errors.Wrap(err, "could not verify payload prestate") + } + + preState, err := s.cfg.StateGen.StateByRoot(ctx, root) + if err != nil { + return nil, errors.Wrap(err, "could not get pre state") + } + if preState == nil || preState.IsNil() { + return nil, errors.Wrap(err, "nil pre state") + } + return preState, nil +} diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index efbc70c7941f..74a2a9f29a56 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -65,6 +65,7 @@ type Service struct { syncComplete chan struct{} blobNotifiers *blobNotifierMap blockBeingSynced *currentlySyncingBlock + payloadBeingSynced *currentlySyncingPayload blobStorage *filesystem.BlobStorage lastPublishedLightClientEpoch primitives.Epoch } @@ -544,7 +545,7 @@ func (s *Service) saveGenesisData(ctx context.Context, genesisState state.Beacon // 2.) Check DB. // Checking 1.) is ten times faster than checking 2.) // this function requires a lock in forkchoice -func (s *Service) hasBlock(ctx context.Context, root [32]byte) bool { +func (s *Service) chainHasBlock(ctx context.Context, root [32]byte) bool { if s.cfg.ForkChoiceStore.HasNode(root) { return true } diff --git a/beacon-chain/blockchain/service_test.go b/beacon-chain/blockchain/service_test.go index a2bf18ac1485..31ec0a6ab7ea 100644 --- a/beacon-chain/blockchain/service_test.go +++ b/beacon-chain/blockchain/service_test.go @@ -382,8 +382,8 @@ func TestHasBlock_ForkChoiceAndDB_DoublyLinkedTree(t *testing.T) { require.NoError(t, err) require.NoError(t, s.cfg.ForkChoiceStore.InsertNode(ctx, beaconState, r)) - assert.Equal(t, false, s.hasBlock(ctx, [32]byte{}), "Should not have block") - assert.Equal(t, true, s.hasBlock(ctx, r), "Should have block") + assert.Equal(t, false, s.chainHasBlock(ctx, [32]byte{}), "Should not have block") + assert.Equal(t, true, s.chainHasBlock(ctx, r), "Should have block") } func TestServiceStop_SaveCachedBlocks(t *testing.T) { diff --git a/beacon-chain/rpc/eth/events/BUILD.bazel b/beacon-chain/rpc/eth/events/BUILD.bazel index 37d627b50820..5671e3f72f0a 100644 --- a/beacon-chain/rpc/eth/events/BUILD.bazel +++ b/beacon-chain/rpc/eth/events/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "//beacon-chain/core/time:go_default_library", "//beacon-chain/core/transition:go_default_library", "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", "//network/httputil:go_default_library", "//proto/eth/v1:go_default_library", "//proto/eth/v2:go_default_library", diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index c4782f8c14b3..04ceb53524b2 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api" "github.com/prysmaticlabs/prysm/v5/api/server/structs" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed/operation" statefeed "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/feed/state" @@ -19,6 +18,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/transition" "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/eth/v1" ethpbv2 "github.com/prysmaticlabs/prysm/v5/proto/eth/v2" @@ -217,7 +217,7 @@ func handleBlockOperationEvents(w http.ResponseWriter, flusher http.Flusher, req if !ok { return write(w, flusher, topicDataMismatch, event.Data, BlobSidecarTopic) } - versionedHash := blockchain.ConvertKzgCommitmentToVersionedHash(blobData.Blob.KzgCommitment) + versionedHash := primitives.ConvertKzgCommitmentToVersionedHash(blobData.Blob.KzgCommitment) blobEvent := &structs.BlobSidecarEvent{ BlockRoot: hexutil.Encode(blobData.Blob.BlockRootSlice()), Index: fmt.Sprintf("%d", blobData.Blob.Index), diff --git a/consensus-types/epbs/BUILD.bazel b/consensus-types/epbs/BUILD.bazel index 8d2f8cb29f6e..b84d1e9eb037 100644 --- a/consensus-types/epbs/BUILD.bazel +++ b/consensus-types/epbs/BUILD.bazel @@ -2,11 +2,21 @@ load("@prysm//tools/go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["indexed_payload_attestation.go"], + srcs = [ + "execution_payload_envelope.go", + "indexed_payload_attestation.go", + ], importpath = "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs", visibility = ["//visibility:public"], deps = [ + "//config/fieldparams:go_default_library", + "//consensus-types:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", + "//encoding/ssz:go_default_library", + "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", ], ) diff --git a/consensus-types/epbs/execution_payload_envelope.go b/consensus-types/epbs/execution_payload_envelope.go new file mode 100644 index 000000000000..56192e7584c7 --- /dev/null +++ b/consensus-types/epbs/execution_payload_envelope.go @@ -0,0 +1,145 @@ +package epbs + +import ( + "github.com/ethereum/go-ethereum/common" + field_params "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + consensus_types "github.com/prysmaticlabs/prysm/v5/consensus-types" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/encoding/ssz" + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" +) + +type signedExecutionPayloadEnvelope struct { + s *enginev1.SignedExecutionPayloadEnvelope +} + +type executionPayloadEnvelope struct { + p *enginev1.ExecutionPayloadEnvelope +} + +// WrappedROSignedExecutionPayloadEnvelope is a constructor which wraps a +// protobuf signed execution payload envelope into an interface. +func WrappedROSignedExecutionPayloadEnvelope(s *enginev1.SignedExecutionPayloadEnvelope) (interfaces.ROSignedExecutionPayloadEnvelope, error) { + w := signedExecutionPayloadEnvelope{s: s} + if w.IsNil() { + return nil, consensus_types.ErrNilObjectWrapped + } + return w, nil +} + +// WrappedROExecutionPayloadEnvelope is a constructor which wraps a +// protobuf execution payload envelope into an interface. +func WrappedROExecutionPayloadEnvelope(p *enginev1.ExecutionPayloadEnvelope) (interfaces.ROExecutionPayloadEnvelope, error) { + w := executionPayloadEnvelope{p: p} + if w.IsNil() { + return nil, consensus_types.ErrNilObjectWrapped + } + return w, nil +} + +// Envelope returns the wrapped object as an interface +func (s signedExecutionPayloadEnvelope) Envelope() (interfaces.ROExecutionPayloadEnvelope, error) { + return WrappedROExecutionPayloadEnvelope(s.s.Message) +} + +// Signature returns the wrapped value +func (s signedExecutionPayloadEnvelope) Signature() ([field_params.BLSSignatureLength]byte, error) { + if s.IsNil() { + return [field_params.BLSSignatureLength]byte{}, consensus_types.ErrNilObjectWrapped + } + return [field_params.BLSSignatureLength]byte(s.s.Signature), nil +} + +// IsNil returns whether the wrapped value is nil +func (s signedExecutionPayloadEnvelope) IsNil() bool { + return s.s == nil +} + +// IsNil returns whether the wrapped value is nil +func (p executionPayloadEnvelope) IsNil() bool { + return p.p == nil +} + +// IsBlinded returns whether the wrapped value is blinded +func (p executionPayloadEnvelope) IsBlinded() bool { + return !p.IsNil() && p.p.Payload == nil +} + +// Execution returns the wrapped payload as an interface +func (p executionPayloadEnvelope) Execution() (interfaces.ExecutionData, error) { + if p.IsBlinded() { + return nil, consensus_types.ErrNilObjectWrapped + } + return blocks.WrappedExecutionPayloadElectra(p.p.Payload) +} + +// BuilderIndex returns the wrapped value +func (p executionPayloadEnvelope) BuilderIndex() (primitives.ValidatorIndex, error) { + if p.IsNil() { + return 0, consensus_types.ErrNilObjectWrapped + } + return p.p.BuilderIndex, nil +} + +// BeaconBlockRoot returns the wrapped value +func (p executionPayloadEnvelope) BeaconBlockRoot() ([field_params.RootLength]byte, error) { + if p.IsNil() { + return [field_params.RootLength]byte{}, consensus_types.ErrNilObjectWrapped + } + return [field_params.RootLength]byte(p.p.BeaconBlockRoot), nil +} + +// BlobKzgCommitments returns the wrapped value +func (p executionPayloadEnvelope) BlobKzgCommitments() ([][]byte, error) { + if p.IsNil() { + return nil, consensus_types.ErrNilObjectWrapped + } + commitments := make([][]byte, len(p.p.BlobKzgCommitments)) + for i, commit := range p.p.BlobKzgCommitments { + commitments[i] = make([]byte, len(commit)) + copy(commitments[i], commit) + } + return commitments, nil +} + +// PayloadWithheld returns the wrapped value +func (p executionPayloadEnvelope) PayloadWithheld() (bool, error) { + if p.IsBlinded() { + return false, consensus_types.ErrNilObjectWrapped + } + return p.p.PayloadWithheld, nil +} + +// StateRoot returns the wrapped value +func (p executionPayloadEnvelope) StateRoot() ([field_params.RootLength]byte, error) { + if p.IsNil() { + return [field_params.RootLength]byte{}, consensus_types.ErrNilObjectWrapped + } + return [field_params.RootLength]byte(p.p.StateRoot), nil +} + +// VersionedHashes returns the Versioned Hashes of the KZG commitments within +// the envelope +func (p executionPayloadEnvelope) VersionedHashes() ([]common.Hash, error) { + if p.IsNil() { + return nil, consensus_types.ErrNilObjectWrapped + } + + commitments := p.p.BlobKzgCommitments + versionedHashes := make([]common.Hash, len(commitments)) + for i, commitment := range commitments { + versionedHashes[i] = primitives.ConvertKzgCommitmentToVersionedHash(commitment) + } + return versionedHashes, nil +} + +// BlobKzgCommitmentsRoot returns the HTR of the KZG commitments in the payload +func (p executionPayloadEnvelope) BlobKzgCommitmentsRoot() ([field_params.RootLength]byte, error) { + if p.IsNil() { + return [field_params.RootLength]byte{}, consensus_types.ErrNilObjectWrapped + } + + return ssz.KzgCommitmentsRoot(p.p.BlobKzgCommitments) +} diff --git a/consensus-types/interfaces/BUILD.bazel b/consensus-types/interfaces/BUILD.bazel index 9e17fe3adf00..54b6268e0ae8 100644 --- a/consensus-types/interfaces/BUILD.bazel +++ b/consensus-types/interfaces/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "beacon_block.go", "error.go", + "execution_payload_envelope.go", "utils.go", "validator.go", ], @@ -17,6 +18,7 @@ go_library( "//proto/prysm/v1alpha1:go_default_library", "//proto/prysm/v1alpha1/validator-client:go_default_library", "//runtime/version:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", diff --git a/consensus-types/interfaces/execution_payload_envelope.go b/consensus-types/interfaces/execution_payload_envelope.go new file mode 100644 index 000000000000..1604b8e77a23 --- /dev/null +++ b/consensus-types/interfaces/execution_payload_envelope.go @@ -0,0 +1,26 @@ +package interfaces + +import ( + "github.com/ethereum/go-ethereum/common" + field_params "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" +) + +type ROSignedExecutionPayloadEnvelope interface { + Envelope() (ROExecutionPayloadEnvelope, error) + Signature() ([field_params.BLSSignatureLength]byte, error) + IsNil() bool +} + +type ROExecutionPayloadEnvelope interface { + Execution() (ExecutionData, error) + BuilderIndex() (primitives.ValidatorIndex, error) + BeaconBlockRoot() ([field_params.RootLength]byte, error) + BlobKzgCommitments() ([][]byte, error) + BlobKzgCommitmentsRoot() ([field_params.RootLength]byte, error) + VersionedHashes() ([]common.Hash, error) + PayloadWithheld() (bool, error) + StateRoot() ([field_params.RootLength]byte, error) + IsBlinded() bool + IsNil() bool +} diff --git a/consensus-types/primitives/BUILD.bazel b/consensus-types/primitives/BUILD.bazel index b3a73a307fe7..1ec9295ce799 100644 --- a/consensus-types/primitives/BUILD.bazel +++ b/consensus-types/primitives/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "domain.go", "epoch.go", "execution_address.go", + "kzg.go", "payload_id.go", "ptc_status.go", "randao.go", @@ -22,6 +23,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//math:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library", "@com_github_prysmaticlabs_go_bitfield//:go_default_library", ], diff --git a/consensus-types/primitives/kzg.go b/consensus-types/primitives/kzg.go new file mode 100644 index 000000000000..c59b5557d52f --- /dev/null +++ b/consensus-types/primitives/kzg.go @@ -0,0 +1,15 @@ +package primitives + +import ( + "crypto/sha256" + + "github.com/ethereum/go-ethereum/common" +) + +const blobCommitmentVersionKZG uint8 = 0x01 + +func ConvertKzgCommitmentToVersionedHash(commitment []byte) common.Hash { + versionedHash := sha256.Sum256(commitment) + versionedHash[0] = blobCommitmentVersionKZG + return versionedHash +} diff --git a/encoding/ssz/htrutils.go b/encoding/ssz/htrutils.go index d0581d47e809..d3a6c5e4a4a0 100644 --- a/encoding/ssz/htrutils.go +++ b/encoding/ssz/htrutils.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "github.com/pkg/errors" + "github.com/prysmaticlabs/gohashtree" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" @@ -116,6 +117,31 @@ func TransactionsRoot(txs [][]byte) ([32]byte, error) { return MixInLength(bytesRoot, bytesRootBufRoot), nil } +// KzgCommitmentsRoot computes the HTR for a list of KZG commitments +func KzgCommitmentsRoot(commitments [][]byte) ([32]byte, error) { + hash := [32]byte{} + leaves := make([][32]byte, 2*len(commitments)) + for i, kzg := range commitments { + copy(leaves[2*i][:], kzg[:32]) + copy(leaves[2*i+1][:], kzg[32:]) + } + if err := gohashtree.Hash(leaves, leaves); err != nil { + return hash, err + } + + bytesRoot, err := BitwiseMerkleize(leaves[:len(commitments)], uint64(len(commitments)), fieldparams.MaxBlobCommitmentsPerBlock) + if err != nil { + return [32]byte{}, errors.Wrap(err, "could not compute merkleization") + } + chunks := make([][32]byte, 2) + chunks[0] = bytesRoot + binary.LittleEndian.PutUint64(chunks[1][24:], uint64(len(commitments))) + if err := gohashtree.Hash(chunks, chunks); err != nil { + return hash, err + } + return chunks[0], nil +} + // WithdrawalSliceRoot computes the HTR of a slice of withdrawals. // The limit parameter is used as input to the bitwise merkleization algorithm. func WithdrawalSliceRoot(withdrawals []*enginev1.Withdrawal, limit uint64) ([32]byte, error) {