From d97e8aff735e2781efc5933d6c918cdde6f77568 Mon Sep 17 00:00:00 2001 From: Sam Stokes <35908605+bitwiseguy@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:30:14 -0400 Subject: [PATCH 1/9] op-deployer: add configurable chainIntent.GasLimit field (#17271) --- .../pkg/deployer/integration_test/apply_test.go | 13 ++++++++++--- op-deployer/pkg/deployer/pipeline/opchain.go | 2 +- op-deployer/pkg/deployer/state/chain_intent.go | 7 +++++++ op-deployer/pkg/deployer/state/deploy_config.go | 2 +- .../pkg/deployer/state/deploy_config_test.go | 2 ++ op-deployer/pkg/deployer/state/intent.go | 7 ++++++- op-e2e/config/init.go | 2 ++ op-e2e/e2eutils/intentbuilder/builder.go | 1 + op-e2e/e2eutils/intentbuilder/builder_test.go | 2 ++ 9 files changed, 32 insertions(+), 6 deletions(-) diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 13fe288b331..b3c31d5c7d9 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" "github.com/ethereum/go-ethereum/params" "github.com/ethereum-optimism/optimism/op-service/testutils" @@ -45,6 +46,8 @@ import ( "github.com/stretchr/testify/require" ) +const testCustomGasLimit = uint64(90_123_456) + type deployerKey struct{} func (d *deployerKey) HDPath() string { @@ -243,7 +246,6 @@ func TestGlobalOverrides(t *testing.T) { defer cancel() opts, intent, st := setupGenesisChain(t, devnet.DefaultChainID) - expectedGasLimit := strings.ToLower("0x1C9C380") expectedBaseFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000001") expectedL1FeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000002") expectedSequencerFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000003") @@ -255,7 +257,6 @@ func TestGlobalOverrides(t *testing.T) { expectedUseFaultProofs := false intent.GlobalDeployOverrides = map[string]interface{}{ "l2BlockTime": float64(3), - "l2GenesisBlockGasLimit": expectedGasLimit, "baseFeeVaultRecipient": expectedBaseFeeVaultRecipient, "l1FeeVaultRecipient": expectedL1FeeVaultRecipient, "sequencerFeeVaultRecipient": expectedSequencerFeeVaultRecipient, @@ -272,7 +273,6 @@ func TestGlobalOverrides(t *testing.T) { cfg, err := state.CombineDeployConfig(intent, intent.Chains[0], st, st.Chains[0]) require.NoError(t, err) require.Equal(t, uint64(3), cfg.L2InitializationConfig.L2CoreDeployConfig.L2BlockTime, "L2 block time should be 3 seconds") - require.Equal(t, expectedGasLimit, strings.ToLower(cfg.L2InitializationConfig.L2GenesisBlockDeployConfig.L2GenesisBlockGasLimit.String()), "L2 Genesis Block Gas Limit should be 30_000_000") require.Equal(t, expectedBaseFeeVaultRecipient, cfg.L2InitializationConfig.L2VaultsDeployConfig.BaseFeeVaultRecipient, "Base Fee Vault Recipient should be the expected address") require.Equal(t, expectedL1FeeVaultRecipient, cfg.L2InitializationConfig.L2VaultsDeployConfig.L1FeeVaultRecipient, "L1 Fee Vault Recipient should be the expected address") require.Equal(t, expectedSequencerFeeVaultRecipient, cfg.L2InitializationConfig.L2VaultsDeployConfig.SequencerFeeVaultRecipient, "Sequencer Fee Vault Recipient should be the expected address") @@ -700,6 +700,7 @@ func newChainIntent(t *testing.T, dk *devkeys.MnemonicDevKeys, l1ChainID *big.In Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, + GasLimit: testCustomGasLimit, Roles: state.ChainRoles{ L1ProxyAdminOwner: addrFor(t, dk, devkeys.L2ProxyAdminOwnerRole.Key(l1ChainID)), L2ProxyAdminOwner: addrFor(t, dk, devkeys.L2ProxyAdminOwnerRole.Key(l1ChainID)), @@ -836,6 +837,12 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int require.False(t, ok, "governance token should not be deployed by default") } + genesis, rollup, err := inspect.GenesisAndRollup(st, chainState.ID) + require.NoError(t, err) + require.Equal(t, rollup.Genesis.SystemConfig.GasLimit, testCustomGasLimit, "rollup gasLimit") + require.Equal(t, genesis.GasLimit, testCustomGasLimit, "genesis gasLimit") + + require.Equal(t, chainIntent.GasLimit, testCustomGasLimit, "chainIntent gasLimit") require.Equal(t, int(chainIntent.Eip1559Denominator), 50, "EIP1559Denominator should be set") require.Equal(t, int(chainIntent.Eip1559Elasticity), 6, "EIP1559Elasticity should be set") } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index f44721dfd57..f038e464aaa 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -101,7 +101,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common L2ChainId: chainID.Big(), Opcm: st.ImplementationsDeployment.OpcmImpl, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization - GasLimit: standard.GasLimit, + GasLimit: thisIntent.GasLimit, DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, DisputeMaxGameDepth: proofParams.DisputeMaxGameDepth, diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index 3ad1a3dea3b..727070fc146 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -64,6 +64,7 @@ type ChainIntent struct { Eip1559DenominatorCanyon uint64 `json:"eip1559DenominatorCanyon" toml:"eip1559DenominatorCanyon"` Eip1559Denominator uint64 `json:"eip1559Denominator" toml:"eip1559Denominator"` Eip1559Elasticity uint64 `json:"eip1559Elasticity" toml:"eip1559Elasticity"` + GasLimit uint64 `json:"gasLimit" toml:"gasLimit"` Roles ChainRoles `json:"roles" toml:"roles"` DeployOverrides map[string]any `json:"deployOverrides" toml:"deployOverrides"` DangerousAltDAConfig genesis.AltDADeployConfig `json:"dangerousAltDAConfig,omitempty" toml:"dangerousAltDAConfig,omitempty"` @@ -87,6 +88,7 @@ type ChainRoles struct { } var ErrFeeVaultZeroAddress = fmt.Errorf("chain has a fee vault set to zero address") +var ErrGasLimitZeroValue = fmt.Errorf("chain has a gas limit set to zero value") var ErrNonStandardValue = fmt.Errorf("chain contains non-standard config value") var ErrEip1559ZeroValue = fmt.Errorf("eip1559 param is set to zero value") var ErrIncompatibleValue = fmt.Errorf("chain contains incompatible config value") @@ -105,6 +107,11 @@ func (c *ChainIntent) Check() error { c.Eip1559Elasticity == 0 { return fmt.Errorf("%w: chainId=%s", ErrEip1559ZeroValue, c.ID) } + + if c.GasLimit == 0 { + return fmt.Errorf("%w: chainId=%s", ErrGasLimitZeroValue, c.ID) + } + if c.BaseFeeVaultRecipient == emptyAddress || c.L1FeeVaultRecipient == emptyAddress || c.SequencerFeeVaultRecipient == emptyAddress { diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index b5a0acaa649..d497e0d08eb 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -39,7 +39,7 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, FundDevAccounts: intent.FundDevAccounts, }, L2GenesisBlockDeployConfig: genesis.L2GenesisBlockDeployConfig{ - L2GenesisBlockGasLimit: 60_000_000, + L2GenesisBlockGasLimit: hexutil.Uint64(chainIntent.GasLimit), L2GenesisBlockBaseFeePerGas: &l2GenesisBlockBaseFeePerGas, }, L2VaultsDeployConfig: genesis.L2VaultsDeployConfig{ diff --git a/op-deployer/pkg/deployer/state/deploy_config_test.go b/op-deployer/pkg/deployer/state/deploy_config_test.go index 53d33d5874f..a91a90a8cbc 100644 --- a/op-deployer/pkg/deployer/state/deploy_config_test.go +++ b/op-deployer/pkg/deployer/state/deploy_config_test.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" @@ -21,6 +22,7 @@ func TestCombineDeployConfig(t *testing.T) { chainIntent := ChainIntent{ Eip1559Denominator: 1, Eip1559Elasticity: 2, + GasLimit: standard.GasLimit, BaseFeeVaultRecipient: common.HexToAddress("0x123"), L1FeeVaultRecipient: common.HexToAddress("0x456"), SequencerFeeVaultRecipient: common.HexToAddress("0x789"), diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index 410f0bd4369..8329dbfa023 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -151,6 +151,9 @@ func (c *Intent) validateStandardValues() error { chain.Eip1559Elasticity != standard.Eip1559Elasticity { return fmt.Errorf("%w: chainId=%s", ErrNonStandardValue, chain.ID) } + if chain.GasLimit != standard.GasLimit { + return fmt.Errorf("%w: chainId=%s", ErrNonStandardValue, chain.ID) + } if len(chain.AdditionalDisputeGames) > 0 { return fmt.Errorf("%w: chainId=%s additionalDisputeGames must be nil", ErrNonStandardValue, chain.ID) } @@ -293,7 +296,8 @@ func NewIntentCustom(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, error) for _, l2ChainID := range l2ChainIds { intent.Chains = append(intent.Chains, &ChainIntent{ - ID: l2ChainID, + ID: l2ChainID, + GasLimit: standard.GasLimit, }) } return intent, nil @@ -332,6 +336,7 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, + GasLimit: standard.GasLimit, Roles: ChainRoles{ Challenger: challenger, L1ProxyAdminOwner: l1ProxyAdminOwner, diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index 07dd198ca77..63ee1e1f161 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/pipeline" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum/go-ethereum/common" @@ -386,6 +387,7 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, Eip1559Denominator: 250, Eip1559DenominatorCanyon: 250, Eip1559Elasticity: 6, + GasLimit: standard.GasLimit, Roles: state.ChainRoles{ // Use deployer as L1PAO to deploy additional dispute impls L1ProxyAdminOwner: deployer, diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 88482bbc11a..448cb72e1c4 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -194,6 +194,7 @@ func (b *intentBuilder) WithL2(l2ChainID eth.ChainID) (Builder, L2Configurator) Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, + GasLimit: standard.GasLimit, DeployOverrides: make(map[string]any), } b.intent.Chains = append(b.intent.Chains, chainIntent) diff --git a/op-e2e/e2eutils/intentbuilder/builder_test.go b/op-e2e/e2eutils/intentbuilder/builder_test.go index eeffb15a14d..ecf7e19d307 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -157,6 +158,7 @@ func TestBuilder(t *testing.T) { Eip1559DenominatorCanyon: 250, Eip1559Denominator: 50, Eip1559Elasticity: 10, + GasLimit: standard.GasLimit, OperatorFeeScalar: 100, OperatorFeeConstant: 200, DeployOverrides: map[string]any{ From a92e804548cca6dfbb941f4ef45ed69f7a81d142 Mon Sep 17 00:00:00 2001 From: serpixel <5087962+serpixel@users.noreply.github.com> Date: Wed, 3 Sep 2025 01:14:02 +0200 Subject: [PATCH 2/9] feat(op-acceptance-tests): Port TestSmokeTestFailure and TestInteropSystemNoop to Devstack (#17300) * feat(op-acceptance-tests): port interop smoke tests * feat(op-acceptance-tests): linter --- .../tests/fjord/check_scripts_test.go | 4 +- .../tests/interop/interop_smoke_test.go | 96 ----- .../tests/interop/mocks_test.go | 396 ------------------ .../tests/interop/smoke/interop_smoke_test.go | 89 ++++ 4 files changed, 91 insertions(+), 494 deletions(-) delete mode 100644 op-acceptance-tests/tests/interop/interop_smoke_test.go delete mode 100644 op-acceptance-tests/tests/interop/mocks_test.go create mode 100644 op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go diff --git a/op-acceptance-tests/tests/fjord/check_scripts_test.go b/op-acceptance-tests/tests/fjord/check_scripts_test.go index 772e262ae22..649f4a5e9de 100644 --- a/op-acceptance-tests/tests/fjord/check_scripts_test.go +++ b/op-acceptance-tests/tests/fjord/check_scripts_test.go @@ -165,7 +165,7 @@ func checkFastLZTransactions(t devtest.T, ctx context.Context, sys *presets.Mini fastLzSize := uint64(types.FlzCompressLen(txUnsigned) + 68) gethGPOFee, err := dsl.CalculateFjordL1Cost(ctx, l2Client, types.RollupCostData{FastLzSize: fastLzSize}, receipt.BlockHash) require.NoError(err) - require.Equal(gethGPOFee.Uint64(), gpoFee.Uint64()) + require.Equalf(gethGPOFee.Uint64(), gpoFee.Uint64(), "GPO L1 fee mismatch (expected=%d actual=%d)", gethGPOFee.Uint64(), gpoFee.Uint64()) expectedFee, err := dsl.CalculateFjordL1Cost(ctx, l2Client, signedTx.RollupCostData(), receipt.BlockHash) require.NoError(err) @@ -178,7 +178,7 @@ func checkFastLZTransactions(t devtest.T, ctx context.Context, sys *presets.Mini flzUpperBound := uint64(txLenGPO + txLenGPO/255 + 16) upperBoundCost, err := dsl.CalculateFjordL1Cost(ctx, l2Client, types.RollupCostData{FastLzSize: flzUpperBound}, receipt.BlockHash) require.NoError(err) - require.Equal(upperBoundCost.Uint64(), upperBound.Uint64()) + require.Equalf(upperBoundCost.Uint64(), upperBound.Uint64(), "GPO L1 upper bound mismatch (expected=%d actual=%d)", upperBoundCost.Uint64(), upperBound.Uint64()) _, err = contractio.Read(gasPriceOracle.BaseFeeScalar(), ctx) require.NoError(err) diff --git a/op-acceptance-tests/tests/interop/interop_smoke_test.go b/op-acceptance-tests/tests/interop/interop_smoke_test.go deleted file mode 100644 index 2fa1be41b36..00000000000 --- a/op-acceptance-tests/tests/interop/interop_smoke_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package interop - -import ( - "context" - "math/big" - "testing" - - "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" - "github.com/ethereum-optimism/optimism/devnet-sdk/system" - "github.com/ethereum-optimism/optimism/devnet-sdk/testing/systest" - "github.com/ethereum-optimism/optimism/devnet-sdk/testing/testlib/validators" - sdktypes "github.com/ethereum-optimism/optimism/devnet-sdk/types" - "github.com/ethereum-optimism/optimism/op-service/testlog" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" - "github.com/stretchr/testify/require" -) - -func smokeTestScenario(chainIdx uint64, walletGetter validators.WalletGetter) systest.SystemTestFunc { - return func(t systest.T, sys system.System) { - ctx := t.Context() - - logger := testlog.Logger(t, log.LevelInfo) - logger = logger.With("test", "TestMinimal", "devnet", sys.Identifier()) - - chain := sys.L2s()[chainIdx] - logger = logger.With("chain", chain.ID()) - logger.Info("starting test") - - funds := sdktypes.NewBalance(big.NewInt(1 * constants.ETH)) - user := walletGetter(ctx) - - wethAddr := constants.WETH - weth, err := chain.Nodes()[0].ContractsRegistry().WETH(wethAddr) - require.NoError(t, err) - initialBalance, err := weth.BalanceOf(user.Address()).Call(ctx) - require.NoError(t, err) - logger = logger.With("user", user.Address()) - logger.Info("initial balance retrieved", "balance", initialBalance) - - logger.Info("sending ETH to contract", "amount", funds) - require.NoError(t, user.SendETH(wethAddr, funds).Send(ctx).Wait()) - - balance, err := weth.BalanceOf(user.Address()).Call(ctx) - require.NoError(t, err) - logger.Info("final balance retrieved", "balance", balance) - - require.Equal(t, initialBalance.Add(funds), balance) - } -} - -func TestInteropSystemNoop(t *testing.T) { - systest.InteropSystemTest(t, func(t systest.T, sys system.InteropSystem) { - testlog.Logger(t, log.LevelInfo).Info("noop") - }) -} - -func TestSmokeTestFailure(t *testing.T) { - // Create mock failing system - mockAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") - mockWallet := &mockFailingWallet{ - addr: mockAddr, - bal: sdktypes.NewBalance(big.NewInt(0.1 * constants.ETH)), - } - mockL1Chain := newMockFailingL1Chain( - sdktypes.ChainID(big.NewInt(1234)), - system.WalletMap{ - "user1": mockWallet, - }, - []system.Node{&mockFailingNode{ - reg: &mockContractsRegistry{}, - }}, - ) - mockL2Chain := newMockFailingL2Chain( - sdktypes.ChainID(big.NewInt(1234)), - system.WalletMap{"user1": mockWallet}, - []system.Node{&mockFailingNode{ - reg: &mockContractsRegistry{}, - }}, - ) - mockSys := &mockFailingSystem{l1Chain: mockL1Chain, l2Chain: mockL2Chain} - - // Run the smoke test logic and capture failures - getter := func(ctx context.Context) system.Wallet { - return mockWallet - } - rt := NewRecordingT(context.TODO()) - rt.TestScenario( - smokeTestScenario(0, getter), - mockSys, - ) - - // Verify that the test failed due to SendETH error - require.True(t, rt.Failed(), "test should have failed") - require.Contains(t, rt.Logs(), "transaction failure", "unexpected failure message") -} diff --git a/op-acceptance-tests/tests/interop/mocks_test.go b/op-acceptance-tests/tests/interop/mocks_test.go deleted file mode 100644 index fe41f0d545a..00000000000 --- a/op-acceptance-tests/tests/interop/mocks_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package interop - -import ( - "bytes" - "context" - "fmt" - "math/big" - "os" - "runtime" - "time" - - "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" - "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/registry/empty" - "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" - "github.com/ethereum-optimism/optimism/devnet-sdk/system" - "github.com/ethereum-optimism/optimism/devnet-sdk/testing/systest" - "github.com/ethereum-optimism/optimism/devnet-sdk/types" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-service/sources" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/params" -) - -var ( - // Ensure mockFailingTx implements WriteInvocation - _ types.WriteInvocation[any] = (*mockFailingTx)(nil) - - // Ensure mockFailingTx implements Wallet - _ system.Wallet = (*mockFailingWallet)(nil) - - // Ensure mockFailingChain implements Chain - _ system.Chain = (*mockFailingChain)(nil) - _ system.L2Chain = (*mockFailingL2Chain)(nil) -) - -// mockFailingTx implements types.WriteInvocation[any] that always fails -type mockFailingTx struct{} - -func (m *mockFailingTx) Call(ctx context.Context) (any, error) { - return nil, fmt.Errorf("simulated transaction failure") -} - -func (m *mockFailingTx) Send(ctx context.Context) types.InvocationResult { - return m -} - -func (m *mockFailingTx) Error() error { - return fmt.Errorf("transaction failure") -} - -func (m *mockFailingTx) Wait() error { - return fmt.Errorf("transaction failure") -} - -func (m *mockFailingTx) Info() any { - return nil -} - -// mockFailingWallet implements types.Wallet that fails on SendETH -type mockFailingWallet struct { - addr types.Address - key types.Key - bal types.Balance -} - -func (m *mockFailingWallet) Client() *ethclient.Client { - return nil -} - -func (m *mockFailingWallet) Address() types.Address { - return m.addr -} - -func (m *mockFailingWallet) PrivateKey() types.Key { - return m.key -} - -func (m *mockFailingWallet) Balance() types.Balance { - return m.bal -} - -func (m *mockFailingWallet) SendETH(to types.Address, amount types.Balance) types.WriteInvocation[any] { - return &mockFailingTx{} -} - -func (m *mockFailingWallet) InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] { - return &mockFailingTx{} -} - -func (m *mockFailingWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] { - return &mockFailingTx{} -} - -func (m *mockFailingWallet) Nonce() uint64 { - return 0 -} - -func (m *mockFailingWallet) Sign(tx system.Transaction) (system.Transaction, error) { - return tx, nil -} - -func (m *mockFailingWallet) Send(ctx context.Context, tx system.Transaction) error { - return nil -} - -func (m *mockFailingWallet) Transactor() *bind.TransactOpts { - return nil -} - -// mockContractsRegistry extends empty.EmptyRegistry to provide mock contract instances -type mockContractsRegistry struct { - empty.EmptyRegistry -} - -// mockWETH implements a minimal WETH interface for testing -type mockWETH struct { - addr types.Address -} - -func (m *mockWETH) BalanceOf(account types.Address) types.ReadInvocation[types.Balance] { - return &mockReadInvocation{balance: types.NewBalance(big.NewInt(0))} -} - -// mockReadInvocation implements a read invocation that returns a fixed balance -type mockReadInvocation struct { - balance types.Balance -} - -func (m *mockReadInvocation) Call(ctx context.Context) (types.Balance, error) { - return m.balance, nil -} - -func (r *mockContractsRegistry) WETH(address types.Address) (interfaces.WETH, error) { - return &mockWETH{addr: address}, nil -} - -// mockFailingChain implements system.Chain with a failing SendETH -type mockFailingChain struct { - id types.ChainID - wallets system.WalletMap - nodes []system.Node -} - -var _ system.Chain = (*mockFailingChain)(nil) - -func newMockFailingL1Chain(id types.ChainID, wallets system.WalletMap, nodes []system.Node) *mockFailingChain { - return &mockFailingChain{ - id: id, - wallets: wallets, - nodes: nodes, - } -} - -func (m *mockFailingChain) Nodes() []system.Node { return m.nodes } -func (m *mockFailingChain) ID() types.ChainID { return m.id } -func (m *mockFailingChain) Wallets() system.WalletMap { - return m.wallets -} -func (m *mockFailingChain) Config() (*params.ChainConfig, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingChain) Addresses() system.AddressMap { - return map[string]common.Address{} -} - -var _ system.Node = (*mockFailingNode)(nil) - -type mockFailingNode struct { - reg interfaces.ContractsRegistry -} - -func (m *mockFailingNode) Client() (*sources.EthClient, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingNode) GasPrice(ctx context.Context) (*big.Int, error) { - return big.NewInt(1), nil -} -func (m *mockFailingNode) GasLimit(ctx context.Context, tx system.TransactionData) (uint64, error) { - return 1000000, nil -} -func (m *mockFailingNode) PendingNonceAt(ctx context.Context, address common.Address) (uint64, error) { - return 0, nil -} -func (m *mockFailingNode) SupportsEIP(ctx context.Context, eip uint64) bool { - return true -} -func (m *mockFailingNode) RPCURL() string { return "mock://failing" } -func (m *mockFailingNode) ContractsRegistry() interfaces.ContractsRegistry { return m.reg } -func (m *mockFailingNode) GethClient() (*ethclient.Client, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingNode) BlockByNumber(ctx context.Context, number *big.Int) (eth.BlockInfo, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingNode) Name() string { - return "mock" -} - -// mockFailingChain implements system.Chain with a failing SendETH -type mockFailingL2Chain struct { - mockFailingChain -} - -func newMockFailingL2Chain(id types.ChainID, wallets system.WalletMap, nodes []system.Node) *mockFailingL2Chain { - return &mockFailingL2Chain{ - mockFailingChain: mockFailingChain{ - id: id, - wallets: wallets, - nodes: nodes, - }, - } -} - -func (m *mockFailingL2Chain) L1Addresses() system.AddressMap { - return map[string]common.Address{} -} -func (m *mockFailingL2Chain) L1Wallets() system.WalletMap { - return map[string]system.Wallet{} -} - -// mockFailingSystem implements system.System -type mockFailingSystem struct { - l1Chain system.Chain - l2Chain system.L2Chain -} - -func (m *mockFailingSystem) Identifier() string { - return "mock-failing-system" -} - -func (m *mockFailingSystem) L1() system.Chain { - return m.l1Chain -} - -func (m *mockFailingSystem) L2s() []system.L2Chain { - return []system.L2Chain{m.l2Chain} -} - -func (m *mockFailingSystem) Close() error { - return nil -} - -// recordingT implements systest.T and records failures -type RecordingT struct { - failed bool - skipped bool - logs *bytes.Buffer - cleanup []func() - ctx context.Context -} - -func NewRecordingT(ctx context.Context) *RecordingT { - return &RecordingT{ - logs: bytes.NewBuffer(nil), - ctx: ctx, - } -} - -var _ systest.T = (*RecordingT)(nil) - -func (r *RecordingT) Context() context.Context { - return r.ctx -} - -func (r *RecordingT) WithContext(ctx context.Context) systest.T { - return &RecordingT{ - failed: r.failed, - skipped: r.skipped, - logs: r.logs, - cleanup: r.cleanup, - ctx: ctx, - } -} - -func (r *RecordingT) Deadline() (deadline time.Time, ok bool) { - // TODO - return time.Time{}, false -} - -func (r *RecordingT) Parallel() { - // TODO -} - -func (r *RecordingT) Run(name string, f func(systest.T)) { - // TODO -} - -func (r *RecordingT) Cleanup(f func()) { - r.cleanup = append(r.cleanup, f) -} - -func (r *RecordingT) Error(args ...interface{}) { - r.Log(args...) - r.Fail() -} - -func (r *RecordingT) Errorf(format string, args ...interface{}) { - r.Logf(format, args...) - r.Fail() -} - -func (r *RecordingT) Fatal(args ...interface{}) { - r.Log(args...) - r.FailNow() -} - -func (r *RecordingT) Fatalf(format string, args ...interface{}) { - r.Logf(format, args...) - r.FailNow() -} - -func (r *RecordingT) FailNow() { - r.Fail() - runtime.Goexit() -} - -func (r *RecordingT) Fail() { - r.failed = true -} - -func (r *RecordingT) Failed() bool { - return r.failed -} - -func (r *RecordingT) Helper() { - // TODO -} - -func (r *RecordingT) Log(args ...interface{}) { - fmt.Fprintln(r.logs, args...) -} - -func (r *RecordingT) Logf(format string, args ...interface{}) { - fmt.Fprintf(r.logs, format, args...) - fmt.Fprintln(r.logs) -} - -func (r *RecordingT) Name() string { - return "RecordingT" // TODO -} - -func (r *RecordingT) Setenv(key, value string) { - // Store original value - origValue, exists := os.LookupEnv(key) - - // Set new value - os.Setenv(key, value) - - // Register cleanup to restore original value - r.Cleanup(func() { - if exists { - os.Setenv(key, origValue) - } else { - os.Unsetenv(key) - } - }) - -} - -func (r *RecordingT) Skip(args ...interface{}) { - r.Log(args...) - r.SkipNow() -} - -func (r *RecordingT) SkipNow() { - r.skipped = true -} - -func (r *RecordingT) Skipf(format string, args ...interface{}) { - r.Logf(format, args...) - r.skipped = true -} - -func (r *RecordingT) Skipped() bool { - return r.skipped -} - -func (r *RecordingT) TempDir() string { - return "" // TODO -} - -func (r *RecordingT) Logs() string { - return r.logs.String() -} - -func (r *RecordingT) TestScenario(scenario systest.SystemTestFunc, sys system.System, values ...interface{}) { - // run in a separate goroutine so we can handle runtime.Goexit() - done := make(chan struct{}) - go func() { - defer close(done) - scenario(r, sys) - }() - <-done -} diff --git a/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go b/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go new file mode 100644 index 00000000000..c056b52b780 --- /dev/null +++ b/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go @@ -0,0 +1,89 @@ +package smoke + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + txib "github.com/ethereum-optimism/optimism/op-service/txintent/bindings" + "github.com/ethereum-optimism/optimism/op-service/txintent/contractio" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestInteropSystemNoop(gt *testing.T) { + t := devtest.SerialT(gt) + _ = presets.NewMinimal(t) + t.Log("noop") +} + +func TestSmokeTest(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + require := t.Require() + ctx := t.Ctx() + + user := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + + l2Client := sys.L2EL.Escape().EthClient() + weth := txib.NewBindings[txib.WETH]( + txib.WithClient(l2Client), + txib.WithTo(predeploys.WETHAddr), + txib.WithTest(t), + ) + + initialBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Initial WETH balance: %s", initialBalance) + + depositAmount := eth.OneHundredthEther + + tx := user.Transfer(predeploys.WETHAddr, depositAmount) + receipt, err := tx.Included.Eval(ctx) + require.NoError(err) + require.Equal(types.ReceiptStatusSuccessful, receipt.Status) + t.Logf("Deposited %s ETH to WETH contract", depositAmount) + + finalBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Final WETH balance: %s", finalBalance) + + expectedBalance := initialBalance.Add(depositAmount) + require.Equal(expectedBalance, finalBalance, "WETH balance should have increased by deposited amount") +} + +func TestSmokeTestFailure(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + require := t.Require() + ctx := t.Ctx() + + user := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + + l2Client := sys.L2EL.Escape().EthClient() + weth := txib.NewBindings[txib.WETH]( + txib.WithClient(l2Client), + txib.WithTo(predeploys.WETHAddr), + txib.WithTest(t), + ) + + initialBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Initial WETH balance: %s", initialBalance) + + depositAmount := eth.OneEther + + userBalance := user.GetBalance() + t.Logf("User balance: %s", userBalance) + + require.True(userBalance.Lt(depositAmount), "user should have insufficient funds for this transaction") + + t.Logf("user has insufficient funds: balance=%s, required=%s", userBalance, depositAmount) + + finalBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Final WETH balance: %s", finalBalance) + + require.Equal(initialBalance, finalBalance, "WETH balance should not have changed") +} From 891015edb7329282160016bfb835665e4da18302 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Wed, 3 Sep 2025 12:37:18 +1000 Subject: [PATCH 3/9] ci: Run the publish-cannon-prestates job on circleci boxes instead of latitude. (#17296) It doesn't need lots of resources and we're seeing network flakiness so try uploading from a different network. --- .circleci/config.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b6554a0ac97..a209d32849a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1390,8 +1390,9 @@ jobs: - "op-program/bin/meta*" publish-cannon-prestates: - machine: true - resource_class: ethereum-optimism/latitude-1 + resource_class: medium + docker: + - image: <> steps: - utils/checkout-with-mise - attach_workspace: From fc6ddc8cba50e080084916f572118cfdf5c174a3 Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Wed, 3 Sep 2025 16:28:58 +0200 Subject: [PATCH 4/9] op-devstack: add sync-tester with external EL (#17251) * op-devstack: add sync-tester with ext cl config * op-devstack: try setting up remote L2 chain * op-devstack: use remote EL and CL for L1 nodes * op-devstack: fix L2CL from sequencer to verifier * op-devstack: remove redundant configs * op-acceptance-tests: use RPC for CI * op-acceptance-tests: skip tests until CI allows tests using external endpoints * circleci: attempt CI endpoints * op-devstack: address comments from PR * op-devstack: address comments from PR --- .circleci/config.yml | 1 + .../tests/sync_tester_ext_el/init_test.go | 39 ++++++++ .../sync_tester_ext_el_test.go | 52 ++++++++++ op-devstack/presets/minimal_external_el.go | 60 ++++++++++++ op-devstack/sysgo/faucet.go | 4 + op-devstack/sysgo/l1_nodes.go | 21 +++++ op-devstack/sysgo/l2_cl_opnode.go | 2 +- .../sysgo/l2_network_superchain_registry.go | 86 +++++++++++++++++ op-devstack/sysgo/sync_tester.go | 41 ++++++++ op-devstack/sysgo/system_synctester_ext.go | 94 +++++++++++++++++++ .../synctester/backend/sync_tester.go | 2 +- 11 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 op-acceptance-tests/tests/sync_tester_ext_el/init_test.go create mode 100644 op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go create mode 100644 op-devstack/presets/minimal_external_el.go create mode 100644 op-devstack/sysgo/l2_network_superchain_registry.go create mode 100644 op-devstack/sysgo/system_synctester_ext.go diff --git a/.circleci/config.yml b/.circleci/config.yml index a209d32849a..eb7bc83998c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1073,6 +1073,7 @@ jobs: default: "go-tests-short-ci" machine: true resource_class: <> + circleci_ip_ranges: true steps: - checkout-from-workspace - run: diff --git a/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go b/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go new file mode 100644 index 00000000000..c28d966bab8 --- /dev/null +++ b/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go @@ -0,0 +1,39 @@ +package sync_tester_ext_el + +import ( + "os" + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +var ( + InitialL2Block = uint64(32012748) +) + +func TestMain(m *testing.M) { + L2NetworkName := "op-sepolia" + L1ChainID := eth.ChainIDFromUInt64(11155111) + + L2ELEndpoint := "https://ci-sepolia-l2.optimism.io" + L1CLBeaconEndpoint := "https://ci-sepolia-beacon.optimism.io" + L1ELEndpoint := "https://ci-sepolia-l1.optimism.io" + + // Endpoints when running with Tailscale networking + if os.Getenv("TAILSCALE_NETWORKING") == "true" { + L2ELEndpoint = "https://proxyd-l2-sepolia.primary.client.dev.oplabs.cloud" + L1CLBeaconEndpoint = "https://beacon-api-proxy-sepolia.primary.client.dev.oplabs.cloud" + L1ELEndpoint = "https://proxyd-l1-sepolia.primary.client.dev.oplabs.cloud" + } + + presets.DoMain(m, presets.WithMinimalExternalELWithSuperchainRegistry(L1CLBeaconEndpoint, L1ELEndpoint, L2ELEndpoint, L1ChainID, L2NetworkName, eth.FCUState{ + Latest: InitialL2Block, + Safe: InitialL2Block, + Finalized: InitialL2Block, + }), + presets.WithCompatibleTypes(compat.SysGo), + ) + +} diff --git a/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go b/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go new file mode 100644 index 00000000000..0d797e529bb --- /dev/null +++ b/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go @@ -0,0 +1,52 @@ +package sync_tester_ext_el + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +func TestSyncTesterExtEL(gt *testing.T) { + t := devtest.SerialT(gt) + + sys := presets.NewMinimalExternalELWithExternalL1(t) + require := t.Require() + + // Test that we can get chain IDs from L2CL node + l2CLChainID := sys.L2CL.ID().ChainID() + require.Equal(eth.ChainIDFromUInt64(11155420), l2CLChainID, "L2CL should be on chain 11155420") + + // Test that the network started successfully + require.NotNil(sys.L1EL, "L1 EL node should be available") + require.NotNil(sys.L2CL, "L2 CL node should be available") + require.NotNil(sys.SyncTester, "SyncTester should be available") + + // Test that we can get sync status from L2CL node + l2CLSyncStatus := sys.L2CL.SyncStatus() + require.NotNil(l2CLSyncStatus, "L2CL should have sync status") + + blocksToSync := uint64(20) + targetBlock := InitialL2Block + blocksToSync + sys.L2CL.Reached(types.LocalUnsafe, targetBlock, 500) + + l2CLSyncStatus = sys.L2CL.SyncStatus() + require.NotNil(l2CLSyncStatus, "L2CL should have sync status") + + unsafeL2Ref := l2CLSyncStatus.UnsafeL2 + blk := sys.L2EL.BlockRefByNumber(unsafeL2Ref.Number) + require.Equal(unsafeL2Ref.Hash, blk.Hash, "L2EL should be on the same block as L2CL") + + stSessions := sys.SyncTester.ListSessions() + require.Equal(len(stSessions), 1, "expect exactly one session") + + stSession := sys.SyncTester.GetSession(stSessions[0]) + require.GreaterOrEqual(stSession.CurrentState.Latest, stSession.InitialState.Latest+blocksToSync, "SyncTester session Latest should be on the same block as L2CL") + require.GreaterOrEqual(stSession.CurrentState.Safe, stSession.InitialState.Safe+blocksToSync, "SyncTester session Safe should be on the same block as L2CL") + + t.Logger().Info("SyncTester ExtEL test completed successfully", + "l2cl_chain_id", l2CLChainID, + "l2cl_sync_status", l2CLSyncStatus) +} diff --git a/op-devstack/presets/minimal_external_el.go b/op-devstack/presets/minimal_external_el.go new file mode 100644 index 00000000000..a14bc897abc --- /dev/null +++ b/op-devstack/presets/minimal_external_el.go @@ -0,0 +1,60 @@ +package presets + +import ( + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type MinimalExternalEL struct { + Log log.Logger + T devtest.T + ControlPlane stack.ControlPlane + + L1Network *dsl.L1Network + L1EL *dsl.L1ELNode + + L2Chain *dsl.L2Network + L2CL *dsl.L2CLNode + L2EL *dsl.L2ELNode + + SyncTester *dsl.SyncTester +} + +func (m *MinimalExternalEL) L2Networks() []*dsl.L2Network { + return []*dsl.L2Network{ + m.L2Chain, + } +} + +func WithMinimalExternalELWithSuperchainRegistry(l1CLBeaconRPC, l1ELRPC, l2ELRPC string, l1ChainID eth.ChainID, networkName string, fcu eth.FCUState) stack.CommonOption { + return stack.MakeCommon(sysgo.DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry(&sysgo.DefaultMinimalExternalELSystemIDs{}, l1CLBeaconRPC, l1ELRPC, l2ELRPC, l1ChainID, networkName, fcu)) +} + +func NewMinimalExternalELWithExternalL1(t devtest.T) *MinimalExternalEL { + system := shim.NewSystem(t) + orch := Orchestrator() + orch.Hydrate(system) + + l2 := system.L2Network(match.Assume(t, match.L2ChainA)) + verifierCL := l2.L2CLNode(match.FirstL2CL) + syncTester := l2.SyncTester(match.Assume(t, match.FirstSyncTester)) + + return &MinimalExternalEL{ + Log: t.Logger(), + T: t, + ControlPlane: orch.ControlPlane(), + L1Network: dsl.NewL1Network(system.L1Network(match.FirstL1Network)), + L1EL: dsl.NewL1ELNode(system.L1Network(match.FirstL1Network).L1ELNode(match.FirstL1EL)), + L2Chain: dsl.NewL2Network(l2, orch.ControlPlane()), + L2CL: dsl.NewL2CLNode(verifierCL, orch.ControlPlane()), + L2EL: dsl.NewL2ELNode(l2.L2ELNode(match.FirstL2EL), orch.ControlPlane()), + SyncTester: dsl.NewSyncTester(syncTester), + } +} diff --git a/op-devstack/sysgo/faucet.go b/op-devstack/sysgo/faucet.go index 0d8f80717aa..f746ade136b 100644 --- a/op-devstack/sysgo/faucet.go +++ b/op-devstack/sysgo/faucet.go @@ -23,6 +23,10 @@ type FaucetService struct { } func (n *FaucetService) hydrate(system stack.ExtensibleSystem) { + if n == nil || n.service == nil { + return + } + require := system.T().Require() for faucetID, chainID := range n.service.Faucets() { diff --git a/op-devstack/sysgo/l1_nodes.go b/op-devstack/sysgo/l1_nodes.go index 272b15ccef0..750182edbc1 100644 --- a/op-devstack/sysgo/l1_nodes.go +++ b/op-devstack/sysgo/l1_nodes.go @@ -113,3 +113,24 @@ func WithL1Nodes(l1ELID stack.L1ELNodeID, l1CLID stack.L1CLNodeID) stack.Option[ require.True(orch.l1CLs.SetIfMissing(l1CLID, l1CLNode), "must not already exist") }) } + +// WithExtL1Nodes initializes L1 EL and CL nodes that connect to external RPC endpoints +func WithExtL1Nodes(l1ELID stack.L1ELNodeID, l1CLID stack.L1CLNodeID, elRPCEndpoint string, clRPCEndpoint string) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + require := orch.P().Require() + + // Create L1 EL node with external RPC + l1ELNode := &L1ELNode{ + id: l1ELID, + userRPC: elRPCEndpoint, + } + require.True(orch.l1ELs.SetIfMissing(l1ELID, l1ELNode), "must not already exist") + + // Create L1 CL node with external RPC + l1CLNode := &L1CLNode{ + id: l1CLID, + beaconHTTPAddr: clRPCEndpoint, + } + require.True(orch.l1CLs.SetIfMissing(l1CLID, l1CLNode), "must not already exist") + }) +} diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index d0377b63aa8..d290496a57c 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -256,7 +256,7 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L L2EngineJWTSecret: jwtSecret, }, Beacon: &config.L1BeaconEndpointConfig{ - BeaconAddr: l1CL.beacon.BeaconAddr(), + BeaconAddr: l1CL.beaconHTTPAddr, }, Driver: driver.Config{ SequencerEnabled: cfg.IsSequencer, diff --git a/op-devstack/sysgo/l2_network_superchain_registry.go b/op-devstack/sysgo/l2_network_superchain_registry.go new file mode 100644 index 00000000000..f395d2e813d --- /dev/null +++ b/op-devstack/sysgo/l2_network_superchain_registry.go @@ -0,0 +1,86 @@ +package sysgo + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core" + + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/superutil" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset" +) + +// WithL2NetworkFromSuperchainRegistry creates an L2 network using the rollup config from the superchain registry +func WithL2NetworkFromSuperchainRegistry(l2NetworkID stack.L2NetworkID, networkName string) stack.Option[*Orchestrator] { + return stack.BeforeDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2NetworkID)) + require := p.Require() + + // Load the rollup config from the superchain registry + rollupCfg, err := chaincfg.GetRollupConfig(networkName) + require.NoError(err, "failed to load rollup config for network %s", networkName) + + // Get the chain config from the superchain registry + chainCfg := chaincfg.ChainByName(networkName) + require.NotNil(chainCfg, "chain config not found for network %s", networkName) + + // Load the chain config using superutil + paramsChainConfig, err := superutil.LoadOPStackChainConfigFromChainID(chainCfg.ChainID) + require.NoError(err, "failed to load chain config for network %s", networkName) + + // Create a genesis config from the chain config + genesis := &core.Genesis{ + Config: paramsChainConfig, + } + + // Create the L2 network + l2Net := &L2Network{ + id: l2NetworkID, + l1ChainID: eth.ChainIDFromBig(rollupCfg.L1ChainID), + genesis: genesis, + rollupCfg: rollupCfg, + keys: orch.keys, + } + + require.True(orch.l2Nets.SetIfMissing(l2NetworkID.ChainID(), l2Net), + fmt.Sprintf("must not already exist: %s", l2NetworkID)) + }) +} + +// WithL2NetworkFromSuperchainRegistryWithDependencySet creates an L2 network using the rollup config from the superchain registry +// and also sets up the dependency set for interop support +func WithL2NetworkFromSuperchainRegistryWithDependencySet(l2NetworkID stack.L2NetworkID, networkName string) stack.Option[*Orchestrator] { + return stack.Combine( + WithL2NetworkFromSuperchainRegistry(l2NetworkID, networkName), + stack.BeforeDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2NetworkID)) + require := p.Require() + + // Load the dependency set from the superchain registry + chainCfg := chaincfg.ChainByName(networkName) + require.NotNil(chainCfg, "chain config not found for network %s", networkName) + + _, err := depset.FromRegistry(eth.ChainIDFromUInt64(chainCfg.ChainID)) + if err != nil { + // If dependency set is not available, that's okay - it's optional + p.Logger().Info("No dependency set available for network", "network", networkName, "err", err) + return + } + + // Create a cluster to hold the dependency set + clusterID := stack.ClusterID(networkName) + + // Create a minimal full config set with just the dependency set + // This is a simplified approach - in a real implementation you might want + // to create a proper FullConfigSetMerged + cluster := &Cluster{ + id: clusterID, + cfgset: depset.FullConfigSetMerged{}, // Empty for now + } + + orch.clusters.Set(clusterID, cluster) + }), + ) +} diff --git a/op-devstack/sysgo/sync_tester.go b/op-devstack/sysgo/sync_tester.go index 2e15316dc6c..ace022fd639 100644 --- a/op-devstack/sysgo/sync_tester.go +++ b/op-devstack/sysgo/sync_tester.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/endpoint" + "github.com/ethereum-optimism/optimism/op-service/eth" oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" "github.com/ethereum-optimism/optimism/op-sync-tester/config" @@ -88,3 +89,43 @@ func WithSyncTester(syncTesterID stack.SyncTesterID, l2ELs []stack.L2ELNodeID) s orch.syncTester = &SyncTesterService{id: syncTesterID, service: srv} }) } + +func WithSyncTesterWithExternalEndpoint(syncTesterID stack.SyncTesterID, endpointRPC string, chainID eth.ChainID) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), syncTesterID)) + + require := p.Require() + + require.Nil(orch.syncTester, "can only support a single sync-tester-service in sysgo") + + syncTesters := make(map[sttypes.SyncTesterID]*stconf.SyncTesterEntry) + + // Create a sync tester entry with the external endpoint + id := sttypes.SyncTesterID(fmt.Sprintf("dev-sync-tester-%s", chainID)) + syncTesters[id] = &stconf.SyncTesterEntry{ + ELRPC: endpoint.MustRPC{Value: endpoint.URL(endpointRPC)}, + ChainID: chainID, + } + + cfg := &config.Config{ + RPC: oprpc.CLIConfig{ + ListenAddr: "127.0.0.1", + }, + SyncTesters: &stconf.Config{ + SyncTesters: syncTesters, + }, + } + logger := p.Logger() + srv, err := synctester.FromConfig(p.Ctx(), cfg, logger) + require.NoError(err, "must setup sync tester service") + require.NoError(srv.Start(p.Ctx())) + p.Cleanup(func() { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // force-quit + logger.Info("Closing sync tester") + _ = srv.Stop(ctx) + logger.Info("Closed sync tester") + }) + orch.syncTester = &SyncTesterService{id: syncTesterID, service: srv} + }) +} diff --git a/op-devstack/sysgo/system_synctester_ext.go b/op-devstack/sysgo/system_synctester_ext.go new file mode 100644 index 00000000000..4d6ef8e211f --- /dev/null +++ b/op-devstack/sysgo/system_synctester_ext.go @@ -0,0 +1,94 @@ +package sysgo + +import ( + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" +) + +type DefaultMinimalExternalELSystemIDs struct { + L1 stack.L1NetworkID + L1EL stack.L1ELNodeID + L1CL stack.L1CLNodeID + + L2 stack.L2NetworkID + L2CL stack.L2CLNodeID + L2EL stack.L2ELNodeID + + SyncTester stack.SyncTesterID +} + +func NewDefaultMinimalExternalELSystemIDs(l1ID, l2ID eth.ChainID) DefaultMinimalExternalELSystemIDs { + ids := DefaultMinimalExternalELSystemIDs{ + L1: stack.L1NetworkID(l1ID), + L1EL: stack.NewL1ELNodeID("l1", l1ID), + L1CL: stack.NewL1CLNodeID("l1", l1ID), + L2: stack.L2NetworkID(l2ID), + L2CL: stack.NewL2CLNodeID("verifier", l2ID), + L2EL: stack.NewL2ELNodeID("sync-tester-el", l2ID), + SyncTester: stack.NewSyncTesterID("sync-tester", l2ID), + } + return ids +} + +// DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry creates a minimal external EL system +// using a network from the superchain registry instead of the deployer +func DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry(dest *DefaultMinimalExternalELSystemIDs, l1CLBeaconRPC, l1ELRPC, l2ELRPC string, l1ChainID eth.ChainID, networkName string, fcu eth.FCUState) stack.Option[*Orchestrator] { + chainCfg := chaincfg.ChainByName(networkName) + if chainCfg == nil { + panic(fmt.Sprintf("network %s not found in superchain registry", networkName)) + } + l2ChainID := eth.ChainIDFromUInt64(chainCfg.ChainID) + + ids := NewDefaultMinimalExternalELSystemIDs(l1ChainID, l2ChainID) + + opt := stack.Combine[*Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + o.P().Logger().Info("Setting up with superchain registry network", "network", networkName) + })) + + opt.Add(WithMnemonicKeys(devkeys.TestMnemonic)) + + // Skip deployer since we're using external L1 and superchain registry for L2 config + // Create L1 network record for external L1 + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + chainID, _ := ids.L1.ChainID().Uint64() + l1Net := &L1Network{ + id: ids.L1, + genesis: &core.Genesis{ + Config: ¶ms.ChainConfig{ + ChainID: big.NewInt(int64(chainID)), + }, + }, + blockTime: 12, + } + o.l1Nets.Set(ids.L1.ChainID(), l1Net) + })) + + opt.Add(WithExtL1Nodes(ids.L1EL, ids.L1CL, l1ELRPC, l1CLBeaconRPC)) + + // Use superchain registry instead of deployer + opt.Add(WithL2NetworkFromSuperchainRegistryWithDependencySet( + stack.L2NetworkID(l2ChainID), + networkName, + )) + + // Add SyncTester service with external endpoint + opt.Add(WithSyncTesterWithExternalEndpoint(ids.SyncTester, l2ELRPC, l2ChainID)) + + // Add SyncTesterL2ELNode as the L2EL replacement for real-world EL endpoint + opt.Add(WithSyncTesterL2ELNode(ids.L2EL, ids.L2EL, fcu)) + opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL)) + + opt.Add(stack.Finally(func(orch *Orchestrator) { + *dest = ids + })) + + return opt +} diff --git a/op-sync-tester/synctester/backend/sync_tester.go b/op-sync-tester/synctester/backend/sync_tester.go index c1373332122..201dcbf8b05 100644 --- a/op-sync-tester/synctester/backend/sync_tester.go +++ b/op-sync-tester/synctester/backend/sync_tester.go @@ -93,7 +93,7 @@ func (s *SyncTester) fetchSession(ctx context.Context) (*eth.SyncTesterSession, return nil, fmt.Errorf("session already deleted: %s", session.SessionID) } if existing, ok := s.sessions[session.SessionID]; ok { - s.log.Info("Using existing session", "session", existing) + s.log.Debug("Using existing session", "session", existing) return existing, nil } else { s.storeSession(session) From 9312db090d0460e4ece54cac9d71c6a8d0c61887 Mon Sep 17 00:00:00 2001 From: zhiqiangxu <652732310@qq.com> Date: Wed, 3 Sep 2025 22:40:40 +0800 Subject: [PATCH 5/9] fix comment for l1.cache-size (#17107) --- op-node/flags/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index d7b8ece67a1..e032e14c439 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -171,7 +171,7 @@ var ( L1CacheSize = &cli.UintFlag{ Name: "l1.cache-size", Usage: "Cache size for blocks, receipts and transactions. " + - "If this flag is set to 0, 2/3 of the sequencing window size is used (usually 2400). " + + "If this flag is set to 0, 3/2 of the sequencing window size is used (usually 2400). " + "The default value of 900 (~3h of L1 blocks) is good for (high-throughput) networks that see frequent safe head increments. " + "On (low-throughput) networks with infrequent safe head increments, it is recommended to set this value to 0, " + "or a value that well covers the typical span between safe head increments. " + From 539c39a1765cb61e3111e380d08f0d2bca89df06 Mon Sep 17 00:00:00 2001 From: smartcontracts Date: Wed, 3 Sep 2025 11:09:35 -0400 Subject: [PATCH 6/9] feat: add feature flagging functionality to SystemConfig (#17281) * feat: add feature flagging functionality to SystemConfig Adds a function to the SystemConfig for feature flagging. Features are identified by 32 byte strings and can be toggled on or off by the ProxyAdmin or the owner of the ProxyAdmin. Note that this commit does not actually use any feature flags but demonstrates what a feature flag would look like by adding in the flag for the ETHLockbox feature. * feat: update for PR feedback * fix: small test tweaks * fix: broken test --- .../interfaces/L1/ISystemConfig.sol | 4 + .../snapshots/abi/SystemConfig.json | 61 ++++++++ .../snapshots/semver-lock.json | 8 +- .../snapshots/storageLayout/SystemConfig.json | 7 + .../OPContractsManagerStandardValidator.sol | 6 +- .../contracts-bedrock/src/L1/SystemConfig.sol | 37 ++++- .../src/libraries/Features.sol | 13 ++ .../test/L1/OPContractsManager.t.sol | 2 +- .../test/L1/SystemConfig.t.sol | 139 ++++++++++++++++++ 9 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 packages/contracts-bedrock/src/libraries/Features.sol diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index ca2c3ebe144..81c5f554f2d 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -24,8 +24,10 @@ interface ISystemConfig is IProxyAdminOwnedBase { } error ReinitializableBase_ZeroInitVersion(); + error SystemConfig_InvalidFeatureState(); event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); + event FeatureSet(bytes32 indexed feature, bool indexed enabled); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -92,6 +94,8 @@ interface ISystemConfig is IProxyAdminOwnedBase { function paused() external view returns (bool); function superchainConfig() external view returns (ISuperchainConfig); function guardian() external view returns (address); + function setFeature(bytes32 _feature, bool _enabled) external; + function isFeatureEnabled(bytes32) external view returns (bool); function __constructor__() external; } diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index a295b986db2..03f82fcb103 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -413,6 +413,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "isFeatureEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1CrossDomainMessenger", @@ -704,6 +723,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_feature", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "_enabled", + "type": "bool" + } + ], + "name": "setFeature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -892,6 +929,25 @@ "name": "ConfigUpdate", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "feature", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "FeatureSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -958,5 +1014,10 @@ "inputs": [], "name": "ReinitializableBase_ZeroInitVersion", "type": "error" + }, + { + "inputs": [], + "name": "SystemConfig_InvalidFeatureState", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 24d25b9b066..af38b21aaa8 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -24,8 +24,8 @@ "sourceCodeHash": "0xb1264c7af50b6134c98cb82d1ffc7891adf97068fa7048ee70992fb94bc15bd1" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { - "initCodeHash": "0xe83e83cddbcae1d6af62f9754517b25f95ae371f3843dd8ee8030ddf8ba4622d", - "sourceCodeHash": "0x5faad71f147b776ed4d3290d319d1f4697e4e37b50eee5a4ef52f901c04efaa0" + "initCodeHash": "0x3e8a3bed20aa10b6d285cd926903c4d8c34aad3ba5c0e9e32da3cde13b179238", + "sourceCodeHash": "0x6a31abe2f73c7279a00a8fcecb6741af8e6fe54a1112420e6a15859753487dbb" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { "initCodeHash": "0x785b09610b2da65d248b49150fafc85b8369c921ddae95b0ea45608b1ce5cbc6", @@ -40,8 +40,8 @@ "sourceCodeHash": "0xad12c20a00dc20683bd3f68e6ee254f968da6cc2d98930be6534107ee5cb11d9" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x07b7039de5b8a4dc57642ee9696e949d70516b7f6dce41dde4920efb17105ef2", - "sourceCodeHash": "0x997212ceadabb306c2abd31918b09bccbba0b21662c1d8930a3599831c374b13" + "initCodeHash": "0x8c6a1fb65d650525a3cd3fd617ae923d44da157b2ef928ab8ec39dbf39e25a98", + "sourceCodeHash": "0xdf526027678f23da79a56c1d6127a3ed3fd92927ec8d5541982b6f1c2119f70e" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index be5a739fa69..53d4ca20c46 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -124,5 +124,12 @@ "offset": 0, "slot": "108", "type": "contract ISuperchainConfig" + }, + { + "bytes": "32", + "label": "isFeatureEnabled", + "offset": 0, + "slot": "109", + "type": "mapping(bytes32 => bool)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol index cd63fa40893..64f48d1624b 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol @@ -36,8 +36,8 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// before and after an upgrade. contract OPContractsManagerStandardValidator is ISemver { /// @notice The semantic version of the OPContractsManagerStandardValidator contract. - /// @custom:semver 1.8.0 - string public constant version = "1.8.0"; + /// @custom:semver 1.9.0 + string public constant version = "1.9.0"; /// @notice The SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -176,7 +176,7 @@ contract OPContractsManagerStandardValidator is ISemver { /// @notice Returns the expected SystemConfig version. function systemConfigVersion() public pure returns (string memory) { - return "3.4.0"; + return "3.5.0"; } /// @notice Returns the expected OptimismPortal version. diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 604a9936d3a..48a9c63f2a7 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -135,16 +135,28 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl /// @notice The SuperchainConfig contract that manages the pause state. ISuperchainConfig public superchainConfig; + /// @notice Bytes32 feature flag name to boolean enabled value. + mapping(bytes32 => bool) public isFeatureEnabled; + /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. /// @param data Encoded update data. event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); + /// @notice Emitted when a feature is set. + /// @param feature Feature that was set. + /// @param enabled Whether the feature is enabled. + event FeatureSet(bytes32 indexed feature, bool indexed enabled); + + /// @notice Thrown when attempting to enable/disable a feature when already enabled/disabled, + /// respectively. + error SystemConfig_InvalidFeatureState(); + /// @notice Semantic version. - /// @custom:semver 3.4.0 + /// @custom:semver 3.5.0 function version() public pure virtual returns (string memory) { - return "3.4.0"; + return "3.5.0"; } /// @notice Constructs the SystemConfig contract. @@ -484,6 +496,27 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl _resourceConfig = _config; } + /// @notice Sets a feature flag enabled or disabled. Can only be called by the ProxyAdmin or + /// its owner. + /// @param _feature Feature to set. + /// @param _enabled Whether the feature should be enabled or disabled. + function setFeature(bytes32 _feature, bool _enabled) external { + // Features can only be set by the ProxyAdmin or its owner. + _assertOnlyProxyAdminOrProxyAdminOwner(); + + // As a sanity check, prevent users from enabling the feature if already enabled or + // disabling the feature if already disabled. This helps to prevent accidental misuse. + if ((_enabled && isFeatureEnabled[_feature]) || (!_enabled && !isFeatureEnabled[_feature])) { + revert SystemConfig_InvalidFeatureState(); + } + + // Set the feature. + isFeatureEnabled[_feature] = _enabled; + + // Emit an event. + emit FeatureSet(_feature, _enabled); + } + /// @notice Returns the current pause state of the system by checking if the SuperchainConfig is paused for this /// chain's ETHLockbox. /// @return bool True if the system is paused, false otherwise. diff --git a/packages/contracts-bedrock/src/libraries/Features.sol b/packages/contracts-bedrock/src/libraries/Features.sol new file mode 100644 index 00000000000..1521b1d1a33 --- /dev/null +++ b/packages/contracts-bedrock/src/libraries/Features.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Features is a library that stores feature name constants. Can be used alongside the +/// feature flagging functionality in the SystemConfig contract to selectively enable or +/// disable customizable features of the OP Stack. +library Features { + /// @notice The ETH_LOCKBOX feature determines if the system is configured to use the + /// ETHLockbox contract in the OptimismPortal. When the ETH_LOCKBOX feature is active + /// and the ETHLockbox contract has been configured, the OptimismPortal will use the + /// ETHLockbox to store ETH instead of storing ETH directly in the portal itself. + bytes32 internal constant ETH_LOCKBOX = "ETH_LOCKBOX"; +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 92113e5d473..2ba83d31423 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -475,7 +475,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { // Make sure that the SystemConfig is upgraded to the right version. It must also have the // right l2ChainId and must be properly initialized. - assertEq(ISemver(address(systemConfig)).version(), "3.4.0"); + assertEq(ISemver(address(systemConfig)).version(), "3.5.0"); assertEq(impls.systemConfigImpl, EIP1967Helper.getImplementation(address(systemConfig))); assertEq(systemConfig.l2ChainId(), l2ChainId); DeployUtils.assertInitialized({ _contractAddress: address(systemConfig), _isProxy: true, _slot: 0, _offset: 0 }); diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index 9b1e1c3d813..92d758db035 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -9,6 +9,7 @@ import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.so // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { Features } from "src/libraries/Features.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces @@ -725,6 +726,144 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { } } +/// @title SystemConfig_SetFeature_Test +/// @notice Test contract for SystemConfig `setFeature` function. +contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { + event FeatureSet(bytes32 indexed feature, bool indexed enabled); + + /// @notice Tests that `setFeature` reverts if the caller is not ProxyAdmin or ProxyAdmin owner. + /// @param _sender The address to test. + function testFuzz_setFeature_notProxyAdminOrProxyAdminOwner_reverts(address _sender) external { + // Ensure sender is not ProxyAdmin or ProxyAdmin owner + vm.assume(_sender != address(systemConfig.proxyAdmin()) && _sender != systemConfig.proxyAdminOwner()); + + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vm.prank(_sender); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + } + + /// @notice Tests that `setFeature` enables a feature successfully when called by ProxyAdmin. + function test_setFeature_enableFeatureByProxyAdmin_succeeds() external { + // Verify feature is initially disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.expectEmit(address(systemConfig)); + emit FeatureSet(Features.ETH_LOCKBOX, true); + + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + + // Verify feature is now enabled + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` disables a feature successfully when called by ProxyAdmin. + function test_setFeature_disableFeatureByProxyAdmin_succeeds() external { + // First enable the feature + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.expectEmit(address(systemConfig)); + emit FeatureSet(Features.ETH_LOCKBOX, false); + + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + + // Verify feature is now disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` enables a feature successfully when called by ProxyAdmin owner. + function test_setFeature_enableFeatureByProxyAdminOwner_succeeds() external { + // Verify feature is initially disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.expectEmit(address(systemConfig)); + emit FeatureSet(Features.ETH_LOCKBOX, true); + + vm.prank(systemConfig.proxyAdminOwner()); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + + // Verify feature is now enabled + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` can toggle the same feature multiple times. + function test_setFeature_multipleToggles_succeeds() external { + address proxyAdmin = address(systemConfig.proxyAdmin()); + + // Initially disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Enable feature + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Disable feature + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Enable again + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` reverts when trying to enable a feature that is already + /// enabled. + function test_setFeature_alreadyEnabled_reverts() external { + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.prank(address(systemConfig.proxyAdmin())); + vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + } + + /// @notice Tests that `setFeature` reverts when trying to disable a feature that is already + /// disabled. + function test_setFeature_alreadyDisabled_reverts() external { + vm.prank(address(systemConfig.proxyAdmin())); + vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + } +} + +/// @title SystemConfig_IsFeatureEnabled_Test +/// @notice Test contract for SystemConfig `isFeatureEnabled` function. +contract SystemConfig_IsFeatureEnabled_Test is SystemConfig_TestInit { + /// @notice Tests that `isFeatureEnabled` returns false for unset features. + /// @param _feature The feature to check. + function testFuzz_isFeatureEnabled_unsetFeature_succeeds(bytes32 _feature) external view { + assertFalse(systemConfig.isFeatureEnabled(_feature)); + } + + /// @notice Tests that `isFeatureEnabled` returns correct value after feature is enabled. + function test_isFeatureEnabled_afterEnable_succeeds() external { + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `isFeatureEnabled` returns correct value after feature is disabled. + function test_isFeatureEnabled_afterDisable_succeeds() external { + // First enable the feature + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Then disable it + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } +} + /// @title SystemConfig_Guardian_Test /// @notice Test contract for SystemConfig `guardian` function. contract SystemConfig_Guardian_Test is SystemConfig_TestInit { From ce174fca525e4d6c075a46408495591e28c91754 Mon Sep 17 00:00:00 2001 From: Stefano Charissis Date: Thu, 4 Sep 2025 10:34:10 +1000 Subject: [PATCH 7/9] feat(op-acceptance-tests): add more sysgo tests. (#16817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(op-acceptance-tests): add more sysgo tests. * fix(op-devstack): ecotone; fees. Use the actual increase in the L1FeeVault balance. This is because in Ecotone, the L1 fee includes both base fee and blob base fee components. * fix(op-acceptance-tests): skipping flaky/broken tests. * fix(op-acceptance-tests): interop; TestInteropSystemSupervisor * fix(op-acceptance-tests): justfile; exit early for gateless. * fix(op-acceptance-tests): tidied justfile * fix(op-acceptance-tests): fix ecotone fees. The recent commit changed L1 fee calculation to use vault balance increases as the source of truth, but base fee and priority fee calculations were still using the old method (calculating from block data). Made all fee calculations by making vault increases the source of truth. * fix(op-acceptance-tests): increase default timeout for gateless mode. * chore: Conditional logic * fix(op-acceptance-tests): TestSuperRootWithdrawal nonce issue and timeouts * fix(op-acceptance-tests): re-enable safeheaddb tests after #17083 fix * fix(op-acceptance-tests): security; env var injection The LOG_LEVEL environment variable is used directly in shell command execution without validation or sanitization, allowing command injection attacks. * feat(op-acceptance-tests): justfile; allow gateless for sysext. Allow gateless mode to run for an external devnet ('sysext' orchestrator) * clean(op-acceptance-tests): circleci; removed memory-base job The new memory-all is a superset of memory-base; so we don't need both. * fix(op-devstack): enhance Ecotone fee validation and enforce operator vault constraints - Add validation that receipt L1Fee matches L1FeeVault increase - Restore receipt-based fee calculations for validation (baseFee and L2Fee) - Enforce OperatorVault must be zero in Ecotone (operator fees introduced in Isthmus) - Exclude OperatorVault from total fee calculations in Ecotone - Add comprehensive receipt fee validation checks * fix(op-devstack): ecotone; cross-validation of fees Added cross-validation between receipt and vault fees: The code now verifies that: - receiptBaseFee (block.BaseFee × gasUsed) equals vaultBaseFee (BaseFeeVault increase) - receiptL2Fee (effectiveGasPrice × gasUsed) equals vaultL2Fee (BaseFee + SequencerFee vault increases) * fix(op-devstack): supervisor sync status L1 mismatch retry Increase retry attempts and handle L1 sync mismatch errors to fix flaky TestExecMsgDifferEventIndexInSingleTx. * fix(op-acceptance-tests): TestPostInteropUpgradeComprehensive Made it serial due to flakiness. * feat(op-acceptance-tests): ci; tweak timeout and verbosity. * fix(op-acceptance-tests): exclude interop tests from ci tag These in-memory devstack tests don't work reliably with gotestsum's retry mechanism. They run fine via op-acceptor in the memory-all job but fail when run directly with go test in go-tests-short. Adding !ci build constraint to exclude from standard CI test suite while keeping them available for dedicated acceptance test runs. * fix(op-acceptance-tests): TestWithdrawal nonce synchronization Use fresh EOA instance for withdrawal initiation to prevent nonce conflicts when shared L1/L2 keys encounter retry logic. * chore(op-acceptance-tests): TestPreNoInbox marked as flaky * chore(op-acceptance-tests): test limiting concurrency * chore(op-acceptance-tests): self-hosted runner Default to using our self-hosted runners. Accept a parameter which allows individual jobs to opt-out and use CCI/Cloud runners. --------- Co-authored-by: Jan Nanista --- .circleci/config.yml | 86 +++++++------ mise.toml | 2 +- op-acceptance-tests/README.md | 22 +++- op-acceptance-tests/cmd/main.go | 5 + op-acceptance-tests/justfile | 113 +++++++++++------- .../tests/base/withdrawal/withdrawal_test.go | 8 +- .../tests/ecotone/fees_test.go | 2 +- .../interop/message/supervisor_smoke_test.go | 21 +++- .../proofs/withdrawal/withdrawal_test.go | 3 + .../interop_sync_test.go | 4 + .../sync/simple_interop/interop_sync_test.go | 2 + .../tests/interop/upgrade/post_test.go | 4 +- .../tests/interop/upgrade/pre_test.go | 4 + op-devstack/dsl/ecotone_fees.go | 60 ++++++++-- op-devstack/dsl/supervisor.go | 9 +- 15 files changed, 237 insertions(+), 108 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb7bc83998c..8ae6eaa7e68 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1183,37 +1183,41 @@ jobs: description: Timeout for when CircleCI kills the job if there's no output type: string default: 30m + use_circleci_runner: + description: Whether to use CircleCI runners (with Docker) instead of self-hosted runners + type: boolean + default: false machine: - image: ubuntu-2404:current - docker_layer_caching: true # Since we are building docker images for components, we'll cache the layers for faster builds - resource_class: xlarge + image: <<# parameters.use_circleci_runner >>ubuntu-2404:current<><<^ parameters.use_circleci_runner >>true<> + docker_layer_caching: <> + resource_class: <<# parameters.use_circleci_runner >>xlarge<><<^ parameters.use_circleci_runner >>ethereum-optimism/latitude-1<> steps: - checkout-from-workspace - - run: - name: Setup Kurtosis (if needed) - command: | - if [[ "<>" != "" ]]; then - echo "Setting up Kurtosis for external devnet testing..." + - unless: + condition: + equal: ["",<>] + steps: + - run: + name: Setup Kurtosis + command: | + echo "Setting up Kurtosis for external devnet testing..." - # Print Kurtosis version - echo "Using Kurtosis from: $(which kurtosis || echo 'not found')" - kurtosis version + # Print Kurtosis version + echo "Using Kurtosis from: $(which kurtosis || echo 'not found')" + kurtosis version - # Start Kurtosis engine - echo "Starting Kurtosis engine..." - kurtosis engine start || true + # Start Kurtosis engine + echo "Starting Kurtosis engine..." + kurtosis engine start || true - # Clean old instances - echo "Cleaning old instances..." - kurtosis clean -a || true + # Clean old instances + echo "Cleaning old instances..." + kurtosis clean -a || true - # Check engine status - kurtosis engine status || true + # Check engine status + kurtosis engine status || true - echo "Kurtosis setup complete" - else - echo "Using in-process testing (sysgo orchestrator) - no Kurtosis setup needed" - fi + echo "Kurtosis setup complete" # Notify us of a setup failure - when: condition: on_fail @@ -1242,7 +1246,7 @@ jobs: command: go test -v -c -o /dev/null $(go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./tests/...) # Run the acceptance tests (if the devnet is running) - run: - name: Run acceptance tests (gate=<>) + name: Run acceptance tests working_directory: op-acceptance-tests no_output_timeout: 1h environment: @@ -1250,8 +1254,12 @@ jobs: GO111MODULE: "on" GOGC: "0" command: | - # Run the tests - LOG_LEVEL=debug just acceptance-test "<>" "<>" + if [[ "<>" == "" ]]; then + echo "Running in gateless mode - auto-discovering all tests in ./op-acceptance-tests/..." + else + echo "Running in gate mode (gate=<>)" + fi + LOG_LEVEL=info just acceptance-test "<>" "<>" - run: name: Print results (summary) working_directory: op-acceptance-tests @@ -2476,13 +2484,11 @@ workflows: - circleci-repo-readonly-authenticated-github-token requires: - initialize - # IN-PROCESS (base) + # IN-MEMORY (all) - op-acceptance-tests: - # Acceptance Testing params - name: memory-base - gate: base - # CircleCI params - no_output_timeout: 10m + name: memory-all + gate: "" # Empty gate = gateless mode + no_output_timeout: 90m context: - circleci-repo-readonly-authenticated-github-token - discord @@ -2495,6 +2501,7 @@ workflows: name: kurtosis-simple devnet: simple gate: base + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2508,6 +2515,7 @@ workflows: name: kurtosis-isthmus devnet: isthmus gate: isthmus + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2521,6 +2529,7 @@ workflows: name: kurtosis-interop devnet: interop gate: interop + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2554,13 +2563,11 @@ workflows: - circleci-repo-readonly-authenticated-github-token requires: - initialize - # IN-PROCESS (base) + # IN-MEMORY (all) - op-acceptance-tests: - # Acceptance Testing params - name: memory-base - gate: base - # CircleCI params - no_output_timeout: 10m + name: memory-all + gate: "" # Empty gate = gateless mode + no_output_timeout: 60m context: - circleci-repo-readonly-authenticated-github-token - discord @@ -2573,6 +2580,7 @@ workflows: name: kurtosis-simple devnet: simple gate: base + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2586,6 +2594,7 @@ workflows: name: kurtosis-isthmus devnet: isthmus gate: isthmus + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2599,6 +2608,7 @@ workflows: name: kurtosis-interop devnet: interop gate: interop + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: diff --git a/mise.toml b/mise.toml index 0fcaefa823d..4ab2451c689 100644 --- a/mise.toml +++ b/mise.toml @@ -37,7 +37,7 @@ anvil = "1.1.0" codecov-uploader = "0.8.0" goreleaser-pro = "2.11.2" kurtosis = "1.8.1" -op-acceptor = "op-acceptor/v3.0.0" +op-acceptor = "op-acceptor/v3.0.1" # Fake dependencies # Put things here if you need to track versions of tools or projects that can't diff --git a/op-acceptance-tests/README.md b/op-acceptance-tests/README.md index 183bee525b2..78b077939ee 100644 --- a/op-acceptance-tests/README.md +++ b/op-acceptance-tests/README.md @@ -96,7 +96,8 @@ For rapid test development, use in-process testing: ```bash cd op-acceptance-tests -just acceptance-test "" base # Uses sysgo orchestrator - faster! +# Not providing a network uses the sysgo orchestrator (in-memory network) which is faster and easier to iterate with. +just acceptance-test "" base ``` ### Testing Against External Devnets @@ -163,8 +164,25 @@ To add new acceptance tests: package: github.com/ethereum-optimism/optimism/your/package/path ``` +### Quick Development + +For rapid development and testing: + +```bash +cd op-acceptance-tests + +# Run all tests (sysgo gateless mode) - most comprehensive coverage +just acceptance-test "" "" + +# Run specific gate-based tests (traditional mode) +just acceptance-test "" base # In-process (sysgo) with gate +just acceptance-test simple base # External devnet (sysext) with gate +``` + +Using an empty gate (`""`) triggers gateless mode with the sysgo orchestrator, auto-discovering all tests. + ## Further Information For more details about `op-acceptor` and the acceptance testing process, refer to the main documentation or ask the team for guidance. -The source code for `op-acceptor` is available at [github.com/ethereum-optimism/infra/op-acceptor](https://github.com/ethereum-optimism/infra/tree/main/op-acceptor). If you discover any bugs or have feature requests, please open an issue in that repository. \ No newline at end of file +The source code for `op-acceptor` is available at [github.com/ethereum-optimism/infra/op-acceptor](https://github.com/ethereum-optimism/infra/tree/main/op-acceptor). If you discover any bugs or have feature requests, please open an issue in that repository. diff --git a/op-acceptance-tests/cmd/main.go b/op-acceptance-tests/cmd/main.go index 1b18c7f39cf..f5e737d14de 100644 --- a/op-acceptance-tests/cmd/main.go +++ b/op-acceptance-tests/cmd/main.go @@ -257,6 +257,11 @@ func runOpAcceptor(ctx context.Context, tracer trace.Tracer, orchestrator string args = append(args, "--devnet-env-url", devnetEnvURL) } + // For sysgo, we allow skips + if orchestrator == "sysgo" { + args = append(args, "--allow-skips") + } + acceptorCmd := exec.CommandContext(ctx, acceptor, args...) acceptorCmd.Env = env acceptorCmd.Stdout = os.Stdout diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index 4c761439037..ae000e84111 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -1,47 +1,48 @@ -REPO_ROOT := `realpath ..` +REPO_ROOT := `realpath ..` # path to the root of the optimism monorepo KURTOSIS_DIR := REPO_ROOT + "/kurtosis-devnet" -ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.0.0") +ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.0.1") DOCKER_REGISTRY := env_var_or_default("DOCKER_REGISTRY", "us-docker.pkg.dev/oplabs-tools-artifacts/images") ACCEPTOR_IMAGE := env_var_or_default("ACCEPTOR_IMAGE", DOCKER_REGISTRY + "/op-acceptor:" + ACCEPTOR_VERSION) # Default recipe - runs acceptance tests default: - @just acceptance-test simple base + @just acceptance-test "" base holocene: - @just acceptance-test simple holocene + @just acceptance-test "" holocene isthmus: - @just acceptance-test isthmus isthmus + @just acceptance-test "" isthmus interop: - @just acceptance-test interop interop + @just acceptance-test "" interop # Run acceptance tests with mise-managed binary +# Usage: just acceptance-test [devnet] [gate] +# Examples: +# just acceptance-test "" base # In-process (sysgo) with specific gate +# just acceptance-test "" "" # In-process gateless mode (all tests) +# just acceptance-test "simple" base # External devnet with specific gate +# just acceptance-test "simple" "" # External devnet gateless mode (all tests) acceptance-test devnet="" gate="holocene": #!/usr/bin/env bash set -euo pipefail - # Check if mise is installed - if command -v mise >/dev/null; then - echo "mise is installed" - else - echo "Mise not installed, falling back to Docker..." - just acceptance-test-docker {{devnet}} {{gate}} - fi + # Determine mode and orchestrator + GATELESS_MODE=$([[ "{{gate}}" == "" ]] && echo "true" || echo "false") + ORCHESTRATOR=$([[ "{{devnet}}" == "" ]] && echo "sysgo" || echo "sysext") - if [[ "{{devnet}}" == "" ]]; then - echo -e "DEVNET: in-memory, GATE: {{gate}}\n" + # Display mode information + if [[ "$GATELESS_MODE" == "true" ]]; then + echo -e "DEVNET: $([[ "$ORCHESTRATOR" == "sysgo" ]] && echo "in-memory" || echo "{{devnet}}") ($ORCHESTRATOR), MODE: gateless (all tests)\n" else - echo -e "DEVNET: {{devnet}}, GATE: {{gate}}\n" + echo -e "DEVNET: $([[ "$ORCHESTRATOR" == "sysgo" ]] && echo "in-memory" || echo "{{devnet}}") ($ORCHESTRATOR), GATE: {{gate}}\n" fi - # For sysgo orchestrator (in-process testing) ensure: - # - contracts are built - # - cannon dependencies are built - # Note: build contracts only if not in CI (CI jobs already take care of this) - if [[ "{{devnet}}" == "" && -z "${CIRCLECI:-}" ]]; then + # Build dependencies for sysgo (in-process) mode if not in CI + # In CI jobs already take care of this, so we skip it. + if [[ "$ORCHESTRATOR" == "sysgo" && -z "${CIRCLECI:-}" ]]; then echo "Building contracts (local build)..." cd {{REPO_ROOT}} echo " - Updating submodules..." @@ -63,46 +64,68 @@ acceptance-test devnet="" gate="holocene": fi fi - # Try to install op-acceptor using mise + cd {{REPO_ROOT}}/op-acceptance-tests + + # Check mise installation and fallback to Docker if needed + if ! command -v mise >/dev/null; then + echo "Mise not installed, falling back to Docker..." + just acceptance-test-docker {{devnet}} {{gate}} + exit 0 + fi + + # Install op-acceptor using mise if ! mise install op-acceptor; then echo "WARNING: Failed to install op-acceptor with mise, falling back to Docker..." just acceptance-test-docker {{devnet}} {{gate}} exit 0 fi - # Print which binary is being used + # Set binary path and log level BINARY_PATH=$(mise which op-acceptor) echo "Using mise-managed binary: $BINARY_PATH" + LOG_LEVEL="$(echo "${LOG_LEVEL:-info}" | grep -E '^(debug|info|warn|error)$' || echo 'info')" + echo "LOG_LEVEL: $LOG_LEVEL" - # Build the command with conditional parameters - CMD_ARGS=( - "go" "run" "cmd/main.go" - "--gate" "{{gate}}" - "--testdir" "{{REPO_ROOT}}" - "--validators" "./acceptance-tests.yaml" - "--log.level" "${LOG_LEVEL:-info}" - "--acceptor" "$BINARY_PATH" - ) - - # Set orchestrator and devnet based on input - if [[ "{{devnet}}" == "" ]]; then - # In-process testing - CMD_ARGS+=("--orchestrator" "sysgo") + # Deploy devnet for sysext if it's a simple name + if [[ "$ORCHESTRATOR" == "sysext" && ! "{{devnet}}" =~ ^(kt://|ktnative://|/) ]]; then + echo "Deploying devnet {{devnet}}..." + just {{KURTOSIS_DIR}}/{{devnet}}-devnet || true + fi + + # Build command arguments based on mode + if [[ "$GATELESS_MODE" == "true" ]]; then + # Gateless mode + CMD_ARGS=( + "$BINARY_PATH" + "--testdir" "{{REPO_ROOT}}/op-acceptance-tests/..." + "--allow-skips" + "--timeout" "90m" + "--default-timeout" "10m" + ) else - # External devnet testing - CMD_ARGS+=("--orchestrator" "sysext") - CMD_ARGS+=("--devnet" "{{devnet}}") - # Include kurtosis-dir for devnet deployment - CMD_ARGS+=("--kurtosis-dir" "{{KURTOSIS_DIR}}") - # For now, run sysext in serial mode - CMD_ARGS+=("--serial") + # Gate mode + CMD_ARGS=( + "go" "run" "cmd/main.go" + "--gate" "{{gate}}" + "--testdir" "{{REPO_ROOT}}" + "--validators" "./acceptance-tests.yaml" + "--acceptor" "$BINARY_PATH" + ) + fi + + # Add common arguments + CMD_ARGS+=("--log.level" "${LOG_LEVEL}" "--orchestrator" "$ORCHESTRATOR") + + # Add sysext-specific arguments + if [[ "$ORCHESTRATOR" == "sysext" ]]; then + CMD_ARGS+=("--devnet" "{{devnet}}" "--kurtosis-dir" "{{KURTOSIS_DIR}}" "--serial") fi # Execute the command - cd {{REPO_ROOT}}/op-acceptance-tests "${CMD_ARGS[@]}" + # Run acceptance tests against a devnet using Docker (fallback if needed) acceptance-test-docker devnet="simple" gate="holocene": #!/usr/bin/env bash diff --git a/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go b/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go index de9125ac670..d2b40beb723 100644 --- a/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go +++ b/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go @@ -33,9 +33,13 @@ func TestWithdrawal(gt *testing.T) { expectedL2UserBalance := depositAmount l2User.VerifyBalanceExact(expectedL2UserBalance) - withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, l2User) + // Force a fresh EOA instance to avoid stale nonce state from shared L1/L2 key usage + // This prevents "nonce too low" errors in the retry logic during withdrawal initiation + freshL2User := l1User.Key().User(sys.L2EL) + + withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, freshL2User) expectedL2UserBalance = expectedL2UserBalance.Sub(withdrawalAmount).Sub(withdrawal.InitiateGasCost()) - l2User.VerifyBalanceExact(expectedL2UserBalance) + freshL2User.VerifyBalanceExact(expectedL2UserBalance) withdrawal.Prove(l1User) expectedL1UserBalance = expectedL1UserBalance.Sub(withdrawal.ProveGasCost()) diff --git a/op-acceptance-tests/tests/ecotone/fees_test.go b/op-acceptance-tests/tests/ecotone/fees_test.go index eefd8def79b..10852f673eb 100644 --- a/op-acceptance-tests/tests/ecotone/fees_test.go +++ b/op-acceptance-tests/tests/ecotone/fees_test.go @@ -28,7 +28,7 @@ func TestFees(gt *testing.T) { ecotoneFees.LogResults(result) - t.Log("Comprehensive Ecotone fees test completed successfully:", + t.Log("Ecotone fees test completed successfully", "gasUsed", result.TransactionReceipt.GasUsed, "l1Fee", result.L1Fee.String(), "l2Fee", result.L2Fee.String(), diff --git a/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go b/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go index 59cc56d3092..36d9817eacc 100644 --- a/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go +++ b/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go @@ -12,13 +12,26 @@ func TestInteropSystemSupervisor(gt *testing.T) { t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) - sys.L1Network.WaitForFinalization() + // First ensure L1 network is online and has blocks + t.Log("Waiting for L1 network to be online...") + sys.L1Network.WaitForOnline() + t.Log("L1 network is online") + + t.Log("Waiting for initial L1 block...") + initialBlock := sys.L1Network.WaitForBlock() + t.Log("Got initial L1 block", "block", initialBlock) + + // Wait for finalization (this may take some time) + t.Log("Waiting for L1 block finalization...") + finalizedBlock := sys.L1Network.WaitForFinalization() + t.Log("L1 block finalized", "block", finalizedBlock) // Get the finalized L1 block from the supervisor + t.Log("Querying supervisor for finalized L1 block...") block, err := sys.Supervisor.Escape().QueryAPI().FinalizedL1(t.Ctx()) - t.Require().NoError(err) + t.Require().NoError(err, "Failed to get finalized block from supervisor") // If we get here, the supervisor has finalized L1 block information - t.Require().NotNil(block) - t.Log("finalized l1 block", "block", block) + t.Require().NotNil(block, "Supervisor returned nil finalized block") + t.Log("Successfully got finalized L1 block from supervisor", "block", block) } diff --git a/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go index c0acd53d599..eff0185c165 100644 --- a/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go +++ b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go @@ -29,6 +29,9 @@ func TestSuperRootWithdrawal(gt *testing.T) { l1User.VerifyBalanceExact(initialL1Balance.Sub(depositAmount).Sub(deposit.GasCost())) l2User.VerifyBalanceExact(initialL2Balance.Add(depositAmount)) + // Wait for a block to ensure nonce synchronization between L1 and L2 EOA instances + sys.L2ChainA.WaitForBlock() + withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, l2User) withdrawal.Prove(l1User) l2User.VerifyBalanceExact(initialL2Balance.Add(depositAmount).Sub(withdrawalAmount).Sub(withdrawal.InitiateGasCost())) diff --git a/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go index 1190322cc1e..e7dc6dbadee 100644 --- a/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go +++ b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package sync import ( @@ -202,6 +204,8 @@ func TestUnsafeChainKnownToL2CL(gt *testing.T) { // TestUnsafeChainUnknownToL2CL tests the below scenario: // supervisor unsafe ahead of L2CL unsafe, aka L2CL processes new blocks first. func TestUnsafeChainUnknownToL2CL(gt *testing.T) { + gt.Skip("TODO(#16972): skipping due to flakiness and impending op-node/supervisor refactor") + t := devtest.SerialT(gt) sys := presets.NewMultiSupervisorInterop(t) diff --git a/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go index 95fe56b4dc6..f7dcd44e6a0 100644 --- a/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go +++ b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package sync import ( diff --git a/op-acceptance-tests/tests/interop/upgrade/post_test.go b/op-acceptance-tests/tests/interop/upgrade/post_test.go index 6ec1266d1fe..4531c9a2334 100644 --- a/op-acceptance-tests/tests/interop/upgrade/post_test.go +++ b/op-acceptance-tests/tests/interop/upgrade/post_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package upgrade import ( @@ -40,7 +42,7 @@ func TestPostInbox(gt *testing.T) { } func TestPostInteropUpgradeComprehensive(gt *testing.T) { - t := devtest.ParallelT(gt) + t := devtest.SerialT(gt) sys := presets.NewSimpleInterop(t) require := t.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/interop/upgrade/pre_test.go b/op-acceptance-tests/tests/interop/upgrade/pre_test.go index 4eea3a7d813..8f5f23567fa 100644 --- a/op-acceptance-tests/tests/interop/upgrade/pre_test.go +++ b/op-acceptance-tests/tests/interop/upgrade/pre_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package upgrade import ( @@ -21,6 +23,8 @@ import ( stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) +// This test is known to be flaky +// See: https://github.com/ethereum-optimism/optimism/issues/17298 func TestPreNoInbox(gt *testing.T) { t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) diff --git a/op-devstack/dsl/ecotone_fees.go b/op-devstack/dsl/ecotone_fees.go index 3412f893c49..d1970a94b92 100644 --- a/op-devstack/dsl/ecotone_fees.go +++ b/op-devstack/dsl/ecotone_fees.go @@ -54,29 +54,40 @@ func (ef *EcotoneFees) ValidateTransaction(from *EOA, to *EOA, amount *big.Int) ef.require.NoError(err) ef.require.Equal(types.ReceiptStatusSuccessful, receipt.Status) + // Get block info for base fee information + blockInfo, err := client.InfoByHash(ef.ctx, receipt.BlockHash) + ef.require.NoError(err) + endBalance := from.GetBalance() vaultsAfter := ef.getVaultBalances(client) vaultIncreases := ef.calculateVaultIncreases(vaultsBefore, vaultsAfter) - l1Fee := big.NewInt(0) - if receipt.L1Fee != nil { - l1Fee = receipt.L1Fee - } + // In Ecotone, L1 fee includes both base fee and blob base fee components + l1Fee := vaultIncreases.L1FeeVault // Use actual vault increase as the source of truth - block, err := client.InfoByHash(ef.ctx, receipt.BlockHash) - ef.require.NoError(err) + // Calculate receipt-based fees for validation + receiptBaseFee := new(big.Int).Mul(blockInfo.BaseFee(), big.NewInt(int64(receipt.GasUsed))) + receiptL2Fee := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) + + // Calculate L2 fees from vault increases + baseFee := vaultIncreases.BaseFeeVault // Use actual vault increase as the source of truth + priorityFee := vaultIncreases.SequencerVault // Use actual vault increase as the source of truth + l2Fee := new(big.Int).Add(baseFee, priorityFee) - baseFee := new(big.Int).Mul(block.BaseFee(), big.NewInt(int64(receipt.GasUsed))) - l2Fee := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) - priorityFee := new(big.Int).Sub(l2Fee, baseFee) - totalFee := new(big.Int).Add(l1Fee, l2Fee) + // Total fee is the sum of all vault increases (excluding OperatorVault which should be zero in Ecotone) + totalFee := new(big.Int).Add(vaultIncreases.BaseFeeVault, vaultIncreases.L1FeeVault) + totalFee.Add(totalFee, vaultIncreases.SequencerVault) walletBalanceDiff := new(big.Int).Sub(startBalance.ToBig(), endBalance.ToBig()) walletBalanceDiff.Sub(walletBalanceDiff, amount) - ef.validateFeeDistribution(l1Fee, baseFee, priorityFee, vaultIncreases) + // Validate total balance first to ensure all fees are accounted for ef.validateTotalBalance(walletBalanceDiff, totalFee, vaultIncreases) + + // Then validate individual fee components + ef.validateFeeDistribution(l1Fee, baseFee, priorityFee, vaultIncreases) ef.validateEcotoneFeatures(receipt, l1Fee) + ef.validateReceiptFees(receipt, l1Fee, baseFee, l2Fee, receiptBaseFee, receiptL2Fee) return EcotoneFeesValidationResult{ TransactionReceipt: receipt, @@ -129,13 +140,15 @@ func (ef *EcotoneFees) validateFeeDistribution(l1Fee, baseFee, priorityFee *big. ef.require.Equal(baseFee, vaults.BaseFeeVault, "Base fee must match BaseFeeVault increase") ef.require.Equal(priorityFee, vaults.SequencerVault, "Priority fee must match SequencerFeeVault increase") - ef.require.True(vaults.OperatorVault.Sign() >= 0, "Operator vault increase must be non-negative") + // In Ecotone, operator fees should not exist (introduced in Isthmus) + ef.require.Equal(vaults.OperatorVault.Cmp(big.NewInt(0)), 0, + "Operator vault increase must be zero in Ecotone (operator fees introduced in Isthmus)") } func (ef *EcotoneFees) validateTotalBalance(walletDiff *big.Int, totalFee *big.Int, vaults VaultBalances) { + // In Ecotone, only BaseFeeVault, L1FeeVault, and SequencerVault should have increases totalVaultIncrease := new(big.Int).Add(vaults.BaseFeeVault, vaults.L1FeeVault) totalVaultIncrease.Add(totalVaultIncrease, vaults.SequencerVault) - totalVaultIncrease.Add(totalVaultIncrease, vaults.OperatorVault) ef.require.Equal(walletDiff, totalFee, "Wallet balance difference must equal total fees") ef.require.Equal(totalVaultIncrease, totalFee, "Total vault increases must equal total fees") @@ -149,6 +162,27 @@ func (ef *EcotoneFees) validateEcotoneFeatures(receipt *types.Receipt, l1Fee *bi ef.require.Greater(receipt.EffectiveGasPrice.Uint64(), uint64(0), "Effective gas price should be > 0") } +func (ef *EcotoneFees) validateReceiptFees(receipt *types.Receipt, l1Fee, vaultBaseFee, vaultL2Fee, receiptBaseFee, receiptL2Fee *big.Int) { + // Check that receipt's L1Fee matches the vault increase + if receipt.L1Fee != nil { + ef.require.Equal(receipt.L1Fee, l1Fee, "Receipt L1Fee must match L1FeeVault increase") + } + + // Sanity check: Receipt-calculated fees should match vault-based fees + ef.require.Equal(receiptBaseFee, vaultBaseFee, + "Receipt-calculated base fee (block.BaseFee * gasUsed) must match BaseFeeVault increase") + ef.require.Equal(receiptL2Fee, vaultL2Fee, + "Receipt-calculated L2 fee (effectiveGasPrice * gasUsed) must match L2 vault increases (BaseFee + SequencerFee)") + + // Validate receipt-based calculations are positive + ef.require.True(receiptBaseFee.Sign() > 0, "Receipt-based base fee must be positive") + ef.require.True(receiptL2Fee.Sign() > 0, "Receipt-based L2 fee must be positive") + + // The effective gas price should be consistent with the calculated L2 fee + ef.require.Equal(receiptL2Fee.Cmp(receiptBaseFee) >= 0, true, + "Receipt L2 fee (effectiveGasPrice * gasUsed) should be >= base fee") +} + func (ef *EcotoneFees) LogResults(result EcotoneFeesValidationResult) { ef.log.Info("Comprehensive Ecotone fees validation completed", "gasUsed", result.TransactionReceipt.GasUsed, diff --git a/op-devstack/dsl/supervisor.go b/op-devstack/dsl/supervisor.go index c3dc800c510..786136ae6f8 100644 --- a/op-devstack/dsl/supervisor.go +++ b/op-devstack/dsl/supervisor.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "github.com/ethereum-optimism/optimism/op-devstack/stack" @@ -94,12 +95,18 @@ func (s *Supervisor) FetchSyncStatus() eth.SupervisorSyncStatus { s.log.Debug("Fetching supervisor sync status") ctx, cancel := context.WithTimeout(s.ctx, DefaultTimeout) defer cancel() - syncStatus, err := retry.Do(ctx, 2, retry.Fixed(500*time.Millisecond), func() (eth.SupervisorSyncStatus, error) { + syncStatus, err := retry.Do(ctx, 10, retry.Fixed(500*time.Millisecond), func() (eth.SupervisorSyncStatus, error) { ctx, cancel := context.WithTimeout(s.ctx, 300*time.Millisecond) defer cancel() syncStatus, err := s.inner.QueryAPI().SyncStatus(ctx) if errors.Is(err, status.ErrStatusTrackerNotReady) { s.log.Debug("Sync status not ready from supervisor") + return syncStatus, err + } + // Check for L1 sync mismatch error and retry + if err != nil && strings.Contains(err.Error(), "min synced L1 mismatch") { + s.log.Debug("L1 sync mismatch, retrying", "error", err) + return syncStatus, err } return syncStatus, err }) From 5a8da84972c7e725cdfbe14c7680cb7fd885f736 Mon Sep 17 00:00:00 2001 From: Stefano Charissis Date: Thu, 4 Sep 2025 11:17:25 +1000 Subject: [PATCH 8/9] feat(op-acceptance-tests): move all ATs to one workflow (#16755) Stop running (sysgo/in-memory) acceptance tests as unit tests in other workflows --- .circleci/config.yml | 2 +- Makefile | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8ae6eaa7e68..d4a9534994e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1246,7 +1246,7 @@ jobs: command: go test -v -c -o /dev/null $(go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./tests/...) # Run the acceptance tests (if the devnet is running) - run: - name: Run acceptance tests + name: Run acceptance tests (devnet=<>, gate=<>) working_directory: op-acceptance-tests no_output_timeout: 1h environment: diff --git a/Makefile b/Makefile index d228d9f92fe..83f5c178874 100644 --- a/Makefile +++ b/Makefile @@ -206,7 +206,6 @@ TEST_PKGS := \ ./packages/contracts-bedrock/scripts/checks/... \ ./op-dripper/... \ ./devnet-sdk/... \ - ./op-acceptance-tests/... \ ./kurtosis-devnet/... \ ./op-devstack/... \ ./op-deployer/pkg/deployer/artifacts/... \ @@ -265,7 +264,7 @@ go-tests-short: $(TEST_DEPS) ## Runs comprehensive Go tests with -short flag go-tests-short-ci: ## Runs short Go tests with gotestsum for CI (assumes deps built by CI) @echo "Setting up test directories..." mkdir -p ./tmp/test-results ./tmp/testlogs - @echo "Running Go tests with gotestsum..." + @echo 'Running Go tests (short) with gotestsum...' $(DEFAULT_TEST_ENV_VARS) && \ $(CI_ENV_VARS) && \ gotestsum --format=testname \ From fac5bbef7da77153ab9ab5d34f741491d92f98e3 Mon Sep 17 00:00:00 2001 From: Stefano Charissis Date: Thu, 4 Sep 2025 15:08:46 +1000 Subject: [PATCH 9/9] feat(op-acceptance-tests): op-acceptor v3.1.0 (#17310) Upgrades op-acceptor to [v3.1.0](https://github.com/ethereum-optimism/infra/releases/tag/op-acceptor%2Fv3.1.0) --- mise.toml | 2 +- op-acceptance-tests/justfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mise.toml b/mise.toml index 4ab2451c689..fb0035a9656 100644 --- a/mise.toml +++ b/mise.toml @@ -37,7 +37,7 @@ anvil = "1.1.0" codecov-uploader = "0.8.0" goreleaser-pro = "2.11.2" kurtosis = "1.8.1" -op-acceptor = "op-acceptor/v3.0.1" +op-acceptor = "op-acceptor/v3.1.0" # Fake dependencies # Put things here if you need to track versions of tools or projects that can't diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index ae000e84111..24e969741e0 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -1,6 +1,6 @@ REPO_ROOT := `realpath ..` # path to the root of the optimism monorepo KURTOSIS_DIR := REPO_ROOT + "/kurtosis-devnet" -ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.0.1") +ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.1.0") DOCKER_REGISTRY := env_var_or_default("DOCKER_REGISTRY", "us-docker.pkg.dev/oplabs-tools-artifacts/images") ACCEPTOR_IMAGE := env_var_or_default("ACCEPTOR_IMAGE", DOCKER_REGISTRY + "/op-acceptor:" + ACCEPTOR_VERSION)