Skip to content

Store blobs locally post fusaka + disk reader#4007

Merged
Tristan-Wilson merged 1 commit into
masterfrom
archive-nitro-blobs-post-fusaka
Nov 17, 2025
Merged

Store blobs locally post fusaka + disk reader#4007
Tristan-Wilson merged 1 commit into
masterfrom
archive-nitro-blobs-post-fusaka

Conversation

@Tristan-Wilson

Copy link
Copy Markdown
Member

The Ethereum Fusaka fork introduced /eth/v1/beacon/blobs as a replacement for the legacy /eth/v1/beacon/blob_sidecars endpoint. Nitro's getBlobs() function fetches from the new endpoint but was not saving blobs to disk.

The legacy blobSidecars() function saves blobs in "Version 0" format with full metadata (commitments, proofs, block roots). The new endpoint returns only blob data, requiring a minimal storage format optimized for hash-based lookups.

Additionally, there was no ReadBlobsFromDisk() function - blobs could be saved but not read back. This commit adds the reader for testing and to enable a future beacon endpoint emulator that we may add.

Storage Format V1 (new):

  • Stores blobs as versioned hash -> blob map
  • File: {blob-directory}/{slot}
  • Format: {"version": 1, "data": {"0xHASH": "0xBLOB", ...}}
  • Used by getBlobs() when fetching from /eth/v1/beacon/blobs

Storage Format V0 (legacy, backward compatible):

  • Stores blob_sidecars array with full metadata
  • Format: {"data": [{"blob": "0x...", "kzg_commitment": "0x...", "kzg_proof": "0x...", ...}]}
  • Used by blobSidecars() when fetching from legacy endpoint
  • Still readable for backward compatibility

ReadBlobsFromDisk() (new):

  • Reads blobs from disk, transparently handling V0 or V1 format
  • Auto-detects version via presence of "version" field
  • Validates blob integrity:
    • V1: Computes commitment -> versioned hash, verifies match to key
    • V0: Computes commitment from blob, verifies match to stored commitment
  • No migration needed - both formats coexist

Renamed functions for clarity:

  • saveBlobDataToDisk -> saveBlobsV0ToDisk (legacy format)
  • Added saveBlobsV1ToDisk (new format)

Testing improvements:

  • Updated createTestBlobs() to use production blobs.EncodeBlobs()
  • All tests now use real KZG blobs with valid commitments
  • Added TestReadBlobsV1ValidationFailure and TestReadBlobsV0ValidationFailure
  • Renamed existing tests to indicate format (e.g., TestSaveBlobsV0ToDisk)

NIT-4113

The Ethereum Fusaka fork introduced /eth/v1/beacon/blobs as a replacement
for the legacy /eth/v1/beacon/blob_sidecars endpoint. Nitro's getBlobs()
function fetches from the new endpoint but was not saving blobs to disk.

The legacy blobSidecars() function saves blobs in "Version 0" format with
full metadata (commitments, proofs, block roots). The new endpoint returns
only blob data, requiring a minimal storage format optimized for hash-based
lookups.

Additionally, there was no ReadBlobsFromDisk() function - blobs could be
saved but not read back. This commit adds the reader for testing and to
enable a future beacon endpoint emulator that we may add.

Storage Format V1 (new):
- Stores blobs as versioned hash -> blob map
- File: {blob-directory}/{slot}
- Format: {"version": 1, "data": {"0xHASH": "0xBLOB", ...}}
- Used by getBlobs() when fetching from /eth/v1/beacon/blobs

Storage Format V0 (legacy, backward compatible):
- Stores blob_sidecars array with full metadata
- Format: {"data": [{"blob": "0x...", "kzg_commitment": "0x...",
  "kzg_proof": "0x...", ...}]}
- Used by blobSidecars() when fetching from legacy endpoint
- Still readable for backward compatibility

ReadBlobsFromDisk() (new):
- Reads blobs from disk, transparently handling V0 or V1 format
- Auto-detects version via presence of "version" field
- Validates blob integrity:
  * V1: Computes commitment -> versioned hash, verifies match to key
  * V0: Computes commitment from blob, verifies match to stored commitment
