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) {
for _, sidecar := range s.l1BuildingBlobSidecars {
for i, h := range sidecar.BlobHashes() {
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})
......
......@@ -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 {
cl := s.node.Attach()
return ethclient.NewClient(cl)
......
......@@ -153,6 +153,10 @@ func (s *L2Engine) PeerCount() int {
return s.node.Server().PeerCount()
}
func (s *L2Engine) HTTPEndpoint() string {
return s.node.HTTPEndpoint()
}
func (s *L2Engine) EthClient() *ethclient.Client {
cl := s.node.Attach()
return ethclient.NewClient(cl)
......
......@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
......@@ -15,20 +14,20 @@ import (
// BlobsStore is a simple in-memory store of blobs, for testing purposes
type BlobsStore struct {
// 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 {
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]
if !ok {
m = make(map[common.Hash]*eth.Blob)
m = make(map[eth.IndexedBlobHash]*eth.Blob)
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) {
......@@ -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)
}
for _, h := range hashes {
b, ok := m[h.Hash]
b, ok := m[h]
if !ok {
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
return nil, fmt.Errorf("no blobs known with given time: %w", ethereum.NotFound)
}
for _, h := range hashes {
b, ok := m[h.Hash]
b, ok := m[h]
if !ok {
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
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 = append(out, &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)
)
func (store *BlobsStore) GetAllSidecars(ctx context.Context, l1Timestamp uint64) ([]*eth.BlobSidecar, error) {
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
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum"
"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"
)
......@@ -27,8 +28,8 @@ import (
type FakeBeacon struct {
log log.Logger
// directory to store blob contents in after the blobs are persisted in a block
blobsDir string
// in-memory blob store
blobStore *e2eutils.BlobsStore
blobsLock sync.Mutex
beaconSrv *http.Server
......@@ -38,10 +39,10 @@ type FakeBeacon struct {
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{
log: log,
blobsDir: blobsDir,
blobStore: blobStore,
genesisTime: genesisTime,
blockTime: blockTime,
}
......@@ -158,20 +159,23 @@ func (f *FakeBeacon) Start(addr string) 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()
defer f.blobsLock.Unlock()
bundlePath := fmt.Sprintf("blobs_bundle_%d.json", slot)
if err := os.MkdirAll(f.blobsDir, 0755); err != nil {
return fmt.Errorf("failed to create dir for blob storage: %w", err)
}
err = os.WriteFile(filepath.Join(f.blobsDir, bundlePath), data, 0755)
if err != nil {
return fmt.Errorf("failed to write blobs bundle of slot %d: %w", slot, err)
// Solve for the slot timestamp.
// slot = (timestamp - genesis) / slot_time
// timestamp = slot * slot_time + genesis
slotTimestamp := slot*f.blockTime + f.genesisTime
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
}
......@@ -179,19 +183,30 @@ func (f *FakeBeacon) StoreBlobsBundle(slot uint64, bundle *engine.BlobsBundleV1)
func (f *FakeBeacon) LoadBlobsBundle(slot uint64) (*engine.BlobsBundleV1, error) {
f.blobsLock.Lock()
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 errors.Is(err, fs.ErrNotExist) {
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)
}
return nil, fmt.Errorf("failed to load blobs from store: %w", err)
}
var out engine.BlobsBundleV1
if err := json.Unmarshal(data, &out); err != nil {
return nil, fmt.Errorf("failed to decode blobs bundle of slot %d (%q): %w", slot, bundlePath, err)
// Convert blobs to the bundle
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
}
......
......@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/fakebeacon"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
......@@ -19,7 +20,8 @@ func TestGetVersion(t *testing.T) {
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() {
_ = beaconApi.Close()
})
......@@ -38,7 +40,8 @@ func Test404NotFound(t *testing.T) {
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() {
_ = beaconApi.Close()
})
......
......@@ -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
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() {
_ = 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