Skip to content

Commit 91ebb83

Browse files
Merge pull request #3826 from OffchainLabs/v3.7.0-backports-support-beacon-blob-api
Add support for new beacon chain /blobs endpoint
2 parents be155d0 + 4537ec9 commit 91ebb83

File tree

3 files changed

+314
-12
lines changed

3 files changed

+314
-12
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ all: build build-replay-env test-gen-proofs
169169
@touch .make/all
170170

171171
.PHONY: build
172-
build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daprovider daserver autonomous-auctioneer bidder-client datool el-proxy mockexternalsigner seq-coordinator-invalidate nitro-val seq-coordinator-manager dbconv genesis-generator)
172+
build: $(patsubst %,$(output_root)/bin/%, nitro deploy relay daprovider daserver autonomous-auctioneer bidder-client datool blobtool el-proxy mockexternalsigner seq-coordinator-invalidate nitro-val seq-coordinator-manager dbconv genesis-generator)
173173
@printf $(done)
174174

175175
.PHONY: build-node-deps
@@ -334,6 +334,9 @@ $(output_root)/bin/el-proxy: $(DEP_PREDICATE) build-node-deps
334334
$(output_root)/bin/datool: $(DEP_PREDICATE) build-node-deps
335335
go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/datool"
336336

337+
$(output_root)/bin/blobtool: $(DEP_PREDICATE) build-node-deps
338+
go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/blobtool"
339+
337340
$(output_root)/bin/genesis-generator: $(DEP_PREDICATE) build-node-deps
338341
go build $(GOLANG_PARAMS) -o $@ "$(CURDIR)/cmd/genesis-generator"
339342

