From d67ee62efa98a9cc049f02e37a09d21773a3b10a Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:20:43 -0500 Subject: [PATCH] Debug data columns API endpoint (#15701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial * fixing from self review * changelog * fixing endpoints and adding test * removed unneeded test * self review * fixing mock columns * fixing tests * gofmt * fixing endpoint * gaz * gofmt * fixing tests * gofmt * gaz * radek comments * gaz * fixing formatting * deduplicating and fixing an old bug, will break into separate PR * better way for version * optimizing post merge and fixing tests * Update beacon-chain/rpc/eth/debug/handlers.go Co-authored-by: Radosław Kapka * Update beacon-chain/rpc/eth/debug/handlers.go Co-authored-by: Radosław Kapka * Update beacon-chain/rpc/lookup/blocker.go Co-authored-by: Radosław Kapka * Update beacon-chain/rpc/lookup/blocker.go Co-authored-by: Radosław Kapka * Update beacon-chain/rpc/lookup/blocker.go Co-authored-by: Radosław Kapka * adding some of radek's feedback * reverting and gaz --------- Co-authored-by: Radosław Kapka --- api/server/structs/endpoints_debug.go | 16 ++ beacon-chain/rpc/endpoints.go | 16 +- beacon-chain/rpc/endpoints_test.go | 7 +- beacon-chain/rpc/eth/debug/BUILD.bazel | 10 + beacon-chain/rpc/eth/debug/handlers.go | 192 ++++++++++++- beacon-chain/rpc/eth/debug/handlers_test.go | 290 +++++++++++++++++++ beacon-chain/rpc/eth/debug/server.go | 2 + beacon-chain/rpc/lookup/BUILD.bazel | 1 - beacon-chain/rpc/lookup/blocker.go | 102 ++++++- beacon-chain/rpc/lookup/blocker_test.go | 303 +++++++++++++++++++- beacon-chain/rpc/testutil/mock_blocker.go | 25 +- changelog/james-prysm_debug-data-columns.md | 3 + 12 files changed, 945 insertions(+), 22 deletions(-) create mode 100644 changelog/james-prysm_debug-data-columns.md diff --git a/api/server/structs/endpoints_debug.go b/api/server/structs/endpoints_debug.go index 16e5b79809..c2a632316c 100644 --- a/api/server/structs/endpoints_debug.go +++ b/api/server/structs/endpoints_debug.go @@ -56,3 +56,19 @@ type ForkChoiceNodeExtraData struct { TimeStamp string `json:"timestamp"` Target string `json:"target"` } + +type GetDebugDataColumnSidecarsResponse struct { + Version string `json:"version"` + ExecutionOptimistic bool `json:"execution_optimistic"` + Finalized bool `json:"finalized"` + Data []*DataColumnSidecar `json:"data"` +} + +type DataColumnSidecar struct { + Index string `json:"index"` + Column []string `json:"column"` + KzgCommitments []string `json:"kzg_commitments"` + KzgProofs []string `json:"kzg_proofs"` + SignedBeaconBlockHeader *SignedBeaconBlockHeader `json:"signed_block_header"` + KzgCommitmentsInclusionProof []string `json:"kzg_commitments_inclusion_proof"` +} diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index daf44a961f..3092955b54 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -106,7 +106,7 @@ func (s *Service) endpoints( } if enableDebug { - endpoints = append(endpoints, s.debugEndpoints(stater)...) + endpoints = append(endpoints, s.debugEndpoints(stater, blocker)...) } return endpoints @@ -1097,7 +1097,7 @@ func (s *Service) lightClientEndpoints() []endpoint { } } -func (s *Service) debugEndpoints(stater lookup.Stater) []endpoint { +func (s *Service) debugEndpoints(stater lookup.Stater, blocker lookup.Blocker) []endpoint { server := &debug.Server{ BeaconDB: s.cfg.BeaconDB, HeadFetcher: s.cfg.HeadFetcher, @@ -1107,6 +1107,8 @@ func (s *Service) debugEndpoints(stater lookup.Stater) []endpoint { ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, FinalizationFetcher: s.cfg.FinalizationFetcher, ChainInfoFetcher: s.cfg.ChainInfoFetcher, + GenesisTimeFetcher: s.cfg.GenesisTimeFetcher, + Blocker: blocker, } const namespace = "debug" @@ -1141,6 +1143,16 @@ func (s *Service) debugEndpoints(stater lookup.Stater) []endpoint { handler: server.GetForkChoice, methods: []string{http.MethodGet}, }, + { + template: "/eth/v1/debug/beacon/data_column_sidecars/{block_id}", + name: namespace + ".GetDataColumnSidecars", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType, api.OctetStreamMediaType}), + middleware.AcceptEncodingHeaderHandler(), + }, + handler: server.DataColumnSidecars, + methods: []string{http.MethodGet}, + }, } } diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index 33760f6c74..27d62a97ee 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -80,9 +80,10 @@ func Test_endpoints(t *testing.T) { } debugRoutes := map[string][]string{ - "/eth/v2/debug/beacon/states/{state_id}": {http.MethodGet}, - "/eth/v2/debug/beacon/heads": {http.MethodGet}, - "/eth/v1/debug/fork_choice": {http.MethodGet}, + "/eth/v2/debug/beacon/states/{state_id}": {http.MethodGet}, + "/eth/v2/debug/beacon/heads": {http.MethodGet}, + "/eth/v1/debug/fork_choice": {http.MethodGet}, + "/eth/v1/debug/beacon/data_column_sidecars/{block_id}": {http.MethodGet}, } eventsRoutes := map[string][]string{ diff --git a/beacon-chain/rpc/eth/debug/BUILD.bazel b/beacon-chain/rpc/eth/debug/BUILD.bazel index 8281be31be..7d85a5c6fc 100644 --- a/beacon-chain/rpc/eth/debug/BUILD.bazel +++ b/beacon-chain/rpc/eth/debug/BUILD.bazel @@ -13,13 +13,19 @@ go_library( "//api/server/structs:go_default_library", "//beacon-chain/blockchain:go_default_library", "//beacon-chain/db:go_default_library", + "//beacon-chain/rpc/core:go_default_library", "//beacon-chain/rpc/eth/helpers:go_default_library", "//beacon-chain/rpc/eth/shared:go_default_library", "//beacon-chain/rpc/lookup:go_default_library", + "//config/params:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/primitives:go_default_library", "//monitoring/tracing/trace:go_default_library", "//network/httputil:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", "//runtime/version:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", + "@com_github_pkg_errors//:go_default_library", ], ) @@ -34,9 +40,13 @@ go_test( "//beacon-chain/db/testing:go_default_library", "//beacon-chain/forkchoice/doubly-linked-tree:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", + "//beacon-chain/rpc/core:go_default_library", "//beacon-chain/rpc/testutil:go_default_library", "//config/params:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/primitives:go_default_library", "//encoding/bytesutil:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", "//runtime/version:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", diff --git a/beacon-chain/rpc/eth/debug/handlers.go b/beacon-chain/rpc/eth/debug/handlers.go index 7a28a2da98..3979c8b947 100644 --- a/beacon-chain/rpc/eth/debug/handlers.go +++ b/beacon-chain/rpc/eth/debug/handlers.go @@ -4,19 +4,31 @@ import ( "context" "encoding/json" "fmt" + "math" "net/http" + "net/url" + "strconv" + "strings" "github.com/OffchainLabs/prysm/v6/api" "github.com/OffchainLabs/prysm/v6/api/server/structs" + "github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/core" "github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/eth/helpers" "github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/eth/shared" + "github.com/OffchainLabs/prysm/v6/config/params" + "github.com/OffchainLabs/prysm/v6/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" "github.com/OffchainLabs/prysm/v6/monitoring/tracing/trace" "github.com/OffchainLabs/prysm/v6/network/httputil" + ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/runtime/version" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" ) -const errMsgStateFromConsensus = "Could not convert consensus state to response" +const ( + errMsgStateFromConsensus = "Could not convert consensus state to response" +) // GetBeaconStateV2 returns the full beacon state for a given state ID. func (s *Server) GetBeaconStateV2(w http.ResponseWriter, r *http.Request) { @@ -208,3 +220,181 @@ func (s *Server) GetForkChoice(w http.ResponseWriter, r *http.Request) { } httputil.WriteJson(w, resp) } + +// DataColumnSidecars retrieves data column sidecars for a given block id. +func (s *Server) DataColumnSidecars(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "debug.DataColumnSidecars") + defer span.End() + + // Check if we're before Fulu fork - data columns are only available from Fulu onwards + fuluForkEpoch := params.BeaconConfig().FuluForkEpoch + if fuluForkEpoch == math.MaxUint64 { + httputil.HandleError(w, "Data columns are not supported - Fulu fork not configured", http.StatusBadRequest) + return + } + + // Check if we're before Fulu fork based on current slot + currentSlot := s.GenesisTimeFetcher.CurrentSlot() + currentEpoch := primitives.Epoch(currentSlot / params.BeaconConfig().SlotsPerEpoch) + if currentEpoch < fuluForkEpoch { + httputil.HandleError(w, "Data columns are not supported - before Fulu fork", http.StatusBadRequest) + return + } + + indices, err := parseDataColumnIndices(r.URL) + if err != nil { + httputil.HandleError(w, err.Error(), http.StatusBadRequest) + return + } + segments := strings.Split(r.URL.Path, "/") + blockId := segments[len(segments)-1] + + verifiedDataColumns, rpcErr := s.Blocker.DataColumns(ctx, blockId, indices) + if rpcErr != nil { + code := core.ErrorReasonToHTTP(rpcErr.Reason) + switch code { + case http.StatusBadRequest: + httputil.HandleError(w, "Bad request: "+rpcErr.Err.Error(), code) + return + case http.StatusNotFound: + httputil.HandleError(w, "Not found: "+rpcErr.Err.Error(), code) + return + case http.StatusInternalServerError: + httputil.HandleError(w, "Internal server error: "+rpcErr.Err.Error(), code) + return + default: + httputil.HandleError(w, rpcErr.Err.Error(), code) + return + } + } + + blk, err := s.Blocker.Block(ctx, []byte(blockId)) + if err != nil { + httputil.HandleError(w, "Could not fetch block: "+err.Error(), http.StatusInternalServerError) + return + } + if blk == nil { + httputil.HandleError(w, "Block not found", http.StatusNotFound) + return + } + + if httputil.RespondWithSsz(r) { + sszResp, err := buildDataColumnSidecarsSSZResponse(verifiedDataColumns) + if err != nil { + httputil.HandleError(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set(api.VersionHeader, version.String(blk.Version())) + httputil.WriteSsz(w, sszResp) + return + } + + blkRoot, err := blk.Block().HashTreeRoot() + if err != nil { + httputil.HandleError(w, "Could not hash block: "+err.Error(), http.StatusInternalServerError) + return + } + isOptimistic, err := s.OptimisticModeFetcher.IsOptimisticForRoot(ctx, blkRoot) + if err != nil { + httputil.HandleError(w, "Could not check if block is optimistic: "+err.Error(), http.StatusInternalServerError) + return + } + + data := buildDataColumnSidecarsJsonResponse(verifiedDataColumns) + resp := &structs.GetDebugDataColumnSidecarsResponse{ + Version: version.String(blk.Version()), + Data: data, + ExecutionOptimistic: isOptimistic, + Finalized: s.FinalizationFetcher.IsFinalized(ctx, blkRoot), + } + w.Header().Set(api.VersionHeader, version.String(blk.Version())) + httputil.WriteJson(w, resp) +} + +// parseDataColumnIndices filters out invalid and duplicate data column indices +func parseDataColumnIndices(url *url.URL) ([]int, error) { + numberOfColumns := params.BeaconConfig().NumberOfColumns + rawIndices := url.Query()["indices"] + indices := make([]int, 0, numberOfColumns) + invalidIndices := make([]string, 0) +loop: + for _, raw := range rawIndices { + ix, err := strconv.Atoi(raw) + if err != nil { + invalidIndices = append(invalidIndices, raw) + continue + } + if !(0 <= ix && uint64(ix) < numberOfColumns) { + invalidIndices = append(invalidIndices, raw) + continue + } + for i := range indices { + if ix == indices[i] { + continue loop + } + } + indices = append(indices, ix) + } + + if len(invalidIndices) > 0 { + return nil, fmt.Errorf("requested data column indices %v are invalid", invalidIndices) + } + return indices, nil +} + +func buildDataColumnSidecarsJsonResponse(verifiedDataColumns []blocks.VerifiedRODataColumn) []*structs.DataColumnSidecar { + sidecars := make([]*structs.DataColumnSidecar, len(verifiedDataColumns)) + for i, dc := range verifiedDataColumns { + column := make([]string, len(dc.Column)) + for j, cell := range dc.Column { + column[j] = hexutil.Encode(cell) + } + + kzgCommitments := make([]string, len(dc.KzgCommitments)) + for j, commitment := range dc.KzgCommitments { + kzgCommitments[j] = hexutil.Encode(commitment) + } + + kzgProofs := make([]string, len(dc.KzgProofs)) + for j, proof := range dc.KzgProofs { + kzgProofs[j] = hexutil.Encode(proof) + } + + kzgCommitmentsInclusionProof := make([]string, len(dc.KzgCommitmentsInclusionProof)) + for j, proof := range dc.KzgCommitmentsInclusionProof { + kzgCommitmentsInclusionProof[j] = hexutil.Encode(proof) + } + + sidecars[i] = &structs.DataColumnSidecar{ + Index: strconv.FormatUint(dc.Index, 10), + Column: column, + KzgCommitments: kzgCommitments, + KzgProofs: kzgProofs, + SignedBeaconBlockHeader: structs.SignedBeaconBlockHeaderFromConsensus(dc.SignedBlockHeader), + KzgCommitmentsInclusionProof: kzgCommitmentsInclusionProof, + } + } + return sidecars +} + +// buildDataColumnSidecarsSSZResponse builds SSZ response for data column sidecars +func buildDataColumnSidecarsSSZResponse(verifiedDataColumns []blocks.VerifiedRODataColumn) ([]byte, error) { + if len(verifiedDataColumns) == 0 { + return []byte{}, nil + } + + // Pre-allocate buffer for all sidecars using the known SSZ size + sizePerSidecar := (ðpb.DataColumnSidecar{}).SizeSSZ() + ssz := make([]byte, 0, sizePerSidecar*len(verifiedDataColumns)) + + // Marshal and append each sidecar + for i, sidecar := range verifiedDataColumns { + sszrep, err := sidecar.MarshalSSZ() + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal data column sidecar at index %d", i) + } + ssz = append(ssz, sszrep...) + } + + return ssz, nil +} diff --git a/beacon-chain/rpc/eth/debug/handlers_test.go b/beacon-chain/rpc/eth/debug/handlers_test.go index 873b3666a7..082f774004 100644 --- a/beacon-chain/rpc/eth/debug/handlers_test.go +++ b/beacon-chain/rpc/eth/debug/handlers_test.go @@ -2,9 +2,13 @@ package debug import ( "bytes" + "context" "encoding/json" + "errors" + "math" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/OffchainLabs/prysm/v6/api" @@ -13,9 +17,13 @@ import ( dbtest "github.com/OffchainLabs/prysm/v6/beacon-chain/db/testing" doublylinkedtree "github.com/OffchainLabs/prysm/v6/beacon-chain/forkchoice/doubly-linked-tree" forkchoicetypes "github.com/OffchainLabs/prysm/v6/beacon-chain/forkchoice/types" + "github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/core" "github.com/OffchainLabs/prysm/v6/beacon-chain/rpc/testutil" "github.com/OffchainLabs/prysm/v6/config/params" + "github.com/OffchainLabs/prysm/v6/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" "github.com/OffchainLabs/prysm/v6/encoding/bytesutil" + ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v6/runtime/version" "github.com/OffchainLabs/prysm/v6/testing/assert" "github.com/OffchainLabs/prysm/v6/testing/require" @@ -515,3 +523,285 @@ func TestGetForkChoice(t *testing.T) { require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) require.Equal(t, "2", resp.FinalizedCheckpoint.Epoch) } + +func TestDataColumnSidecars(t *testing.T) { + t.Run("Fulu fork not configured", func(t *testing.T) { + // Save the original config + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + // Set Fulu fork epoch to MaxUint64 (unconfigured) + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = math.MaxUint64 + params.OverrideBeaconConfig(config) + + chainService := &blockchainmock.ChainService{} + + // Create a mock blocker to avoid nil pointer + mockBlocker := &testutil.MockBlocker{} + + s := &Server{ + GenesisTimeFetcher: chainService, + Blocker: mockBlocker, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/debug/beacon/data_column_sidecars/head", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusBadRequest, writer.Code) + assert.StringContains(t, "Data columns are not supported - Fulu fork not configured", writer.Body.String()) + }) + + t.Run("Before Fulu fork", func(t *testing.T) { + // Save the original config + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + // Set Fulu fork epoch to 100 + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = 100 + params.OverrideBeaconConfig(config) + + chainService := &blockchainmock.ChainService{} + currentSlot := primitives.Slot(0) // Current slot 0 (epoch 0, before epoch 100) + chainService.Slot = ¤tSlot + + // Create a mock blocker to avoid nil pointer + mockBlocker := &testutil.MockBlocker{ + DataColumnsFunc: func(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + return nil, &core.RpcError{Err: errors.New("before Fulu fork"), Reason: core.BadRequest} + }, + } + + s := &Server{ + GenesisTimeFetcher: chainService, + Blocker: mockBlocker, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/debug/beacon/data_column_sidecars/head", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusBadRequest, writer.Code) + assert.StringContains(t, "Data columns are not supported - before Fulu fork", writer.Body.String()) + }) + + t.Run("Invalid indices", func(t *testing.T) { + // Save the original config + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + // Set Fulu fork epoch to 0 (already activated) + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = 0 + params.OverrideBeaconConfig(config) + + chainService := &blockchainmock.ChainService{} + currentSlot := primitives.Slot(0) // Current slot 0 (epoch 0, at fork) + chainService.Slot = ¤tSlot + + // Create a mock blocker to avoid nil pointer + mockBlocker := &testutil.MockBlocker{ + DataColumnsFunc: func(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + return nil, &core.RpcError{Err: errors.New("invalid index"), Reason: core.BadRequest} + }, + } + + s := &Server{ + GenesisTimeFetcher: chainService, + Blocker: mockBlocker, + } + + // Test with invalid index (out of range) + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/debug/beacon/data_column_sidecars/head?indices=9999", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusBadRequest, writer.Code) + assert.StringContains(t, "requested data column indices [9999] are invalid", writer.Body.String()) + }) + + t.Run("Block not found", func(t *testing.T) { + // Save the original config + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + // Set Fulu fork epoch to 0 (already activated) + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = 0 + params.OverrideBeaconConfig(config) + + chainService := &blockchainmock.ChainService{} + currentSlot := primitives.Slot(0) // Current slot 0 + chainService.Slot = ¤tSlot + + // Create a mock blocker that returns block not found + mockBlocker := &testutil.MockBlocker{ + DataColumnsFunc: func(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + return nil, &core.RpcError{Err: errors.New("block not found"), Reason: core.NotFound} + }, + BlockToReturn: nil, // Block not found + } + + s := &Server{ + GenesisTimeFetcher: chainService, + Blocker: mockBlocker, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/debug/beacon/data_column_sidecars/head", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusNotFound, writer.Code) + }) + + t.Run("Empty data columns", func(t *testing.T) { + // Save the original config + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + // Set Fulu fork epoch to 0 + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = 0 + params.OverrideBeaconConfig(config) + + // Create a simple test block + signedTestBlock := util.NewBeaconBlock() + roBlock, err := blocks.NewSignedBeaconBlock(signedTestBlock) + require.NoError(t, err) + + chainService := &blockchainmock.ChainService{} + currentSlot := primitives.Slot(0) // Current slot 0 + chainService.Slot = ¤tSlot + chainService.OptimisticRoots = make(map[[32]byte]bool) + chainService.FinalizedRoots = make(map[[32]byte]bool) + + mockBlocker := &testutil.MockBlocker{ + DataColumnsFunc: func(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + return []blocks.VerifiedRODataColumn{}, nil // Empty data columns + }, + BlockToReturn: roBlock, + } + + s := &Server{ + GenesisTimeFetcher: chainService, + OptimisticModeFetcher: chainService, + FinalizationFetcher: chainService, + Blocker: mockBlocker, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/debug/beacon/data_column_sidecars/head", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.GetDebugDataColumnSidecarsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.Equal(t, 0, len(resp.Data)) + }) +} + +func TestParseDataColumnIndices(t *testing.T) { + // Save the original config + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + // Set NumberOfColumns to 128 for testing + config := params.BeaconConfig().Copy() + config.NumberOfColumns = 128 + params.OverrideBeaconConfig(config) + + tests := []struct { + name string + queryParams map[string][]string + expected []int + expectError bool + }{ + { + name: "no indices", + queryParams: map[string][]string{}, + expected: []int{}, + expectError: false, + }, + { + name: "valid indices", + queryParams: map[string][]string{"indices": {"0", "1", "127"}}, + expected: []int{0, 1, 127}, + expectError: false, + }, + { + name: "duplicate indices", + queryParams: map[string][]string{"indices": {"0", "1", "0"}}, + expected: []int{0, 1}, + expectError: false, + }, + { + name: "invalid string index", + queryParams: map[string][]string{"indices": {"abc"}}, + expected: nil, + expectError: true, + }, + { + name: "negative index", + queryParams: map[string][]string{"indices": {"-1"}}, + expected: nil, + expectError: true, + }, + { + name: "index too large", + queryParams: map[string][]string{"indices": {"128"}}, // 128 >= NumberOfColumns (128) + expected: nil, + expectError: true, + }, + { + name: "mixed valid and invalid", + queryParams: map[string][]string{"indices": {"0", "abc", "1"}}, + expected: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse("http://example.com/test") + require.NoError(t, err) + + q := u.Query() + for key, values := range tt.queryParams { + for _, value := range values { + q.Add(key, value) + } + } + u.RawQuery = q.Encode() + + result, err := parseDataColumnIndices(u) + + if tt.expectError { + assert.NotNil(t, err) + } else { + require.NoError(t, err) + assert.DeepEqual(t, tt.expected, result) + } + }) + } +} + +func TestBuildDataColumnSidecarsSSZResponse(t *testing.T) { + t.Run("empty data columns", func(t *testing.T) { + result, err := buildDataColumnSidecarsSSZResponse([]blocks.VerifiedRODataColumn{}) + require.NoError(t, err) + require.DeepEqual(t, []byte{}, result) + }) + + t.Run("get SSZ size", func(t *testing.T) { + size := (ðpb.DataColumnSidecar{}).SizeSSZ() + assert.Equal(t, true, size > 0) + }) +} diff --git a/beacon-chain/rpc/eth/debug/server.go b/beacon-chain/rpc/eth/debug/server.go index f93714ee24..e0c822e19e 100644 --- a/beacon-chain/rpc/eth/debug/server.go +++ b/beacon-chain/rpc/eth/debug/server.go @@ -20,4 +20,6 @@ type Server struct { ForkchoiceFetcher blockchain.ForkchoiceFetcher FinalizationFetcher blockchain.FinalizationFetcher ChainInfoFetcher blockchain.ChainInfoFetcher + GenesisTimeFetcher blockchain.TimeFetcher + Blocker lookup.Blocker } diff --git a/beacon-chain/rpc/lookup/BUILD.bazel b/beacon-chain/rpc/lookup/BUILD.bazel index 5c968ee13a..b5d6a5e24e 100644 --- a/beacon-chain/rpc/lookup/BUILD.bazel +++ b/beacon-chain/rpc/lookup/BUILD.bazel @@ -25,7 +25,6 @@ go_library( "//consensus-types/primitives:go_default_library", "//encoding/bytesutil:go_default_library", "//monitoring/tracing/trace:go_default_library", - "//runtime/version:go_default_library", "//time/slots:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_pkg_errors//:go_default_library", diff --git a/beacon-chain/rpc/lookup/blocker.go b/beacon-chain/rpc/lookup/blocker.go index 1f68ef6393..bb9c99a2c3 100644 --- a/beacon-chain/rpc/lookup/blocker.go +++ b/beacon-chain/rpc/lookup/blocker.go @@ -19,7 +19,6 @@ import ( "github.com/OffchainLabs/prysm/v6/consensus-types/interfaces" "github.com/OffchainLabs/prysm/v6/consensus-types/primitives" "github.com/OffchainLabs/prysm/v6/encoding/bytesutil" - "github.com/OffchainLabs/prysm/v6/runtime/version" "github.com/OffchainLabs/prysm/v6/time/slots" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" @@ -62,6 +61,7 @@ func (e BlockIdParseError) Error() string { type Blocker interface { Block(ctx context.Context, id []byte) (interfaces.ReadOnlySignedBeaconBlock, error) Blobs(ctx context.Context, id string, opts ...options.BlobsOption) ([]*blocks.VerifiedROBlob, *core.RpcError) + DataColumns(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) } // BeaconDbBlocker is an implementation of Blocker. It retrieves blocks from the beacon chain database. @@ -267,13 +267,9 @@ func (p *BeaconDbBlocker) Blobs(ctx context.Context, id string, opts ...options. return nil, &core.RpcError{Err: err, Reason: core.ErrorReason(reason)} } - // Validate fork epoch for Deneb (blobs) - if roSignedBlock != nil { - slot := roSignedBlock.Block().Slot() - if slots.ToEpoch(slot) < params.BeaconConfig().DenebForkEpoch { - forkName := version.String(slots.ToForkVersion(slot)) - return nil, &core.RpcError{Err: fmt.Errorf("not supported before %s fork", forkName), Reason: core.BadRequest} - } + slot := roSignedBlock.Block().Slot() + if slots.ToEpoch(slot) < params.BeaconConfig().DenebForkEpoch { + return nil, &core.RpcError{Err: errors.New("blobs are not supported before Deneb fork"), Reason: core.BadRequest} } roBlock := roSignedBlock.Block() @@ -489,3 +485,93 @@ func (p *BeaconDbBlocker) neededDataColumnSidecars(root [fieldparams.RootLength] return verifiedRoSidecars, nil } + +// DataColumns returns the data column sidecars for a given block id identifier and column indices. The identifier can be one of: +// - "head" (canonical head in node's view) +// - "genesis" +// - "finalized" +// - "justified" +// - +// - +// - +// +// cases: +// - no block, 404 +// - block exists, before Fulu fork, 400 (data columns are not supported before Fulu fork) +func (p *BeaconDbBlocker) DataColumns(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + // Check for genesis block first (not supported for data columns) + if id == "genesis" { + return nil, &core.RpcError{Err: errors.New("data columns are not supported for Phase 0 fork"), Reason: core.BadRequest} + } + + // Resolve block ID to root and block + root, roSignedBlock, err := p.resolveBlockID(ctx, id) + if err != nil { + var blockNotFound *BlockNotFoundError + var blockIdParseErr *BlockIdParseError + + reason := core.Internal // Default to Internal for unexpected errors + if errors.As(err, &blockNotFound) { + reason = core.NotFound + } else if errors.As(err, &blockIdParseErr) { + reason = core.BadRequest + } + return nil, &core.RpcError{Err: err, Reason: core.ErrorReason(reason)} + } + + slot := roSignedBlock.Block().Slot() + fuluForkEpoch := params.BeaconConfig().FuluForkEpoch + fuluForkSlot, err := slots.EpochStart(fuluForkEpoch) + if err != nil { + return nil, &core.RpcError{Err: errors.Wrap(err, "could not calculate Fulu start slot"), Reason: core.Internal} + } + if slot < fuluForkSlot { + return nil, &core.RpcError{Err: errors.New("data columns are not supported before Fulu fork"), Reason: core.BadRequest} + } + + roBlock := roSignedBlock.Block() + + commitments, err := roBlock.Body().BlobKzgCommitments() + if err != nil { + return nil, &core.RpcError{Err: errors.Wrapf(err, "failed to retrieve kzg commitments from block %#x", root), Reason: core.Internal} + } + + // If there are no commitments return 200 w/ empty list + if len(commitments) == 0 { + return make([]blocks.VerifiedRODataColumn, 0), nil + } + + // Get column indices to retrieve + columnIndices := make([]uint64, 0) + if len(indices) == 0 { + // If no indices specified, return all columns this node is custodying + summary := p.DataColumnStorage.Summary(root) + stored := summary.Stored() + for index := range stored { + columnIndices = append(columnIndices, index) + } + } else { + // Validate and convert indices + numberOfColumns := params.BeaconConfig().NumberOfColumns + for _, index := range indices { + if index < 0 || uint64(index) >= numberOfColumns { + return nil, &core.RpcError{ + Err: fmt.Errorf("requested index %d is outside valid range [0, %d)", index, numberOfColumns), + Reason: core.BadRequest, + } + } + columnIndices = append(columnIndices, uint64(index)) + } + } + + // Retrieve data column sidecars from storage + verifiedRoDataColumns, err := p.DataColumnStorage.Get(root, columnIndices) + if err != nil { + return nil, &core.RpcError{ + Err: errors.Wrapf(err, "could not retrieve data columns for block root %#x", root), + Reason: core.Internal, + } + } + + return verifiedRoDataColumns, nil +} diff --git a/beacon-chain/rpc/lookup/blocker_test.go b/beacon-chain/rpc/lookup/blocker_test.go index c34108ed29..ff3bd10956 100644 --- a/beacon-chain/rpc/lookup/blocker_test.go +++ b/beacon-chain/rpc/lookup/blocker_test.go @@ -262,7 +262,7 @@ func TestBlobsErrorHandling(t *testing.T) { predenebBlock := util.NewBeaconBlock() predenebBlock.Block.Slot = 100 util.SaveBlock(t, ctx, db, predenebBlock) - + // Create blocker without ChainInfoFetcher to trigger internal error when checking canonical status blocker := &BeaconDbBlocker{ BeaconDB: db, @@ -329,9 +329,9 @@ func TestGetBlob(t *testing.T) { for _, blob := range fuluBlobSidecars { var kzgBlob kzg.Blob copy(kzgBlob[:], blob.Blob) - cellsAndProogs, err := kzg.ComputeCellsAndKZGProofs(&kzgBlob) + cellsAndProofs, err := kzg.ComputeCellsAndKZGProofs(&kzgBlob) require.NoError(t, err) - cellsAndProofsList = append(cellsAndProofsList, cellsAndProogs) + cellsAndProofsList = append(cellsAndProofsList, cellsAndProofs) } roDataColumnSidecars, err := peerdas.DataColumnSidecars(cellsAndProofsList, peerdas.PopulateFromBlock(fuluBlock)) @@ -794,3 +794,300 @@ func TestBlobs_CommitmentOrdering(t *testing.T) { require.StringContains(t, "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", rpcErr.Err.Error()) }) } + +func TestGetDataColumns(t *testing.T) { + const ( + blobCount = 4 + fuluForkEpoch = 2 + ) + + setupFulu := func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.DenebForkEpoch = 1 + cfg.FuluForkEpoch = fuluForkEpoch + params.OverrideBeaconConfig(cfg) + } + + setupPreFulu := func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.DenebForkEpoch = 1 + cfg.FuluForkEpoch = 1000 // Set to a high epoch to ensure we're before Fulu + params.OverrideBeaconConfig(cfg) + } + + ctx := t.Context() + db := testDB.SetupDB(t) + + // Start the trusted setup. + err := kzg.Start() + require.NoError(t, err) + + // Create Fulu block and convert blob sidecars to data column sidecars. + fuluForkSlot := fuluForkEpoch * params.BeaconConfig().SlotsPerEpoch + fuluBlock, fuluBlobSidecars := util.GenerateTestElectraBlockWithSidecar(t, [fieldparams.RootLength]byte{}, fuluForkSlot, blobCount) + fuluBlockRoot := fuluBlock.Root() + + cellsAndProofsList := make([]kzg.CellsAndProofs, 0, len(fuluBlobSidecars)) + for _, blob := range fuluBlobSidecars { + var kzgBlob kzg.Blob + copy(kzgBlob[:], blob.Blob) + cellsAndProofs, err := kzg.ComputeCellsAndKZGProofs(&kzgBlob) + require.NoError(t, err) + cellsAndProofsList = append(cellsAndProofsList, cellsAndProofs) + } + + roDataColumnSidecars, err := peerdas.DataColumnSidecars(cellsAndProofsList, peerdas.PopulateFromBlock(fuluBlock)) + require.NoError(t, err) + + verifiedRoDataColumnSidecars := make([]blocks.VerifiedRODataColumn, 0, len(roDataColumnSidecars)) + for _, roDataColumn := range roDataColumnSidecars { + verifiedRoDataColumn := blocks.NewVerifiedRODataColumn(roDataColumn) + verifiedRoDataColumnSidecars = append(verifiedRoDataColumnSidecars, verifiedRoDataColumn) + } + + err = db.SaveBlock(t.Context(), fuluBlock) + require.NoError(t, err) + + _, dataColumnStorage := filesystem.NewEphemeralDataColumnStorageAndFs(t) + err = dataColumnStorage.Save(verifiedRoDataColumnSidecars) + require.NoError(t, err) + + t.Run("pre-fulu fork", func(t *testing.T) { + setupPreFulu(t) + + // Create a block at slot 123 (before Fulu fork since FuluForkEpoch is set to MaxUint64) + preFuluBlock := util.NewBeaconBlock() + preFuluBlock.Block.Slot = 123 + util.SaveBlock(t, ctx, db, preFuluBlock) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + ChainInfoFetcher: &mockChain.ChainService{}, + BeaconDB: db, + } + + _, rpcErr := blocker.DataColumns(ctx, "123", nil) + require.NotNil(t, rpcErr) + require.Equal(t, core.ErrorReason(core.BadRequest), rpcErr.Reason) + require.StringContains(t, "not supported before Fulu fork", rpcErr.Err.Error()) + }) + + t.Run("genesis", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + ChainInfoFetcher: &mockChain.ChainService{}, + } + + _, rpcErr := blocker.DataColumns(ctx, "genesis", nil) + require.NotNil(t, rpcErr) + require.Equal(t, http.StatusBadRequest, core.ErrorReasonToHTTP(rpcErr.Reason)) + require.StringContains(t, "not supported for Phase 0 fork", rpcErr.Err.Error()) + }) + + t.Run("head", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + ChainInfoFetcher: &mockChain.ChainService{ + Root: fuluBlockRoot[:], + Block: fuluBlock, + }, + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, "head", nil) + require.IsNil(t, rpcErr) + require.Equal(t, len(verifiedRoDataColumnSidecars), len(retrievedDataColumns)) + + // Create a map of expected indices for easier verification + expectedIndices := make(map[uint64]bool) + for _, expected := range verifiedRoDataColumnSidecars { + expectedIndices[expected.RODataColumn.DataColumnSidecar.Index] = true + } + + // Verify we got data columns with the expected indices + for _, actual := range retrievedDataColumns { + require.Equal(t, true, expectedIndices[actual.RODataColumn.DataColumnSidecar.Index]) + } + }) + + t.Run("finalized", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + ChainInfoFetcher: &mockChain.ChainService{FinalizedCheckPoint: ðpb.Checkpoint{Root: fuluBlockRoot[:]}}, + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, "finalized", nil) + require.IsNil(t, rpcErr) + require.Equal(t, len(verifiedRoDataColumnSidecars), len(retrievedDataColumns)) + }) + + t.Run("justified", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + ChainInfoFetcher: &mockChain.ChainService{CurrentJustifiedCheckPoint: ðpb.Checkpoint{Root: fuluBlockRoot[:]}}, + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, "justified", nil) + require.IsNil(t, rpcErr) + require.Equal(t, len(verifiedRoDataColumnSidecars), len(retrievedDataColumns)) + }) + + t.Run("root", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, hexutil.Encode(fuluBlockRoot[:]), nil) + require.IsNil(t, rpcErr) + require.Equal(t, len(verifiedRoDataColumnSidecars), len(retrievedDataColumns)) + }) + + t.Run("slot", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + ChainInfoFetcher: &mockChain.ChainService{}, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + slotStr := fmt.Sprintf("%d", fuluForkSlot) + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, slotStr, nil) + require.IsNil(t, rpcErr) + require.Equal(t, len(verifiedRoDataColumnSidecars), len(retrievedDataColumns)) + }) + + t.Run("specific indices", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + // Request specific indices (first 3 data columns) + indices := []int{0, 1, 2} + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, hexutil.Encode(fuluBlockRoot[:]), indices) + require.IsNil(t, rpcErr) + require.Equal(t, 3, len(retrievedDataColumns)) + + for i, dataColumn := range retrievedDataColumns { + require.Equal(t, uint64(indices[i]), dataColumn.RODataColumn.DataColumnSidecar.Index) + } + }) + + t.Run("no data columns returns empty array", func(t *testing.T) { + setupFulu(t) + + _, emptyDataColumnStorage := filesystem.NewEphemeralDataColumnStorageAndFs(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: emptyDataColumnStorage, + } + + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, hexutil.Encode(fuluBlockRoot[:]), nil) + require.IsNil(t, rpcErr) + require.Equal(t, 0, len(retrievedDataColumns)) + }) + + t.Run("index too big", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + _, rpcErr := blocker.DataColumns(ctx, hexutil.Encode(fuluBlockRoot[:]), []int{0, math.MaxInt}) + require.NotNil(t, rpcErr) + require.Equal(t, core.ErrorReason(core.BadRequest), rpcErr.Reason) + }) + + t.Run("outside retention period", func(t *testing.T) { + setupFulu(t) + + // Create a data column storage with very short retention period + shortRetentionStorage, err := filesystem.NewDataColumnStorage(ctx, + filesystem.WithDataColumnBasePath(t.TempDir()), + filesystem.WithDataColumnRetentionEpochs(1), // Only 1 epoch retention + ) + require.NoError(t, err) + + // Mock genesis time to make current slot much later than the block slot + // This simulates being outside retention period + genesisTime := time.Now().Add(-time.Duration(fuluForkSlot+1000) * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: genesisTime, + }, + BeaconDB: db, + DataColumnStorage: shortRetentionStorage, + } + + // Since the block is outside retention period, should return empty array + retrievedDataColumns, rpcErr := blocker.DataColumns(ctx, hexutil.Encode(fuluBlockRoot[:]), nil) + require.IsNil(t, rpcErr) + require.Equal(t, 0, len(retrievedDataColumns)) + }) + + t.Run("block not found", func(t *testing.T) { + setupFulu(t) + + blocker := &BeaconDbBlocker{ + GenesisTimeFetcher: &testutil.MockGenesisTimeFetcher{ + Genesis: time.Now(), + }, + BeaconDB: db, + DataColumnStorage: dataColumnStorage, + } + + nonExistentRoot := bytesutil.PadTo([]byte("nonexistent"), 32) + _, rpcErr := blocker.DataColumns(ctx, hexutil.Encode(nonExistentRoot), nil) + require.NotNil(t, rpcErr) + require.Equal(t, core.ErrorReason(core.NotFound), rpcErr.Reason) + }) +} diff --git a/beacon-chain/rpc/testutil/mock_blocker.go b/beacon-chain/rpc/testutil/mock_blocker.go index e256ed9344..284aa6158c 100644 --- a/beacon-chain/rpc/testutil/mock_blocker.go +++ b/beacon-chain/rpc/testutil/mock_blocker.go @@ -14,10 +14,13 @@ import ( // MockBlocker is a fake implementation of lookup.Blocker. type MockBlocker struct { - BlockToReturn interfaces.ReadOnlySignedBeaconBlock - ErrorToReturn error - SlotBlockMap map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock - RootBlockMap map[[32]byte]interfaces.ReadOnlySignedBeaconBlock + BlockToReturn interfaces.ReadOnlySignedBeaconBlock + ErrorToReturn error + SlotBlockMap map[primitives.Slot]interfaces.ReadOnlySignedBeaconBlock + RootBlockMap map[[32]byte]interfaces.ReadOnlySignedBeaconBlock + DataColumnsFunc func(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) + DataColumnsToReturn []blocks.VerifiedRODataColumn + DataColumnsErrorToReturn *core.RpcError } // Block -- @@ -40,3 +43,17 @@ func (m *MockBlocker) Block(_ context.Context, b []byte) (interfaces.ReadOnlySig func (*MockBlocker) Blobs(_ context.Context, _ string, _ ...options.BlobsOption) ([]*blocks.VerifiedROBlob, *core.RpcError) { return nil, &core.RpcError{} } + +// DataColumns -- +func (m *MockBlocker) DataColumns(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + if m.DataColumnsFunc != nil { + return m.DataColumnsFunc(ctx, id, indices) + } + if m.DataColumnsErrorToReturn != nil { + return nil, m.DataColumnsErrorToReturn + } + if m.DataColumnsToReturn != nil { + return m.DataColumnsToReturn, nil + } + return nil, &core.RpcError{} +} diff --git a/changelog/james-prysm_debug-data-columns.md b/changelog/james-prysm_debug-data-columns.md new file mode 100644 index 0000000000..24da7794dd --- /dev/null +++ b/changelog/james-prysm_debug-data-columns.md @@ -0,0 +1,3 @@ +### Added + +- Adding `/eth/v1/debug/beacon/data_column_sidecars/{block_id}` endpoint. \ No newline at end of file