Skip to content

Commit 1fea4f9

Browse files
authored
op-acceptance-tests: various improvements to the Fusaka acceptance test (#17944)
* split out helpers and initialization to a separate file * fix comment in mise.toml * rename osaka test * rename to fusaka * txplan: support non-cell proof blob txs * also verify the l1 blob base fee in the l1 block contract * optimization #17529 (comment) * op-acceptance-tests: harden the BPO blob base fee calculation test (#17953) Before, we merely waited for the blob base fee to rise above 1 before assuming that the blob parameters would have a visible impact on the fee. Due to rounding, it is possible that different parameters still result in the same base fee even above the minimum (either 1 or the reserve price set by EIP-7918). Calculating the precise intersections between different blob base fee formulae is complicated, so we opt for the simpler but maximally robust approach: calculate both the BPO1 base fee and the Osaka base fee, and only proceed when they differ. This resolves a nit on a previous PR that was merged due to time pressure: #17529 (comment) * add log
1 parent d985c50 commit 1fea4f9

File tree

6 files changed

+261
-221
lines changed

6 files changed

+261
-221
lines changed

mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +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.
18+
"go:github.com/ethereum/go-ethereum/cmd/geth" = "1.16.4" # Osaka testnet release.
1919
"go:gotest.tools/gotestsum" = "1.12.1"
2020
"go:github.com/vektra/mockery/v2" = "2.46.0"
2121
"go:github.com/golangci/golangci-lint/cmd/golangci-lint" = "1.64.8"
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package fusaka
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"errors"
7+
"math/big"
8+
"sync"
9+
"testing"
10+
"time"
11+
12+
"github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop/loadtest"
13+
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
14+
"github.com/ethereum-optimism/optimism/op-devstack/presets"
15+
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
16+
"github.com/ethereum-optimism/optimism/op-service/eth"
17+
"github.com/ethereum-optimism/optimism/op-service/predeploys"
18+
"github.com/ethereum-optimism/optimism/op-service/txinclude"
19+
"github.com/ethereum-optimism/optimism/op-service/txintent/bindings"
20+
"github.com/ethereum-optimism/optimism/op-service/txintent/contractio"
21+
"github.com/ethereum-optimism/optimism/op-service/txplan"
22+
"github.com/ethereum/go-ethereum"
23+
"github.com/ethereum/go-ethereum/common"
24+
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
25+
"github.com/ethereum/go-ethereum/core/types"
26+
)
27+
28+
func TestSafeHeadAdvancesAfterOsaka(gt *testing.T) {
29+
t := devtest.SerialT(gt)
30+
sys := presets.NewMinimal(t)
31+
l1Config := sys.L1Network.Escape().ChainConfig()
32+
t.Log("Waiting for Osaka to activate")
33+
t.Require().NotNil(l1Config.OsakaTime)
34+
sys.L1EL.WaitForTime(*l1Config.OsakaTime)
35+
t.Log("Osaka activated")
36+
37+
l2BlockTime := time.Duration(sys.L2Chain.Escape().RollupConfig().BlockTime) * time.Second
38+
for {
39+
l2SafeRef := sys.L2EL.BlockRefByLabel(eth.Safe)
40+
if l1Config.IsOsaka(new(big.Int).SetUint64(l2SafeRef.Number), l2SafeRef.Time) {
41+
return
42+
}
43+
t.Log("L2 safe head predates Osaka activation on L1, waiting for it to advance...")
44+
select {
45+
case <-time.After(l2BlockTime):
46+
case <-t.Ctx().Done():
47+
t.Require().Fail("Never found a safe L2 block after Osaka activated on L1")
48+
}
49+
}
50+
}
51+
52+
func TestBlobBaseFeeIsCorrectAfterBPOFork(gt *testing.T) {
53+
t := devtest.SerialT(gt)
54+
sys := presets.NewMinimal(t)
55+
t.Log("Waiting for BPO1 to activate")
56+
t.Require().NotNil(sys.L1Network.Escape().ChainConfig().BPO1Time)
57+
sys.L1EL.WaitForTime(*sys.L1Network.Escape().ChainConfig().BPO1Time)
58+
t.Log("BPO1 activated")
59+
60+
spamBlobs(t, sys) // Raise the blob base fee to make blob parameter changes visible.
61+
62+
l2UnsafeHash, l1BlobBaseFee := waitForNonTrivialBPO1Block(t, sys)
63+
l2Info, l2Txs, err := sys.L2EL.Escape().EthClient().InfoAndTxsByHash(t.Ctx(), l2UnsafeHash)
64+
t.Require().NoError(err)
65+
66+
// Check the L1 blob base fee in the system deposit tx.
67+
blockInfo, err := derive.L1BlockInfoFromBytes(sys.L2Chain.Escape().RollupConfig(), l2Info.Time(), l2Txs[0].Data())
68+
t.Require().NoError(err)
69+
l2BlobBaseFee := blockInfo.BlobBaseFee
70+
t.Require().Equal(l1BlobBaseFee, l2BlobBaseFee)
71+
72+
// Check the L1 Blob base fee in the L1Block contract.
73+
l1Block := bindings.NewL1Block(bindings.WithClient(sys.L2EL.Escape().EthClient()), bindings.WithTo(predeploys.L1BlockAddr))
74+
l2BlobBaseFee, err = contractio.Read(l1Block.BlobBaseFee(), t.Ctx(), func(tx *txplan.PlannedTx) {
75+
tx.AgainstBlock.Set(l2Info)
76+
})
77+
t.Require().NoError(err)
78+
t.Require().Equal(l1BlobBaseFee, l2BlobBaseFee)
79+
}
80+
81+
// waitForNonTrivialBPO1Block will return an L1 blob base fee that can only be calculated using the
82+
// correct BPO1 parameters (i.e., the Osaka parameters result in a different value). It also
83+
// returns an L2 block hash from the same epoch.
84+
func waitForNonTrivialBPO1Block(t devtest.T, sys *presets.Minimal) (common.Hash, *big.Int) {
85+
l1ChainConfig := sys.L1Network.Escape().ChainConfig()
86+
l1BlockTime := sys.L1EL.EstimateBlockTime()
87+
for {
88+
l2UnsafeRef := sys.L2CL.SyncStatus().UnsafeL2
89+
90+
l1Info, _, err := sys.L1EL.EthClient().InfoAndTxsByHash(t.Ctx(), l2UnsafeRef.L1Origin.Hash)
91+
if errors.Is(err, ethereum.NotFound) { // Possible reorg, try again.
92+
continue
93+
}
94+
t.Require().NoError(err)
95+
96+
// Calculate expected blob base fee with old Osaka parameters.
97+
osakaBlobBaseFee := eip4844.CalcBlobFee(l1ChainConfig, &types.Header{
98+
Time: *l1ChainConfig.OsakaTime,
99+
ExcessBlobGas: l1Info.ExcessBlobGas(),
100+
})
101+
102+
// Calculate expected blob base fee with new BPO1 parameters.
103+
bpo1BlobBaseFee := eip4844.CalcBlobFee(l1ChainConfig, &types.Header{
104+
Time: l1Info.Time(),
105+
ExcessBlobGas: l1Info.ExcessBlobGas(),
106+
})
107+
108+
if bpo1BlobBaseFee.Cmp(osakaBlobBaseFee) != 0 {
109+
return l2UnsafeRef.Hash, bpo1BlobBaseFee
110+
}
111+
112+
select {
113+
case <-t.Ctx().Done():
114+
t.Require().Fail("context canceled before finding a block with a divergent base fee")
115+
case <-time.After(l1BlockTime):
116+
}
117+
}
118+
}
119+
120+
func spamBlobs(t devtest.T, sys *presets.Minimal) {
121+
l1BlockTime := sys.L1EL.EstimateBlockTime()
122+
l1ChainConfig := sys.L1Network.Escape().ChainConfig()
123+
124+
eoa := sys.FunderL1.NewFundedEOA(eth.OneEther.Mul(5))
125+
signer := txinclude.NewPkSigner(eoa.Key().Priv(), sys.L1Network.ChainID().ToBig())
126+
l1ETHClient := sys.L1EL.EthClient()
127+
syncEOA := loadtest.NewSyncEOA(txinclude.NewPersistent(signer, struct {
128+
*txinclude.Monitor
129+
*txinclude.Resubmitter
130+
}{
131+
txinclude.NewMonitor(l1ETHClient, l1BlockTime),
132+
txinclude.NewResubmitter(l1ETHClient, l1BlockTime),
133+
}), eoa.Plan())
134+
135+
var blob eth.Blob
136+
_, err := rand.Read(blob[:])
137+
t.Require().NoError(err)
138+
// get the field-elements into a valid range
139+
for i := range 4096 {
140+
blob[32*i] &= 0b0011_1111
141+
}
142+
143+
const maxBlobTxsPerAccountInMempool = 16 // Private policy param in geth.
144+
spammer := loadtest.SpammerFunc(func(t devtest.T) error {
145+
_, err := syncEOA.Include(t, txplan.WithBlobs([]*eth.Blob{&blob}, l1ChainConfig), txplan.WithTo(&common.Address{}))
146+
return err
147+
})
148+
txsPerSlot := min(l1ChainConfig.BlobScheduleConfig.BPO1.Max*3/4, maxBlobTxsPerAccountInMempool)
149+
schedule := loadtest.NewConstant(l1BlockTime, loadtest.WithBaseRPS(uint64(txsPerSlot)))
150+
151+
ctx, cancel := context.WithCancel(t.Ctx())
152+
var wg sync.WaitGroup
153+
t.Cleanup(func() {
154+
cancel()
155+
wg.Wait()
156+
})
157+
wg.Add(1)
158+
go func() {
159+
defer wg.Done()
160+
schedule.Run(t.WithCtx(ctx), spammer)
161+
}()
162+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package fusaka
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
"testing"
10+
11+
"github.com/ethereum-optimism/optimism/op-batcher/batcher"
12+
"github.com/ethereum-optimism/optimism/op-batcher/flags"
13+
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
14+
"github.com/ethereum-optimism/optimism/op-devstack/devtest"
15+
"github.com/ethereum-optimism/optimism/op-devstack/presets"
16+
"github.com/ethereum-optimism/optimism/op-devstack/stack"
17+
"github.com/ethereum-optimism/optimism/op-devstack/sysgo"
18+
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/intentbuilder"
19+
"github.com/ethereum/go-ethereum/params"
20+
)
21+
22+
// configureDevstackEnvVars sets the appropriate env vars to use a mise-installed geth binary for
23+
// the L1 EL. This is useful in Osaka acceptance tests since op-geth does not include full Osaka
24+
// support. This is meant to run before presets.DoMain in a TestMain function. It will log to
25+
// stdout. ResetDevstackEnvVars should be used to reset the environment variables when TestMain
26+
// exits.
27+
//
28+
// Note that this is a no-op if either [sysgo.DevstackL1ELKindVar] or [sysgo.GethExecPathEnvVar]
29+
// are set.
30+
//
31+
// The returned callback resets any modified environment variables.
32+
func configureDevstackEnvVars() func() {
33+
if _, ok := os.LookupEnv(sysgo.DevstackL1ELKindEnvVar); ok {
34+
return func() {}
35+
}
36+
if _, ok := os.LookupEnv(sysgo.GethExecPathEnvVar); ok {
37+
return func() {}
38+
}
39+
40+
cmd := exec.Command("mise", "which", "geth")
41+
buf := bytes.NewBuffer([]byte{})
42+
cmd.Stdout = buf
43+
if err := cmd.Run(); err != nil {
44+
fmt.Printf("Failed to find mise-installed geth: %v\n", err)
45+
return func() {}
46+
}
47+
execPath := strings.TrimSpace(buf.String())
48+
fmt.Println("Found mise-installed geth:", execPath)
49+
_ = os.Setenv(sysgo.GethExecPathEnvVar, execPath)
50+
_ = os.Setenv(sysgo.DevstackL1ELKindEnvVar, "geth")
51+
return func() {
52+
_ = os.Unsetenv(sysgo.GethExecPathEnvVar)
53+
_ = os.Unsetenv(sysgo.DevstackL1ELKindEnvVar)
54+
}
55+
}
56+
57+
func TestMain(m *testing.M) {
58+
resetEnvVars := configureDevstackEnvVars()
59+
defer resetEnvVars()
60+
61+
presets.DoMain(m, stack.MakeCommon(stack.Combine[*sysgo.Orchestrator](
62+
sysgo.DefaultMinimalSystem(&sysgo.DefaultMinimalSystemIDs{}),
63+
sysgo.WithDeployerOptions(func(_ devtest.P, _ devkeys.Keys, builder intentbuilder.Builder) {
64+
_, l1Config := builder.WithL1(sysgo.DefaultL1ID)
65+
l1Config.WithOsakaOffset(0)
66+
// Make the BPO fork happen after Osaka so we can easily use geth's eip4844.CalcBlobFee
67+
// to calculate the blob base fee using the Osaka parameters.
68+
l1Config.WithBPO1Offset(1)
69+
l1Config.WithL1BlobSchedule(&params.BlobScheduleConfig{
70+
Cancun: params.DefaultCancunBlobConfig,
71+
Osaka: params.DefaultOsakaBlobConfig,
72+
Prague: params.DefaultPragueBlobConfig,
73+
BPO1: params.DefaultBPO1BlobConfig,
74+
})
75+
}),
76+
sysgo.WithBatcherOption(func(_ stack.L2BatcherID, cfg *batcher.CLIConfig) {
77+
cfg.DataAvailabilityType = flags.BlobsType
78+
cfg.TxMgrConfig.CellProofTime = 0 // Force cell proofs to be used
79+
}),
80+
)))
81+
}

0 commit comments

Comments
 (0)