diff --git a/.gitignore b/.gitignore index c81fc6c..229a365 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .env api/bin archiver/bin +validator/bin diff --git a/Dockerfile b/Dockerfile index 3227b02..36b0e30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,5 @@ RUN make build FROM alpine:3.19 COPY --from=builder /app/archiver/bin/blob-archiver /usr/local/bin/blob-archiver -COPY --from=builder /app/api/bin/blob-api /usr/local/bin/blob-api \ No newline at end of file +COPY --from=builder /app/api/bin/blob-api /usr/local/bin/blob-api +COPY --from=builder /app/validator/bin/blob-validator /usr/local/bin/blob-validator diff --git a/Makefile b/Makefile index 605a46b..d2a9528 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ build: make -C ./archiver blob-archiver make -C ./api blob-api + make -C ./validator blob-validator .PHONY: build build-docker: @@ -10,11 +11,13 @@ build-docker: clean: make -C ./archiver clean make -C ./api clean + make -C ./validator clean .PHONY: clean test: make -C ./archiver test make -C ./api test + make -C ./validator test .PHONY: test integration: diff --git a/common/beacon/beacontest/stub.go b/common/beacon/beacontest/stub.go index 9e1211a..509c289 100644 --- a/common/beacon/beacontest/stub.go +++ b/common/beacon/beacontest/stub.go @@ -59,11 +59,15 @@ func NewDefaultStubBeaconClient(t *testing.T) *StubBeaconClient { } } - headBlobs := blobtest.NewBlobSidecars(t, 6) - finalizedBlobs := blobtest.NewBlobSidecars(t, 4) - startSlot := blobtest.StartSlot + originBlobs := blobtest.NewBlobSidecars(t, 1) + oneBlobs := blobtest.NewBlobSidecars(t, 2) + twoBlobs := blobtest.NewBlobSidecars(t, 0) + threeBlobs := blobtest.NewBlobSidecars(t, 4) + fourBlobs := blobtest.NewBlobSidecars(t, 5) + fiveBlobs := blobtest.NewBlobSidecars(t, 6) + return &StubBeaconClient{ Headers: map[string]*v1.BeaconBlockHeader{ // Lookup by hash @@ -87,14 +91,25 @@ func NewDefaultStubBeaconClient(t *testing.T) *StubBeaconClient { strconv.FormatUint(startSlot+5, 10): makeHeader(startSlot+5, blobtest.Five, blobtest.Four), }, Blobs: map[string][]*deneb.BlobSidecar{ - blobtest.OriginBlock.String(): blobtest.NewBlobSidecars(t, 1), - blobtest.One.String(): blobtest.NewBlobSidecars(t, 2), - blobtest.Two.String(): blobtest.NewBlobSidecars(t, 0), - blobtest.Three.String(): finalizedBlobs, - blobtest.Four.String(): blobtest.NewBlobSidecars(t, 5), - blobtest.Five.String(): headBlobs, - "head": headBlobs, - "finalized": finalizedBlobs, + // Lookup by hash + blobtest.OriginBlock.String(): originBlobs, + blobtest.One.String(): oneBlobs, + blobtest.Two.String(): twoBlobs, + blobtest.Three.String(): threeBlobs, + blobtest.Four.String(): fourBlobs, + blobtest.Five.String(): fiveBlobs, + + // Lookup by identifier + "head": fiveBlobs, + "finalized": threeBlobs, + + // Lookup by slot + strconv.FormatUint(startSlot, 10): originBlobs, + strconv.FormatUint(startSlot+1, 10): oneBlobs, + strconv.FormatUint(startSlot+2, 10): twoBlobs, + strconv.FormatUint(startSlot+3, 10): threeBlobs, + strconv.FormatUint(startSlot+4, 10): fourBlobs, + strconv.FormatUint(startSlot+5, 10): fiveBlobs, }, } } diff --git a/common/blobtest/helpers.go b/common/blobtest/helpers.go index a37d354..e5d65a8 100644 --- a/common/blobtest/helpers.go +++ b/common/blobtest/helpers.go @@ -19,6 +19,7 @@ var ( Five = common.Hash{5} StartSlot = uint64(10) + EndSlot = uint64(15) ) func RandBytes(t *testing.T, size uint) []byte { diff --git a/validator/Makefile b/validator/Makefile new file mode 100644 index 0000000..5af4166 --- /dev/null +++ b/validator/Makefile @@ -0,0 +1,13 @@ +blob-validator: + env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/blob-validator ./cmd/main.go + +clean: + rm -f bin/blob-validator + +test: + go test -v -race ./... + +.PHONY: \ + blob-validator \ + clean \ + test diff --git a/validator/cmd/main.go b/validator/cmd/main.go new file mode 100644 index 0000000..c2efe0d --- /dev/null +++ b/validator/cmd/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/base-org/blob-archiver/common/beacon" + "github.com/base-org/blob-archiver/validator/flags" + "github.com/base-org/blob-archiver/validator/service" + opservice "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" +) + +var ( + Version = "v0.0.1" + GitCommit = "" + GitDate = "" +) + +func main() { + oplog.SetupDefaults() + + app := cli.NewApp() + app.Flags = cliapp.ProtectFlags(flags.Flags) + app.Version = opservice.FormatVersion(Version, GitCommit, GitDate, "") + app.Name = "blob-validator" + app.Usage = "Job that checks the validity of blobs" + app.Description = "The blob-validator is a job that checks the validity of blobs" + app.Action = cliapp.LifecycleCmd(Main()) + + err := app.Run(os.Args) + if err != nil { + log.Crit("Application failed", "message", err) + } +} + +// Main is the entrypoint into the API. +// This method returns a cliapp.LifecycleAction, to create an op-service CLI-lifecycle-managed API Server. +func Main() cliapp.LifecycleAction { + return func(cliCtx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) { + cfg := flags.ReadConfig(cliCtx) + if err := cfg.Check(); err != nil { + return nil, fmt.Errorf("config check failed: %w", err) + } + + l := oplog.NewLogger(oplog.AppOut(cliCtx), cfg.LogConfig) + oplog.SetGlobalLogHandler(l.GetHandler()) + opservice.ValidateEnvVars(flags.EnvVarPrefix, flags.Flags, l) + + headerClient, err := beacon.NewBeaconClient(cliCtx.Context, cfg.BeaconConfig) + if err != nil { + return nil, fmt.Errorf("failed to create beacon client: %w", err) + } + + beaconClient := service.NewBlobSidecarClient(cfg.BeaconConfig.BeaconURL) + blobClient := service.NewBlobSidecarClient(cfg.BeaconConfig.BeaconURL) + + return service.NewValidator(l, headerClient, beaconClient, blobClient, closeApp), nil + } +} diff --git a/validator/flags/config.go b/validator/flags/config.go new file mode 100644 index 0000000..ea6144c --- /dev/null +++ b/validator/flags/config.go @@ -0,0 +1,44 @@ +package flags + +import ( + "fmt" + "time" + + common "github.com/base-org/blob-archiver/common/flags" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/urfave/cli/v2" +) + +type ValidatorConfig struct { + LogConfig oplog.CLIConfig + BeaconConfig common.BeaconConfig + BlobConfig common.BeaconConfig +} + +func (c ValidatorConfig) Check() error { + if err := c.BeaconConfig.Check(); err != nil { + return fmt.Errorf("beacon config check failed: %w", err) + } + + if err := c.BlobConfig.Check(); err != nil { + return fmt.Errorf("blob config check failed: %w", err) + } + + return nil +} + +func ReadConfig(cliCtx *cli.Context) ValidatorConfig { + timeout, _ := time.ParseDuration(cliCtx.String(BeaconClientTimeoutFlag.Name)) + + return ValidatorConfig{ + LogConfig: oplog.ReadCLIConfig(cliCtx), + BeaconConfig: common.BeaconConfig{ + BeaconURL: cliCtx.String(L1BeaconClientUrlFlag.Name), + BeaconClientTimeout: timeout, + }, + BlobConfig: common.BeaconConfig{ + BeaconURL: cliCtx.String(BlobApiClientUrlFlag.Name), + BeaconClientTimeout: timeout, + }, + } +} diff --git a/validator/flags/flags.go b/validator/flags/flags.go new file mode 100644 index 0000000..befb67b --- /dev/null +++ b/validator/flags/flags.go @@ -0,0 +1,38 @@ +package flags + +import ( + opservice "github.com/ethereum-optimism/optimism/op-service" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/urfave/cli/v2" +) + +const EnvVarPrefix = "BLOB_VALIDATOR" + +var ( + BeaconClientTimeoutFlag = &cli.StringFlag{ + Name: "beacon-client-timeout", + Usage: "The timeout duration for the beacon client", + Value: "10s", + EnvVars: opservice.PrefixEnvVar(EnvVarPrefix, "CLIENT_TIMEOUT"), + } + L1BeaconClientUrlFlag = &cli.StringFlag{ + Name: "l1-beacon-http", + Usage: "URL for a L1 Beacon-node API", + Required: true, + EnvVars: opservice.PrefixEnvVar(EnvVarPrefix, "L1_BEACON_HTTP"), + } + BlobApiClientUrlFlag = &cli.StringFlag{ + Name: "blob-api-http", + Usage: "URL for a Blob API", + Required: true, + EnvVars: opservice.PrefixEnvVar(EnvVarPrefix, "BLOB_API_HTTP"), + } +) + +func init() { + Flags = append(Flags, oplog.CLIFlags(EnvVarPrefix)...) + Flags = append(Flags, BeaconClientTimeoutFlag, L1BeaconClientUrlFlag, BlobApiClientUrlFlag) +} + +// Flags contains the list of configuration options available to the binary. +var Flags []cli.Flag diff --git a/validator/service/client.go b/validator/service/client.go new file mode 100644 index 0000000..cd3d7da --- /dev/null +++ b/validator/service/client.go @@ -0,0 +1,85 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/attestantio/go-eth2-client/api" + "github.com/base-org/blob-archiver/common/storage" +) + +type Format string + +const ( + // FormatJson instructs the client to request the response in JSON format + FormatJson Format = "application/json" + // FormatSSZ instructs the client to request the response in SSZ format + FormatSSZ Format = "application/octet-stream" +) + +// BlobSidecarClient is a minimal client for fetching sidecars from the blob service. This client is used instead of an +// existing client for two reasons. +// 1) Does not require any endpoints except /eth/v1/blob_sidecar, which is the only endpoint that the Blob API supports +// 2) Exposes implementation details, e.g. status code, as well as allowing us to specify the format +type BlobSidecarClient interface { + // FetchSidecars fetches the sidecars for a given slot from the blob sidecar API. It returns the HTTP status code and + // the sidecars. + FetchSidecars(id string, format Format) (int, storage.BlobSidecars, error) +} + +type httpBlobSidecarClient struct { + url string + client *http.Client +} + +// NewBlobSidecarClient creates a new BlobSidecarClient that fetches sidecars from the given URL. +func NewBlobSidecarClient(url string) BlobSidecarClient { + return &httpBlobSidecarClient{ + url: url, + client: &http.Client{}, + } +} + +func (c *httpBlobSidecarClient) FetchSidecars(id string, format Format) (int, storage.BlobSidecars, error) { + url := fmt.Sprintf("%s/eth/v1/beacon/blob_sidecars/%s", c.url, id) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return http.StatusInternalServerError, storage.BlobSidecars{}, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", string(format)) + + response, err := c.client.Do(req) + if err != nil { + return http.StatusInternalServerError, storage.BlobSidecars{}, fmt.Errorf("failed to fetch sidecars: %w", err) + } + + if response.StatusCode != http.StatusOK { + return response.StatusCode, storage.BlobSidecars{}, nil + } + + defer response.Body.Close() + + var sidecars storage.BlobSidecars + if format == FormatJson { + if err := json.NewDecoder(response.Body).Decode(&sidecars); err != nil { + return response.StatusCode, storage.BlobSidecars{}, fmt.Errorf("failed to decode json response: %w", err) + } + } else { + body, err := io.ReadAll(response.Body) + if err != nil { + return response.StatusCode, storage.BlobSidecars{}, fmt.Errorf("failed to read response: %w", err) + } + + s := api.BlobSidecars{} + if err := s.UnmarshalSSZ(body); err != nil { + return response.StatusCode, storage.BlobSidecars{}, fmt.Errorf("failed to decode ssz response: %w", err) + } + + sidecars.Data = s.Sidecars + } + + return response.StatusCode, sidecars, nil +} diff --git a/validator/service/service.go b/validator/service/service.go new file mode 100644 index 0000000..1b6cdff --- /dev/null +++ b/validator/service/service.go @@ -0,0 +1,165 @@ +package service + +import ( + "context" + "errors" + "fmt" + "net/http" + "reflect" + "strconv" + "sync/atomic" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/base-org/blob-archiver/common/storage" + "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum/go-ethereum/log" +) + +var ErrAlreadyStopped = errors.New("already stopped") + +const ( + // 5 blocks per minute, 120 minutes + twoHoursOfBlocks = 5 * 120 + // finalized l1 offset + finalizedL1Offset = 64 + // Known log for any validation errors + validationErrorLog = "validation error" + // Number of attempts to fetch blobs from blob-api and beacon-node + retryAttempts = 10 +) + +func NewValidator(l log.Logger, headerClient client.BeaconBlockHeadersProvider, beaconAPI BlobSidecarClient, blobAPI BlobSidecarClient, app context.CancelCauseFunc) *ValidatorService { + return &ValidatorService{ + log: l, + headerClient: headerClient, + beaconAPI: beaconAPI, + blobAPI: blobAPI, + closeApp: app, + } +} + +type ValidatorService struct { + stopped atomic.Bool + log log.Logger + headerClient client.BeaconBlockHeadersProvider + beaconAPI BlobSidecarClient + blobAPI BlobSidecarClient + closeApp context.CancelCauseFunc +} + +// Start starts the validator service. This will fetch the current range of blocks to validate and start the validation +// process. +func (a *ValidatorService) Start(ctx context.Context) error { + header, err := retry.Do(ctx, retryAttempts, retry.Exponential(), func() (*api.Response[*v1.BeaconBlockHeader], error) { + return a.headerClient.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{ + Block: "head", + }) + }) + + if err != nil { + return fmt.Errorf("failed to get beacon block header: %w", err) + } + + end := header.Data.Header.Message.Slot - finalizedL1Offset + start := end - twoHoursOfBlocks + + go a.checkBlobs(ctx, start, end) + + return nil +} + +// Stops the validator service. +func (a *ValidatorService) Stop(ctx context.Context) error { + if a.stopped.Load() { + return ErrAlreadyStopped + } + + a.log.Info("Stopping validator") + a.stopped.Store(true) + + return nil +} + +func (a *ValidatorService) Stopped() bool { + return a.stopped.Load() +} + +// CheckBlobResult contains the summary of the blob checks +type CheckBlobResult struct { + // ErrorFetching contains the list of slots for which the blob-api or beacon-node returned an error + ErrorFetching []string + // MismatchedStatus contains the list of slots for which the status code from the blob-api and beacon-node did not match + MismatchedStatus []string + // MismatchedData contains the list of slots for which the data from the blob-api and beacon-node did not match + MismatchedData []string +} + +// checkBlobs iterates all blocks in the range start:end and checks that the blobs from the beacon-node and blob-api +// are identical, when encoded in both JSON and SSZ. +func (a *ValidatorService) checkBlobs(ctx context.Context, start phase0.Slot, end phase0.Slot) CheckBlobResult { + var result CheckBlobResult + + for slot := start; slot <= end; slot++ { + for _, format := range []Format{FormatJson, FormatSSZ} { + id := strconv.FormatUint(uint64(slot), 10) + + l := a.log.New("format", format, "slot", slot) + + blobStatus, blobResponse, blobError := retry.Do2(ctx, retryAttempts, retry.Exponential(), func() (int, storage.BlobSidecars, error) { + return a.blobAPI.FetchSidecars(id, format) + }) + + if blobError != nil { + result.ErrorFetching = append(result.ErrorFetching, id) + l.Error(validationErrorLog, "reason", "error-blob-api", "error", blobError, "status", blobStatus) + continue + } + + beaconStatus, beaconResponse, beaconErr := retry.Do2(ctx, retryAttempts, retry.Exponential(), func() (int, storage.BlobSidecars, error) { + return a.beaconAPI.FetchSidecars(id, format) + }) + + if beaconErr != nil { + result.ErrorFetching = append(result.ErrorFetching, id) + l.Error(validationErrorLog, "reason", "error-beacon-api", "error", beaconErr, "status", beaconStatus) + continue + } + + if beaconStatus != blobStatus { + result.MismatchedStatus = append(result.MismatchedStatus, id) + l.Error(validationErrorLog, "reason", "status-code-mismatch", "beaconStatus", beaconStatus, "blobStatus", blobStatus) + continue + } + + if beaconStatus != http.StatusOK { + // This can happen if the slot has been missed + l.Info("matching error status", "beacon", beaconStatus, "blob", blobStatus) + continue + + } + + if !reflect.DeepEqual(beaconResponse, blobResponse) { + result.MismatchedData = append(result.MismatchedData, id) + l.Error(validationErrorLog, "reason", "response-mismatch") + } + + l.Info("completed blob check", "blobs", len(beaconResponse.Data)) + } + + // Check if we should stop validation otherwise continue + select { + case <-ctx.Done(): + return result + default: + continue + } + } + + // Validation is complete, shutdown the app + a.closeApp(nil) + + return result +} diff --git a/validator/service/service_test.go b/validator/service/service_test.go new file mode 100644 index 0000000..33de554 --- /dev/null +++ b/validator/service/service_test.go @@ -0,0 +1,213 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/base-org/blob-archiver/common/beacon/beacontest" + "github.com/base-org/blob-archiver/common/blobtest" + "github.com/base-org/blob-archiver/common/storage" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +var ( + blockOne = strconv.FormatUint(blobtest.StartSlot+1, 10) +) + +type response struct { + data storage.BlobSidecars + err error + statusCode int +} + +type stubBlobSidecarClient struct { + data map[string]response +} + +// setResponses configures the stub to return the same data as the beacon client for all FetchSidecars invocations +func (s *stubBlobSidecarClient) setResponses(sbc *beacontest.StubBeaconClient) { + for k, v := range sbc.Blobs { + s.data[k] = response{ + data: storage.BlobSidecars{Data: v}, + err: nil, + statusCode: 200, + } + } +} + +// setResponse overrides a single FetchSidecars response +func (s *stubBlobSidecarClient) setResponse(id string, statusCode int, data storage.BlobSidecars, err error) { + s.data[id] = response{ + data: data, + err: err, + statusCode: statusCode, + } +} + +func (s *stubBlobSidecarClient) FetchSidecars(id string, format Format) (int, storage.BlobSidecars, error) { + response, ok := s.data[id] + if !ok { + return 0, storage.BlobSidecars{}, fmt.Errorf("not found") + } + return response.statusCode, response.data, response.err +} + +func setup(t *testing.T) (*ValidatorService, *beacontest.StubBeaconClient, *stubBlobSidecarClient, *stubBlobSidecarClient) { + l := testlog.Logger(t, log.LvlInfo) + headerClient := beacontest.NewDefaultStubBeaconClient(t) + cancel := func(error) {} + + beacon := &stubBlobSidecarClient{ + data: make(map[string]response), + } + blob := &stubBlobSidecarClient{ + data: make(map[string]response), + } + + return NewValidator(l, headerClient, beacon, blob, cancel), headerClient, beacon, blob +} + +func TestValidatorService_OnFetchError(t *testing.T) { + validator, _, _, _ := setup(t) + + result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.StartSlot+1)) + + // Expect an error for both SSZ and JSON + startSlot := strconv.FormatUint(blobtest.StartSlot, 10) + endSlot := strconv.FormatUint(blobtest.StartSlot+1, 10) + require.Equal(t, result.ErrorFetching, []string{startSlot, startSlot, endSlot, endSlot}) + require.Empty(t, result.MismatchedStatus) + require.Empty(t, result.MismatchedData) +} + +func TestValidatorService_AllMatch(t *testing.T) { + validator, headers, beacon, blob := setup(t) + + // Set the beacon + blob APIs to return the same data + beacon.setResponses(headers) + blob.setResponses(headers) + + result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot)) + + require.Empty(t, result.MismatchedStatus) + require.Empty(t, result.MismatchedData) + require.Empty(t, result.ErrorFetching) +} + +func TestValidatorService_MismatchedStatus(t *testing.T) { + validator, headers, beacon, blob := setup(t) + + // Set the blob API to return a 404 for blob=1 + beacon.setResponses(headers) + blob.setResponses(headers) + blob.setResponse(blockOne, 404, storage.BlobSidecars{}, nil) + + result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot)) + + require.Empty(t, result.MismatchedData) + require.Empty(t, result.ErrorFetching) + require.Len(t, result.MismatchedStatus, 2) + // The first mismatch is the JSON format, the second is the SSZ format + require.Equal(t, result.MismatchedStatus, []string{blockOne, blockOne}) +} + +func TestValidatorService_CompletelyDifferentBlobData(t *testing.T) { + validator, headers, beacon, blob := setup(t) + + // Modify the blobs for block 1 to be new random data + beacon.setResponses(headers) + blob.setResponses(headers) + blob.setResponse(blockOne, 200, storage.BlobSidecars{ + Data: blobtest.NewBlobSidecars(t, 1), + }, nil) + + result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot)) + + require.Empty(t, result.MismatchedStatus) + require.Empty(t, result.ErrorFetching) + require.Len(t, result.MismatchedData, 2) + // The first mismatch is the JSON format, the second is the SSZ format + require.Equal(t, result.MismatchedData, []string{blockOne, blockOne}) +} + +func TestValidatorService_MistmatchedBlobFields(t *testing.T) { + tests := []struct { + name string + modification func(i *[]*deneb.BlobSidecar) + }{ + { + name: "mismatched index", + modification: func(i *[]*deneb.BlobSidecar) { + (*i)[0].Index = deneb.BlobIndex(9) + }, + }, + { + name: "mismatched blob", + modification: func(i *[]*deneb.BlobSidecar) { + (*i)[0].Blob = deneb.Blob{0, 0, 0} + }, + }, + { + name: "mismatched kzg commitment", + modification: func(i *[]*deneb.BlobSidecar) { + (*i)[0].KZGCommitment = deneb.KZGCommitment{0, 0, 0} + }, + }, + { + name: "mismatched kzg proof", + modification: func(i *[]*deneb.BlobSidecar) { + (*i)[0].KZGProof = deneb.KZGProof{0, 0, 0} + }, + }, + { + name: "mismatched signed block header", + modification: func(i *[]*deneb.BlobSidecar) { + (*i)[0].SignedBlockHeader = nil + }, + }, + { + name: "mismatched kzg commitment inclusion proof", + modification: func(i *[]*deneb.BlobSidecar) { + (*i)[0].KZGCommitmentInclusionProof = deneb.KZGCommitmentInclusionProof{{1, 2, 9}} + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validator, headers, beacon, blob := setup(t) + + // Modify the blobs for block 1 to be new random data + beacon.setResponses(headers) + blob.setResponses(headers) + + // Deep copy the blob data + d, err := json.Marshal(headers.Blobs[blockOne]) + require.NoError(t, err) + var c []*deneb.BlobSidecar + err = json.Unmarshal(d, &c) + require.NoError(t, err) + + test.modification(&c) + + blob.setResponse(blockOne, 200, storage.BlobSidecars{ + Data: c, + }, nil) + + result := validator.checkBlobs(context.Background(), phase0.Slot(blobtest.StartSlot), phase0.Slot(blobtest.EndSlot)) + + require.Empty(t, result.MismatchedStatus) + require.Empty(t, result.ErrorFetching) + require.Len(t, result.MismatchedData, 2) + // The first mismatch is the JSON format, the second is the SSZ format + require.Equal(t, result.MismatchedData, []string{blockOne, blockOne}) + }) + } +}