diff --git a/node/derivation/base_client_test.go b/node/derivation/base_client_test.go index fe371c41c..2a48d748f 100644 --- a/node/derivation/base_client_test.go +++ b/node/derivation/base_client_test.go @@ -10,29 +10,51 @@ import ( "testing" "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/crypto/kzg4844" "github.com/stretchr/testify/require" ) -// zero-value but correctly-sized hex so the beacon JSON decodes into a -// BlobSidecar. GetBlobSidecarsEnhanced does not verify blob contents (that -// happens downstream), it only counts sidecars, so dummy bytes are fine here. +// The fallback verifies blob content, so accept-path stubs must serve a real +// (blob, commitment) pair; the all-zero blob is the cheapest valid one. var ( hex32 = "0x" + strings.Repeat("00", 32) hex48 = "0x" + strings.Repeat("00", 48) + + zeroBlobHex = "0x" + strings.Repeat("00", BlobSize) + zeroBlobCommitment = mustZeroBlobCommitment() + zeroBlobHash = KZGToVersionedHash(zeroBlobCommitment) + + // Valid blob bytes served under the zero blob's commitment: count and + // commitment lookup pass, only verifyBlob catches it. + corruptBlobHex = "0x01" + strings.Repeat("00", BlobSize-1) ) -func sidecarJSON(index int) string { - return fmt.Sprintf(`{"block_root":%q,"slot":"1","blob":"0x00","index":"%d","kzg_commitment":%q,"kzg_proof":%q}`, - hex32, index, hex48, hex48) +func mustZeroBlobCommitment() kzg4844.Commitment { + var blob Blob + commitment, err := kzg4844.BlobToCommitment(blob.KZGBlob()) + if err != nil { + panic(err) + } + return commitment +} + +func sidecarJSON(index int, blobHex, commitmentHex string) string { + return fmt.Sprintf(`{"block_root":%q,"slot":"1","blob":%q,"index":"%d","kzg_commitment":%q,"kzg_proof":%q}`, + hex32, blobHex, index, commitmentHex, hex48) } // beaconBehavior controls what a stub beacon returns for blob_sidecars. type beaconBehavior int const ( - beaconServesBlob beaconBehavior = iota // 200 with one sidecar - beaconServesEmpty // 200 with an empty list (pruned / not indexed) - beaconServerError // 500 + beaconServesBlob beaconBehavior = iota // 200 with one valid sidecar + beaconServesEmpty // 200 with an empty list (pruned / not indexed) + beaconServerError // 500 + beaconServesCorruptBlob // 200, right count and commitment, blob bytes do not match + beaconServesWrongCommitment // 200, right count, commitment of some other blob + beaconServesNullSidecar // 200, JSON null entry in the sidecar list ) // newStubBeacon serves the genesis + spec endpoints (needed for slot math) and @@ -51,11 +73,17 @@ func newStubBeacon(t *testing.T, behavior beaconBehavior) (string, *int32) { atomic.AddInt32(&blobHits, 1) switch behavior { case beaconServesBlob: - _, _ = w.Write([]byte(`{"data":[` + sidecarJSON(0) + `]}`)) + _, _ = w.Write([]byte(`{"data":[` + sidecarJSON(0, zeroBlobHex, hexutil.Encode(zeroBlobCommitment[:])) + `]}`)) case beaconServesEmpty: _, _ = w.Write([]byte(`{"data":[]}`)) case beaconServerError: w.WriteHeader(http.StatusInternalServerError) + case beaconServesCorruptBlob: + _, _ = w.Write([]byte(`{"data":[` + sidecarJSON(0, corruptBlobHex, hexutil.Encode(zeroBlobCommitment[:])) + `]}`)) + case beaconServesWrongCommitment: + _, _ = w.Write([]byte(`{"data":[` + sidecarJSON(0, zeroBlobHex, hex48) + `]}`)) + case beaconServesNullSidecar: + _, _ = w.Write([]byte(`{"data":[null]}`)) } default: w.WriteHeader(http.StatusNotFound) @@ -66,7 +94,13 @@ func newStubBeacon(t *testing.T, behavior beaconBehavior) (string, *int32) { } func oneHash() []IndexedBlobHash { - return []IndexedBlobHash{{Index: 0, Hash: common.Hash{}}} + return []IndexedBlobHash{{Index: 0, Hash: zeroBlobHash}} +} + +// wantHashes is the verification set the caller always has (tx.BlobHashes()), +// independent of whether index hints could be built. +func wantHashes() []common.Hash { + return []common.Hash{zeroBlobHash} } type canceledHTTP struct { @@ -78,9 +112,9 @@ func (h canceledHTTP) Get(ctx context.Context, _ string, _ http.Header) (*http.R return nil, ctx.Err() } -func fetch(t *testing.T, c *FallbackBeaconClient) ([]*BlobSidecar, error) { +func fetch(t *testing.T, c *FallbackBeaconClient) (types.BlobTxSidecar, error) { t.Helper() - return c.GetBlobSidecarsEnhanced(context.Background(), L1BlockRef{Time: 12}, oneHash()) + return c.GetVerifiedBlobSidecar(context.Background(), L1BlockRef{Time: 12}, wantHashes(), oneHash()) } // The primary serves the blob and the fallback is never queried. @@ -91,7 +125,7 @@ func TestFallbackBeacon_PrimaryServesBlob(t *testing.T) { c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) sidecars, err := fetch(t, c) require.NoError(t, err) - require.Len(t, sidecars, 1) + require.Len(t, sidecars.Blobs, 1) require.EqualValues(t, 1, atomic.LoadInt32(primaryHits)) require.EqualValues(t, 0, atomic.LoadInt32(fallbackHits), "fallback must not be queried while primary serves the blob") } @@ -105,7 +139,7 @@ func TestFallbackBeacon_FallsBackOnEmptyResult(t *testing.T) { c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) sidecars, err := fetch(t, c) require.NoError(t, err) - require.Len(t, sidecars, 1) + require.Len(t, sidecars.Blobs, 1) require.Positive(t, atomic.LoadInt32(primaryHits)) require.Positive(t, atomic.LoadInt32(fallbackHits)) } @@ -118,7 +152,7 @@ func TestFallbackBeacon_FallsBackOnServerError(t *testing.T) { c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) sidecars, err := fetch(t, c) require.NoError(t, err) - require.Len(t, sidecars, 1) + require.Len(t, sidecars.Blobs, 1) require.Positive(t, atomic.LoadInt32(fallbackHits)) } @@ -129,10 +163,89 @@ func TestFallbackBeacon_FallsBackOnTransportError(t *testing.T) { c := NewFallbackBeaconClient([]string{"http://127.0.0.1:0", fallback}, nil, nil) sidecars, err := fetch(t, c) require.NoError(t, err) - require.Len(t, sidecars, 1) + require.Len(t, sidecars.Blobs, 1) require.Positive(t, atomic.LoadInt32(fallbackHits)) } +// 200 with the right count and commitment but corrupted blob bytes must +// trigger fallback instead of handing bad bytes downstream. +func TestFallbackBeacon_FallsBackOnCorruptBlobContent(t *testing.T) { + primary, primaryHits := newStubBeacon(t, beaconServesCorruptBlob) + fallback, fallbackHits := newStubBeacon(t, beaconServesBlob) + + c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) + sidecars, err := fetch(t, c) + require.NoError(t, err) + require.Len(t, sidecars.Blobs, 1) + require.Positive(t, atomic.LoadInt32(primaryHits)) + require.Positive(t, atomic.LoadInt32(fallbackHits), "corrupt blob content must trigger fallback") +} + +// 200 with the right count but none of the sidecars carries the requested +// hash (e.g. another fork's sidecars at the same slot) must trigger fallback. +func TestFallbackBeacon_FallsBackOnMissingRequestedHash(t *testing.T) { + primary, primaryHits := newStubBeacon(t, beaconServesWrongCommitment) + fallback, fallbackHits := newStubBeacon(t, beaconServesBlob) + + c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) + sidecars, err := fetch(t, c) + require.NoError(t, err) + require.Len(t, sidecars.Blobs, 1) + require.Positive(t, atomic.LoadInt32(primaryHits)) + require.Positive(t, atomic.LoadInt32(fallbackHits), "missing requested hash must trigger fallback") +} + +// A JSON null in the sidecar list decodes to a nil pointer; it must count as +// a verification failure and trigger fallback, not panic. +func TestFallbackBeacon_FallsBackOnNullSidecar(t *testing.T) { + primary, primaryHits := newStubBeacon(t, beaconServesNullSidecar) + fallback, fallbackHits := newStubBeacon(t, beaconServesBlob) + + c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) + sidecars, err := fetch(t, c) + require.NoError(t, err) + require.Len(t, sidecars.Blobs, 1) + require.Positive(t, atomic.LoadInt32(primaryHits)) + require.Positive(t, atomic.LoadInt32(fallbackHits), "null sidecar must trigger fallback") +} + +// The #745 self-heal: when the caller cannot build index hints (the L1 block +// body was unavailable), it passes nil hints. The client must still fetch all +// sidecars at the slot and authenticate them by hash — a nil hint is NOT a +// failure and must not become a hard error. +func TestFallbackBeacon_VerifiesWithoutIndexHints(t *testing.T) { + primary, primaryHits := newStubBeacon(t, beaconServesBlob) + + c := NewFallbackBeaconClient([]string{primary}, nil, nil) + sidecar, err := c.GetVerifiedBlobSidecar(context.Background(), L1BlockRef{Time: 12}, wantHashes(), nil) + require.NoError(t, err) + require.Len(t, sidecar.Blobs, 1) + require.Positive(t, atomic.LoadInt32(primaryHits)) +} + +// Verification stays active on the no-index fetch-all path: a corrupt primary +// must still rotate to a healthy fallback even without index hints. +func TestFallbackBeacon_FallsBackWithoutIndexHintsOnCorruptContent(t *testing.T) { + primary, primaryHits := newStubBeacon(t, beaconServesCorruptBlob) + fallback, fallbackHits := newStubBeacon(t, beaconServesBlob) + + c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) + sidecar, err := c.GetVerifiedBlobSidecar(context.Background(), L1BlockRef{Time: 12}, wantHashes(), nil) + require.NoError(t, err) + require.Len(t, sidecar.Blobs, 1) + require.Positive(t, atomic.LoadInt32(primaryHits)) + require.Positive(t, atomic.LoadInt32(fallbackHits), "corrupt content must trigger fallback even without index hints") +} + +// A client constructed with no endpoints must error instead of silently +// returning an empty sidecar as success. +func TestFallbackBeacon_NoEndpointsReturnsError(t *testing.T) { + c := NewFallbackBeaconClient(nil, nil, nil) + sidecar, err := c.GetVerifiedBlobSidecar(context.Background(), L1BlockRef{Time: 12}, wantHashes(), nil) + require.Error(t, err) + require.Empty(t, sidecar.Blobs) +} + func TestFallbackBeacon_StopsOnContextCancellation(t *testing.T) { var primaryCalls, fallbackCalls int32 c := &FallbackBeaconClient{ @@ -145,25 +258,41 @@ func TestFallbackBeacon_StopsOnContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - sidecars, err := c.GetBlobSidecarsEnhanced(ctx, L1BlockRef{Time: 12}, oneHash()) + sidecars, err := c.GetVerifiedBlobSidecar(ctx, L1BlockRef{Time: 12}, wantHashes(), oneHash()) require.ErrorIs(t, err, context.Canceled) - require.Nil(t, sidecars) + require.Empty(t, sidecars.Blobs) require.Positive(t, atomic.LoadInt32(&primaryCalls)) require.Zero(t, atomic.LoadInt32(&fallbackCalls)) } -// When every beacon fails to serve the blob, an error is returned and every -// endpoint's failure is recorded in metrics (exercised via a real *Metrics). +// Every configured beacon answers 200 with an empty sidecar list (blob +// pruned everywhere / not yet indexed): the caller must get an error, never +// an empty sidecar as success. +func TestFallbackBeacon_AllEmptyReturnsError(t *testing.T) { + primary, primaryHits := newStubBeacon(t, beaconServesEmpty) + fallback, fallbackHits := newStubBeacon(t, beaconServesEmpty) + + c := NewFallbackBeaconClient([]string{primary, fallback}, nil, nil) + sidecar, err := fetch(t, c) + require.Error(t, err) + require.Contains(t, err.Error(), "no sidecars") + require.Empty(t, sidecar.Blobs) + require.Positive(t, atomic.LoadInt32(primaryHits)) + require.Positive(t, atomic.LoadInt32(fallbackHits)) +} + +// When every beacon fails to serve a valid blob, an error is returned and +// every endpoint's failure is recorded in metrics. func TestFallbackBeacon_AllFailReturnsError(t *testing.T) { primary, primaryHits := newStubBeacon(t, beaconServesEmpty) - fallback, fallbackHits := newStubBeacon(t, beaconServerError) + fallback, fallbackHits := newStubBeacon(t, beaconServesCorruptBlob) m := PrometheusMetrics("morphnode_test_" + t.Name()) c := NewFallbackBeaconClient([]string{primary, fallback}, nil, m) sidecars, err := fetch(t, c) require.Error(t, err) - require.Nil(t, sidecars) + require.Empty(t, sidecars.Blobs) require.Positive(t, atomic.LoadInt32(primaryHits)) require.Positive(t, atomic.LoadInt32(fallbackHits)) } diff --git a/node/derivation/beacon.go b/node/derivation/beacon.go index c63688185..f5b85f40a 100644 --- a/node/derivation/beacon.go +++ b/node/derivation/beacon.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -13,6 +14,7 @@ import ( "sync" "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" "github.com/morph-l2/go-ethereum/core/types" "github.com/morph-l2/go-ethereum/crypto/kzg4844" "github.com/morph-l2/go-ethereum/params" @@ -209,14 +211,17 @@ func dataAndHashesFromTxs(txs types.Transactions, targetTx *types.Transaction) [ return hashes } -// FallbackBeaconClient queries several beacon nodes in order for blob sidecars. -// A beacon is skipped and the next one tried when it errors, is unreachable, or -// answers with too few sidecars — a beacon that pruned the slot or has not -// indexed it yet still replies 200 with an empty/partial list, which is exactly -// the "temporarily failed to fetch blob" case seen in production and which a -// transport-level fallback would miss. The failing endpoint is recorded in the -// beacon_request_failure_total metric so a flaky node is visible on dashboards. -// With a single endpoint it behaves like a bare L1BeaconClient. +// FallbackBeaconClient queries several beacon nodes in order and rotates to +// the next one when an endpoint cannot serve verified blobs (error, missing +// data, or content failing hash verification). Failing endpoints are recorded +// in the beacon_request_failure_total metric. +// +// Scope: fallback only covers per-endpoint data faults (corruption, pruned or +// unsynced data, client bugs). It is not a consistency mechanism and should +// not grow into one — safety against bad data comes from the KZG hash +// verification itself, and EL/CL fork mismatches near the chain head are +// eliminated by running derivation with confirmations=finalized, not by +// trying more beacons. type FallbackBeaconClient struct { clients []*L1BeaconClient endpoints []string // parallel to clients, used only for logs/metrics @@ -237,37 +242,104 @@ func NewFallbackBeaconClient(endpoints []string, log tmlog.Logger, metrics *Metr } } -// GetBlobSidecarsEnhanced tries each configured beacon in order and returns the -// first response that actually carries the requested blobs (at least len(hashes) -// sidecars, or at least one when no explicit hashes are requested). A beacon -// that errors or returns an incomplete set is recorded as a failure and skipped. -// If every beacon fails, the last error is returned. -func (c *FallbackBeaconClient) GetBlobSidecarsEnhanced(ctx context.Context, ref L1BlockRef, hashes []IndexedBlobHash) ([]*BlobSidecar, error) { +// GetVerifiedBlobSidecar fetches and content-verifies the blobs identified by +// wantHashes — the L1 tx's versioned blob hashes, in tx order — trying each +// configured beacon in turn and returning the assembled BlobTxSidecar. A +// beacon that errors or serves an incomplete/invalid set is recorded as a +// failure and skipped; if every beacon fails, the last error is returned. +// +// indexHints is an optional fetch optimization (?indices= filter), not a +// correctness input: verification is purely by hash, so callers that cannot +// build indices may pass nil and the whole slot is fetched and matched. +func (c *FallbackBeaconClient) GetVerifiedBlobSidecar(ctx context.Context, ref L1BlockRef, wantHashes []common.Hash, indexHints []IndexedBlobHash) (types.BlobTxSidecar, error) { + if len(wantHashes) == 0 { + return types.BlobTxSidecar{}, nil + } + // Guards direct construction; config validation already rejects an empty + // beacon list at startup. + if len(c.clients) == 0 { + return types.BlobTxSidecar{}, errors.New("no beacon endpoints configured") + } var lastErr error for i, cl := range c.clients { - sidecars, err := cl.GetBlobSidecarsEnhanced(ctx, ref, hashes) - if err == nil && len(sidecars) > 0 && len(sidecars) >= len(hashes) { - return sidecars, nil - } - if err := ctx.Err(); err != nil { - return nil, err + sidecars, err := cl.GetBlobSidecarsEnhanced(ctx, ref, indexHints) + // Empty list (slot pruned / not yet indexed) is an availability + // failure of this endpoint. + if err == nil && len(sidecars) == 0 { + err = errors.New("beacon returned no sidecars for slot") } if err == nil { - err = fmt.Errorf("beacon returned %d sidecars, want at least %d", len(sidecars), len(hashes)) + var verified types.BlobTxSidecar + verified, err = blobsFromSidecars(sidecars, wantHashes) + if err == nil { + return verified, nil + } + } + if ctxErr := ctx.Err(); ctxErr != nil { + return types.BlobTxSidecar{}, ctxErr } if c.metrics != nil { c.metrics.IncBeaconRequestFailure(c.endpoints[i]) } + // indexHints is logged because stale hints (e.g. a reorg between the + // header and block fetches) make every healthy beacon fail hash + // matching; the hint count distinguishes that from real endpoint + // faults when reading beacon_request_failure_total spikes. if c.log != nil { c.log.Error("beacon failed to serve blob sidecars, trying next endpoint", - "endpoint", c.endpoints[i], "err", err) + "endpoint", c.endpoints[i], "indexHints", len(indexHints), "err", err) } lastErr = err } - return nil, lastErr + return types.BlobTxSidecar{}, lastErr } -// Note: ForceGetAllBlobs is defined in derivation.go in the same package +// blobsFromSidecars matches each wanted versioned hash to a sidecar via its +// commitment-derived versioned hash, authenticates the blob bytes with +// verifyBlob, and assembles the result in wantHashes (i.e. tx) order — batches +// are decoded by concatenating blob bodies, so order matters. Extra sidecars +// are ignored. Proofs are intentionally left empty: no consumer needs them +// and computing them costs an extra KZG op per blob. +func blobsFromSidecars(sidecars []*BlobSidecar, wantHashes []common.Hash) (types.BlobTxSidecar, error) { + byHash := make(map[common.Hash]*BlobSidecar, len(sidecars)) + for _, sidecar := range sidecars { + // JSON null entries decode to nil; skipping them surfaces as a + // "not found" error below instead of a panic. + if sidecar == nil { + continue + } + var commitment kzg4844.Commitment + copy(commitment[:], sidecar.KZGCommitment[:]) + byHash[KZGToVersionedHash(commitment)] = sidecar + } + out := types.BlobTxSidecar{ + Blobs: make([]kzg4844.Blob, 0, len(wantHashes)), + Commitments: make([]kzg4844.Commitment, 0, len(wantHashes)), + } + for i, expected := range wantHashes { + sidecar, ok := byHash[expected] + if !ok { + return types.BlobTxSidecar{}, fmt.Errorf("blob %d (hash=%s) not found in beacon sidecars", i, expected.Hex()) + } + b, err := hexutil.Decode(sidecar.Blob) + if err != nil { + return types.BlobTxSidecar{}, fmt.Errorf("failed to decode blob %d: %w", i, err) + } + if len(b) != BlobSize { + return types.BlobTxSidecar{}, fmt.Errorf("blob %d: unexpected length %d (want %d, hash=%s)", i, len(b), BlobSize, expected.Hex()) + } + var blob Blob + copy(blob[:], b) + if err := verifyBlob(&blob, expected); err != nil { + return types.BlobTxSidecar{}, fmt.Errorf("blob %d: %w", i, err) + } + var commitment kzg4844.Commitment + copy(commitment[:], sidecar.KZGCommitment[:]) + out.Blobs = append(out.Blobs, *blob.KZGBlob()) + out.Commitments = append(out.Commitments, commitment) + } + return out, nil +} // GetBlobSidecarsEnhanced is an enhanced version of GetBlobSidecars method, combining two approaches to fetch blob data // If the first method fails or returns no blobs, it will try the second method diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index ed31bf191..b050f3483 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -12,10 +12,8 @@ import ( "github.com/morph-l2/go-ethereum/accounts/abi" "github.com/morph-l2/go-ethereum/accounts/abi/bind" "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/common/hexutil" eth "github.com/morph-l2/go-ethereum/core/types" "github.com/morph-l2/go-ethereum/crypto" - "github.com/morph-l2/go-ethereum/crypto/kzg4844" geth "github.com/morph-l2/go-ethereum/eth" "github.com/morph-l2/go-ethereum/ethclient" "github.com/morph-l2/go-ethereum/ethclient/authclient" @@ -580,76 +578,16 @@ func (d *Derivation) fetchRollupDataByTxHash(txHash common.Hash, blockNumber uin d.logger.Info("Failed to get block, will try fetching all blobs", "blockNumber", blockNumber, "error", err) } - // Get all blobs corresponding to this timestamp - blobSidecars, err := d.l1BeaconClient.GetBlobSidecarsEnhanced(d.ctx, L1BlockRef{ + // Get all blobs corresponding to this timestamp, content-verified + // against the tx's blob hashes inside the fallback client. + sidecar, err := d.l1BeaconClient.GetVerifiedBlobSidecar(d.ctx, L1BlockRef{ Time: header.Time, - }, indexedBlobHashes) + }, blobHashes, indexedBlobHashes) if err != nil { return nil, fmt.Errorf("failed to get blobs, continuing processing:%v", err) } - if len(blobSidecars) > 0 { - // Index beacon sidecars by their KZG-derived versioned hash so we - // can assemble the local sidecar in the exact order the L1 tx - // declared its blobs. Multi-blob batches are decoded by - // concatenating blob bodies in tx order; any reordering here - // would corrupt the resulting zstd stream. The map key is - // derived from the beacon-supplied commitment; verifyBlob below - // re-derives the same hash from the actual blob bytes, so a - // malicious beacon cannot forge an entry by lying about the - // commitment. - byHash := make(map[common.Hash]*BlobSidecar, len(blobSidecars)) - for _, sidecar := range blobSidecars { - var commitment kzg4844.Commitment - copy(commitment[:], sidecar.KZGCommitment[:]) - byHash[KZGToVersionedHash(commitment)] = sidecar - } - - // Downstream (ParseBatch) only consumes Sidecar.Blobs and - // Sidecar.Commitments; Proofs is intentionally left empty to - // avoid an extra ~O(n) KZG op per blob per batch on every - // sync. If a future consumer needs Proofs, compute them - // lazily there or call kzg4844.ComputeBlobProof here. - var blobTxSidecar eth.BlobTxSidecar - for i, expectedHash := range blobHashes { - sidecar, ok := byHash[expectedHash] - if !ok { - return nil, fmt.Errorf("blob %d (hash=%s) not found in beacon sidecars", i, expectedHash.Hex()) - } - - b, err := hexutil.Decode(sidecar.Blob) - if err != nil { - return nil, fmt.Errorf("failed to decode blob %d: %w", i, err) - } - // Reject malformed beacon responses up front. copy(blob[:], b) - // silently: - // - zero-pads when len(b) < BlobSize (tail of the - // zero-initialized array stays zero) - // - truncates when len(b) > BlobSize (extra bytes dropped) - // Either case would otherwise surface later as a confusing - // blob-hash mismatch instead of a clear length error. - if len(b) != BlobSize { - return nil, fmt.Errorf("blob %d: unexpected length %d (want %d, hash=%s)", i, len(b), BlobSize, expectedHash.Hex()) - } - var blob Blob - copy(blob[:], b) - - if err := verifyBlob(&blob, expectedHash); err != nil { - return nil, fmt.Errorf("blob %d: %w", i, err) - } - - var commitment kzg4844.Commitment - copy(commitment[:], sidecar.KZGCommitment[:]) - - d.logger.Info("Matched blob", "txOrder", i, "beaconIndex", sidecar.Index, "hash", expectedHash.Hex()) - blobTxSidecar.Blobs = append(blobTxSidecar.Blobs, *blob.KZGBlob()) - blobTxSidecar.Commitments = append(blobTxSidecar.Commitments, commitment) - } - - d.logger.Info("Blob matching results", "matched", len(blobTxSidecar.Blobs), "expected", len(blobHashes)) - batch.Sidecar = blobTxSidecar - } else { - return nil, fmt.Errorf("not matched blob,txHash:%v,blockNumber:%v", txHash, blockNumber) - } + d.logger.Info("Blob matching results", "matched", len(sidecar.Blobs), "expected", len(blobHashes)) + batch.Sidecar = sidecar } rollupData, err := d.parseBatch(batch)