Skip to content

Commit 4e726c6

Browse files
authored
sysgo: add Osaka activation test (ethereum-optimism#17529)
* op-service/txplan: support blob txs * op-service/txinclude: handle future nonce gaps This can happen when we get mempool errors like "nonce too high", which itself can occur when we hit "account limit exceeded" errors in the blob pool. * sysgo: add Osaka activation test * sysext: infer L1 config when possible
1 parent 1fd459d commit 4e726c6

15 files changed

Lines changed: 386 additions & 10 deletions

File tree

‎devnet-sdk/shell/env/devnet.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/ethereum-optimism/optimism/devnet-sdk/controller/surface"
1212
"github.com/ethereum-optimism/optimism/devnet-sdk/descriptors"
1313
"github.com/ethereum-optimism/optimism/op-node/rollup"
14+
"github.com/ethereum-optimism/optimism/op-service/eth"
1415
"github.com/ethereum/go-ethereum/params"
1516
)
1617

@@ -138,8 +139,12 @@ func fixupDevnetConfig(config *descriptors.DevnetEnvironment) error {
138139
return fmt.Errorf("invalid L1 ID: %s", config.L1.ID)
139140
}
140141
if config.L1.Config == nil {
141-
config.L1.Config = &params.ChainConfig{
142-
ChainID: l1ID,
142+
if l1Config := eth.L1ChainConfigByChainID(eth.ChainIDFromBig(l1ID)); l1Config != nil {
143+
config.L1.Config = l1Config
144+
} else {
145+
config.L1.Config = &params.ChainConfig{
146+
ChainID: l1ID,
147+
}
143148
}
144149
}
145150
for _, l2Chain := range config.L2 {

‎mise.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ svm-rs = "0.5.19"
1515

1616
# Go dependencies
1717
"go:github.com/ethereum/go-ethereum/cmd/abigen" = "1.15.10"
18+
"go:github.com/ethereum/go-ethereum/cmd/geth" = "1.16.4" # Osaka release.
1819
"go:gotest.tools/gotestsum" = "1.12.1"
1920
"go:github.com/vektra/mockery/v2" = "2.46.0"
2021
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = "1.64.8"

‎op-acceptance-tests/tests/interop/loadtest/schedule.go‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ type Spammer interface {
171171
Spam(devtest.T) error
172172
}
173173

174+
type SpammerFunc func(t devtest.T) error
175+
176+
func (s SpammerFunc) Spam(t devtest.T) error {
177+
return s(t)
178+
}
179+
174180
// Schedule schedules a Spammer. It determines how often to spam and when to stop.
175181
type Schedule interface {
176182
Run(devtest.T, Spammer)
@@ -326,12 +332,16 @@ func setupAIMD(t devtest.T, blockTime time.Duration, aimdOpts ...AIMDOption) *AI
326332
t.Require().NoError(err)
327333
}
328334
aimd := NewAIMD(targetMessagePassesPerBlock, blockTime, aimdOpts...)
335+
ctx, cancel := context.WithCancel(t.Ctx())
329336
var wg sync.WaitGroup
330-
t.Cleanup(wg.Wait)
337+
t.Cleanup(func() {
338+
cancel()
339+
wg.Wait()
340+
})
331341
wg.Add(1)
332342
go func() {
333343
defer wg.Done()
334-
aimd.Start(t.Ctx())
344+
aimd.Start(ctx)
335345
}()
336346
return aimd
337347
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package osaka
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/rand"
7+
"fmt"
8+
"math/big"
9+
"os"
10+
"os/exec"
11+
"strings"
12+
"sync"
13+
"testing"
14+
"time"
15+
16+
"github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/loadtest"
17+
"github.com/ethereum-optimism/optimism/op-batcher/batcher"
18+
"github.com/ethereum-optimism/optimism/op-batcher/flags"
19+
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
20+
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
21+
"github.com/ethereum-optimism/optimism/op-devstack/presets"
22+
"github.com/ethereum-optimism/optimism/op-devstack/stack"
23+
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
24+
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/intentbuilder"
25+
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
26+
"github.com/ethereum-optimism/optimism/op-service/eth"
27+
"github.com/ethereum-optimism/optimism/op-service/txinclude"
28+
"github.com/ethereum-optimism/optimism/op-service/txplan"
29+
"github.com/ethereum/go-ethereum/common"
30+
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
31+
"github.com/ethereum/go-ethereum/core/types"
32+
"github.com/ethereum/go-ethereum/params"
33+
)
34+
35+
// configureDevstackEnvVars sets the appropriate env vars to use a mise-installed geth binary for
36+
// the L1 EL. This is useful in Osaka acceptance tests since op-geth does not include full Osaka
37+
// support. This is meant to run before presets.DoMain in a TestMain function. It will log to
38+
// stdout. ResetDevstackEnvVars should be used to reset the environment variables when TestMain
39+
// exits.
40+
//
41+
// Note that this is a no-op if either [sysgo.DevstackL1ELKindVar] or [sysgo.GethExecPathEnvVar]
42+
// are set.
43+
//
44+
// The returned callback resets any modified environment variables.
45+
func configureDevstackEnvVars() func() {
46+
if _, ok := os.LookupEnv(sysgo.DevstackL1ELKindEnvVar); ok {
47+
return func() {}
48+
}
49+
if _, ok := os.LookupEnv(sysgo.GethExecPathEnvVar); ok {
50+
return func() {}
51+
}
52+
53+
cmd := exec.Command("mise", "which", "geth")
54+
buf := bytes.NewBuffer([]byte{})
55+
cmd.Stdout = buf
56+
if err := cmd.Run(); err != nil {
57+
fmt.Printf("Failed to find mise-installed geth: %v\n", err)
58+
return func() {}
59+
}
60+
execPath := strings.TrimSpace(buf.String())
61+
fmt.Println("Found mise-installed geth:", execPath)
62+
_ = os.Setenv(sysgo.GethExecPathEnvVar, execPath)
63+
_ = os.Setenv(sysgo.DevstackL1ELKindEnvVar, "geth")
64+
return func() {
65+
_ = os.Unsetenv(sysgo.GethExecPathEnvVar)
66+
_ = os.Unsetenv(sysgo.DevstackL1ELKindEnvVar)
67+
}
68+
}
69+
70+
func TestMain(m *testing.M) {
71+
resetEnvVars := configureDevstackEnvVars()
72+
defer resetEnvVars()
73+
74+
presets.DoMain(m, stack.MakeCommon(stack.Combine[*sysgo.Orchestrator](
75+
sysgo.DefaultMinimalSystem(&sysgo.DefaultMinimalSystemIDs{}),
76+
sysgo.WithDeployerOptions(func(_ devtest.P, _ devkeys.Keys, builder intentbuilder.Builder) {
77+
_, l1Config := builder.WithL1(sysgo.DefaultL1ID)
78+
l1Config.WithOsakaOffset(0)
79+
l1Config.WithBPO1Offset(0)
80+
l1Config.WithL1BlobSchedule(&params.BlobScheduleConfig{
81+
Cancun: params.DefaultCancunBlobConfig,
82+
Osaka: params.DefaultOsakaBlobConfig,
83+
Prague: params.DefaultPragueBlobConfig,
84+
BPO1: params.DefaultBPO1BlobConfig,
85+
})
86+
}),
87+
sysgo.WithBatcherOption(func(_ stack.L2BatcherID, cfg *batcher.CLIConfig) {
88+
cfg.DataAvailabilityType = flags.BlobsType
89+
}),
90+
)))
91+
}
92+
93+
func TestBatcherUsesNewSidecarFormatAfterOsaka(gt *testing.T) {
94+
t := devtest.SerialT(gt)
95+
sys := presets.NewMinimal(t)
96+
t.Log("Waiting for Osaka to activate")
97+
t.Require().NotNil(sys.L1Network.Escape().ChainConfig().OsakaTime)
98+
sys.L1EL.WaitForTime(*sys.L1Network.Escape().ChainConfig().OsakaTime)
99+
t.Log("Osaka activated")
100+
101+
// 1. Wait for the sequencer to build a block after Osaka is activated. This avoids a race
102+
// condition where the unsafe head has been posted as part of a blob, but has not been
103+
// marked as "safe" yet.
104+
sys.L2EL.WaitForBlock()
105+
106+
// 2. Wait for the batcher to include target in a batch and post it to L1. Because the batch is
107+
// posted after Osaka has activated, it means the batcher must have successfully used the
108+
// new format.
109+
target := sys.L2EL.BlockRefByLabel(eth.Unsafe)
110+
blockTime := time.Duration(sys.L2Chain.Escape().RollupConfig().BlockTime) * time.Second
111+
for range time.Tick(blockTime) {
112+
if sys.L2EL.BlockRefByLabel(eth.Safe).Number >= target.Number {
113+
// If the safe head is ahead of the target height and the target block is part of the
114+
// canonical chain, then the target block is safe.
115+
_, err := sys.L2EL.Escape().EthClient().BlockRefByHash(t.Ctx(), target.Hash)
116+
t.Require().NoError(err)
117+
return
118+
}
119+
}
120+
}
121+
122+
func TestBlobBaseFeeIsCorrectAfterBPOFork(gt *testing.T) {
123+
t := devtest.SerialT(gt)
124+
sys := presets.NewMinimal(t)
125+
t.Log("Waiting for BPO1 to activate")
126+
t.Require().NotNil(sys.L1Network.Escape().ChainConfig().BPO1Time)
127+
sys.L1EL.WaitForTime(*sys.L1Network.Escape().ChainConfig().BPO1Time)
128+
t.Log("BPO1 activated")
129+
130+
sys.L1EL.WaitForBlock()
131+
l1BlockTime := sys.L1EL.EstimateBlockTime()
132+
l1ChainConfig := sys.L1Network.Escape().ChainConfig()
133+
134+
spamBlobs(t, sys) // Raise the blob base fee to make blob parameter changes visible.
135+
136+
// Wait for the blob base fee to rise above 1 so the blob parameter changes will be visible.
137+
for range time.Tick(l1BlockTime) {
138+
info, _, err := sys.L1EL.EthClient().InfoAndTxsByLabel(t.Ctx(), eth.Unsafe)
139+
t.Require().NoError(err)
140+
if calcBlobBaseFee(l1ChainConfig, info).Cmp(big.NewInt(1)) > 0 {
141+
break
142+
}
143+
t.Logf("Waiting for blob base fee to rise above 1")
144+
}
145+
146+
l2UnsafeRef := sys.L2CL.SyncStatus().UnsafeL2
147+
148+
// Get the L1 blob base fee.
149+
l1OriginInfo, err := sys.L1EL.EthClient().InfoByHash(t.Ctx(), l2UnsafeRef.L1Origin.Hash)
150+
t.Require().NoError(err)
151+
l1BlobBaseFee := calcBlobBaseFee(l1ChainConfig, l1OriginInfo)
152+
153+
// Get the L2 blob base fee from the system deposit tx.
154+
info, txs, err := sys.L2EL.Escape().EthClient().InfoAndTxsByHash(t.Ctx(), l2UnsafeRef.Hash)
155+
t.Require().NoError(err)
156+
blockInfo, err := derive.L1BlockInfoFromBytes(sys.L2Chain.Escape().RollupConfig(), info.Time(), txs[0].Data())
157+
t.Require().NoError(err)
158+
l2BlobBaseFee := blockInfo.BlobBaseFee
159+
160+
t.Require().Equal(l1BlobBaseFee, l2BlobBaseFee)
161+
}
162+
163+
func spamBlobs(t devtest.T, sys *presets.Minimal) {
164+
l1BlockTime := sys.L1EL.EstimateBlockTime()
165+
l1ChainConfig := sys.L1Network.Escape().ChainConfig()
166+
167+
eoa := sys.FunderL1.NewFundedEOA(eth.OneEther.Mul(5))
168+
signer := txinclude.NewPkSigner(eoa.Key().Priv(), sys.L1Network.ChainID().ToBig())
169+
l1ETHClient := sys.L1EL.EthClient()
170+
syncEOA := loadtest.NewSyncEOA(txinclude.NewPersistent(signer, struct {
171+
*txinclude.Monitor
172+
*txinclude.Resubmitter
173+
}{
174+
txinclude.NewMonitor(l1ETHClient, l1BlockTime),
175+
txinclude.NewResubmitter(l1ETHClient, l1BlockTime),
176+
}), eoa.Plan())
177+
178+
var blob eth.Blob
179+
_, err := rand.Read(blob[:])
180+
t.Require().NoError(err)
181+
// get the field-elements into a valid range
182+
for i := range 4096 {
183+
blob[32*i] &= 0b0011_1111
184+
}
185+
186+
const maxBlobTxsPerAccountInMempool = 16 // Private policy param in geth.
187+
spammer := loadtest.SpammerFunc(func(t devtest.T) error {
188+
_, err := syncEOA.Include(t, txplan.WithBlobs([]*eth.Blob{&blob}, l1ChainConfig), txplan.WithTo(&common.Address{}))
189+
return err
190+
})
191+
txsPerSlot := min(l1ChainConfig.BlobScheduleConfig.BPO1.Max*3/4, maxBlobTxsPerAccountInMempool)
192+
schedule := loadtest.NewConstant(l1BlockTime, loadtest.WithBaseRPS(uint64(txsPerSlot)))
193+
194+
ctx, cancel := context.WithCancel(t.Ctx())
195+
var wg sync.WaitGroup
196+
t.Cleanup(func() {
197+
cancel()
198+
wg.Wait()
199+
})
200+
wg.Add(1)
201+
go func() {
202+
defer wg.Done()
203+
schedule.Run(t.WithCtx(ctx), spammer)
204+
}()
205+
}
206+
207+
func calcBlobBaseFee(cfg *params.ChainConfig, info eth.BlockInfo) *big.Int {
208+
return eip4844.CalcBlobFee(cfg, &types.Header{
209+
// It's unfortunate that we can't build a proper header from a BlockInfo.
210+
// We do our best to work around deficiencies in the BlockInfo implementation here.
211+
Time: info.Time(),
212+
ExcessBlobGas: info.ExcessBlobGas(),
213+
})
214+
}

‎op-deployer/pkg/deployer/pipeline/seal_l1_dev_genesis.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ func SealL1DevGenesis(env *Env, intent *state.Intent, st *state.State) error {
4848
},
4949
L1ChainID: eth.ChainIDFromUInt64(intent.L1ChainID),
5050
L1PragueTimeOffset: l1DevParams.PragueTimeOffset,
51+
L1OsakaTimeOffset: l1DevParams.OsakaTimeOffset,
52+
L1BPO1TimeOffset: l1DevParams.BPO1TimeOffset,
53+
BlobScheduleConfig: l1DevParams.BlobSchedule,
5154
})
5255
if err != nil {
5356
return fmt.Errorf("failed to create dev L1 genesis template: %w", err)

‎op-deployer/pkg/deployer/state/intent.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/ethereum/go-ethereum/common"
1010
"github.com/ethereum/go-ethereum/common/hexutil"
11+
"github.com/ethereum/go-ethereum/params"
1112

1213
"github.com/ethereum-optimism/optimism/op-chain-ops/addresses"
1314
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
@@ -55,6 +56,16 @@ type L1DevGenesisParams struct {
5556
// PragueTimeOffset configures Prague (aka Pectra) to be activated at the given time after L1 dev genesis time.
5657
PragueTimeOffset *uint64 `json:"pragueTimeOffset" toml:"pragueTimeOffset"`
5758

59+
// OsakaTimeOffset configures Osaka (the EL changes in the Fusaka Ethereum fork) to be
60+
// activated at the given time after L1 dev genesis time.
61+
OsakaTimeOffset *uint64 `json:"osakaTimeOffset" toml:"osakaTimeOffset"`
62+
63+
// BPO1TimeOffset configures the BPO1 fork to be activated at the given time after L1 dev
64+
// genesis time.
65+
BPO1TimeOffset *uint64 `json:"bpo1TimeOffset" toml:"bpo1TimeOffset"`
66+
67+
BlobSchedule *params.BlobScheduleConfig `json:"blobSchedule"`
68+
5869
// Prefund is a map of addresses to balances (in wei), to prefund in the L1 dev genesis state.
5970
// This is independent of the "Prefund" functionality that may fund a default 20 test accounts.
6071
Prefund map[common.Address]*hexutil.U256 `json:"prefund" toml:"prefund"`

‎op-devstack/dsl/el.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ func (el *elNode) waitForNextBlock(blocksFromNow uint64) eth.BlockRef {
103103
return newRef
104104
}
105105

106+
// WaitForTime waits until the chain has reached or surpassed the given timestamp.
107+
func (el *elNode) WaitForTime(timestamp uint64) eth.BlockRef {
108+
for range time.Tick(500 * time.Millisecond) {
109+
ref, err := el.inner.EthClient().BlockRefByLabel(el.ctx, eth.Unsafe)
110+
el.require.NoError(err)
111+
if ref.Time >= timestamp {
112+
return ref
113+
}
114+
}
115+
return eth.BlockRef{} // Should never be reached.
116+
}
117+
106118
func (el *elNode) stackEL() stack.ELNode {
107119
return el.inner
108120
}

‎op-devstack/sysgo/engine_client.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ func (e *engineClient) GetPayloadV4(id engine.PayloadID) (*engine.ExecutionPaylo
6464
return e.getPayload(id, "engine_getPayloadV4")
6565
}
6666

67+
func (e *engineClient) GetPayloadV5(id engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) {
68+
return e.getPayload(id, "engine_getPayloadV5")
69+
}
70+
6771
func (e *engineClient) NewPayloadV2(data engine.ExecutableData) (engine.PayloadStatusV1, error) {
6872
var result engine.PayloadStatusV1
6973
if err := e.inner.CallContext(context.Background(), &result, "engine_newPayloadV2", data); err != nil {

‎op-devstack/sysgo/l1_nodes_subprocess.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,13 @@ func WithL1NodesSubprocess(id stack.L1ELNodeID, clID stack.L1CLNodeID) stack.Opt
182182
args := []string{
183183
"--log.format", "json",
184184
"--datadir", dataDirPath,
185-
"--ws", "--ws.addr", "127.0.0.1", "--ws.port", "0",
185+
"--ws", "--ws.addr", "127.0.0.1", "--ws.port", "0", "--ws.origins", "*", "--ws.api", "admin,debug,eth,net,txpool",
186186
"--authrpc.addr", "127.0.0.1", "--authrpc.port", "0", "--authrpc.jwtsecret", jwtPath,
187+
"--ipcdisable",
188+
"--nodiscover",
189+
"--verbosity", "5",
190+
"--miner.recommit", "2s",
191+
"--gcmode", "archive",
187192
}
188193

189194
l1EL := &ExternalL1Geth{

‎op-e2e/e2eutils/geth/fakepos.go‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type EngineAPI interface {
5959
ForkchoiceUpdatedV3(engine.ForkchoiceStateV1, *engine.PayloadAttributes) (engine.ForkChoiceResponse, error)
6060
ForkchoiceUpdatedV2(engine.ForkchoiceStateV1, *engine.PayloadAttributes) (engine.ForkChoiceResponse, error)
6161

62+
GetPayloadV5(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
6263
GetPayloadV4(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
6364
GetPayloadV3(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
6465
GetPayloadV2(engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error)
@@ -157,8 +158,10 @@ func (f *FakePoS) Start() error {
157158
Withdrawals: withdrawals,
158159
}
159160
parentBeaconBlockRoot := f.FakeBeaconBlockRoot(head.Time) // parent beacon block root
160-
isCancun := f.config.IsCancun(new(big.Int).SetUint64(head.Number.Uint64()+1), newBlockTime)
161-
isPrague := f.config.IsPrague(new(big.Int).SetUint64(head.Number.Uint64()+1), newBlockTime)
161+
nextHeight := new(big.Int).SetUint64(head.Number.Uint64() + 1)
162+
isCancun := f.config.IsCancun(nextHeight, newBlockTime)
163+
isPrague := f.config.IsPrague(nextHeight, newBlockTime)
164+
isOsaka := f.config.IsOsaka(nextHeight, newBlockTime)
162165
if isCancun {
163166
attrs.BeaconRoot = &parentBeaconBlockRoot
164167
}
@@ -192,7 +195,9 @@ func (f *FakePoS) Start() error {
192195
return nil
193196
}
194197
var envelope *engine.ExecutionPayloadEnvelope
195-
if isPrague {
198+
if isOsaka {
199+
envelope, err = f.engineAPI.GetPayloadV5(*res.PayloadID)
200+
} else if isPrague {
196201
envelope, err = f.engineAPI.GetPayloadV4(*res.PayloadID)
197202
} else if isCancun {
198203
envelope, err = f.engineAPI.GetPayloadV3(*res.PayloadID)

0 commit comments

Comments
 (0)