Skip to content

Commit a2fcba2

Browse files
authored
feat: implement reconstruct data column sidecars (#15005)
1 parent abe8638 commit a2fcba2

File tree

5 files changed

+243
-2
lines changed

5 files changed

+243
-2
lines changed

beacon-chain/execution/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ go_library(
2525
"//testing/spectest:__subpackages__",
2626
],
2727
deps = [
28+
"//beacon-chain/blockchain/kzg:go_default_library",
2829
"//beacon-chain/cache:go_default_library",
2930
"//beacon-chain/cache/depositsnapshot:go_default_library",
3031
"//beacon-chain/core/altair:go_default_library",
3132
"//beacon-chain/core/feed:go_default_library",
3233
"//beacon-chain/core/feed/state:go_default_library",
3334
"//beacon-chain/core/helpers:go_default_library",
35+
"//beacon-chain/core/peerdas:go_default_library",
3436
"//beacon-chain/core/transition:go_default_library",
3537
"//beacon-chain/db:go_default_library",
3638
"//beacon-chain/execution/types:go_default_library",
@@ -97,6 +99,7 @@ go_test(
9799
embed = [":go_default_library"],
98100
deps = [
99101
"//async/event:go_default_library",
102+
"//beacon-chain/blockchain/kzg:go_default_library",
100103
"//beacon-chain/cache/depositsnapshot:go_default_library",
101104
"//beacon-chain/core/feed:go_default_library",
102105
"//beacon-chain/core/feed/state:go_default_library",

beacon-chain/execution/engine_client.go

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
gethRPC "github.com/ethereum/go-ethereum/rpc"
1414
"github.com/holiman/uint256"
1515
"github.com/pkg/errors"
16+
"github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/kzg"
17+
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/peerdas"
1618
"github.com/prysmaticlabs/prysm/v5/beacon-chain/execution/types"
1719
"github.com/prysmaticlabs/prysm/v5/beacon-chain/verification"
1820
fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
@@ -44,11 +46,15 @@ var (
4446
GetPayloadMethodV3,
4547
GetPayloadBodiesByHashV1,
4648
GetPayloadBodiesByRangeV1,
49+
GetBlobsV1,
4750
}
4851
electraEngineEndpoints = []string{
4952
NewPayloadMethodV4,
5053
GetPayloadMethodV4,
5154
}
55+
fuluEngineEndpoints = []string{
56+
GetBlobsV2,
57+
}
5258
)
5359

5460
const (
@@ -85,6 +91,8 @@ const (
8591
ExchangeCapabilities = "engine_exchangeCapabilities"
8692
// GetBlobsV1 request string for JSON-RPC.
8793
GetBlobsV1 = "engine_getBlobsV1"
94+
// GetBlobsV2 request string for JSON-RPC.
95+
GetBlobsV2 = "engine_getBlobsV2"
8896
// Defines the seconds before timing out engine endpoints with non-block execution semantics.
8997
defaultEngineTimeout = time.Second
9098
)
@@ -108,6 +116,7 @@ type Reconstructor interface {
108116
ctx context.Context, blindedBlocks []interfaces.ReadOnlySignedBeaconBlock,
109117
) ([]interfaces.SignedBeaconBlock, error)
110118
ReconstructBlobSidecars(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, hi func(uint64) bool) ([]blocks.VerifiedROBlob, error)
119+
ReconstructDataColumnSidecars(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte) ([]blocks.VerifiedRODataColumn, error)
111120
}
112121

113122
// EngineCaller defines a client that can interact with an Ethereum
@@ -302,6 +311,9 @@ func (s *Service) ExchangeCapabilities(ctx context.Context) ([]string, error) {
302311
if params.ElectraEnabled() {
303312
supportedEngineEndpoints = append(supportedEngineEndpoints, electraEngineEndpoints...)
304313
}
314+
if params.FuluEnabled() {
315+
supportedEngineEndpoints = append(supportedEngineEndpoints, fuluEngineEndpoints...)
316+
}
305317
var result []string
306318
err := s.rpcClient.CallContext(ctx, &result, ExchangeCapabilities, supportedEngineEndpoints)
307319
if err != nil {
@@ -495,16 +507,30 @@ func (s *Service) HeaderByNumber(ctx context.Context, number *big.Int) (*types.H
495507
func (s *Service) GetBlobs(ctx context.Context, versionedHashes []common.Hash) ([]*pb.BlobAndProof, error) {
496508
ctx, span := trace.StartSpan(ctx, "powchain.engine-api-client.GetBlobs")
497509
defer span.End()
510+
498511
// If the execution engine does not support `GetBlobsV1`, return early to prevent encountering an error later.
499512
if !s.capabilityCache.has(GetBlobsV1) {
500-
return nil, nil
513+
return nil, errors.New(fmt.Sprintf("%s is not supported", GetBlobsV1))
501514
}
502515

503516
result := make([]*pb.BlobAndProof, len(versionedHashes))
504517
err := s.rpcClient.CallContext(ctx, &result, GetBlobsV1, versionedHashes)
505518
return result, handleRPCError(err)
506519
}
507520

521+
func (s *Service) GetBlobsV2(ctx context.Context, versionedHashes []common.Hash) ([]*pb.BlobAndProofV2, error) {
522+
ctx, span := trace.StartSpan(ctx, "powchain.engine-api-client.GetBlobsV2")
523+
defer span.End()
524+
525+
if !s.capabilityCache.has(GetBlobsV2) {
526+
return nil, errors.New(fmt.Sprintf("%s is not supported", GetBlobsV2))
527+
}
528+
529+
result := make([]*pb.BlobAndProofV2, len(versionedHashes))
530+
err := s.rpcClient.CallContext(ctx, &result, GetBlobsV2, versionedHashes)
531+
return result, handleRPCError(err)
532+
}
533+
508534
// ReconstructFullBlock takes in a blinded beacon block and reconstructs
509535
// a beacon block with a full execution payload via the engine API.
510536
func (s *Service) ReconstructFullBlock(
@@ -615,6 +641,83 @@ func (s *Service) ReconstructBlobSidecars(ctx context.Context, block interfaces.
615641
return verifiedBlobs, nil
616642
}
617643

644+
// ReconstructDataColumnSidecars reconstructs the verified data column sidecars for a given beacon block.
645+
// It retrieves the KZG commitments from the block body, fetches the associated blobs and cell proofs,
646+
// and constructs the corresponding verified read-only data column sidecars.
647+
func (s *Service) ReconstructDataColumnSidecars(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte) ([]blocks.VerifiedRODataColumn, error) {
648+
blockBody := block.Block().Body()
649+
kzgCommitments, err := blockBody.BlobKzgCommitments()
650+
if err != nil {
651+
return nil, wrapWithBlockRoot(err, blockRoot, "could not get blob KZG commitments")
652+
}
653+
654+
// Collect KZG hashes for all blobs
655+
var kzgHashes []common.Hash
656+
for _, commitment := range kzgCommitments {
657+
kzgHashes = append(kzgHashes, primitives.ConvertKzgCommitmentToVersionedHash(commitment))
658+
}
659+
660+
// Fetch all blobs from EL
661+
blobs, err := s.GetBlobsV2(ctx, kzgHashes)
662+
if err != nil {
663+
return nil, wrapWithBlockRoot(err, blockRoot, "could not get blobs")
664+
}
665+
666+
for _, blobAndCellProofs := range blobs {
667+
if blobAndCellProofs == nil {
668+
return nil, wrapWithBlockRoot(errors.New("unable to reconstruct data column sidecars, did not get all blobs from EL"), blockRoot, "")
669+
}
670+
}
671+
672+
var cellsAndProofs []kzg.CellsAndProofs
673+
for _, blobAndCellProofs := range blobs {
674+
var blob kzg.Blob
675+
copy(blob[:], blobAndCellProofs.Blob)
676+
cells, err := kzg.ComputeCells(&blob)
677+
if err != nil {
678+
return nil, wrapWithBlockRoot(err, blockRoot, "could not compute cells")
679+
}
680+
681+
proofs := make([]kzg.Proof, len(blobAndCellProofs.CellProofs))
682+
for i, proof := range blobAndCellProofs.CellProofs {
683+
proofs[i] = kzg.Proof(proof)
684+
}
685+
cellsAndProofs = append(cellsAndProofs, kzg.CellsAndProofs{
686+
Cells: cells,
687+
Proofs: proofs,
688+
})
689+
}
690+
691+
header, err := block.Header()
692+
if err != nil {
693+
return nil, wrapWithBlockRoot(err, blockRoot, "could not get header")
694+
}
695+
696+
kzgCommitmentsInclusionProof, err := blocks.MerkleProofKZGCommitments(blockBody)
697+
if err != nil {
698+
return nil, wrapWithBlockRoot(err, blockRoot, "could not get Merkle proof for KZG commitments")
699+
}
700+
701+
dataColumnSidecars, err := peerdas.DataColumnSidecarsForReconstruct(kzgCommitments, header, kzgCommitmentsInclusionProof, cellsAndProofs)
702+
if err != nil {
703+
return nil, wrapWithBlockRoot(err, blockRoot, "could not reconstruct data column sidecars")
704+
}
705+
706+
verifiedRODataColumns := make([]blocks.VerifiedRODataColumn, len(dataColumnSidecars))
707+
for i, dataColumnSidecar := range dataColumnSidecars {
708+
roDataColumn, err := blocks.NewRODataColumnWithRoot(dataColumnSidecar, blockRoot)
709+
if err != nil {
710+
return nil, wrapWithBlockRoot(err, blockRoot, "new read-only data column with root")
711+
}
712+
713+
verifiedRODataColumns[i] = blocks.NewVerifiedRODataColumn(roDataColumn)
714+
}
715+
716+
log.WithField("root", fmt.Sprintf("%x", blockRoot)).Debug("Data columns reconstructed successfully")
717+
718+
return verifiedRODataColumns, nil
719+
}
720+
618721
func fullPayloadFromPayloadBody(
619722
header interfaces.ExecutionData, body *pb.ExecutionPayloadBody, bVersion int,
620723
) (interfaces.ExecutionData, error) {
@@ -902,3 +1005,8 @@ func toBlockNumArg(number *big.Int) string {
9021005
}
9031006
return hexutil.EncodeBig(number)
9041007
}
1008+
1009+
// wrapWithBlockRoot returns a new error with the given block root.
1010+
func wrapWithBlockRoot(err error, blockRoot [32]byte, message string) error {
1011+
return errors.Wrap(err, fmt.Sprintf("%s for block %#x", message, blockRoot))
1012+
}

beacon-chain/execution/engine_client_test.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/ethereum/go-ethereum/rpc"
2121
"github.com/holiman/uint256"
2222
"github.com/pkg/errors"
23+
"github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/kzg"
2324
"github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filesystem"
2425
mocks "github.com/prysmaticlabs/prysm/v5/beacon-chain/execution/testing"
2526
"github.com/prysmaticlabs/prysm/v5/beacon-chain/verification"
@@ -2424,7 +2425,7 @@ func TestReconstructBlobSidecars(t *testing.T) {
24242425
t.Run("get-blobs end point is not supported", func(t *testing.T) {
24252426
hi := mockSummary(t, []bool{true, true, true, true, true, false})
24262427
verifiedBlobs, err := client.ReconstructBlobSidecars(ctx, sb, r, hi)
2427-
require.NoError(t, err)
2428+
require.ErrorContains(t, "engine_getBlobsV1 is not supported", err)
24282429
require.Equal(t, 0, len(verifiedBlobs))
24292430
})
24302431

@@ -2476,6 +2477,63 @@ func TestReconstructBlobSidecars(t *testing.T) {
24762477
})
24772478
}
24782479

2480+
func TestReconstructDataColumnSidecars(t *testing.T) {
2481+
// Start the trusted setup.
2482+
err := kzg.Start()
2483+
require.NoError(t, err)
2484+
2485+
// Setup right fork epoch
2486+
params.SetupTestConfigCleanup(t)
2487+
cfg := params.BeaconConfig().Copy()
2488+
cfg.CapellaForkEpoch = 1
2489+
cfg.DenebForkEpoch = 2
2490+
cfg.ElectraForkEpoch = 3
2491+
cfg.FuluForkEpoch = 4
2492+
params.OverrideBeaconConfig(cfg)
2493+
2494+
client := &Service{capabilityCache: &capabilityCache{}}
2495+
b := util.NewBeaconBlockFulu()
2496+
b.Block.Slot = 4 * params.BeaconConfig().SlotsPerEpoch
2497+
kzgCommitments := createRandomKzgCommitments(t, 6)
2498+
b.Block.Body.BlobKzgCommitments = kzgCommitments
2499+
r, err := b.Block.HashTreeRoot()
2500+
require.NoError(t, err)
2501+
sb, err := blocks.NewSignedBeaconBlock(b)
2502+
require.NoError(t, err)
2503+
2504+
ctx := context.Background()
2505+
t.Run("GetBlobsV2 is not supported", func(t *testing.T) {
2506+
_, err := client.ReconstructDataColumnSidecars(ctx, sb, r)
2507+
require.ErrorContains(t, "could not get blobs", err)
2508+
})
2509+
2510+
t.Run("receiving all blobs", func(t *testing.T) {
2511+
blobMasks := []bool{true, true, true, true, true, true}
2512+
srv := createBlobServerV2(t, 6, blobMasks)
2513+
defer srv.Close()
2514+
2515+
rpcClient, client := setupRpcClientV2(t, srv.URL, client)
2516+
defer rpcClient.Close()
2517+
2518+
dataColumns, err := client.ReconstructDataColumnSidecars(ctx, sb, r)
2519+
require.NoError(t, err)
2520+
require.Equal(t, 128, len(dataColumns))
2521+
})
2522+
2523+
t.Run("missing some blobs", func(t *testing.T) {
2524+
blobMasks := []bool{false, true, true, true, true, true}
2525+
srv := createBlobServerV2(t, 6, blobMasks)
2526+
defer srv.Close()
2527+
2528+
rpcClient, client := setupRpcClientV2(t, srv.URL, client)
2529+
defer rpcClient.Close()
2530+
2531+
dataColumns, err := client.ReconstructDataColumnSidecars(ctx, sb, r)
2532+
require.ErrorContains(t, "unable to reconstruct data column sidecars, did not get all blobs from EL", err)
2533+
require.Equal(t, 0, len(dataColumns))
2534+
})
2535+
}
2536+
24792537
func createRandomKzgCommitments(t *testing.T, num int) [][]byte {
24802538
kzgCommitments := make([][]byte, num)
24812539
for i := range kzgCommitments {
@@ -2511,6 +2569,41 @@ func createBlobServer(t *testing.T, numBlobs int, callbackFuncs ...func()) *http
25112569
}))
25122570
}
25132571

2572+
func createBlobServerV2(t *testing.T, numBlobs int, blobMasks []bool) *httptest.Server {
2573+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2574+
w.Header().Set("Content-Type", "application/json")
2575+
defer func() {
2576+
require.NoError(t, r.Body.Close())
2577+
}()
2578+
2579+
require.Equal(t, len(blobMasks), numBlobs)
2580+
2581+
blobAndCellProofs := make([]*pb.BlobAndCellProofJson, numBlobs)
2582+
for i := range blobAndCellProofs {
2583+
if !blobMasks[i] {
2584+
continue
2585+
}
2586+
2587+
blobAndCellProofs[i] = &pb.BlobAndCellProofJson{
2588+
Blob: []byte("0xblob"),
2589+
CellProofs: []hexutil.Bytes{},
2590+
}
2591+
for j := 0; j < int(params.BeaconConfig().NumberOfColumns); j++ {
2592+
blobAndCellProofs[i].CellProofs = append(blobAndCellProofs[i].CellProofs, []byte(fmt.Sprintf("0xproof%d", j)))
2593+
}
2594+
}
2595+
2596+
respJSON := map[string]interface{}{
2597+
"jsonrpc": "2.0",
2598+
"id": 1,
2599+
"result": blobAndCellProofs,
2600+
}
2601+
2602+
err := json.NewEncoder(w).Encode(respJSON)
2603+
require.NoError(t, err)
2604+
}))
2605+
}
2606+
25142607
func setupRpcClient(t *testing.T, url string, client *Service) (*rpc.Client, *Service) {
25152608
rpcClient, err := rpc.DialHTTP(url)
25162609
require.NoError(t, err)
@@ -2522,6 +2615,12 @@ func setupRpcClient(t *testing.T, url string, client *Service) (*rpc.Client, *Se
25222615
return rpcClient, client
25232616
}
25242617

2618+
func setupRpcClientV2(t *testing.T, url string, client *Service) (*rpc.Client, *Service) {
2619+
rpcClient, client := setupRpcClient(t, url, client)
2620+
client.capabilityCache = &capabilityCache{capabilities: map[string]interface{}{GetBlobsV2: nil}}
2621+
return rpcClient, client
2622+
}
2623+
25252624
func testNewBlobVerifier() verification.NewBlobVerifier {
25262625
return func(b blocks.ROBlob, reqs []verification.Requirement) verification.BlobVerifier {
25272626
return &verification.MockBlobVerifier{

beacon-chain/execution/testing/mock_engine_client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ func (e *EngineClient) ReconstructBlobSidecars(context.Context, interfaces.ReadO
113113
return e.BlobSidecars, e.ErrorBlobSidecars
114114
}
115115

116+
func (e *EngineClient) ReconstructDataColumnSidecars(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte) ([]blocks.VerifiedRODataColumn, error) {
117+
return nil, nil
118+
}
119+
116120
// GetTerminalBlockHash --
117121
func (e *EngineClient) GetTerminalBlockHash(ctx context.Context, transitionTime uint64) ([]byte, bool, error) {
118122
ttd := new(big.Int)

proto/engine/v1/json_marshal_unmarshal.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,3 +1282,30 @@ func (b *BlobAndProof) UnmarshalJSON(enc []byte) error {
12821282

12831283
return nil
12841284
}
1285+
1286+
type BlobAndCellProofJson struct {
1287+
Blob hexutil.Bytes `json:"blob"`
1288+
CellProofs []hexutil.Bytes `json:"cell_proofs"`
1289+
}
1290+
1291+
// UnmarshalJSON implements the json unmarshaler interface for BlobAndCellProofJson.
1292+
func (b *BlobAndProofV2) UnmarshalJSON(enc []byte) error {
1293+
var dec *BlobAndCellProofJson
1294+
if err := json.Unmarshal(enc, &dec); err != nil {
1295+
return err
1296+
}
1297+
1298+
blob := make([]byte, fieldparams.BlobLength)
1299+
copy(blob, dec.Blob)
1300+
b.Blob = blob
1301+
1302+
cellProofs := make([][]byte, len(dec.CellProofs))
1303+
for i, proof := range dec.CellProofs {
1304+
p := make([]byte, fieldparams.BLSPubkeyLength)
1305+
copy(p, proof)
1306+
cellProofs[i] = p
1307+
}
1308+
b.CellProofs = cellProofs
1309+
1310+
return nil
1311+
}

0 commit comments

Comments
 (0)