Skip to content

Commit f6291e5

Browse files
authored
fix: accept KZG proof mismatches when blob API has valid proofs (#53)
Post-Fulu, beacon nodes may return invalid/zeroed KZG proofs (0xc0...) for blob sidecars while the blob archiver computes and stores valid proofs. This caused false validation errors. Changes: - Add verifyKZGProofs() to cryptographically validate KZG proofs - Accept data mismatches when only KZG fields differ and blob API proofs are valid - Reject data when blob API proofs are invalid or non-KZG fields differ - Add comprehensive tests for KZG validation scenarios This fixes validator errors for slots around the Fulu fork while maintaining data integrity checks.
1 parent b727154 commit f6291e5

File tree

2 files changed

+158
-22
lines changed

2 files changed

+158
-22
lines changed

validator/service/service.go

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import (
1212
client "github.com/attestantio/go-eth2-client"
1313
"github.com/attestantio/go-eth2-client/api"
1414
v1 "github.com/attestantio/go-eth2-client/api/v1"
15+
"github.com/attestantio/go-eth2-client/spec/deneb"
1516
"github.com/attestantio/go-eth2-client/spec/phase0"
1617
"github.com/base/blob-archiver/common/storage"
1718
"github.com/base/blob-archiver/validator/flags"
19+
gokzg4844 "github.com/crate-crypto/go-kzg-4844"
1820
"github.com/ethereum-optimism/optimism/op-service/retry"
1921
"github.com/ethereum/go-ethereum/log"
2022
)
@@ -115,6 +117,29 @@ func shouldRetry(status int) bool {
115117
}
116118
}
117119

120+
// verifyKZGProofs validates that the KZG proofs in the blob sidecars are valid for the blob data.
121+
// Returns nil if all proofs are valid, error otherwise.
122+
func verifyKZGProofs(sidecars []*deneb.BlobSidecar) error {
123+
kzgCtx, err := gokzg4844.NewContext4096Secure()
124+
if err != nil {
125+
return fmt.Errorf("failed to create KZG context: %w", err)
126+
}
127+
128+
for i, sidecar := range sidecars {
129+
kzgBlob := (*gokzg4844.Blob)(&sidecar.Blob)
130+
commitment := gokzg4844.KZGCommitment(sidecar.KZGCommitment)
131+
proof := gokzg4844.KZGProof(sidecar.KZGProof)
132+
133+
// Verify the KZG proof - returns error if invalid
134+
err := kzgCtx.VerifyBlobKZGProof(kzgBlob, commitment, proof)
135+
if err != nil {
136+
return fmt.Errorf("KZG proof verification failed for sidecar %d: %w", i, err)
137+
}
138+
}
139+
140+
return nil
141+
}
142+
118143
// fetchWithRetries fetches the sidecar and handles retryable error cases (5xx status codes + 429 + connection errors)
119144
func fetchWithRetries(ctx context.Context, endpoint BlobSidecarClient, id string, format Format) (int, storage.BlobSidecars, error) {
120145
return retry.Do2(ctx, retryAttempts, retry.Exponential(), func() (int, storage.BlobSidecars, error) {
@@ -171,11 +196,43 @@ func (a *ValidatorService) checkBlobs(ctx context.Context, start phase0.Slot, en
171196
}
172197

173198
if !reflect.DeepEqual(beaconResponse, blobResponse) {
174-
result.MismatchedData = append(result.MismatchedData, id)
175-
l.Error(validationErrorLog, "reason", "response-mismatch")
199+
// Data mismatch detected - first check if sidecar counts match
200+
if len(beaconResponse.Data) != len(blobResponse.Data) {
201+
// Different number of sidecars - this is a real error
202+
result.MismatchedData = append(result.MismatchedData, id)
203+
l.Error(validationErrorLog, "reason", "response-mismatch", "beaconCount", len(beaconResponse.Data), "blobApiCount", len(blobResponse.Data))
204+
continue
205+
}
206+
207+
// Same number of sidecars - verify if the blob API's KZG proofs are valid
208+
// This handles cases where the beacon node returns zeroed out KZG proofs post-Fulu
209+
if err := verifyKZGProofs(blobResponse.Data); err != nil {
210+
// Blob API has invalid KZG proofs - this is a real error
211+
result.MismatchedData = append(result.MismatchedData, id)
212+
l.Error(validationErrorLog, "reason", "response-mismatch", "kzgVerification", "failed", "error", err)
213+
} else {
214+
// Blob API's KZG proofs are valid - now check if only KZG fields differ
215+
// Create a copy of blobResponse with beacon's KZG proofs
216+
normalizedBlobResponse := storage.BlobSidecars{Data: make([]*deneb.BlobSidecar, len(blobResponse.Data))}
217+
for i, sidecar := range blobResponse.Data {
218+
normalized := *sidecar // shallow copy
219+
// Copy KZG fields from beacon response
220+
normalized.KZGProof = beaconResponse.Data[i].KZGProof
221+
normalized.KZGCommitment = beaconResponse.Data[i].KZGCommitment
222+
normalized.KZGCommitmentInclusionProof = beaconResponse.Data[i].KZGCommitmentInclusionProof
223+
normalizedBlobResponse.Data[i] = &normalized
224+
}
225+
226+
// Compare again with normalized KZG values
227+
if !reflect.DeepEqual(beaconResponse, normalizedBlobResponse) {
228+
// Other fields differ - this is a real error
229+
result.MismatchedData = append(result.MismatchedData, id)
230+
l.Error(validationErrorLog, "reason", "response-mismatch", "kzgVerification", "passed", "note", "blob data differs beyond KZG fields")
231+
}
232+
}
233+
} else {
234+
l.Info("completed blob check", "blobs", len(beaconResponse.Data))
176235
}
177-
178-
l.Info("completed blob check", "blobs", len(beaconResponse.Data))
179236
}
180237

181238
// Check if we should stop validation otherwise continue

validator/service/service_test.go

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -158,30 +158,12 @@ func TestValidatorService_MistmatchedBlobFields(t *testing.T) {
158158
(*i)[0].Blob = deneb.Blob{0, 0, 0}
159159
},
160160
},
161-
{
162-
name: "mismatched kzg commitment",
163-
modification: func(i *[]*deneb.BlobSidecar) {
164-
(*i)[0].KZGCommitment = deneb.KZGCommitment{0, 0, 0}
165-
},
166-
},
167-
{
168-
name: "mismatched kzg proof",
169-
modification: func(i *[]*deneb.BlobSidecar) {
170-
(*i)[0].KZGProof = deneb.KZGProof{0, 0, 0}
171-
},
172-
},
173161
{
174162
name: "mismatched signed block header",
175163
modification: func(i *[]*deneb.BlobSidecar) {
176164
(*i)[0].SignedBlockHeader = nil
177165
},
178166
},
179-
{
180-
name: "mismatched kzg commitment inclusion proof",
181-
modification: func(i *[]*deneb.BlobSidecar) {
182-
(*i)[0].KZGCommitmentInclusionProof = deneb.KZGCommitmentInclusionProof{{1, 2, 9}}
183-
},
184-
},
185167
}
186168

