diff --git a/beacon-chain/rpc/eth/debug/BUILD.bazel b/beacon-chain/rpc/eth/debug/BUILD.bazel index f19a891c261b..6b7ab7beb77b 100644 --- a/beacon-chain/rpc/eth/debug/BUILD.bazel +++ b/beacon-chain/rpc/eth/debug/BUILD.bazel @@ -24,7 +24,6 @@ go_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", diff --git a/beacon-chain/rpc/eth/debug/handlers.go b/beacon-chain/rpc/eth/debug/handlers.go index dbc9208b43ea..cdcac7fe777f 100644 --- a/beacon-chain/rpc/eth/debug/handlers.go +++ b/beacon-chain/rpc/eth/debug/handlers.go @@ -2,6 +2,7 @@ package debug import ( "context" + "encoding/binary" "encoding/json" "fmt" "math" @@ -22,7 +23,6 @@ import ( "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" "github.com/OffchainLabs/prysm/v7/network/httputil" - ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" @@ -495,23 +495,28 @@ func buildDataColumnSidecarsGloasJsonResponse(verifiedDataColumns []blocks.Verif return sidecars } -// buildDataColumnSidecarsSSZResponse builds SSZ response for data column sidecars +// buildDataColumnSidecarsSSZResponse encodes the sidecars as an SSZ list of variable-size +// elements: a 4-byte offset per element, followed by the elements themselves. 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 + elements := make([][]byte, len(verifiedDataColumns)) + total := 4 * len(verifiedDataColumns) 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...) + elements[i] = sszrep + total += len(sszrep) + } + + ssz := make([]byte, 0, total) + offset := 4 * len(verifiedDataColumns) + for _, element := range elements { + ssz = binary.LittleEndian.AppendUint32(ssz, uint32(offset)) + offset += len(element) + } + for _, element := range elements { + ssz = append(ssz, element...) } return ssz, nil diff --git a/beacon-chain/rpc/eth/debug/handlers_test.go b/beacon-chain/rpc/eth/debug/handlers_test.go index c1645121deff..b8309c357209 100644 --- a/beacon-chain/rpc/eth/debug/handlers_test.go +++ b/beacon-chain/rpc/eth/debug/handlers_test.go @@ -3,6 +3,7 @@ package debug import ( "bytes" "context" + "encoding/binary" "encoding/json" "errors" "math" @@ -904,6 +905,106 @@ func TestDataColumnSidecars(t *testing.T) { require.Equal(t, 1, len(data[0].KzgProofs)) require.Equal(t, hexutil.Encode(proof), data[0].KzgProofs[0]) }) + + t.Run("SSZ response uses list framing", func(t *testing.T) { + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = 0 + config.GloasForkEpoch = 0 + params.OverrideBeaconConfig(config) + + signedTestBlock := util.NewBeaconBlockGloas() + signedTestBlock.Block.Slot = 7 + roBlock, err := blocks.NewSignedBeaconBlock(signedTestBlock) + require.NoError(t, err) + + chainService := &blockchainmock.ChainService{} + currentSlot := primitives.Slot(7) + chainService.Slot = ¤tSlot + chainService.OptimisticRoots = make(map[[32]byte]bool) + chainService.FinalizedRoots = make(map[[32]byte]bool) + + sidecars := []blocks.VerifiedRODataColumn{testGloasDataColumn(t, 0), testGloasDataColumn(t, 1)} + mockBlocker := &testutil.MockBlocker{ + DataColumnsFunc: func(ctx context.Context, id string, indices []int) ([]blocks.VerifiedRODataColumn, *core.RpcError) { + return sidecars, nil + }, + 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) + request.Header.Set("Accept", api.OctetStreamMediaType) + request.SetPathValue("block_id", "head") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + require.Equal(t, version.String(version.Gloas), writer.Header().Get(api.VersionHeader)) + + first, err := sidecars[0].MarshalSSZ() + require.NoError(t, err) + second, err := sidecars[1].MarshalSSZ() + require.NoError(t, err) + body := writer.Body.Bytes() + require.Equal(t, 8+len(first)+len(second), len(body)) + assert.Equal(t, uint32(8), binary.LittleEndian.Uint32(body[0:4])) + assert.Equal(t, uint32(8+len(first)), binary.LittleEndian.Uint32(body[4:8])) + require.DeepEqual(t, first, body[8:8+len(first)]) + require.DeepEqual(t, second, body[8+len(first):]) + }) + + t.Run("SSZ response with no sidecars is empty", func(t *testing.T) { + originalConfig := params.BeaconConfig() + defer func() { params.OverrideBeaconConfig(originalConfig) }() + + config := params.BeaconConfig().Copy() + config.FuluForkEpoch = 0 + params.OverrideBeaconConfig(config) + + signedTestBlock := util.NewBeaconBlock() + roBlock, err := blocks.NewSignedBeaconBlock(signedTestBlock) + require.NoError(t, err) + + chainService := &blockchainmock.ChainService{} + currentSlot := primitives.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 + }, + 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) + request.Header.Set("Accept", api.OctetStreamMediaType) + request.SetPathValue("block_id", "head") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.DataColumnSidecars(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + require.Equal(t, 0, writer.Body.Len()) + }) } func TestParseDataColumnIndices(t *testing.T) { @@ -982,6 +1083,19 @@ func TestParseDataColumnIndices(t *testing.T) { } } +// testGloasDataColumn builds a minimal Gloas data column sidecar for SSZ framing assertions. +func testGloasDataColumn(t *testing.T, index uint64) blocks.VerifiedRODataColumn { + roDc, err := blocks.NewRODataColumnGloas(ðpb.DataColumnSidecarGloas{ + Index: index, + Column: [][]byte{bytesutil.PadTo([]byte{byte(index)}, 2048)}, + KzgProofs: [][]byte{bytesutil.PadTo([]byte{0x33}, 48)}, + Slot: 7, + BeaconBlockRoot: bytesutil.PadTo([]byte{0xab, 0xcd}, 32), + }) + require.NoError(t, err) + return blocks.NewVerifiedRODataColumn(roDc) +} + func TestBuildDataColumnSidecarsSSZResponse(t *testing.T) { t.Run("empty data columns", func(t *testing.T) { result, err := buildDataColumnSidecarsSSZResponse([]blocks.VerifiedRODataColumn{}) @@ -989,8 +1103,19 @@ func TestBuildDataColumnSidecarsSSZResponse(t *testing.T) { require.DeepEqual(t, []byte{}, result) }) - t.Run("get SSZ size", func(t *testing.T) { - size := (ðpb.DataColumnSidecar{}).SizeSSZ() - assert.Equal(t, true, size > 0) + t.Run("list framing", func(t *testing.T) { + sidecars := []blocks.VerifiedRODataColumn{testGloasDataColumn(t, 0), testGloasDataColumn(t, 1)} + first, err := sidecars[0].MarshalSSZ() + require.NoError(t, err) + second, err := sidecars[1].MarshalSSZ() + require.NoError(t, err) + + result, err := buildDataColumnSidecarsSSZResponse(sidecars) + require.NoError(t, err) + require.Equal(t, 8+len(first)+len(second), len(result)) + assert.Equal(t, uint32(8), binary.LittleEndian.Uint32(result[0:4])) + assert.Equal(t, uint32(8+len(first)), binary.LittleEndian.Uint32(result[4:8])) + require.DeepEqual(t, first, result[8:8+len(first)]) + require.DeepEqual(t, second, result[8+len(first):]) }) } diff --git a/changelog/syjn99_fix-dcs-ssz-list-framing.md b/changelog/syjn99_fix-dcs-ssz-list-framing.md new file mode 100644 index 000000000000..2ba234e9f444 --- /dev/null +++ b/changelog/syjn99_fix-dcs-ssz-list-framing.md @@ -0,0 +1,3 @@ +### Fixed + +- Encode the SSZ response of `GET /eth/v1/debug/beacon/data_column_sidecars/{block_id}` as an SSZ list of variable-size elements (4-byte offsets followed by the elements) instead of plain concatenation, per [beacon-APIs#633](https://github.com/ethereum/beacon-APIs/pull/633).