diff --git a/go/integration_test/processor/gas_billing_test.go b/go/integration_test/processor/gas_billing_test.go new file mode 100644 index 000000000..ea8d6ad32 --- /dev/null +++ b/go/integration_test/processor/gas_billing_test.go @@ -0,0 +1,118 @@ +// Copyright (c) 2024 Fantom Foundation +// +// Use of this software is governed by the Business Source License included +// in the LICENSE file and at fantom.foundation/bsl11. +// +// Change Date: 2028-4-16 +// +// On the date above, in accordance with the Business Source License, use of +// this software will be governed by the GNU Lesser General Public License v3. + +package processor + +import ( + "fmt" + "testing" + + "github.com/Fantom-foundation/Tosca/go/tosca" + "github.com/Fantom-foundation/Tosca/go/tosca/vm" + op "github.com/ethereum/go-ethereum/core/vm" + "go.uber.org/mock/gomock" + + _ "github.com/Fantom-foundation/Tosca/go/processor/opera" // < registers opera processor for testing +) + +func TestProcessor_GasBillingEndToEnd(t *testing.T) { + senderBalance := tosca.NewValue(1000000) + gasLimit := tosca.Gas(100000) + gasRefund := tosca.Gas(3000) + gasPrice := tosca.NewValue(5) + gasLeftSuccess := tosca.Gas(5000) + + tests := map[string]struct { + result tosca.Result + gasUsed tosca.Gas + success bool + }{ + "success": { + result: tosca.Result{ + GasLeft: gasLeftSuccess, + Success: true, + GasRefund: gasRefund, + }, + gasUsed: gasLimit - (gasLeftSuccess - gasLeftSuccess/10 + gasRefund), + success: true, + }, + "failed": { + result: tosca.Result{ + GasLeft: 0, + Success: false, + GasRefund: gasRefund, + }, + gasUsed: gasLimit, + success: false, + }, + } + + ctrl := gomock.NewController(t) + interpreter := tosca.NewMockInterpreter(ctrl) + + sender := tosca.Address{1} + recipient := tosca.Address{2} + before := WorldState{ + sender: Account{Balance: senderBalance, Nonce: 4}, + recipient: Account{Balance: tosca.NewValue(0), + Code: tosca.Code{ + byte(vm.PUSH1), byte(0), // < push 0 + byte(vm.PUSH1), byte(0), // < push 0 + byte(op.RETURN), + }, + }, + } + + transaction := tosca.Transaction{ + Sender: sender, + Recipient: &recipient, + GasLimit: gasLimit, + GasPrice: gasPrice, + Nonce: 4, + } + + for name, test := range tests { + for processorName, processor := range processorsWithInterpreter("mockInterpreter", interpreter) { + t.Run(fmt.Sprintf("%s/%s", processorName, name), func(t *testing.T) { + + after := before.Clone() + afterBalance := tosca.Sub(senderBalance, gasPrice.Scale(uint64(test.gasUsed))) + after[sender] = Account{Balance: afterBalance, Nonce: after[sender].Nonce + 1} + + receipt := tosca.Receipt{ + Success: test.success, + GasUsed: test.gasUsed, + } + + scenario := Scenario{ + Before: before, + Transaction: transaction, + After: after, + Receipt: receipt, + } + + interpreter.EXPECT().Run(gomock.Any()).Return(test.result, nil) + scenario.Run(t, processor) + }) + } + } + +} + +func processorsWithInterpreter(name string, interpreter tosca.Interpreter) map[string]tosca.Processor { + factories := tosca.GetAllRegisteredProcessorFactories() + res := map[string]tosca.Processor{} + for processorName, factory := range factories { + processor := factory(interpreter) + res[fmt.Sprintf("%s/%s", processorName, name)] = processor + } + + return res +} diff --git a/go/integration_test/processor/processor_gas_test.go b/go/integration_test/processor/processor_gas_test.go index 6f51d6996..c5a0bb62e 100644 --- a/go/integration_test/processor/processor_gas_test.go +++ b/go/integration_test/processor/processor_gas_test.go @@ -20,10 +20,7 @@ import ( op "github.com/ethereum/go-ethereum/core/vm" ) -func getGasTestScenarios() map[string]Scenario { - // cost for 2 PUSH1 operations - const executionGasCost = 3 + 3 - +func gasTestScenarios() map[string]Scenario { exactTestCases := map[string]Scenario{ "ValueTransfer": { Before: WorldState{ @@ -145,132 +142,195 @@ func getGasTestScenarios() map[string]Scenario { }, } - allTestCases := make(map[string]Scenario) + testCases := make(map[string]Scenario) for name, exactScenario := range exactTestCases { gasTests := exactSufficientAndInsufficientScenarios(exactScenario, name) - maps.Copy(allTestCases, gasTests) + maps.Copy(testCases, gasTests) } + return testCases +} - allTestCases["SimpleCodeExact"] = Scenario{ - Before: WorldState{ - {1}: Account{Balance: tosca.NewValue(100), Nonce: 4}, - {2}: Account{Balance: tosca.NewValue(0), - Code: tosca.Code{ - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.RETURN), - }, +func gasLimitTestCases() map[string]Scenario { + // cost for 2 PUSH1 operations + const executionGasCost = 3 + 3 + + cases := map[string]struct { + gasLimit tosca.Gas + receipt tosca.Receipt + OperaError error + }{ + "SimpleCodeExact": { + gasLimit: floria.TxGas + executionGasCost, + receipt: tosca.Receipt{ + Success: true, + GasUsed: floria.TxGas + executionGasCost, }, }, - Transaction: tosca.Transaction{ - Sender: tosca.Address{1}, - Recipient: &tosca.Address{2}, - GasLimit: floria.TxGas + executionGasCost, - Nonce: 4, + "SimpleCodeSufficient": { + gasLimit: floria.TxGas + executionGasCost + 100, + receipt: tosca.Receipt{ + Success: true, + GasUsed: floria.TxGas + executionGasCost + 100/10, + }, }, - After: WorldState{ - {1}: Account{Balance: tosca.NewValue(100), Nonce: 5}, - {2}: Account{Balance: tosca.NewValue(0), - Code: tosca.Code{ - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.RETURN), - }, + "SimpleCodeInsufficient": { + gasLimit: floria.TxGas + executionGasCost - 1, + receipt: tosca.Receipt{ + Success: false, + GasUsed: floria.TxGas + executionGasCost - 1, }, + OperaError: fmt.Errorf("gas too low"), }, - Receipt: tosca.Receipt{ - Success: true, - GasUsed: floria.TxGas + executionGasCost, + } + + code := tosca.Code{ + byte(op.PUSH1), byte(0), // < PUSH 0 + byte(op.PUSH1), byte(0), // < PUSH 0 + byte(op.RETURN), + } + before := WorldState{ + {1}: Account{Balance: tosca.NewValue(100), Nonce: 4}, + {2}: Account{Balance: tosca.NewValue(0), + Code: code, }, } - allTestCases["SimpleCodeSufficient"] = Scenario{ - Before: WorldState{ - {1}: Account{Balance: tosca.NewValue(100), Nonce: 4}, - {2}: Account{Balance: tosca.NewValue(0), - Code: tosca.Code{ - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.RETURN), - }, - }, + after := WorldState{ + {1}: Account{Balance: tosca.NewValue(100), Nonce: 5}, + {2}: Account{Balance: tosca.NewValue(0), + Code: code, }, - Transaction: tosca.Transaction{ + } + + testCases := make(map[string]Scenario, len(cases)) + for name, test := range cases { + transaction := tosca.Transaction{ Sender: tosca.Address{1}, Recipient: &tosca.Address{2}, - GasLimit: floria.TxGas + executionGasCost + 100, + GasLimit: test.gasLimit, Nonce: 4, - }, - After: WorldState{ - {1}: Account{Balance: tosca.NewValue(100), Nonce: 5}, - {2}: Account{Balance: tosca.NewValue(0), - Code: tosca.Code{ - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.RETURN), - }, - }, - }, - Receipt: tosca.Receipt{ - Success: true, - GasUsed: floria.TxGas + executionGasCost + 100/10, - }, + } + + testCases[name] = Scenario{ + Before: before, + Transaction: transaction, + After: after, + Receipt: test.receipt, + OperaError: test.OperaError, + } } - allTestCases["SimpleCodeInsufficient"] = Scenario{ - Before: WorldState{ - {1}: Account{Balance: tosca.NewValue(100), Nonce: 4}, - {2}: Account{Balance: tosca.NewValue(0), - Code: tosca.Code{ - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.RETURN), - }, + + return testCases +} + +func gasPricingTestCases() map[string]Scenario { + gasPrice := uint64(10) + sender := tosca.Address{1} + tests := map[string]struct { + Before Account + After Account + Receipt tosca.Receipt + OperaError error + }{ + "GasPriceCalculation": { + Before: Account{Balance: tosca.NewValue(floria.TxGas * gasPrice), Nonce: 4}, + After: Account{Balance: tosca.NewValue(0), Nonce: 5}, + Receipt: tosca.Receipt{ + Success: true, + GasUsed: floria.TxGas, }, }, - Transaction: tosca.Transaction{ - Sender: tosca.Address{1}, - Recipient: &tosca.Address{2}, - GasLimit: floria.TxGas + executionGasCost - 1, - Nonce: 4, - }, - After: WorldState{ - {1}: Account{Balance: tosca.NewValue(100), Nonce: 5}, - {2}: Account{Balance: tosca.NewValue(0), - Code: tosca.Code{ - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.PUSH1), byte(0), // < PUSH 0 - byte(op.RETURN), - }, + "GasPriceCalculationExcessBalance": { + Before: Account{Balance: tosca.NewValue(floria.TxGas*gasPrice + 100), Nonce: 4}, + After: Account{Balance: tosca.NewValue(100), Nonce: 5}, + Receipt: tosca.Receipt{ + Success: true, + GasUsed: floria.TxGas, }, }, - Receipt: tosca.Receipt{ - Success: false, - GasUsed: floria.TxGas + executionGasCost - 1, + "GasPriceCalculationInsufficientBalance": { + Before: Account{Balance: tosca.NewValue(floria.TxGas*gasPrice - 1), Nonce: 4}, + After: Account{Balance: tosca.NewValue(floria.TxGas*gasPrice - 1), Nonce: 4}, + Receipt: tosca.Receipt{ + Success: false, + GasUsed: 0, + }, + OperaError: fmt.Errorf("insufficient balance"), }, - OperaError: fmt.Errorf("gas too low"), } - allTestCases["InternalCallDoesNotConsume10RemainingPercentGas"] = Scenario{ - Before: WorldState{ - {}: Account{Balance: tosca.NewValue(100), Nonce: 4}, - {2}: Account{Balance: tosca.NewValue(0)}, - }, - Transaction: tosca.Transaction{ - Sender: tosca.Address{}, - Recipient: &tosca.Address{2}, - GasLimit: floria.TxGas + 100, - Nonce: 4, - }, - After: WorldState{ - {}: Account{Balance: tosca.NewValue(100), Nonce: 5}, - {2}: Account{Balance: tosca.NewValue(0)}, - }, - Receipt: tosca.Receipt{ - Success: true, - GasUsed: floria.TxGas, + transaction := tosca.Transaction{ + Sender: sender, + Recipient: &tosca.Address{2}, + GasLimit: floria.TxGas, + GasPrice: tosca.NewValue(gasPrice), + Nonce: 4, + } + + testCases := make(map[string]Scenario, len(tests)) + for name, test := range tests { + testCases[name] = Scenario{ + Before: WorldState{sender: test.Before}, + Transaction: transaction, + After: WorldState{sender: test.After}, + Receipt: test.Receipt, + OperaError: test.OperaError, + } + } + + return testCases +} + +func gasSpecificTestCases() map[string]Scenario { + cases := map[string]Scenario{ + "InternalCallDoesNotConsume10PercentOfRemainingGas": { + Before: WorldState{ + {}: Account{Balance: tosca.NewValue(100), Nonce: 4}, + {2}: Account{Balance: tosca.NewValue(0)}, + }, + Transaction: tosca.Transaction{ + Sender: tosca.Address{}, + Recipient: &tosca.Address{2}, + GasLimit: floria.TxGas + 100, + Nonce: 4, + }, + After: WorldState{ + {}: Account{Balance: tosca.NewValue(100), Nonce: 5}, + {2}: Account{Balance: tosca.NewValue(0)}, + }, + Receipt: tosca.Receipt{ + Success: true, + GasUsed: floria.TxGas, + }, }, } + return cases +} + +func getGasTestScenarios() map[string]Scenario { + testCases := gasTestScenarios() + + specificCases := gasLimitTestCases() + maps.Copy(testCases, specificCases) + + refundCases := gasPricingTestCases() + maps.Copy(testCases, refundCases) - return allTestCases + specificCases = gasSpecificTestCases() + maps.Copy(testCases, specificCases) + + return testCases +} + +func TestProcessor_GasSpecificScenarios(t *testing.T) { + for name, processor := range getProcessors() { + t.Run(name, func(t *testing.T) { + for name, s := range getGasTestScenarios() { + t.Run(name, func(t *testing.T) { + s.Run(t, processor) + }) + } + }) + } } func exactSufficientAndInsufficientScenarios(exactScenario Scenario, name string) map[string]Scenario { @@ -297,15 +357,3 @@ func exactSufficientAndInsufficientScenarios(exactScenario Scenario, name string name + "Insufficient": insufficient, } } - -func TestProcessor_GasSpecificScenarios(t *testing.T) { - for name, processor := range getProcessors() { - t.Run(name, func(t *testing.T) { - for name, s := range getGasTestScenarios() { - t.Run(name, func(t *testing.T) { - s.Run(t, processor) - }) - } - }) - } -} diff --git a/go/processor/floria/processor.go b/go/processor/floria/processor.go index 1093306c4..baa09f319 100644 --- a/go/processor/floria/processor.go +++ b/go/processor/floria/processor.go @@ -53,7 +53,7 @@ func (p *processor) Run( gas := transaction.GasLimit if err := buyGas(transaction, context); err != nil { - return errorReceipt, nil + return tosca.Receipt{}, nil } intrinsicGas := setupGasBilling(transaction) @@ -101,24 +101,54 @@ func (p *processor) Run( } } - gasUsed := gasUsed(transaction, result.GasLeft) + gasLeft := calculateGasLeft(transaction, result, blockParameters.Revision) + refundGas(transaction, context, gasLeft) + + logs := context.GetLogs() return tosca.Receipt{ Success: result.Success, - GasUsed: gasUsed, + GasUsed: transaction.GasLimit - gasLeft, ContractAddress: nil, Output: result.Output, - Logs: nil, + Logs: logs, }, nil } -func gasUsed(transaction tosca.Transaction, gasLeft tosca.Gas) tosca.Gas { +func calculateGasLeft(transaction tosca.Transaction, result tosca.CallResult, revision tosca.Revision) tosca.Gas { + gasLeft := result.GasLeft // 10% of remaining gas is charged for non-internal transactions if transaction.Sender != (tosca.Address{}) { gasLeft -= gasLeft / 10 } - return transaction.GasLimit - gasLeft + if result.Success { + gasUsed := transaction.GasLimit - gasLeft + refund := result.GasRefund + + maxRefund := tosca.Gas(0) + if revision < tosca.R10_London { + // Before EIP-3529: refunds were capped to gasUsed / 2 + maxRefund = gasUsed / 2 + } else { + // After EIP-3529: refunds are capped to gasUsed / 5 + maxRefund = gasUsed / 5 + } + + if refund > maxRefund { + refund = maxRefund + } + gasLeft += refund + } + + return gasLeft +} + +func refundGas(transaction tosca.Transaction, context tosca.TransactionContext, gasLeft tosca.Gas) { + refundValue := transaction.GasPrice.Scale(uint64(gasLeft)) + senderBalance := context.GetBalance(transaction.Sender) + senderBalance = tosca.Add(senderBalance, refundValue) + context.SetBalance(transaction.Sender, senderBalance) } func setupGasBilling(transaction tosca.Transaction) tosca.Gas { diff --git a/go/processor/floria/processor_test.go b/go/processor/floria/processor_test.go index 0abe7aac1..3c576e599 100644 --- a/go/processor/floria/processor_test.go +++ b/go/processor/floria/processor_test.go @@ -11,7 +11,6 @@ package floria import ( - "fmt" "testing" "github.com/Fantom-foundation/Tosca/go/tosca" @@ -116,41 +115,138 @@ func TestProcessor_BuyGasInsufficientBalance(t *testing.T) { } func TestGasUsed(t *testing.T) { - tests := []struct { - sender tosca.Address - expectedGasUsed tosca.Gas + tests := map[string]struct { + transaction tosca.Transaction + result tosca.CallResult + revision tosca.Revision + expectedGasLeft tosca.Gas }{ - { - sender: tosca.Address{}, - expectedGasUsed: 500, + "InternalTransaction": { + transaction: tosca.Transaction{ + Sender: tosca.Address{}, + GasLimit: 1000, + }, + result: tosca.CallResult{ + GasLeft: 500, + Success: true, + GasRefund: 0, + }, + revision: tosca.R10_London, + expectedGasLeft: 500, }, - { - sender: tosca.Address{1}, - expectedGasUsed: 550, + "NonInternalTransaction": { + transaction: tosca.Transaction{ + Sender: tosca.Address{1}, + GasLimit: 1000, + }, + result: tosca.CallResult{ + GasLeft: 500, + Success: true, + GasRefund: 0, + }, + revision: tosca.R10_London, + expectedGasLeft: 450, + }, + "RefundPreLondon": { + transaction: tosca.Transaction{ + Sender: tosca.Address{}, + GasLimit: 1000, + }, + result: tosca.CallResult{ + GasLeft: 500, + Success: true, + GasRefund: 300, + }, + revision: tosca.R09_Berlin, + expectedGasLeft: 750, }, - { - sender: tosca.Address{42}, - expectedGasUsed: 550, + "RefundLondon": { + transaction: tosca.Transaction{ + Sender: tosca.Address{}, + GasLimit: 1000, + }, + result: tosca.CallResult{ + GasLeft: 500, + Success: true, + GasRefund: 300, + }, + revision: tosca.R10_London, + expectedGasLeft: 600, }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("sender%v", test.sender), func(t *testing.T) { - transaction := tosca.Transaction{ - Sender: test.sender, + "RefundPostLondon": { + transaction: tosca.Transaction{ + Sender: tosca.Address{}, GasLimit: 1000, - } + }, + result: tosca.CallResult{ + GasLeft: 500, + Success: true, + GasRefund: 300, + }, + revision: tosca.R13_Cancun, + expectedGasLeft: 600, + }, + "smallRefund": { + transaction: tosca.Transaction{ + Sender: tosca.Address{}, + GasLimit: 1000, + }, + result: tosca.CallResult{ + GasLeft: 500, + Success: true, + GasRefund: 5, + }, + revision: tosca.R10_London, + expectedGasLeft: 505, + }, + "UnsuccessfulResult": { + transaction: tosca.Transaction{ + Sender: tosca.Address{}, + GasLimit: 1000, + }, + result: tosca.CallResult{ + GasLeft: 0, + Success: false, + GasRefund: 500, + }, + revision: tosca.R10_London, + expectedGasLeft: 0, + }, + } - gasLeft := tosca.Gas(500) - actualGasUsed := gasUsed(transaction, gasLeft) + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + actualGasLeft := calculateGasLeft(test.transaction, test.result, test.revision) - if actualGasUsed != test.expectedGasUsed { - t.Errorf("gasUsed returned incorrect result, got: %d, want: %d", actualGasUsed, test.expectedGasUsed) + if actualGasLeft != test.expectedGasLeft { + t.Errorf("gasUsed returned incorrect result, got: %d, want: %d", actualGasLeft, test.expectedGasLeft) } }) } } +func TestProcessor_RefundGas(t *testing.T) { + gasPrice := 5 + gasLeft := 50 + senderBalance := 1000 + + sender := tosca.Address{1} + + ctrl := gomock.NewController(t) + context := tosca.NewMockTransactionContext(ctrl) + + context.EXPECT().GetBalance(sender).Return(tosca.NewValue(uint64(senderBalance))) + context.EXPECT().SetBalance(sender, tosca.NewValue(uint64(senderBalance+gasLeft*gasPrice))) + + transaction := tosca.Transaction{ + Sender: sender, + GasPrice: tosca.NewValue(uint64(gasPrice)), + } + + refundGas(transaction, context, tosca.Gas(gasLeft)) + +} + func TestProcessor_SetupGasBilling(t *testing.T) { tests := map[string]struct { recipient *tosca.Address