- No migration needed - both formats coexist

Renamed functions for clarity:
- saveBlobDataToDisk -> saveBlobsV0ToDisk (legacy format)
- Added saveBlobsV1ToDisk (new format)

Testing improvements:
- Updated createTestBlobs() to use production blobs.EncodeBlobs()
- All tests now use real KZG blobs with valid commitments
- Added TestReadBlobsV1ValidationFailure and TestReadBlobsV0ValidationFailure
- Renamed existing tests to indicate format (e.g., TestSaveBlobsV0ToDisk)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds disk storage and reading functionality for blobs fetched from the new Fusaka-era /eth/v1/beacon/blobs endpoint. The changes introduce a new storage format (V1) optimized for hash-based lookups while maintaining backward compatibility with the legacy format (V0), and implement a unified reader that handles both formats with validation.

Key Changes:

  • Added saveBlobsV1ToDisk() to store blobs as versioned hash → blob maps for new endpoint
  • Implemented ReadBlobsFromDisk() with automatic format detection and validation for both V0 and V1 formats
  • Updated getBlobs() to persist fetched blobs to disk in V1 format

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
util/headerreader/blob_client.go Implements V1 storage format, disk reader with validation, and integrates disk saving into getBlobs()
util/headerreader/blob_client_test.go Adds comprehensive tests for both storage formats, validation failures, and format detection

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// createTestBlobs creates test blobs and their versioned hashes
func createTestBlobs(count int) ([]kzg4844.Blob, []common.Hash, error) {
testData := make([]byte, count*blobs.BlobEncodableData)
r := rand.New(rand.NewSource(1))

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using deprecated rand.New() with rand.Source. In Go 1.20+, use rand.NewSource directly or the new ChaCha8 source. Consider using r := rand.New(rand.NewChaCha8(rand.NewSource(1))) for better randomness characteristics.

Suggested change
r := rand.New(rand.NewSource(1))
r := rand.New(rand.NewChaCha8(rand.NewSource(1)))

Copilot uses AI. Check for mistakes.
// Save blobs to disk in version 1 format if blobDirectory is configured
if b.blobDirectory != "" {
if err := saveBlobsV1ToDisk(output, computedHashes, slot, b.blobDirectory); err != nil {
return nil, err

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error returned from saveBlobsV1ToDisk lacks context about which operation failed. Consider wrapping the error with additional context: return nil, fmt.Errorf("failed to save blobs to disk for slot %d: %w", slot, err)

Suggested change
return nil, err
return nil, fmt.Errorf("failed to save blobs to disk for slot %d: %w", slot, err)

Copilot uses AI. Check for mistakes.
if err != nil {
return fmt.Errorf("unable to marshal blobs into JSON: %w", err)
}

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] File permissions 0600 make the file readable/writable only by the owner, which is appropriate for blob data. However, consider if the blob directory itself has appropriate permissions to prevent unauthorized access.

Suggested change
// Ensure the blob directory exists with secure permissions
if err := os.MkdirAll(blobDirectory, 0700); err != nil {
return fmt.Errorf("failed to create blob directory: %w", err)
}

Copilot uses AI. Check for mistakes.
@github-actions

Copy link
Copy Markdown
Contributor

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
2169 4 2165 0
View the top 3 failed tests by shortest run time
TestValidationInputsAtWithWasmTarget
Stack Traces | 5.300s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
�[38;5;48;1myay!! we validated block 5 in 96.15ms�[0;0m
�[38;5;48;1myay!! we validated block 3 in 105.44ms�[0;0m
�[90mposted new batch 3�[0;0m
�[90mposted new batch 3�[0;0m
    validation_inputs_at_test.go:70: goroutine 1492848 [running]:
        runtime/debug.Stack()
        	/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/debug/stack.go:26 +0x5e
        github.com/offchainlabs/nitro/util/testhelpers.RequireImpl({0x40db510, 0xc0d60ecfc0}, {0x4098900, 0xc080271e60}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/util/testhelpers/testhelpers.go:29 +0x55
        github.com/offchainlabs/nitro/system_tests.Require(0xc0d60ecfc0, {0x4098900, 0xc080271e60}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/system_tests/common_test.go:1759 +0x5d
        github.com/offchainlabs/nitro/system_tests.TestValidationInputsAtWithWasmTarget(0xc0d60ecfc0)
        	/home/runner/work/nitro/nitro/system_tests/validation_inputs_at_test.go:70 +0x970
        testing.tRunner(0xc0d60ecfc0, 0x3d1fca0)
        	/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1997 +0x465
        
    validation_inputs_at_test.go:70: �[31;1m [] failed calculating position for validation: batch not found on L1 yet �[0;0m
--- FAIL: TestValidationInputsAtWithWasmTarget (5.30s)
TestVersion30
Stack Traces | 6.350s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
�[38;5;48;1myay!! we validated block 4 in 333.16ms�[0;0m
�[90mTime to activate hostio-test: 402.236186ms�[0;0m
    precompile_inclusion_test.go:90: goroutine 455994 [running]:
        runtime/debug.Stack()
        	/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/debug/stack.go:26 +0x5e
        github.com/offchainlabs/nitro/util/testhelpers.RequireImpl({0x40db510, 0xc0a6cbcfc0}, {0x4098d80, 0xc0d87644e0}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/util/testhelpers/testhelpers.go:29 +0x55
        github.com/offchainlabs/nitro/system_tests.Require(0xc0a6cbcfc0, {0x4098d80, 0xc0d87644e0}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/system_tests/common_test.go:1759 +0x5d
        github.com/offchainlabs/nitro/system_tests.testPrecompiles(0xc0a6cbcfc0, 0x1e, {0xc07678fdb0, 0x6, 0x40?})
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:90 +0x371
        github.com/offchainlabs/nitro/system_tests.TestVersion30(0xc0a6cbcfc0?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:67 +0x798
        testing.tRunner(0xc0a6cbcfc0, 0x3d1fcb8)
        	/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1997 +0x465
        
    precompile_inclusion_test.go:90: �[31;1m [] execution aborted (timeout = 5s) �[0;0m
--- FAIL: TestVersion30 (6.35s)
TestVersion40
Stack Traces | 6.360s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
=== PAUSE TestVersion40
=== CONT  TestVersion40
    precompile_inclusion_test.go:90: goroutine 455995 [running]:
        runtime/debug.Stack()
        	/opt/hostedtoolcache/go/1.25.3/x64/src/runtime/debug/stack.go:26 +0x5e
        github.com/offchainlabs/nitro/util/testhelpers.RequireImpl({0x40db510, 0xc0a6cbd180}, {0x4098d80, 0xc0e5303ce0}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/util/testhelpers/testhelpers.go:29 +0x55
        github.com/offchainlabs/nitro/system_tests.Require(0xc0a6cbd180, {0x4098d80, 0xc0e5303ce0}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/system_tests/common_test.go:1759 +0x5d
        github.com/offchainlabs/nitro/system_tests.testPrecompiles(0xc0a6cbd180, 0x28, {0xc0bfd9ddf8, 0x5, 0x39?})
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:90 +0x371
        github.com/offchainlabs/nitro/system_tests.TestVersion40(0xc0a6cbd180?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:71 +0x64b
        testing.tRunner(0xc0a6cbd180, 0x3d1fcc0)
        	/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.3/x64/src/testing/testing.go:1997 +0x465
        
    precompile_inclusion_test.go:90: �[31;1m [] execution aborted (timeout = 5s) �[0;0m
--- FAIL: TestVersion40 (6.36s)

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

@gligneul

Copy link
Copy Markdown
Contributor

Looks good, but I still have the feeling Nitro is the wrong place to cache this.

@Tristan-Wilson

Copy link
Copy Markdown
Member Author

@gligneul wrote:

Looks good, but I still have the feeling Nitro is the wrong place to cache this.

Agree. I'd like to build a separate service, but for now this change is just to make sure we maintain existing functionality.

Merged via the queue into master with commit 5d545a1 Nov 17, 2025
25 checks passed
@Tristan-Wilson Tristan-Wilson deleted the archive-nitro-blobs-post-fusaka branch November 17, 2025 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants