Commit 8dd6fb36 authored by clabby's avatar clabby Committed by GitHub

feat(op-e2e): Expose `L1Replica` + `L2Engine` + `BlobsStore` endpoints (#11926)

* feat(op-e2e): Expose `L1Replica` + `L2Engine` + `BlobsStore` endpoints

* mutex

* deterministic blob indexing

* proto review

* lint
parent d4467a1f
...@@ -240,7 +240,8 @@ func (s *L1Miner) ActL1EndBlock(t Testing) { ...@@ -240,7 +240,8 @@ func (s *L1Miner) ActL1EndBlock(t Testing) {
for _, sidecar := range s.l1BuildingBlobSidecars { for _, sidecar := range s.l1BuildingBlobSidecars {
for i, h := range sidecar.BlobHashes() { for i, h := range sidecar.BlobHashes() {
blob := (*eth.Blob)(&sidecar.Blobs[i]) blob := (*eth.Blob)(&sidecar.Blobs[i])
s.blobStore.StoreBlob(block.Time(), h, blob) indexedHash := eth.IndexedBlobHash{Index: uint64(i), Hash: h}
s.blobStore.StoreBlob(block.Time(), indexedHash, blob)
} }
} }
_, err = s.l1Chain.InsertChain(types.Blocks{block}) _, err = s.l1Chain.InsertChain(types.Blocks{block})
......
...@@ -168,6 +168,10 @@ func (s *L1Replica) MockL1RPCErrors(fn func() error) { ...@@ -168,6 +168,10 @@ func (s *L1Replica) MockL1RPCErrors(fn func() error) {
} }
} }
func (s *L1Replica) HTTPEndpoint() string {
return s.node.HTTPEndpoint()
}
func (s *L1Replica) EthClient() *ethclient.Client { func (s *L1Replica) EthClient() *ethclient.Client {
cl := s.node.Attach() cl := s.node.Attach()
return ethclient.NewClient(cl) return ethclient.NewClient(cl)
......
...@@ -153,6 +153,10 @@ func (s *L2Engine) PeerCount() int { ...@@ -153,6 +153,10 @@ func (s *L2Engine) PeerCount() int {
return s.node.Server().PeerCount() return s.node.Server().PeerCount()
} }
func (s *L2Engine) HTTPEndpoint() string {
return s.node.HTTPEndpoint()
}
func (s *L2Engine) EthClient() *ethclient.Client { func (s *L2Engine) EthClient() *ethclient.Client {
cl := s.node.Attach() cl := s.node.Attach()
return ethclient.NewClient(cl) return ethclient.NewClient(cl)
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
...@@ -15,20 +14,20 @@ import ( ...@@ -15,20 +14,20 @@ import (
// BlobsStore is a simple in-memory store of blobs, for testing purposes // BlobsStore is a simple in-memory store of blobs, for testing purposes
type BlobsStore struct { type BlobsStore struct {
// block timestamp -> blob versioned hash -> blob // block timestamp -> blob versioned hash -> blob
blobs map[uint64]map[common.Hash]*eth.Blob blobs map[uint64]map[eth.IndexedBlobHash]*eth.Blob
} }
func NewBlobStore() *BlobsStore { func NewBlobStore() *BlobsStore {
return &BlobsStore{blobs: make(map[uint64]map[common.Hash]*eth.Blob)} return &BlobsStore{blobs: make(map[uint64]map[eth.IndexedBlobHash]*eth.Blob)}
} }
func (store *BlobsStore) StoreBlob(blockTime uint64, versionedHash common.Hash, blob *eth.Blob) { func (store *BlobsStore) StoreBlob(blockTime uint64, indexedHash eth.IndexedBlobHash, blob *eth.Blob) {
m, ok := store.blobs[blockTime] m, ok := store.blobs[blockTime]
if !ok { if !ok {
m = make(map[common.Hash]*eth.Blob) m = make(map[eth.IndexedBlobHash]*eth.Blob)
store.blobs[blockTime] = m store.blobs[blockTime] = m
} }
m[versionedHash] = blob m[indexedHash] = blob
} }
func (store *BlobsStore) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) { func (store *BlobsStore) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
...@@ -38,7 +37,7 @@ func (store *BlobsStore) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashe ...@@ -38,7 +37,7 @@ func (store *BlobsStore) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashe
return nil, fmt.Errorf("no blobs known with given time: %w", ethereum.NotFound) return nil, fmt.Errorf("no blobs known with given time: %w", ethereum.NotFound)
} }
for _, h := range hashes { for _, h := range hashes {
b, ok := m[h.Hash] b, ok := m[h]
if !ok { if !ok {
return nil, fmt.Errorf("blob %d %s is not in store: %w", h.Index, h.Hash, ethereum.NotFound) return nil, fmt.Errorf("blob %d %s is not in store: %w", h.Index, h.Hash, ethereum.NotFound)
} }
...@@ -54,7 +53,7 @@ func (store *BlobsStore) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRef ...@@ -54,7 +53,7 @@ func (store *BlobsStore) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRef
return nil, fmt.Errorf("no blobs known with given time: %w", ethereum.NotFound) return nil, fmt.Errorf("no blobs known with given time: %w", ethereum.NotFound)
} }
for _, h := range hashes { for _, h := range hashes {
b, ok := m[h.Hash] b, ok := m[h]
if !ok { if !ok {
return nil, fmt.Errorf("blob %d %s is not in store: %w", h.Index, h.Hash, ethereum.NotFound) return nil, fmt.Errorf("blob %d %s is not in store: %w", h.Index, h.Hash, ethereum.NotFound)
} }
...@@ -66,15 +65,47 @@ func (store *BlobsStore) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRef ...@@ -66,15 +65,47 @@ func (store *BlobsStore) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRef
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert blob to commitment: %w", err) return nil, fmt.Errorf("failed to convert blob to commitment: %w", err)
} }
proof, err := kzg4844.ComputeBlobProof(b.KZGBlob(), commitment)
if err != nil {
return nil, fmt.Errorf("failed to compute blob proof: %w", err)
}
out = append(out, &eth.BlobSidecar{ out = append(out, &eth.BlobSidecar{
Index: eth.Uint64String(h.Index), Index: eth.Uint64String(h.Index),
Blob: *b, Blob: *b,
KZGCommitment: eth.Bytes48(commitment), KZGCommitment: eth.Bytes48(commitment),
KZGProof: eth.Bytes48(proof),
}) })
} }
return out, nil return out, nil
} }
var ( func (store *BlobsStore) GetAllSidecars(ctx context.Context, l1Timestamp uint64) ([]*eth.BlobSidecar, error) {
_ derive.L1BlobsFetcher = (*BlobsStore)(nil) m, ok := store.blobs[l1Timestamp]
) if !ok {
return nil, fmt.Errorf("no blobs known with given time: %w", ethereum.NotFound)
}
out := make([]*eth.BlobSidecar, len(m))
for h, b := range m {
if b == nil {
return nil, fmt.Errorf("blob %d %s is nil, cannot copy: %w", h.Index, h.Hash, ethereum.NotFound)
}
commitment, err := kzg4844.BlobToCommitment(b.KZGBlob())
if err != nil {
return nil, fmt.Errorf("failed to convert blob to commitment: %w", err)
}
proof, err := kzg4844.ComputeBlobProof(b.KZGBlob(), commitment)
if err != nil {
return nil, fmt.Errorf("failed to compute blob proof: %w", err)
}
out[h.Index] = &eth.BlobSidecar{
Index: eth.Uint64String(h.Index),
Blob: *b,
KZGCommitment: eth.Bytes48(commitment),
KZGProof: eth.Bytes48(proof),
}
}
return out, nil
}
var _ derive.L1BlobsFetcher = (*BlobsStore)(nil)
package fakebeacon package fakebeacon
import ( import (
"context"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net" "net"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -27,8 +28,8 @@ import ( ...@@ -27,8 +28,8 @@ import (
type FakeBeacon struct { type FakeBeacon struct {
log log.Logger log log.Logger
// directory to store blob contents in after the blobs are persisted in a block // in-memory blob store
blobsDir string blobStore *e2eutils.BlobsStore
blobsLock sync.Mutex blobsLock sync.Mutex
beaconSrv *http.Server beaconSrv *http.Server
...@@ -38,10 +39,10 @@ type FakeBeacon struct { ...@@ -38,10 +39,10 @@ type FakeBeacon struct {
blockTime uint64 blockTime uint64
} }
func NewBeacon(log log.Logger, blobsDir string, genesisTime uint64, blockTime uint64) *FakeBeacon { func NewBeacon(log log.Logger, blobStore *e2eutils.BlobsStore, genesisTime uint64, blockTime uint64) *FakeBeacon {
return &FakeBeacon{ return &FakeBeacon{
log: log, log: log,
blobsDir: blobsDir, blobStore: blobStore,
genesisTime: genesisTime, genesisTime: genesisTime,
blockTime: blockTime, blockTime: blockTime,
} }
...@@ -158,20 +159,23 @@ func (f *FakeBeacon) Start(addr string) error { ...@@ -158,20 +159,23 @@ func (f *FakeBeacon) Start(addr string) error {
} }
func (f *FakeBeacon) StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1) error { func (f *FakeBeacon) StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1) error {
data, err := json.Marshal(bundle)
if err != nil {
return fmt.Errorf("failed to encode blobs bundle of slot %d: %w", slot, err)
}
f.blobsLock.Lock() f.blobsLock.Lock()
defer f.blobsLock.Unlock() defer f.blobsLock.Unlock()
bundlePath := fmt.Sprintf("blobs_bundle_%d.json", slot)
if err := os.MkdirAll(f.blobsDir, 0755); err != nil { // Solve for the slot timestamp.
return fmt.Errorf("failed to create dir for blob storage: %w", err) // slot = (timestamp - genesis) / slot_time
} // timestamp = slot * slot_time + genesis
err = os.WriteFile(filepath.Join(f.blobsDir, bundlePath), data, 0755) slotTimestamp := slot*f.blockTime + f.genesisTime
if err != nil {
return fmt.Errorf("failed to write blobs bundle of slot %d: %w", slot, err) for i, b := range bundle.Blobs {
f.blobStore.StoreBlob(
slotTimestamp,
eth.IndexedBlobHash{
Index: uint64(i),
Hash: eth.KZGToVersionedHash(kzg4844.Commitment(bundle.Commitments[i])),
},
(*eth.Blob)(b[:]),
)
} }
return nil return nil
} }
...@@ -179,19 +183,30 @@ func (f *FakeBeacon) StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1) ...@@ -179,19 +183,30 @@ func (f *FakeBeacon) StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1)
func (f *FakeBeacon) LoadBlobsBundle(slot uint64) (*engine.BlobsBundleV1, error) { func (f *FakeBeacon) LoadBlobsBundle(slot uint64) (*engine.BlobsBundleV1, error) {
f.blobsLock.Lock() f.blobsLock.Lock()
defer f.blobsLock.Unlock() defer f.blobsLock.Unlock()
bundlePath := fmt.Sprintf("blobs_bundle_%d.json", slot)
data, err := os.ReadFile(filepath.Join(f.blobsDir, bundlePath)) // Solve for the slot timestamp.
// slot = (timestamp - genesis) / slot_time
// timestamp = slot * slot_time + genesis
slotTimestamp := slot*f.blockTime + f.genesisTime
// Load blobs from the store
blobs, err := f.blobStore.GetAllSidecars(context.Background(), slotTimestamp)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("failed to load blobs from store: %w", err)
return nil, fmt.Errorf("no blobs bundle found for slot %d (%q): %w", slot, bundlePath, ethereum.NotFound)
} else {
return nil, fmt.Errorf("failed to read blobs bundle of slot %d (%q): %w", slot, bundlePath, err)
}
} }
var out engine.BlobsBundleV1
if err := json.Unmarshal(data, &out); err != nil { // Convert blobs to the bundle
return nil, fmt.Errorf("failed to decode blobs bundle of slot %d (%q): %w", slot, bundlePath, err) out := engine.BlobsBundleV1{
Commitments: make([]hexutil.Bytes, len(blobs)),
Proofs: make([]hexutil.Bytes, len(blobs)),
Blobs: make([]hexutil.Bytes, len(blobs)),
}
for _, b := range blobs {
out.Commitments[b.Index] = hexutil.Bytes(b.KZGCommitment[:])
out.Proofs[b.Index] = hexutil.Bytes(b.KZGProof[:])
out.Blobs[b.Index] = hexutil.Bytes(b.Blob[:])
} }
return &out, nil return &out, nil
} }
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/fakebeacon" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/fakebeacon"
"github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
...@@ -19,7 +20,8 @@ func TestGetVersion(t *testing.T) { ...@@ -19,7 +20,8 @@ func TestGetVersion(t *testing.T) {
l := testlog.Logger(t, log.LevelInfo) l := testlog.Logger(t, log.LevelInfo)
beaconApi := fakebeacon.NewBeacon(l, t.TempDir(), uint64(0), uint64(0)) blobStore := e2eutils.NewBlobStore()
beaconApi := fakebeacon.NewBeacon(l, blobStore, uint64(0), uint64(0))
t.Cleanup(func() { t.Cleanup(func() {
_ = beaconApi.Close() _ = beaconApi.Close()
}) })
...@@ -38,7 +40,8 @@ func Test404NotFound(t *testing.T) { ...@@ -38,7 +40,8 @@ func Test404NotFound(t *testing.T) {
l := testlog.Logger(t, log.LevelInfo) l := testlog.Logger(t, log.LevelInfo)
beaconApi := fakebeacon.NewBeacon(l, t.TempDir(), uint64(0), uint64(12)) blobStore := e2eutils.NewBlobStore()
beaconApi := fakebeacon.NewBeacon(l, blobStore, uint64(0), uint64(12))
t.Cleanup(func() { t.Cleanup(func() {
_ = beaconApi.Close() _ = beaconApi.Close()
}) })
......
...@@ -588,7 +588,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste ...@@ -588,7 +588,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
// Create a fake Beacon node to hold on to blobs created by the L1 miner, and to serve them to L2 // Create a fake Beacon node to hold on to blobs created by the L1 miner, and to serve them to L2
bcn := fakebeacon.NewBeacon(testlog.Logger(t, log.LevelInfo).New("role", "l1_cl"), bcn := fakebeacon.NewBeacon(testlog.Logger(t, log.LevelInfo).New("role", "l1_cl"),
path.Join(cfg.BlobsPath, "l1_cl"), l1Genesis.Timestamp, cfg.DeployConfig.L1BlockTime) e2eutils.NewBlobStore(), l1Genesis.Timestamp, cfg.DeployConfig.L1BlockTime)
t.Cleanup(func() { t.Cleanup(func() {
_ = bcn.Close() _ = bcn.Close()
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment