Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions op-acceptance-tests/acceptance-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ gates:
- package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/sgt
timeout: 10m

- id: l2blob
description: "L2 blob (EIP-4844) transaction tests."
tests:
- package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/l2blob
timeout: 10m

- id: supernode
description: "Supernode tests - tests for the op-supernode multi-chain consensus layer."
tests:
Expand Down
5 changes: 4 additions & 1 deletion op-acceptance-tests/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ cgt:
@just acceptance-test "" cgt

sgt:
@just acceptance-test "" sgt
DEVSTACK_L2EL_KIND=op-reth OP_RETH_EXEC_PATH={{REPO_ROOT}}/rust/target/release/op-reth just acceptance-test "" sgt

l2blob:
DEVSTACK_L2EL_KIND=op-reth OP_RETH_EXEC_PATH={{REPO_ROOT}}/rust/target/release/op-reth just acceptance-test "" l2blob


# Run acceptance tests with mise-managed binary
Expand Down
42 changes: 42 additions & 0 deletions op-acceptance-tests/tests/l2blob/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package l2blob

import (
"context"
"fmt"
"os"
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-devstack/stack"
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
"github.com/ethstorage/da-server/pkg/da"
)

func TestMain(m *testing.M) {
// Start DAC server before the system — required by op-node when l2BlobTime is set.
dacCfg := da.Config{
SequencerIP: "127.0.0.1",
ListenAddr: fmt.Sprintf("0.0.0.0:%d", dacPort),
StorePath: os.TempDir(),
}
dacServer := da.NewServer(&dacCfg)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := dacServer.Start(ctx); err != nil {
panic(fmt.Sprintf("failed to start DAC server: %v", err))
}
defer func() {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer stopCancel()
_ = dacServer.Stop(stopCtx)
}()

presets.DoMain(m,
presets.WithMinimal(),
stack.MakeCommon(stack.Combine[*sysgo.Orchestrator](
sysgo.WithDeployerOptions(WithL2BlobAtGenesis),
sysgo.WithGlobalL2CLOption(sysgo.L2CLDACUrls([]string{dacUrl})),
)),
)
}
151 changes: 151 additions & 0 deletions op-acceptance-tests/tests/l2blob/l2blob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package l2blob

import (
"bytes"
"context"
"fmt"
mrand "math/rand"
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
opforks "github.com/ethereum-optimism/optimism/op-core/forks"
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
"github.com/ethereum-optimism/optimism/op-devstack/presets"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/intentbuilder"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txplan"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
daclient "github.com/ethstorage/da-server/pkg/da/client"
"github.com/holiman/uint256"
)

const (
dacPort = 39777
)

var (
dacUrl = fmt.Sprintf("http://127.0.0.1:%d", dacPort)
)

// WithL2BlobAtGenesis enables L2 blob support at genesis for all L2 chains.
func WithL2BlobAtGenesis(_ devtest.P, _ devkeys.Keys, builder intentbuilder.Builder) {
offset := uint64(0)
for _, l2Cfg := range builder.L2s() {
l2Cfg.WithForkAtGenesis(opforks.Isthmus)
}
// Set L2GenesisBlobTimeOffset directly via global deploy overrides
// since l2BlobTime is not a standard fork.
builder.WithGlobalOverride("l2GenesisBlobTimeOffset", (*hexutil.Uint64)(&offset))
}

// withL2Blobs is like txplan.WithBlobs but computes the blob base fee from the
// header's ExcessBlobGas instead of BlobScheduleConfig (which is not populated
// for L2 chains).
func withL2Blobs(blobs []*eth.Blob) txplan.Option {
return func(tx *txplan.PlannedTx) {
tx.Type.Set(types.BlobTxType)
tx.BlobFeeCap.DependOn(&tx.AgainstBlock)
tx.BlobFeeCap.Fn(func(_ context.Context) (*uint256.Int, error) {
header := tx.AgainstBlock.Value()
if ebg := header.ExcessBlobGas(); ebg != nil && *ebg > 0 {
fee := eth.CalcBlobFeeCancun(*ebg)
return uint256.MustFromBig(fee), nil
}
// Genesis or no excess blob gas — use minimum
return uint256.NewInt(1), nil
})
var blobHashes []common.Hash
tx.Sidecar.Fn(func(_ context.Context) (*types.BlobTxSidecar, error) {
sidecar, hashes, err := txmgr.MakeSidecar(blobs, false)
if err != nil {
return nil, fmt.Errorf("make blob tx sidecar: %w", err)
}
blobHashes = hashes
return sidecar, nil
})
tx.BlobHashes.DependOn(&tx.Sidecar)
tx.BlobHashes.Fn(func(_ context.Context) ([]common.Hash, error) {
return blobHashes, nil
})
}
}

// TestSubmitL2BlobTransaction tests that blob transactions can be submitted on L2
// and that the blobs are retrievable from the DAC server.
// Mirrors op-e2e/l2blob/l2blob_test.go::TestSubmitTXWithBlobsFunctionSuccess.
func TestSubmitL2BlobTransaction(gt *testing.T) {
t := devtest.SerialT(gt)
sys := presets.NewMinimal(t)

t.Require().True(sys.L2Chain.IsForkActive(opforks.Isthmus), "Isthmus fork must be active")

alice := sys.FunderL2.NewFundedEOA(eth.OneEther)

// Create random blobs
numBlobs := 3
blobs := make([]*eth.Blob, numBlobs)
for i := range blobs {
b := getRandBlob(t, int64(i))
blobs[i] = &b
}

// Send a blob transaction
planned := alice.Transact(
alice.Plan(),
withL2Blobs(blobs),
txplan.WithTo(&common.Address{}), // blob tx requires a 'to' address
)

receipt, err := planned.Included.Eval(t.Ctx())
t.Require().NoError(err, "blob transaction must be included")
t.Require().NotNil(receipt, "receipt must not be nil")
t.Require().Equal(uint64(1), receipt.Status, "blob transaction must succeed")

// Verify the transaction has blob hashes
tx, err := planned.Signed.Eval(t.Ctx())
t.Require().NoError(err, "must get signed transaction")
blobHashes := tx.BlobHashes()
t.Require().Equal(numBlobs, len(blobHashes), "transaction must have correct number of blob hashes")

// Verify blob gas usage in the block
blockNum := receipt.BlockNumber
client := sys.L2EL.Escape().L2EthClient()
header, err := client.InfoByNumber(t.Ctx(), blockNum.Uint64())
t.Require().NoError(err, "must get block header")
blobGasUsed := header.BlobGasUsed()
t.Require().NotZero(blobGasUsed, "blob gas used must be non-zero for block with blob transactions")

// Download blobs from DAC server and verify content
dacCtx, cancel := context.WithTimeout(t.Ctx(), 5*time.Second)
defer cancel()
dacClient := daclient.New([]string{dacUrl})
dblobs, err := dacClient.GetBlobs(dacCtx, blobHashes)
t.Require().NoError(err, "must download blobs from DAC server")
t.Require().Equal(len(blobHashes), len(dblobs), "downloaded blobs count must match blob hashes")

for i, blob := range dblobs {
t.Require().Equal(eth.BlobSize, len(blob), "downloaded blob %d must have correct size", i)
t.Require().True(bytes.Equal(blob, blobs[i][:]),
"blob %d content mismatch: got %s vs expected %s",
i, common.Bytes2Hex(blob[:32]), common.Bytes2Hex(blobs[i][:32]))
}

t.Logf("L2 blob transaction included: block=%d, blobGasUsed=%d, blobHashes=%d",
blockNum, blobGasUsed, len(blobHashes))
}

// getRandBlob generates a random blob with the given seed.
func getRandBlob(t devtest.T, seed int64) eth.Blob {
r := mrand.New(mrand.NewSource(seed))
bigData := eth.Data(make([]byte, eth.MaxBlobDataSize))
_, err := r.Read(bigData)
t.Require().NoError(err)
var b eth.Blob
err = b.FromData(bigData)
t.Require().NoError(err)
return b
}
9 changes: 9 additions & 0 deletions op-devstack/sysgo/l2_cl.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ type L2CLConfig struct {
NoDiscovery bool

FollowSource string

// DACUrls is the list of DAC server URLs for the sequencer (required when l2BlobTime is set).
DACUrls []string
}

func L2CLSequencer() L2CLOption {
Expand All @@ -52,6 +55,12 @@ func L2CLIndexing() L2CLOption {
})
}

func L2CLDACUrls(urls []string) L2CLOption {
return L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *L2CLConfig) {
cfg.DACUrls = urls
})
}

func L2CLFollowSource(source string) L2CLOption {
return L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *L2CLConfig) {
cfg.FollowSource = source
Expand Down
3 changes: 3 additions & 0 deletions op-devstack/sysgo/l2_cl_opnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ func withOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L
if cfg.SafeDBPath != "" {
nodeCfg.SafeDBPath = cfg.SafeDBPath
}
if len(cfg.DACUrls) > 0 {
nodeCfg.DACConfig = &config.DACConfig{URLS: cfg.DACUrls}
}

l2CLNode := &OpNode{
id: l2CLID,
Expand Down
Loading