187169
for _, test := range tests {
@@ -215,3 +197,100 @@ func TestValidatorService_MistmatchedBlobFields(t *testing.T) {
215197
})
216198
}
217199
}
200+
201+
// TestValidatorService_KZGProofMismatchWithValidBlobAPI tests the scenario where:
202+
// - Beacon node returns invalid/zeroed KZG proofs (e.g., post-Fulu blob)
203+
// - Blob API has valid computed KZG proofs
204+
// - All other data matches
205+
// This should be accepted as valid
206+
func TestValidatorService_KZGProofMismatchWithValidBlobAPI(t *testing.T) {
207+
validator, headers, beacon, blob := setup(t)
208+
209+
beacon.setResponses(headers)
210+
blob.setResponses(headers)
211+
212+
// Deep copy the beacon data and zero out KZG fields to simulate beacon node bug
213+
d, err := json.Marshal(headers.SidecarsByBlock[blockOne])
214+
require.NoError(t, err)
215+
var beaconData []*deneb.BlobSidecar
216+
err = json.Unmarshal(d, &beaconData)
217+
require.NoError(t, err)
218+
219+
// Simulate beacon node bug: set KZG proof to point at infinity (0xc0...)
220+
for i := range beaconData {
221+
beaconData[i].KZGProof = deneb.KZGProof{0xc0}
222+
beaconData[i].KZGCommitmentInclusionProof = deneb.KZGCommitmentInclusionProof{}
223+
}
224+
225+
beacon.setResponse(blockOne, 200, storage.BlobSidecars{Data: beaconData}, nil)
226+
227+
result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot))
228+
229+
// Should have no errors - KZG mismatch accepted because blob API has valid proofs
230+
require.Empty(t, result.MismatchedStatus)
231+
require.Empty(t, result.ErrorFetching)
232+
require.Empty(t, result.MismatchedData)
233+
}
234+
235+
// TestValidatorService_KZGProofMismatchWithInvalidBlobAPI tests the scenario where:
236+
// - Both beacon node and blob API have different KZG proofs
237+
// - Blob API's KZG proofs are INVALID
238+
// This should be reported as an error
239+
func TestValidatorService_KZGProofMismatchWithInvalidBlobAPI(t *testing.T) {
240+
validator, headers, beacon, blob := setup(t)
241+
242+
beacon.setResponses(headers)
243+
blob.setResponses(headers)
244+
245+
// Deep copy the blob data and set invalid KZG proof
246+
d, err := json.Marshal(headers.SidecarsByBlock[blockOne])
247+
require.NoError(t, err)
248+
var blobData []*deneb.BlobSidecar
249+
err = json.Unmarshal(d, &blobData)
250+
require.NoError(t, err)
251+
252+
// Set invalid KZG proof on blob API
253+
for i := range blobData {
254+
blobData[i].KZGProof = deneb.KZGProof{0xde, 0xad, 0xbe, 0xef}
255+
}
256+
257+
blob.setResponse(blockOne, 200, storage.BlobSidecars{Data: blobData}, nil)
258+
259+
result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot))
260+
261+
// Should report mismatch because blob API has invalid KZG proofs
262+
require.Empty(t, result.MismatchedStatus)
263+
require.Empty(t, result.ErrorFetching)
264+
require.Len(t, result.MismatchedData, 2)
265+
require.Equal(t, result.MismatchedData, []string{blockOne, blockOne})
266+
}
267+
268+
// TestValidatorService_DifferentSidecarCount tests the scenario where:
269+
// - Beacon and blob API return different numbers of sidecars
270+
// This should be reported as an error
271+
func TestValidatorService_DifferentSidecarCount(t *testing.T) {
272+
validator, headers, beacon, blob := setup(t)
273+
274+
beacon.setResponses(headers)
275+
blob.setResponses(headers)
276+
277+
// Set blob API to return fewer sidecars
278+
d, err := json.Marshal(headers.SidecarsByBlock[blockOne])
279+
require.NoError(t, err)
280+
var blobData []*deneb.BlobSidecar
281+
err = json.Unmarshal(d, &blobData)
282+
require.NoError(t, err)
283+
284+
// Remove one sidecar
285+
blobData = blobData[:len(blobData)-1]
286+
287+
blob.setResponse(blockOne, 200, storage.BlobSidecars{Data: blobData}, nil)
288+
289+
result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot))
290+
291+
// Should report mismatch due to different sidecar counts
292+
require.Empty(t, result.MismatchedStatus)
293+
require.Empty(t, result.ErrorFetching)
294+
require.Len(t, result.MismatchedData, 2)
295+
require.Equal(t, result.MismatchedData, []string{blockOne, blockOne})
296+
}

0 commit comments

Comments
 (0)