cmd/blobtool/blobtool.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright 2025, Offchain Labs, Inc.
2+
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md
3+
4+
// This is a command line tool for testing beacon/blobs and blob_sidecars endpoints.
5+
package main
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"os"
11+
"strings"
12+
"time"
13+
14+
flag "github.com/spf13/pflag"
15+
16+
"github.com/ethereum/go-ethereum/common"
17+
"github.com/ethereum/go-ethereum/crypto/kzg4844"
18+
19+
"github.com/offchainlabs/nitro/cmd/util/confighelpers"
20+
"github.com/offchainlabs/nitro/util/blobs"
21+
"github.com/offchainlabs/nitro/util/headerreader"
22+
)
23+
24+
func main() {
25+
args := os.Args
26+
if len(args) < 2 {
27+
fmt.Println("Usage: blobtool [fetch] ...")
28+
os.Exit(1)
29+
}
30+
31+
var err error
32+
switch strings.ToLower(args[1]) {
33+
case "fetch":
34+
err = fetchBlobs(args[2:])
35+
default:
36+
err = fmt.Errorf("unknown command '%s', valid commands are: fetch", args[1])
37+
}
38+
if err != nil {
39+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
40+
os.Exit(1)
41+
}
42+
}
43+
44+
type FetchConfig struct {
45+
BeaconURL string `koanf:"beacon-url"`
46+
Slot uint64 `koanf:"slot"`
47+
VersionedHashes []string `koanf:"versioned-hashes"`
48+
UseLegacyEndpoint bool `koanf:"use-legacy-endpoint"`
49+
CompareEndpoints bool `koanf:"compare-endpoints"`
50+
}
51+
52+
func parseFetchConfig(args []string) (*FetchConfig, error) {
53+
f := flag.NewFlagSet("blobtool fetch", flag.ContinueOnError)
54+
f.String("beacon-url", "", "Beacon Chain RPC URL. For example with --beacon-url=http://localhost, an RPC call will be made to http://localhost/eth/v1/beacon/blobs")
55+
f.Uint64("slot", 0, "Beacon chain slot number to fetch blobs from")
56+
f.StringSlice("versioned-hashes", []string{}, "Comma-separated list of versioned hashes to fetch (optional - fetches all if not provided)")
57+
f.Bool("use-legacy-endpoint", false, "Use the legacy blob_sidecars endpoint")
58+
f.Bool("compare-endpoints", false, "Fetch using both endpoints and compare results")
59+
60+
k, err := confighelpers.BeginCommonParse(f, args)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
var config FetchConfig
66+
if err := confighelpers.EndCommonParse(k, &config); err != nil {
67+
return nil, err
68+
}
69+
70+
if config.BeaconURL == "" {
71+
return nil, fmt.Errorf("--beacon-url is required")
72+
}
73+
if config.Slot == 0 {
74+
return nil, fmt.Errorf("--slot is required")
75+
}
76+
77+
return &config, nil
78+
}
79+
80+
func fetchBlobs(args []string) error {
81+
config, err := parseFetchConfig(args)
82+
if err != nil {
83+
return err
84+
}
85+
86+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
87+
defer cancel()
88+
89+
versionedHashes := make([]common.Hash, len(config.VersionedHashes))
90+
for i, hashStr := range config.VersionedHashes {
91+
if !common.IsHexAddress(hashStr) && len(hashStr) != 66 {
92+
return fmt.Errorf("invalid versioned hash at index %d: %s", i, hashStr)
93+
}
94+
versionedHashes[i] = common.HexToHash(hashStr)
95+
}
96+
97+
if config.UseLegacyEndpoint && len(versionedHashes) == 0 {
98+
return fmt.Errorf("--versioned-hashes is required when using --use-legacy-endpoint")
99+
}
100+
101+
if config.CompareEndpoints {
102+
if len(versionedHashes) == 0 {
103+
return fmt.Errorf("--versioned-hashes is required when using --compare-endpoints")
104+
}
105+
return compareEndpoints(ctx, config, versionedHashes)
106+
}
107+
108+
blobClientConfig := headerreader.BlobClientConfig{
109+
BeaconUrl: config.BeaconURL,
110+
UseLegacyEndpoint: config.UseLegacyEndpoint,
111+
}
112+
113+
blobClient, err := headerreader.NewBlobClient(blobClientConfig, nil)
114+
if err != nil {
115+
return fmt.Errorf("failed to create blob client: %w", err)
116+
}
117+
118+
if err := blobClient.Initialize(ctx); err != nil {
119+
return fmt.Errorf("failed to initialize blob client: %w", err)
120+
}
121+
122+
endpointType := "new blobs"
123+
if config.UseLegacyEndpoint {
124+
endpointType = "legacy blob_sidecars"
125+
}
126+
127+
if len(versionedHashes) > 0 {
128+
fmt.Printf("Fetching %d blobs for slot %d using %s endpoint...\n", len(versionedHashes), config.Slot, endpointType)
129+
} else {
130+
fmt.Printf("Fetching all blobs for slot %d using %s endpoint...\n", config.Slot, endpointType)
131+
}
132+
133+
startTime := time.Now()
134+
fetchedBlobs, err := blobClient.GetBlobsBySlot(ctx, config.Slot, versionedHashes)
135+
if err != nil {
136+
return fmt.Errorf("failed to fetch blobs: %w", err)
137+
}
138+
duration := time.Since(startTime)
139+
140+
fmt.Printf("Successfully fetched %d blobs in %v\n", len(fetchedBlobs), duration)
141+
142+
for i, blob := range fetchedBlobs {
143+
_, hashes, err := blobs.ComputeCommitmentsAndHashes([]kzg4844.Blob{blob})
144+
if err != nil {
145+
return fmt.Errorf("failed to compute commitment for blob %d: %w", i, err)
146+
}
147+
if len(versionedHashes) > 0 {
148+
fmt.Printf("Blob %d: versioned_hash=%s (computed=%s), size=%d bytes\n", i, versionedHashes[i].Hex(), hashes[0].Hex(), len(blob))
149+
} else {
150+
fmt.Printf("Blob %d: versioned_hash=%s, size=%d bytes\n", i, hashes[0].Hex(), len(blob))
151+
}
152+
}
153+
154+
return nil
155+
}
156+
157+
func compareEndpoints(ctx context.Context, config *FetchConfig, versionedHashes []common.Hash) error {
158+
fmt.Println("Comparing legacy blob_sidecars and new blobs endpoints...")
159+
fmt.Println()
160+
161+
legacyConfig := headerreader.BlobClientConfig{
162+
BeaconUrl: config.BeaconURL,
163+
UseLegacyEndpoint: true,
164+
}
165+
legacyClient, err := headerreader.NewBlobClient(legacyConfig, nil)
166+
if err != nil {
167+
return fmt.Errorf("failed to create legacy blob client: %w", err)
168+
}
169+
if err := legacyClient.Initialize(ctx); err != nil {
170+
return fmt.Errorf("failed to initialize legacy blob client: %w", err)
171+
}
172+
173+
fmt.Println("Fetching with legacy blob_sidecars endpoint...")
174+
legacyStart := time.Now()
175+
legacyBlobs, err := legacyClient.GetBlobsBySlot(ctx, config.Slot, versionedHashes)
176+
legacyDuration := time.Since(legacyStart)
177+
if err != nil {
178+
return fmt.Errorf("failed to fetch blobs with legacy endpoint: %w", err)
179+
}
180+
fmt.Printf("✓ Legacy endpoint: fetched %d blobs in %v\n", len(legacyBlobs), legacyDuration)
181+
fmt.Println()
182+
183+
newConfig := headerreader.BlobClientConfig{
184+
BeaconUrl: config.BeaconURL,
185+
UseLegacyEndpoint: false,
186+
}
187+
newClient, err := headerreader.NewBlobClient(newConfig, nil)
188+
if err != nil {
189+
return fmt.Errorf("failed to create new blob client: %w", err)
190+
}
191+
if err := newClient.Initialize(ctx); err != nil {
192+
return fmt.Errorf("failed to initialize new blob client: %w", err)
193+
}
194+
195+
fmt.Println("Fetching with new blobs endpoint...")
196+
newStart := time.Now()
197+
newBlobs, err := newClient.GetBlobsBySlot(ctx, config.Slot, versionedHashes)
198+
newDuration := time.Since(newStart)
199+
if err != nil {
200+
return fmt.Errorf("failed to fetch blobs with new endpoint: %w", err)
201+
}
202+
fmt.Printf("✓ New endpoint: fetched %d blobs in %v\n", len(newBlobs), newDuration)
203+
fmt.Println()
204+
205+
if len(legacyBlobs) != len(newBlobs) {
206+
return fmt.Errorf("blob count mismatch: legacy=%d, new=%d", len(legacyBlobs), len(newBlobs))
207+
}
208+
209+
fmt.Println("Comparing blob data...")
210+
for i := range legacyBlobs {
211+
if legacyBlobs[i] != newBlobs[i] {
212+
return fmt.Errorf("blob %d data mismatch", i)
213+
}
214+
_, hashes, err := blobs.ComputeCommitmentsAndHashes([]kzg4844.Blob{legacyBlobs[i]})
215+
if err != nil {
216+
return fmt.Errorf("failed to compute hash for blob %d: %w", i, err)
217+
}
218+
fmt.Printf(" Blob %d: ✓ identical (%s)\n", i, hashes[0].Hex())
219+
}
220+
221+
fmt.Println()
222+
fmt.Printf("Performance comparison:\n")
223+
fmt.Printf(" Legacy endpoint: %v\n", legacyDuration)
224+
fmt.Printf(" New endpoint: %v\n", newDuration)
225+
if newDuration < legacyDuration {
226+
improvement := float64(legacyDuration-newDuration) / float64(legacyDuration) * 100
227+
fmt.Printf(" New endpoint is %.1f%% faster\n", improvement)
228+
} else {
229+
slower := float64(newDuration-legacyDuration) / float64(legacyDuration) * 100
230+
fmt.Printf(" New endpoint is %.1f%% slower\n", slower)
231+
}
232+
233+
return nil
234+
}

0 commit comments

Comments
 (0)