diff --git a/op-e2e/actions/helpers/user.go b/op-e2e/actions/helpers/user.go index f85368baaaa1f..d26474232db01 100644 --- a/op-e2e/actions/helpers/user.go +++ b/op-e2e/actions/helpers/user.go @@ -166,6 +166,7 @@ func (s *BasicUser[B]) ActResetTxOpts(t Testing) { } func (s *BasicUser[B]) ActRandomTxToAddr(t Testing) { + t.Helper() i := s.rng.Intn(len(s.env.AddressCorpora)) var to *common.Address if i > 0 { // 0 == nil @@ -204,6 +205,7 @@ func (s *BasicUser[B]) ActSetTxValue(value *big.Int) Action { } func (s *BasicUser[B]) ActRandomTxData(t Testing) { + t.Helper() dataLen := s.rng.Intn(128_000) out := make([]byte, dataLen) _, err := s.rng.Read(out[:]) @@ -212,6 +214,7 @@ func (s *BasicUser[B]) ActRandomTxData(t Testing) { } func (s *BasicUser[B]) PendingNonce(t Testing) uint64 { + t.Helper() if s.txOpts.Nonce != nil { return s.txOpts.Nonce.Uint64() } @@ -229,6 +232,7 @@ func (s *BasicUser[B]) TxValue() *big.Int { } func (s *BasicUser[B]) LastTxReceipt(t Testing) *types.Receipt { + t.Helper() require.NotEqual(t, s.lastTxHash, common.Hash{}, "must send tx before getting last receipt") receipt, err := s.env.EthCl.TransactionReceipt(t.Ctx(), s.lastTxHash) require.NoError(t, err) @@ -264,6 +268,7 @@ func (s *BasicUser[B]) MakeTransaction(t Testing) *types.Transaction { // ActMakeTx makes a tx with the predetermined contents (see randomization and other actions) // and sends it to the tx pool func (s *BasicUser[B]) ActMakeTx(t Testing) { + t.Helper() tx := s.MakeTransaction(t) err := s.env.EthCl.SendTransaction(t.Ctx(), tx) require.NoError(t, err, "must send tx") @@ -279,6 +284,7 @@ func (s *BasicUser[B]) ActCheckReceiptStatusOfLastTx(success bool) func(t Testin } func (s *BasicUser[B]) CheckReceipt(t Testing, success bool, txHash common.Hash) *types.Receipt { + t.Helper() receipt, err := s.env.EthCl.TransactionReceipt(t.Ctx(), txHash) if receipt != nil && err == nil { expected := types.ReceiptStatusFailed @@ -391,6 +397,7 @@ func (s *CrossLayerUser) ActCheckDepositStatus(l1Success, l2Success bool) Action } func (s *CrossLayerUser) CheckDepositTx(t Testing, l1TxHash common.Hash, index int, l1Success, l2Success bool) { + t.Helper() depositReceipt := s.L1.CheckReceipt(t, l1Success, l1TxHash) if depositReceipt == nil { require.False(t, l1Success) @@ -427,7 +434,15 @@ func (s *CrossLayerUser) Address() common.Address { return s.L1.address } -func (s *CrossLayerUser) getLatestWithdrawalParams(t Testing) (*withdrawals.ProvenWithdrawalParameters, error) { +func (s *CrossLayerUser) GetLastDepositL2Receipt(t Testing) (*types.Receipt, error) { + depositL1Receipt := s.L1.CheckReceipt(t, true, s.lastL1DepositTxHash) + reconstructedDep, err := derive.UnmarshalDepositLogEvent(depositL1Receipt.Logs[0]) + require.NoError(t, err, "Could not reconstruct L2 Deposit") + l2Tx := types.NewTx(reconstructedDep) + return s.L2.CheckReceipt(t, true, l2Tx.Hash()), nil +} + +func (s *CrossLayerUser) getLastWithdrawalParams(t Testing) (*withdrawals.ProvenWithdrawalParameters, error) { receipt := s.L2.CheckReceipt(t, true, s.lastL2WithdrawalTxHash) l2WithdrawalBlock, err := s.L2.env.EthCl.BlockByNumber(t.Ctx(), receipt.BlockNumber) require.NoError(t, err) @@ -504,7 +519,7 @@ func (s *CrossLayerUser) ActProveWithdrawal(t Testing) { // ProveWithdrawal creates a L1 proveWithdrawal tx for the given L2 withdrawal tx, returning the tx hash. func (s *CrossLayerUser) ProveWithdrawal(t Testing, l2TxHash common.Hash) common.Hash { - params, err := s.getLatestWithdrawalParams(t) + params, err := s.getLastWithdrawalParams(t) if err != nil { t.InvalidAction("cannot prove withdrawal: %v", err) return common.Hash{} @@ -543,7 +558,7 @@ func (s *CrossLayerUser) ActCompleteWithdrawal(t Testing) { // CompleteWithdrawal creates a L1 withdrawal finalization tx for the given L2 withdrawal tx, returning the tx hash. // It's an invalid action to attempt to complete a withdrawal that has not passed the L1 finalization period yet func (s *CrossLayerUser) CompleteWithdrawal(t Testing, l2TxHash common.Hash) common.Hash { - params, err := s.getLatestWithdrawalParams(t) + params, err := s.getLastWithdrawalParams(t) if err != nil { t.InvalidAction("cannot complete withdrawal: %v", err) return common.Hash{} @@ -576,7 +591,7 @@ func (s *CrossLayerUser) ActResolveClaim(t Testing) { // ResolveClaim creates a L1 resolveClaim tx for the given L2 withdrawal tx, returning the tx hash. func (s *CrossLayerUser) ResolveClaim(t Testing, l2TxHash common.Hash) common.Hash { - params, err := s.getLatestWithdrawalParams(t) + params, err := s.getLastWithdrawalParams(t) if err != nil { t.InvalidAction("cannot resolve claim: %v", err) return common.Hash{} @@ -614,7 +629,7 @@ func (s *CrossLayerUser) ActResolve(t Testing) { // Resolve creates a L1 resolve tx for the given L2 withdrawal tx, returning the tx hash. func (s *CrossLayerUser) Resolve(t Testing, l2TxHash common.Hash) common.Hash { - params, err := s.getLatestWithdrawalParams(t) + params, err := s.getLastWithdrawalParams(t) if err != nil { t.InvalidAction("cannot resolve game: %v", err) return common.Hash{} diff --git a/op-e2e/actions/proofs/operator_fee_test.go b/op-e2e/actions/proofs/operator_fee_test.go index cfc4fa63db8c8..8c5ddccad33d4 100644 --- a/op-e2e/actions/proofs/operator_fee_test.go +++ b/op-e2e/actions/proofs/operator_fee_test.go @@ -4,25 +4,62 @@ import ( "math/big" "testing" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" "github.com/ethereum-optimism/optimism/op-e2e/bindings" - "github.com/ethereum-optimism/optimism/op-program/client/claim" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" ) func Test_ProgramAction_OperatorFeeConstistency(gt *testing.T) { + type testCase int64 + + const ( + NormalTx testCase = iota + DepositTx + StateRefund + IsthmusTransitionBlock + ) const testOperatorFeeScalar = uint32(20000) const testOperatorFeeConstant = uint64(500) + const testDepositValue = uint64(10000) + testStorageUpdateContractAddress := common.HexToAddress("0xffffffff") + // contract TestSetter { + // uint x; + // function set(uint _x) public { x = _x; } + // } + // The deployed bytecode below is from the contract above + testStorageUpdateContractCode := common.FromHex("0x6080604052348015600e575f80fd5b50600436106026575f3560e01c806360fe47b114602a575b5f80fd5b60406004803603810190603c9190607d565b6042565b005b805f8190555050565b5f80fd5b5f819050919050565b605f81604f565b81146068575f80fd5b50565b5f813590506077816058565b92915050565b5f60208284031215608f57608e604b565b5b5f609a84828501606b565b9150509291505056fea26469706673582212201712a1e6e9c5e2ba1f8f7403f5d6e00090c6fa2b70c632beea4be8009331bd2064736f6c63430008190033") - runIsthmusDerivationTest := func(gt *testing.T, testCfg *helpers.TestCfg[any]) { + runIsthmusDerivationTest := func(gt *testing.T, testCfg *helpers.TestCfg[testCase]) { t := actionsHelpers.NewDefaultTesting(gt) + deployConfigOverrides := func(dp *genesis.DeployConfig) {} + + if testCfg.Custom == StateRefund { + testCfg.Allocs = actionsHelpers.DefaultAlloc + testCfg.Allocs.L2Alloc = make(map[common.Address]types.Account) + testCfg.Allocs.L2Alloc[testStorageUpdateContractAddress] = types.Account{ + Code: testStorageUpdateContractCode, + Nonce: 1, + Balance: new(big.Int), + } + } + + if testCfg.Custom == IsthmusTransitionBlock { + deployConfigOverrides = func(dp *genesis.DeployConfig) { + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(0)) + dp.L2GenesisIsthmusTimeOffset = ptr(hexutil.Uint64(13)) + } + } - env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), helpers.NewBatcherCfg()) + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), helpers.NewBatcherCfg(), deployConfigOverrides) balanceAt := func(a common.Address) *big.Int { t.Helper() @@ -31,6 +68,31 @@ func Test_ProgramAction_OperatorFeeConstistency(gt *testing.T) { return bal } + getCurrentBalances := func() (alice *big.Int, l1FeeVault *big.Int, baseFeeVault *big.Int, sequencerFeeVault *big.Int, operatorFeeVault *big.Int) { + alice = balanceAt(env.Alice.Address()) + l1FeeVault = balanceAt(predeploys.L1FeeVaultAddr) + baseFeeVault = balanceAt(predeploys.BaseFeeVaultAddr) + sequencerFeeVault = balanceAt(predeploys.SequencerFeeVaultAddr) + operatorFeeVault = balanceAt(predeploys.OperatorFeeVaultAddr) + + return alice, l1FeeVault, baseFeeVault, sequencerFeeVault, operatorFeeVault + } + + setStorageInUpdateContractTo := func(value byte) { + t.Helper() + input := common.RightPadBytes(common.FromHex("0x60fe47b1"), 36) + input[35] = value + env.Sequencer.ActL2StartBlock(t) + env.Alice.L2.ActResetTxOpts(t) + env.Alice.L2.ActSetTxToAddr(&testStorageUpdateContractAddress)(t) + env.Alice.L2.ActSetTxCalldata(input)(t) + env.Alice.L2.ActMakeTx(t) + env.Engine.ActL2IncludeTx(env.Alice.Address())(t) + env.Sequencer.ActL2EndBlock(t) + r := env.Alice.L2.LastTxReceipt(t) + require.Equal(t, types.ReceiptStatusSuccessful, r.Status, "tx unsuccessful") + } + t.Logf("L2 Genesis Time: %d, IsthmusTime: %d ", env.Sequencer.RollupCfg.Genesis.L2Time, *env.Sequencer.RollupCfg.IsthmusTime) sysCfgContract, err := bindings.NewSystemConfig(env.Sd.RollupCfg.L1SystemConfigAddress, env.Miner.EthClient()) @@ -50,86 +112,152 @@ func Test_ProgramAction_OperatorFeeConstistency(gt *testing.T) { // sequence L2 blocks, and submit with new batcher env.Sequencer.ActL1HeadSignal(t) env.Sequencer.ActBuildToL1Head(t) - env.Batcher.ActSubmitAll(t) - env.Miner.ActL1StartBlock(12)(t) - env.Miner.ActL1EndBlock(t) + env.BatchAndMine(t) + + env.Sequencer.ActL1HeadSignal(t) - aliceInitialBalance := balanceAt(env.Alice.Address()) - operatorFeeVaultInitialBalance := balanceAt(predeploys.OperatorFeeVaultAddr) + var aliceInitialBalance *big.Int + var baseFeeVaultInitialBalance *big.Int + var l1FeeVaultInitialBalance *big.Int + var sequencerFeeVaultInitialBalance *big.Int + var operatorFeeVaultInitialBalance *big.Int - require.Equal(t, operatorFeeVaultInitialBalance.Sign(), 0) + var receipt *types.Receipt - env.Sequencer.ActL2StartBlock(t) - // Send an L2 tx - env.Alice.L2.ActResetTxOpts(t) - env.Alice.L2.ActSetTxToAddr(&env.Dp.Addresses.Bob) - env.Alice.L2.ActMakeTx(t) - env.Engine.ActL2IncludeTx(env.Alice.Address())(t) - env.Sequencer.ActL2EndBlock(t) + switch testCfg.Custom { + case NormalTx, IsthmusTransitionBlock: + aliceInitialBalance, l1FeeVaultInitialBalance, baseFeeVaultInitialBalance, sequencerFeeVaultInitialBalance, operatorFeeVaultInitialBalance = getCurrentBalances() - receipt := env.Alice.L2.LastTxReceipt(t) + require.Equal(t, operatorFeeVaultInitialBalance.Sign(), 0) - // Check that the operator fee was applied - require.Equal(t, testOperatorFeeScalar, uint32(*receipt.OperatorFeeScalar)) - require.Equal(t, testOperatorFeeConstant, *receipt.OperatorFeeConstant) + // Send an L2 tx + env.Sequencer.ActL2StartBlock(t) + env.Alice.L2.ActResetTxOpts(t) + env.Alice.L2.ActSetTxToAddr(&env.Dp.Addresses.Bob) + env.Alice.L2.ActMakeTx(t) + // we usually don't include txs in the transition block, so we force-include it + env.Engine.ActL2IncludeTxIgnoreForcedEmpty(env.Alice.Address())(t) + env.Sequencer.ActL2EndBlock(t) - l1FeeVaultBalance := balanceAt(predeploys.L1FeeVaultAddr) - baseFeeVaultBalance := balanceAt(predeploys.BaseFeeVaultAddr) - sequencerFeeVaultBalance := balanceAt(predeploys.SequencerFeeVaultAddr) - operatorFeeVaultFinalBalance := balanceAt(predeploys.OperatorFeeVaultAddr) - aliceFinalBalance := balanceAt(env.Alice.Address()) + if testCfg.Custom == IsthmusTransitionBlock { + require.True(t, env.Sd.RollupCfg.IsIsthmusActivationBlock(env.Sequencer.L2Unsafe().Time)) + } - require.True(t, aliceFinalBalance.Cmp(aliceInitialBalance) < 0, "Alice's balance should decrease") + case StateRefund: + setStorageInUpdateContractTo(1) + rSet := env.Alice.L2.LastTxReceipt(t) + require.Equal(t, uint64(43696), rSet.GasUsed) + aliceInitialBalance, l1FeeVaultInitialBalance, baseFeeVaultInitialBalance, sequencerFeeVaultInitialBalance, operatorFeeVaultInitialBalance = getCurrentBalances() + setStorageInUpdateContractTo(0) + rUnset := env.Alice.L2.LastTxReceipt(t) + // we assert on the exact gas used to show that a refund is happening + require.Equal(t, uint64(21784), rUnset.GasUsed) - // Check that the operator fee sent to the vault is correct - require.Equal(t, - new(big.Int).Add( - new(big.Int).Div( - new(big.Int).Mul( - new(big.Int).SetUint64(receipt.GasUsed), - new(big.Int).SetUint64(uint64(testOperatorFeeScalar)), + case DepositTx: + aliceInitialBalance, l1FeeVaultInitialBalance, baseFeeVaultInitialBalance, sequencerFeeVaultInitialBalance, operatorFeeVaultInitialBalance = getCurrentBalances() + + bobInitialBalance := balanceAt(env.Bob.Address()) + + // regular Deposit, in new L1 block + env.Alice.L1.ActResetTxOpts(t) + env.Alice.L2.ActSetTxToAddr(&env.Dp.Addresses.Bob)(t) + env.Alice.L2.ActSetTxValue(new(big.Int).SetUint64(testDepositValue))(t) + env.Alice.ActDeposit(t) + env.Miner.ActL1StartBlock(12)(t) + env.Miner.ActL1IncludeTx(env.Alice.Address())(t) + env.Miner.ActL1EndBlock(t) + + // sync sequencer build enough blocks to adopt latest L1 origin + env.Sequencer.ActL1HeadSignal(t) + env.Sequencer.ActBuildToL1HeadUnsafe(t) + + env.Alice.ActCheckDepositStatus(true, true)(t) + + bobFinalBalance := balanceAt(env.Bob.Address()) + + require.Equal(t, bobInitialBalance.Uint64()+testDepositValue, bobFinalBalance.Uint64()) + + receipt, err = env.Alice.GetLastDepositL2Receipt(t) + require.NoError(t, err) + } + + aliceFinalBalance, l1FeeVaultFinalBalance, baseFeeVaultFinalBalance, sequencerFeeVaultFinalBalance, operatorFeeVaultFinalBalance := getCurrentBalances() + + if receipt == nil { + receipt = env.Alice.L2.LastTxReceipt(t) + } + + if testCfg.Custom == DepositTx || testCfg.Custom == IsthmusTransitionBlock { + require.Nil(t, receipt.OperatorFeeScalar) + require.Nil(t, receipt.OperatorFeeConstant) + + // Nothing should has been sent to operator fee vault + require.Equal(t, operatorFeeVaultInitialBalance, operatorFeeVaultFinalBalance) + } else { + // Check that the operator fee was applied + require.Equal(t, testOperatorFeeScalar, uint32(*receipt.OperatorFeeScalar)) + require.Equal(t, testOperatorFeeConstant, *receipt.OperatorFeeConstant) + + // Check that the operator fee sent to the vault is correct + require.Equal(t, + new(big.Int).Add( + new(big.Int).Div( + new(big.Int).Mul( + new(big.Int).SetUint64(receipt.GasUsed), + new(big.Int).SetUint64(uint64(testOperatorFeeScalar)), + ), + new(big.Int).SetUint64(1e6), ), - new(big.Int).SetUint64(1e6), + new(big.Int).SetUint64(testOperatorFeeConstant), ), - new(big.Int).SetUint64(testOperatorFeeConstant), - ), - operatorFeeVaultFinalBalance, - ) + new(big.Int).Sub(operatorFeeVaultFinalBalance, operatorFeeVaultInitialBalance), + ) + } + require.True(t, aliceFinalBalance.Cmp(aliceInitialBalance) < 0, "Alice's balance should decrease") // Check that no Ether has been minted or burned - // All vault balances are 0 at the beginning of the test finalTotalBalance := new(big.Int).Add( aliceFinalBalance, new(big.Int).Add( - new(big.Int).Add(l1FeeVaultBalance, sequencerFeeVaultBalance), - new(big.Int).Add(operatorFeeVaultFinalBalance, baseFeeVaultBalance), + new(big.Int).Add( + new(big.Int).Sub(l1FeeVaultFinalBalance, l1FeeVaultInitialBalance), + new(big.Int).Sub(sequencerFeeVaultFinalBalance, sequencerFeeVaultInitialBalance), + ), + new(big.Int).Add( + new(big.Int).Sub(operatorFeeVaultFinalBalance, operatorFeeVaultInitialBalance), + new(big.Int).Sub(baseFeeVaultFinalBalance, baseFeeVaultInitialBalance), + ), ), ) - require.Equal(t, aliceInitialBalance, finalTotalBalance) + if testCfg.Custom == DepositTx { + // Minus the deposit value that was sent to Bob + require.Equal(t, aliceInitialBalance.Uint64()-testDepositValue, finalTotalBalance.Uint64()) + } else { + require.Equal(t, aliceInitialBalance, finalTotalBalance) + } - l2SafeHead := env.Sequencer.L2Safe() + l2UnsafeHead := env.Engine.L2Chain().CurrentHeader() - env.RunFaultProofProgram(t, l2SafeHead.Number, testCfg.CheckResult, testCfg.InputParams...) - } + env.BatchAndMine(t) + env.Sequencer.ActL1HeadSignal(t) + env.Sequencer.ActL2PipelineFull(t) - matrix := helpers.NewMatrix[any]() - defer matrix.Run(gt) + l2SafeHead := env.Engine.L2Chain().CurrentSafeBlock() - matrix.AddTestCase( - "HonestClaim-OperatorFeeConstistency", - nil, - helpers.NewForkMatrix(helpers.Isthmus), - runIsthmusDerivationTest, - helpers.ExpectNoError(), - ) + require.Equal(t, eth.HeaderBlockID(l2SafeHead), eth.HeaderBlockID(l2UnsafeHead), "derivation leads to the same block") - matrix.AddTestCase( - "JunkClaim-OperatorFeeConstistency", - nil, - helpers.NewForkMatrix(helpers.Isthmus), - runIsthmusDerivationTest, - helpers.ExpectError(claim.ErrClaimNotValid), - helpers.WithL2Claim(common.HexToHash("0xdeadbeef")), - ) + env.RunFaultProofProgram(t, l2SafeHead.Number.Uint64(), testCfg.CheckResult, testCfg.InputParams...) + } + + matrix := helpers.NewMatrix[testCase]() + matrix.AddDefaultTestCasesWithName("NormalTx", NormalTx, helpers.NewForkMatrix(helpers.Isthmus), runIsthmusDerivationTest) + matrix.AddDefaultTestCasesWithName("DepositTx", DepositTx, helpers.NewForkMatrix(helpers.Isthmus), runIsthmusDerivationTest) + matrix.AddDefaultTestCasesWithName("StateRefund", StateRefund, helpers.NewForkMatrix(helpers.Isthmus), runIsthmusDerivationTest) + matrix.AddDefaultTestCasesWithName("IsthmusTransitionBlock", IsthmusTransitionBlock, helpers.NewForkMatrix(helpers.Holocene), runIsthmusDerivationTest) + matrix.Run(gt) +} + +func ptr[T any](v T) *T { + return &v }