diff --git a/op-acceptance-tests/acceptance-tests.yaml b/op-acceptance-tests/acceptance-tests.yaml index 36fe719a7aef9..8df3940774de0 100644 --- a/op-acceptance-tests/acceptance-tests.yaml +++ b/op-acceptance-tests/acceptance-tests.yaml @@ -10,8 +10,6 @@ gates: tests: - name: TestFindRPCEndpoints package: github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/run - - name: TestSystemWrapETH - package: github.com/ethereum-optimism/optimism/kurtosis-devnet/tests/interop - - name: TestInteropSystemNoop - package: github.com/ethereum-optimism/optimism/kurtosis-devnet/tests/interop - + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/fjord diff --git a/kurtosis-devnet/tests/fees/fees_test.go b/op-acceptance-tests/tests/ecotone/fees_test.go similarity index 71% rename from kurtosis-devnet/tests/fees/fees_test.go rename to op-acceptance-tests/tests/ecotone/fees_test.go index fc24502069fd8..e1f00821cdfed 100644 --- a/kurtosis-devnet/tests/fees/fees_test.go +++ b/op-acceptance-tests/tests/ecotone/fees_test.go @@ -1,4 +1,4 @@ -package fees +package ecotone import ( "context" @@ -20,7 +20,6 @@ import ( gethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/require" ) @@ -33,56 +32,17 @@ func TestFees(t *testing.T) { lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(chainIdx, types.NewBalance(big.NewInt(params.Ether))) - // Run pre-regolith test - forkGetter, forkValidator := validators.AcquireL2WithoutFork(chainIdx, rollup.Regolith) - systest.SystemTest(t, - feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx, forkGetter), - lowLevelSystemValidator, - walletValidator, - forkValidator, - ) - - // Run regolith test - forkGetter, forkValidator = validators.AcquireL2WithFork(chainIdx, rollup.Regolith) - _, notForkValidator := validators.AcquireL2WithoutFork(chainIdx, rollup.Ecotone) - systest.SystemTest(t, - feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx, forkGetter), - lowLevelSystemValidator, - walletValidator, - forkValidator, - notForkValidator, - ) - // Run ecotone test - forkGetter, forkValidator = validators.AcquireL2WithFork(chainIdx, rollup.Ecotone) - _, notForkValidator = validators.AcquireL2WithoutFork(chainIdx, rollup.Fjord) - systest.SystemTest(t, - feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx, forkGetter), - lowLevelSystemValidator, - walletValidator, - forkValidator, - notForkValidator, - ) - - // Run fjord test - forkGetter, forkValidator = validators.AcquireL2WithFork(chainIdx, rollup.Fjord) - _, notForkValidator = validators.AcquireL2WithoutFork(chainIdx, rollup.Isthmus) + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Ecotone) + _, notForkValidator := validators.AcquireL2WithoutFork(chainIdx, rollup.Fjord) systest.SystemTest(t, - feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx, forkGetter), + feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx), lowLevelSystemValidator, walletValidator, forkValidator, notForkValidator, ) - // Run isthmus test - forkGetter, forkValidator = validators.AcquireL2WithFork(chainIdx, rollup.Isthmus) - systest.SystemTest(t, - feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx, forkGetter), - lowLevelSystemValidator, - walletValidator, - forkValidator, - ) } // stateGetterAdapter adapts the ethclient to implement the StateGetter interface @@ -129,7 +89,6 @@ func feesTestScenario( lowLevelSystemGetter validators.LowLevelSystemGetter, walletGetter validators.WalletGetter, chainIdx uint64, - configGetter validators.ChainConfigGetter, ) systest.SystemTestFunc { return func(t systest.T, sys system.System) { ctx := t.Context() @@ -137,23 +96,19 @@ func feesTestScenario( // Get the low-level system and wallet llsys := lowLevelSystemGetter(ctx) wallet := walletGetter(ctx) - config := configGetter(ctx) // Get the L2 client l2Chain := llsys.L2s()[chainIdx] l2Client, err := l2Chain.Client() require.NoError(t, err) - // Get the L1 client - l1Chain := llsys.L1() - l1Client, err := l1Chain.Client() - require.NoError(t, err) - // TODO: Wait for first block after genesis // The genesis block has zero L1Block values and will throw off the GPO checks - _, err = l2Client.HeaderByNumber(ctx, big.NewInt(1)) + header, err := l2Client.HeaderByNumber(ctx, big.NewInt(1)) require.NoError(t, err) + startBlockNumber := header.Number + // Get the genesis config chainConfig, err := l2Chain.Config() require.NoError(t, err) @@ -180,27 +135,27 @@ func feesTestScenario( require.NoError(t, err) // Get wallet balance before test - startBalance, err := l2Client.BalanceAt(ctx, fromAddr, big.NewInt(rpc.EarliestBlockNumber.Int64())) + startBalance, err := l2Client.BalanceAt(ctx, fromAddr, startBlockNumber) require.NoError(t, err) require.Greater(t, startBalance.Uint64(), big.NewInt(0).Uint64()) // Get initial balances of fee recipients - baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, big.NewInt(rpc.EarliestBlockNumber.Int64())) + baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, startBlockNumber) require.NoError(t, err) - l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, big.NewInt(rpc.EarliestBlockNumber.Int64())) + l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, startBlockNumber) require.NoError(t, err) - sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, big.NewInt(rpc.EarliestBlockNumber.Int64())) + sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, startBlockNumber) require.NoError(t, err) - operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, big.NewInt(rpc.EarliestBlockNumber.Int64())) + operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, startBlockNumber) require.NoError(t, err) - genesisBlock, err := l2Client.BlockByNumber(ctx, big.NewInt(rpc.EarliestBlockNumber.Int64())) + genesisBlock, err := l2Client.BlockByNumber(ctx, startBlockNumber) require.NoError(t, err) - coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), big.NewInt(rpc.EarliestBlockNumber.Int64())) + coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), startBlockNumber) require.NoError(t, err) // Send a simple transfer from wallet to a test address @@ -225,7 +180,7 @@ func feesTestScenario( require.NoError(t, err) // Get latest header to get the base fee - header, err := l2Client.HeaderByNumber(ctx, nil) + header, err = l2Client.HeaderByNumber(ctx, nil) require.NoError(t, err) // Calculate a reasonable gas fee cap based on the base fee @@ -280,9 +235,6 @@ func feesTestScenario( operatorFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, header.Number) require.NoError(t, err) - l1Header, err := l1Client.HeaderByNumber(ctx, nil) - require.NoError(t, err) - l1FeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, header.Number) require.NoError(t, err) @@ -307,7 +259,7 @@ func feesTestScenario( require.Equal(t, baseFee, baseFeeRecipientDiff, "base fee mismatch") // Verify L1 fee - txBytes, err := signedTx.MarshalBinary() + txBytes, err := tx.MarshalBinary() require.NoError(t, err) // Calculate L1 fee based on transaction data and blocktime @@ -326,46 +278,17 @@ func feesTestScenario( ) // Verify GPO matches expected state - gpoEcotone, err := gpoContract.IsEcotone(nil) - require.NoError(t, err) - - block, err := l2Chain.Node().BlockByNumber(t.Context(), nil) - require.NoError(t, err) - time := block.Header().Time - - ecotoneEnabled, err := validators.IsForkActivated(config, rollup.Ecotone, time) - require.NoError(t, err) - require.Equal(t, ecotoneEnabled, gpoEcotone, "GPO and chain must have same ecotone view") - - gpoFjord, err := gpoContract.IsFjord(nil) + gpoEcotone, err := gpoContract.IsEcotone(&bind.CallOpts{BlockNumber: header.Number}) require.NoError(t, err) - fjordEnabled, err := validators.IsForkActivated(config, rollup.Fjord, time) - require.NoError(t, err) - require.Equal(t, fjordEnabled, gpoFjord, "GPO and chain must have same fjord view") - gpoIsthmus, err := gpoContract.IsIsthmus(nil) - require.NoError(t, err) - isthmusEnabled, err := validators.IsForkActivated(config, rollup.Isthmus, time) require.NoError(t, err) - require.Equal(t, isthmusEnabled, gpoIsthmus, "GPO and chain must have same isthmus view") + require.True(t, gpoEcotone, "GPO and chain must have same ecotone view") // Verify gas price oracle L1 fee calculation - gpoL1Fee, err := gpoContract.GetL1Fee(&bind.CallOpts{}, txBytes) - require.NoError(t, err) - - regolithEnabled, err := validators.IsForkActivated(config, rollup.Regolith, time) + gpoL1Fee, err := gpoContract.GetL1Fee(&bind.CallOpts{BlockNumber: header.Number}, txBytes) require.NoError(t, err) adjustedGPOFee := gpoL1Fee - if fjordEnabled { - // The fjord calculations are handled differently and may be bounded by the minimum value - // This is a simplification as the full test adapts more precisely - } else if regolithEnabled && !ecotoneEnabled { - // For post-regolith (but pre-ecotone), adjust the GPO fee to account for signature overhead - artificialGPOOverhead := big.NewInt(68 * 16) // 68 bytes to cover signature and RLP data - l1BaseFee := l1Header.BaseFee - adjustedGPOFee = new(big.Int).Sub(gpoL1Fee, new(big.Int).Mul(artificialGPOOverhead, l1BaseFee)) - } require.Equal(t, l1Fee, adjustedGPOFee, "GPO reports L1 fee mismatch") // Verify receipt L1 fee diff --git a/kurtosis-devnet/tests/fjord/check_scripts_test.go b/op-acceptance-tests/tests/fjord/check_scripts_test.go similarity index 100% rename from kurtosis-devnet/tests/fjord/check_scripts_test.go rename to op-acceptance-tests/tests/fjord/check_scripts_test.go diff --git a/op-acceptance-tests/tests/fjord/fees_test.go b/op-acceptance-tests/tests/fjord/fees_test.go new file mode 100644 index 0000000000000..33f255caf029d --- /dev/null +++ b/op-acceptance-tests/tests/fjord/fees_test.go @@ -0,0 +1,300 @@ +package fjord + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestFees verifies that L1/L2 fees are handled properly in different fork configurations +func TestFees(t *testing.T) { + // Define which L2 chain we'll test + chainIdx := uint64(0) + + // Get validators and getters for accessing the system and wallets + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(chainIdx, types.NewBalance(big.NewInt(params.Ether))) + + // Run fjord test + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Fjord) + _, notForkValidator := validators.AcquireL2WithoutFork(chainIdx, rollup.Isthmus) + systest.SystemTest(t, + feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx), + lowLevelSystemValidator, + walletValidator, + forkValidator, + notForkValidator, + ) +} + +// stateGetterAdapter adapts the ethclient to implement the StateGetter interface +type stateGetterAdapter struct { + ctx context.Context + t systest.T + client *ethclient.Client +} + +// GetState implements the StateGetter interface +func (sga *stateGetterAdapter) GetState(addr common.Address, key common.Hash) common.Hash { + var result common.Hash + val, err := sga.client.StorageAt(sga.ctx, addr, key, nil) + require.NoError(sga.t, err) + copy(result[:], val) + return result +} + +// waitForTransaction polls for a transaction receipt until it is available or the context is canceled. +// It's a simpler version of the functionality in SimpleTxManager. +func waitForTransaction(ctx context.Context, client *ethclient.Client, hash common.Hash) (*gethTypes.Receipt, error) { + ticker := time.NewTicker(500 * time.Millisecond) // Poll every 500ms + defer ticker.Stop() + + for { + receipt, err := client.TransactionReceipt(ctx, hash) + if receipt != nil && err == nil { + return receipt, nil + } else if err != nil && !errors.Is(err, ethereum.NotFound) { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + // Continue polling + } + } +} + +// feesTestScenario creates a test scenario for verifying fee calculations +func feesTestScenario( + lowLevelSystemGetter validators.LowLevelSystemGetter, + walletGetter validators.WalletGetter, + chainIdx uint64, +) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + // Get the low-level system and wallet + llsys := lowLevelSystemGetter(ctx) + wallet := walletGetter(ctx) + + // Get the L2 client + l2Chain := llsys.L2s()[chainIdx] + l2Client, err := l2Chain.Client() + require.NoError(t, err) + + // TODO: Wait for first block after genesis + // The genesis block has zero L1Block values and will throw off the GPO checks + header, err := l2Client.HeaderByNumber(ctx, big.NewInt(1)) + require.NoError(t, err) + + startBlockNumber := header.Number + + // Get the genesis config + chainConfig, err := l2Chain.Config() + require.NoError(t, err) + + // Create state getter adapter for L1 cost function + sga := &stateGetterAdapter{ + ctx: ctx, + t: t, + client: l2Client, + } + + // Create L1 cost function + l1CostFn := gethTypes.NewL1CostFunc(chainConfig, sga) + + // Create operator fee function + operatorFeeFn := gethTypes.NewOperatorCostFunc(chainConfig, sga) + + // Get wallet private key and address + fromAddr := wallet.Address() + privateKey := wallet.PrivateKey() + + // Find gaspriceoracle contract + gpoContract, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, l2Client) + require.NoError(t, err) + + // Get wallet balance before test + startBalance, err := l2Client.BalanceAt(ctx, fromAddr, startBlockNumber) + require.NoError(t, err) + require.Greater(t, startBalance.Uint64(), big.NewInt(0).Uint64()) + + // Get initial balances of fee recipients + baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + genesisBlock, err := l2Client.BlockByNumber(ctx, startBlockNumber) + require.NoError(t, err) + + coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), startBlockNumber) + require.NoError(t, err) + + // Send a simple transfer from wallet to a test address + transferAmount := big.NewInt(params.Ether / 10) // 0.1 ETH + targetAddr := common.Address{0xff, 0xff} + + // Get suggested gas tip from the client instead of using a hardcoded value + gasTip, err := l2Client.SuggestGasTipCap(ctx) + require.NoError(t, err, "Failed to get suggested gas tip") + + // Estimate gas for the transaction instead of using a hardcoded value + msg := ethereum.CallMsg{ + From: fromAddr, + To: &targetAddr, + Value: transferAmount, + } + gasLimit, err := l2Client.EstimateGas(ctx, msg) + require.NoError(t, err, "Failed to estimate gas") + + // Create and sign transaction with the suggested values + nonce, err := l2Client.PendingNonceAt(ctx, fromAddr) + require.NoError(t, err) + + // Get latest header to get the base fee + header, err = l2Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + + // Calculate a reasonable gas fee cap based on the base fee + // A common approach is to set fee cap to 2x the base fee + tip + gasFeeCap := new(big.Int).Add( + new(big.Int).Mul(header.BaseFee, big.NewInt(2)), + gasTip, + ) + + txData := &gethTypes.DynamicFeeTx{ + ChainID: l2Chain.ID(), + Nonce: nonce, + GasTipCap: gasTip, + GasFeeCap: gasFeeCap, + Gas: gasLimit, + To: &targetAddr, + Value: transferAmount, + Data: nil, + } + + // Sign transaction + tx := gethTypes.NewTx(txData) + signedTx, err := gethTypes.SignTx(tx, gethTypes.LatestSignerForChainID(l2Chain.ID()), privateKey) + require.NoError(t, err) + + // Send transaction + err = l2Client.SendTransaction(ctx, signedTx) + require.NoError(t, err) + + // Wait for transaction receipt with timeout + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + receipt, err := waitForTransaction(ctx, l2Client, signedTx.Hash()) + require.NoError(t, err, "Failed to wait for transaction receipt") + require.NotNil(t, receipt) + require.Equal(t, gethTypes.ReceiptStatusSuccessful, receipt.Status) + + // Get block header where transaction was included + header, err = l2Client.HeaderByNumber(ctx, receipt.BlockNumber) + require.NoError(t, err) + + // Get final balances after transaction + coinbaseEndBalance, err := l2Client.BalanceAt(ctx, header.Coinbase, header.Number) + require.NoError(t, err) + + endBalance, err := l2Client.BalanceAt(ctx, fromAddr, header.Number) + require.NoError(t, err) + + baseFeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, header.Number) + require.NoError(t, err) + + operatorFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, header.Number) + require.NoError(t, err) + + l1FeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, header.Number) + require.NoError(t, err) + + sequencerFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, header.Number) + require.NoError(t, err) + + // Calculate differences in balances + baseFeeRecipientDiff := new(big.Int).Sub(baseFeeRecipientEndBalance, baseFeeRecipientStartBalance) + l1FeeRecipientDiff := new(big.Int).Sub(l1FeeRecipientEndBalance, l1FeeRecipientStartBalance) + sequencerFeeVaultDiff := new(big.Int).Sub(sequencerFeeVaultEndBalance, sequencerFeeVaultStartBalance) + coinbaseDiff := new(big.Int).Sub(coinbaseEndBalance, coinbaseStartBalance) + operatorFeeVaultDiff := new(big.Int).Sub(operatorFeeVaultEndBalance, operatorFeeVaultStartBalance) + + // Verify L2 fee + l2Fee := new(big.Int).Mul(gasTip, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, sequencerFeeVaultDiff, coinbaseDiff, "coinbase is always sequencer fee vault") + require.Equal(t, l2Fee, coinbaseDiff, "l2 fee mismatch") + require.Equal(t, l2Fee, sequencerFeeVaultDiff) + + // Verify base fee + baseFee := new(big.Int).Mul(header.BaseFee, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, baseFee, baseFeeRecipientDiff, "base fee mismatch") + + // Verify L1 fee + txBytes, err := tx.MarshalBinary() + require.NoError(t, err) + + // Calculate L1 fee based on transaction data and blocktime + l1Fee := l1CostFn(tx.RollupCostData(), header.Time) + require.Equal(t, l1Fee, l1FeeRecipientDiff, "L1 fee mismatch") + + // Calculate operator fee + expectedOperatorFee := operatorFeeFn(receipt.GasUsed, header.Time) + expectedOperatorFeeVaultEndBalance := new(big.Int).Sub(operatorFeeVaultStartBalance, expectedOperatorFee.ToBig()) + require.True(t, + operatorFeeVaultDiff.Cmp(expectedOperatorFee.ToBig()) == 0, + "operator fee mismatch: operator fee vault start balance %v, actual end balance %v, expected end balance %v", + operatorFeeVaultStartBalance, + operatorFeeVaultEndBalance, + expectedOperatorFeeVaultEndBalance, + ) + + gpoFjord, err := gpoContract.IsFjord(&bind.CallOpts{BlockNumber: header.Number}) + require.NoError(t, err) + require.True(t, gpoFjord, "GPO must report Fjord") + + // Verify gas price oracle L1 fee calculation + gpoL1Fee, err := gpoContract.GetL1Fee(&bind.CallOpts{BlockNumber: header.Number}, txBytes) + require.NoError(t, err) + require.Equal(t, l1Fee, gpoL1Fee, "GPO reports L1 fee mismatch") + + // Verify receipt L1 fee + require.Equal(t, receipt.L1Fee, l1Fee, "l1 fee in receipt is correct") + + // Calculate total fee and verify wallet balance difference + totalFeeRecipient := new(big.Int).Add(baseFeeRecipientDiff, sequencerFeeVaultDiff) + totalFee := new(big.Int).Add(totalFeeRecipient, l1FeeRecipientDiff) + totalFee = new(big.Int).Add(totalFee, operatorFeeVaultDiff) + + balanceDiff := new(big.Int).Sub(startBalance, endBalance) + balanceDiff.Sub(balanceDiff, transferAmount) + require.Equal(t, balanceDiff, totalFee, "balances should add up") + } +} diff --git a/kurtosis-devnet/tests/interop/boilerplate_test.go b/op-acceptance-tests/tests/interop/boilerplate_test.go similarity index 100% rename from kurtosis-devnet/tests/interop/boilerplate_test.go rename to op-acceptance-tests/tests/interop/boilerplate_test.go diff --git a/kurtosis-devnet/tests/interop/interop_smoke_test.go b/op-acceptance-tests/tests/interop/interop_smoke_test.go similarity index 100% rename from kurtosis-devnet/tests/interop/interop_smoke_test.go rename to op-acceptance-tests/tests/interop/interop_smoke_test.go diff --git a/kurtosis-devnet/tests/interop/mocks_test.go b/op-acceptance-tests/tests/interop/mocks_test.go similarity index 100% rename from kurtosis-devnet/tests/interop/mocks_test.go rename to op-acceptance-tests/tests/interop/mocks_test.go diff --git a/kurtosis-devnet/tests/isthmus/erc20_bridge_test.go b/op-acceptance-tests/tests/isthmus/erc20_bridge_test.go similarity index 100% rename from kurtosis-devnet/tests/isthmus/erc20_bridge_test.go rename to op-acceptance-tests/tests/isthmus/erc20_bridge_test.go diff --git a/op-acceptance-tests/tests/isthmus/fees_test.go b/op-acceptance-tests/tests/isthmus/fees_test.go new file mode 100644 index 0000000000000..6e860446dd9dd --- /dev/null +++ b/op-acceptance-tests/tests/isthmus/fees_test.go @@ -0,0 +1,302 @@ +package isthmus + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestFees verifies that L1/L2 fees are handled properly in different fork configurations +func TestFees(t *testing.T) { + // Define which L2 chain we'll test + chainIdx := uint64(0) + + // Get validators and getters for accessing the system and wallets + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(chainIdx, types.NewBalance(big.NewInt(params.Ether))) + + // Run isthmus test + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Isthmus) + systest.SystemTest(t, + feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx), + lowLevelSystemValidator, + walletValidator, + forkValidator, + ) +} + +// stateGetterAdapter adapts the ethclient to implement the StateGetter interface +type stateGetterAdapter struct { + ctx context.Context + t systest.T + client *ethclient.Client +} + +// GetState implements the StateGetter interface +func (sga *stateGetterAdapter) GetState(addr common.Address, key common.Hash) common.Hash { + var result common.Hash + val, err := sga.client.StorageAt(sga.ctx, addr, key, nil) + require.NoError(sga.t, err) + copy(result[:], val) + return result +} + +// waitForTransaction polls for a transaction receipt until it is available or the context is canceled. +// It's a simpler version of the functionality in SimpleTxManager. +func waitForTransaction(ctx context.Context, client *ethclient.Client, hash common.Hash) (*gethTypes.Receipt, error) { + ticker := time.NewTicker(500 * time.Millisecond) // Poll every 500ms + defer ticker.Stop() + + for { + receipt, err := client.TransactionReceipt(ctx, hash) + if receipt != nil && err == nil { + return receipt, nil + } else if err != nil && !errors.Is(err, ethereum.NotFound) { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + // Continue polling + } + } +} + +// feesTestScenario creates a test scenario for verifying fee calculations +func feesTestScenario( + lowLevelSystemGetter validators.LowLevelSystemGetter, + walletGetter validators.WalletGetter, + chainIdx uint64, +) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + // Get the low-level system and wallet + llsys := lowLevelSystemGetter(ctx) + wallet := walletGetter(ctx) + + // Get the L2 client + l2Chain := llsys.L2s()[chainIdx] + l2Client, err := l2Chain.Client() + require.NoError(t, err) + + // TODO: Wait for first block after genesis + // The genesis block has zero L1Block values and will throw off the GPO checks + _, err = l2Client.HeaderByNumber(ctx, big.NewInt(1)) + require.NoError(t, err) + + // Get the genesis config + chainConfig, err := l2Chain.Config() + require.NoError(t, err) + + // Create state getter adapter for L1 cost function + sga := &stateGetterAdapter{ + ctx: ctx, + t: t, + client: l2Client, + } + + // Create L1 cost function + l1CostFn := gethTypes.NewL1CostFunc(chainConfig, sga) + + // Create operator fee function + operatorFeeFn := gethTypes.NewOperatorCostFunc(chainConfig, sga) + + // Get wallet private key and address + fromAddr := wallet.Address() + privateKey := wallet.PrivateKey() + + // Find gaspriceoracle contract + gpoContract, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, l2Client) + require.NoError(t, err) + + block, err := l2Client.BlockByNumber(t.Context(), nil) + require.NoError(t, err) + startBlockNumber := block.Number() + + // Get wallet balance before test + startBalance, err := l2Client.BalanceAt(ctx, fromAddr, startBlockNumber) + require.NoError(t, err) + require.Greater(t, startBalance.Uint64(), big.NewInt(0).Uint64()) + + // Get initial balances of fee recipients + baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + genesisBlock, err := l2Client.BlockByNumber(ctx, startBlockNumber) + require.NoError(t, err) + + coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), startBlockNumber) + require.NoError(t, err) + + // Send a simple transfer from wallet to a test address + transferAmount := big.NewInt(params.Ether / 10) // 0.1 ETH + targetAddr := common.Address{0xff, 0xff} + + // Get suggested gas tip from the client instead of using a hardcoded value + gasTip, err := l2Client.SuggestGasTipCap(ctx) + require.NoError(t, err, "Failed to get suggested gas tip") + + // Estimate gas for the transaction instead of using a hardcoded value + msg := ethereum.CallMsg{ + From: fromAddr, + To: &targetAddr, + Value: transferAmount, + } + gasLimit, err := l2Client.EstimateGas(ctx, msg) + require.NoError(t, err, "Failed to estimate gas") + + // Create and sign transaction with the suggested values + nonce, err := l2Client.PendingNonceAt(ctx, fromAddr) + require.NoError(t, err) + + // Get latest header to get the base fee + header, err := l2Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + + // Calculate a reasonable gas fee cap based on the base fee + // A common approach is to set fee cap to 2x the base fee + tip + gasFeeCap := new(big.Int).Add( + new(big.Int).Mul(header.BaseFee, big.NewInt(2)), + gasTip, + ) + + txData := &gethTypes.DynamicFeeTx{ + ChainID: l2Chain.ID(), + Nonce: nonce, + GasTipCap: gasTip, + GasFeeCap: gasFeeCap, + Gas: gasLimit, + To: &targetAddr, + Value: transferAmount, + Data: nil, + } + + // Sign transaction + tx := gethTypes.NewTx(txData) + signedTx, err := gethTypes.SignTx(tx, gethTypes.LatestSignerForChainID(l2Chain.ID()), privateKey) + require.NoError(t, err) + + // Send transaction + err = l2Client.SendTransaction(ctx, signedTx) + require.NoError(t, err) + + // Wait for transaction receipt with timeout + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + receipt, err := waitForTransaction(ctx, l2Client, signedTx.Hash()) + require.NoError(t, err, "Failed to wait for transaction receipt") + require.NotNil(t, receipt) + require.Equal(t, gethTypes.ReceiptStatusSuccessful, receipt.Status) + + // Get block header where transaction was included + header, err = l2Client.HeaderByNumber(ctx, receipt.BlockNumber) + require.NoError(t, err) + require.Equal(t, header.Coinbase, predeploys.SequencerFeeVaultAddr, "coinbase address should always be the same as the sequencer fee vault address") + + // Get final balances after transaction + coinbaseEndBalance, err := l2Client.BalanceAt(ctx, header.Coinbase, header.Number) + require.NoError(t, err) + + endBalance, err := l2Client.BalanceAt(ctx, fromAddr, header.Number) + require.NoError(t, err) + + baseFeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, header.Number) + require.NoError(t, err) + + operatorFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, header.Number) + require.NoError(t, err) + + l1FeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, header.Number) + require.NoError(t, err) + + sequencerFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, header.Number) + require.NoError(t, err) + + // Calculate differences in balances + baseFeeRecipientDiff := new(big.Int).Sub(baseFeeRecipientEndBalance, baseFeeRecipientStartBalance) + l1FeeRecipientDiff := new(big.Int).Sub(l1FeeRecipientEndBalance, l1FeeRecipientStartBalance) + sequencerFeeVaultDiff := new(big.Int).Sub(sequencerFeeVaultEndBalance, sequencerFeeVaultStartBalance) + coinbaseDiff := new(big.Int).Sub(coinbaseEndBalance, coinbaseStartBalance) + operatorFeeVaultDiff := new(big.Int).Sub(operatorFeeVaultEndBalance, operatorFeeVaultStartBalance) + + // Verify L2 fee + l2Fee := new(big.Int).Mul(gasTip, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, sequencerFeeVaultDiff, coinbaseDiff, "coinbase is always sequencer fee vault") + require.Equal(t, l2Fee, coinbaseDiff, "l2 fee mismatch") + require.Equal(t, l2Fee, sequencerFeeVaultDiff) + + // Verify base fee + baseFee := new(big.Int).Mul(header.BaseFee, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, baseFee, baseFeeRecipientDiff, "base fee mismatch") + + // Verify L1 fee + txBytes, err := tx.MarshalBinary() + require.NoError(t, err) + + // Calculate L1 fee based on transaction data and blocktime + l1Fee := l1CostFn(tx.RollupCostData(), header.Time) + require.Equal(t, l1Fee, l1FeeRecipientDiff, "L1 fee mismatch") + + // Calculate operator fee + expectedOperatorFee := operatorFeeFn(receipt.GasUsed, header.Time) + expectedOperatorFeeVaultEndBalance := new(big.Int).Sub(operatorFeeVaultStartBalance, expectedOperatorFee.ToBig()) + require.True(t, + operatorFeeVaultDiff.Cmp(expectedOperatorFee.ToBig()) == 0, + "operator fee mismatch: operator fee vault start balance %v, actual end balance %v, expected end balance %v", + operatorFeeVaultStartBalance, + operatorFeeVaultEndBalance, + expectedOperatorFeeVaultEndBalance, + ) + + gpoIsthmus, err := gpoContract.IsIsthmus(&bind.CallOpts{BlockNumber: header.Number}) + require.NoError(t, err) + require.True(t, gpoIsthmus, "GPO and chain must have same isthmus view") + + // Verify gas price oracle L1 fee calculation + adjustedGPOFee, err := gpoContract.GetL1Fee(&bind.CallOpts{BlockNumber: header.Number}, txBytes) + require.NoError(t, err) + + require.Equal(t, l1Fee, adjustedGPOFee, "GPO reports L1 fee mismatch") + + // Verify receipt L1 fee + require.Equal(t, receipt.L1Fee, l1Fee, "l1 fee in receipt is correct") + + // Calculate total fee and verify wallet balance difference + totalFeeRecipient := new(big.Int).Add(baseFeeRecipientDiff, sequencerFeeVaultDiff) + totalFee := new(big.Int).Add(totalFeeRecipient, l1FeeRecipientDiff) + totalFee = new(big.Int).Add(totalFee, operatorFeeVaultDiff) + + balanceDiff := new(big.Int).Sub(startBalance, endBalance) + balanceDiff.Sub(balanceDiff, transferAmount) + require.Equal(t, balanceDiff, totalFee, "balances should add up") + } +} diff --git a/kurtosis-devnet/tests/isthmus/withdrawal_root_test.go b/op-acceptance-tests/tests/isthmus/withdrawal_root_test.go similarity index 100% rename from kurtosis-devnet/tests/isthmus/withdrawal_root_test.go rename to op-acceptance-tests/tests/isthmus/withdrawal_root_test.go