feat: fetch blobs via /eth/v1/beacon/blobs from beacon node (#1244)

* feat: featch blobs via /eth/v1/beacon/blobs from beacon node

* fix

* fix query parameter syntax

* bump version
This commit is contained in:
Péter Garamvölgyi 2025-11-12 15:24:45 +01:00 committed by GitHub
parent 9cfdf865ca
commit ff72fff220
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 40 additions and 50 deletions

View file

@ -24,7 +24,7 @@ import (
const ( const (
VersionMajor = 5 // Major version component of the current release VersionMajor = 5 // Major version component of the current release
VersionMinor = 9 // Minor version component of the current release VersionMinor = 9 // Minor version component of the current release
VersionPatch = 9 // Patch version component of the current release VersionPatch = 10 // Patch version component of the current release
VersionMeta = "mainnet" // Version metadata to append to the version string VersionMeta = "mainnet" // Version metadata to append to the version string
) )

View file

@ -2,7 +2,6 @@ package blob_client
import ( import (
"context" "context"
"crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -29,7 +28,7 @@ type BeaconNodeClient struct {
var ( var (
beaconNodeGenesisEndpoint = "/eth/v1/beacon/genesis" beaconNodeGenesisEndpoint = "/eth/v1/beacon/genesis"
beaconNodeSpecEndpoint = "/eth/v1/config/spec" beaconNodeSpecEndpoint = "/eth/v1/config/spec"
beaconNodeBlobEndpoint = "/eth/v1/beacon/blob_sidecars" beaconNodeBlobEndpoint = "/eth/v1/beacon/blobs"
) )
func NewBeaconNodeClient(apiEndpoint string) (*BeaconNodeClient, error) { func NewBeaconNodeClient(apiEndpoint string) (*BeaconNodeClient, error) {
@ -106,15 +105,31 @@ func NewBeaconNodeClient(apiEndpoint string) (*BeaconNodeClient, error) {
}, nil }, nil
} }
func (c *BeaconNodeClient) getBlobsPath(slot uint64, versionedHash common.Hash) (string, error) {
basePath, err := url.JoinPath(c.apiEndpoint, beaconNodeBlobEndpoint, fmt.Sprintf("%d", slot))
if err != nil {
return "", fmt.Errorf("failed to join path, err: %w", err)
}
u, err := url.Parse(basePath)
if err != nil {
return "", fmt.Errorf("failed to parse path, err: %w", err)
}
q := u.Query()
q.Set("versioned_hashes", versionedHash.Hex())
u.RawQuery = q.Encode()
queryPath := u.String()
return queryPath, nil
}
func (c *BeaconNodeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Context, versionedHash common.Hash, blockTime uint64) (*kzg4844.Blob, error) { func (c *BeaconNodeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Context, versionedHash common.Hash, blockTime uint64) (*kzg4844.Blob, error) {
slot := (blockTime - c.genesisTime) / c.secondsPerSlot slot := (blockTime - c.genesisTime) / c.secondsPerSlot
// get blob sidecar for slot // get blob by slot and versioned hash
blobSidecarPath, err := url.JoinPath(c.apiEndpoint, beaconNodeBlobEndpoint, fmt.Sprintf("%d", slot)) getBlobsPath, err := c.getBlobsPath(slot, versionedHash)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to join path, err: %w", err) return nil, fmt.Errorf("failed to create getBlobs path, err: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, "GET", blobSidecarPath, nil) req, err := http.NewRequestWithContext(ctx, "GET", getBlobsPath, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create request, err: %w", err) return nil, fmt.Errorf("failed to create request, err: %w", err)
} }
@ -133,25 +148,21 @@ func (c *BeaconNodeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Contex
return nil, fmt.Errorf("beacon node request failed, status: %s, body: %s", resp.Status, bodyStr) return nil, fmt.Errorf("beacon node request failed, status: %s, body: %s", resp.Status, bodyStr)
} }
var blobSidecarResp BlobSidecarResp var blobsResp BlobsResp
err = json.NewDecoder(resp.Body).Decode(&blobSidecarResp) err = json.NewDecoder(resp.Body).Decode(&blobsResp)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decode result into struct, err: %w", err) return nil, fmt.Errorf("failed to decode result into struct, err: %w", err)
} }
// find blob with desired versionedHash // sanity check response length
for _, blob := range blobSidecarResp.Data { if len(blobsResp.Data) == 0 {
// calculate blob hash from commitment and check it with desired return nil, fmt.Errorf("missing blob %v in slot %d", versionedHash, slot)
commitmentBytes := common.FromHex(blob.KzgCommitment) }
if len(commitmentBytes) != lenKZGCommitment { if len(blobsResp.Data) > 1 {
return nil, fmt.Errorf("len of kzg commitment is not correct, expected: %d, got: %d", lenKZGCommitment, len(commitmentBytes)) return nil, fmt.Errorf("more than 1 blob returned from beacon node for slot %d, requested blob hash: %s, expected 1, got: %d", slot, versionedHash.Hex(), len(blobsResp.Data))
} }
commitment := kzg4844.Commitment(commitmentBytes)
blobVersionedHash := kzg4844.CalcBlobHashV1(sha256.New(), &commitment)
if blobVersionedHash == versionedHash { blobBytes := common.FromHex(blobsResp.Data[0])
// found desired blob
blobBytes := common.FromHex(blob.Blob)
if len(blobBytes) != lenBlobBytes { if len(blobBytes) != lenBlobBytes {
return nil, fmt.Errorf("len of blob data is not correct, expected: %d, got: %d", lenBlobBytes, len(blobBytes)) return nil, fmt.Errorf("len of blob data is not correct, expected: %d, got: %d", lenBlobBytes, len(blobBytes))
} }
@ -159,10 +170,6 @@ func (c *BeaconNodeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Contex
b := kzg4844.Blob(blobBytes) b := kzg4844.Blob(blobBytes)
return &b, nil return &b, nil
} }
}
return nil, fmt.Errorf("missing blob %v in slot %d", versionedHash, slot)
}
type GenesisResp struct { type GenesisResp struct {
Data struct { Data struct {
@ -176,22 +183,6 @@ type SpecResp struct {
} `json:"data"` } `json:"data"`
} }
type BlobSidecarResp struct { type BlobsResp struct {
Data []struct { Data []string `json:"data"` // array of blobs as hex strings
Index string `json:"index"`
Blob string `json:"blob"`
KzgCommitment string `json:"kzg_commitment"`
KzgProof string `json:"kzg_proof"`
SignedBlockHeader struct {
Message struct {
Slot string `json:"slot"`
ProposerIndex string `json:"proposer_index"`
ParentRoot string `json:"parent_root"`
StateRoot string `json:"state_root"`
BodyRoot string `json:"body_root"`
} `json:"message"`
Signature string `json:"signature"`
} `json:"signed_block_header"`
KzgCommitmentInclusionProof []string `json:"kzg_commitment_inclusion_proof"`
} `json:"data"`
} }

View file

@ -13,7 +13,6 @@ import (
const ( const (
lenBlobBytes int = 131072 lenBlobBytes int = 131072
lenKZGCommitment int = 48
) )
type BlobClient interface { type BlobClient interface {