diff --git a/arbos/l2pricing/base_fees.go b/arbos/l2pricing/base_fees.go new file mode 100644 index 0000000000..540f3e744d --- /dev/null +++ b/arbos/l2pricing/base_fees.go @@ -0,0 +1,44 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package l2pricing + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/arbitrum/multigas" + + "github.com/offchainlabs/nitro/arbos/storage" +) + +const ( + baseFeesOffset uint64 = iota +) + +// MultiGasBaseFees defines the base fees tracked for multiple gas resource kinds. +type MultiGasBaseFees struct { + baseFees [multigas.NumResourceKind]storage.StorageBackedBigInt +} + +// OpenMultiGasBaseFees opens or initializes base fees in the given storage subspace. +func OpenMultiGasBaseFees(sto *storage.Storage) *MultiGasBaseFees { + r := &MultiGasBaseFees{ + baseFees: [multigas.NumResourceKind]storage.StorageBackedBigInt{}, + } + for i := range int(multigas.NumResourceKind) { + // #nosec G115 safe: NumResourceKind < 2^32 + offset := baseFeesOffset + uint64(i) + r.baseFees[i] = sto.OpenStorageBackedBigInt(offset) + } + return r +} + +// Get retrieves the base fee for the given resource kind. +func (bf *MultiGasBaseFees) Get(kind multigas.ResourceKind) (*big.Int, error) { + return bf.baseFees[kind].Get() +} + +// Set sets the base fee for the given resource kind. +func (bf *MultiGasBaseFees) Set(kind multigas.ResourceKind, v *big.Int) error { + return bf.baseFees[kind].SetChecked(v) +} diff --git a/arbos/l2pricing/l2pricing.go b/arbos/l2pricing/l2pricing.go index b5dbdbc9f6..86435e45bb 100644 --- a/arbos/l2pricing/l2pricing.go +++ b/arbos/l2pricing/l2pricing.go @@ -25,7 +25,8 @@ type L2PricingState struct { backlogTolerance storage.StorageBackedUint64 perTxGasLimit storage.StorageBackedUint64 gasConstraints *storage.SubStorageVector - multigasConstraints *storage.SubStorageVector + multiGasConstraints *storage.SubStorageVector + multiGasBaseFees *MultiGasBaseFees ArbosVersion uint64 } @@ -42,7 +43,8 @@ const ( ) var gasConstraintsKey []byte = []byte{0} -var multigasConstraintsKey []byte = []byte{1} +var multiGasConstraintsKey []byte = []byte{1} +var multiGasBaseFeesKey []byte = []byte{2} const GethBlockGasLimit = 1 << 50 @@ -75,7 +77,8 @@ func OpenL2PricingState(sto *storage.Storage, arbosVersion uint64) *L2PricingSta backlogTolerance: sto.OpenStorageBackedUint64(backlogToleranceOffset), perTxGasLimit: sto.OpenStorageBackedUint64(perTxGasLimitOffset), gasConstraints: storage.OpenSubStorageVector(sto.OpenSubStorage(gasConstraintsKey)), - multigasConstraints: storage.OpenSubStorageVector(sto.OpenSubStorage(multigasConstraintsKey)), + multiGasConstraints: storage.OpenSubStorageVector(sto.OpenSubStorage(multiGasConstraintsKey)), + multiGasBaseFees: OpenMultiGasBaseFees(sto.OpenSubStorage(multiGasBaseFeesKey)), ArbosVersion: arbosVersion, } } @@ -268,11 +271,11 @@ func (ps *L2PricingState) ClearGasConstraints() error { } func (ps *L2PricingState) MultiGasConstraintsLength() (uint64, error) { - return ps.multigasConstraints.Length() + return ps.multiGasConstraints.Length() } func (ps *L2PricingState) OpenMultiGasConstraintAt(i uint64) *MultiGasConstraint { - return OpenMultiGasConstraint(ps.multigasConstraints.At(i)) + return OpenMultiGasConstraint(ps.multiGasConstraints.At(i)) } func (ps *L2PricingState) AddMultiGasConstraint( @@ -281,7 +284,7 @@ func (ps *L2PricingState) AddMultiGasConstraint( backlog uint64, weights map[uint8]uint64, ) error { - subStorage, err := ps.multigasConstraints.Push() + subStorage, err := ps.multiGasConstraints.Push() if err != nil { return fmt.Errorf("failed to push multi-gas constraint: %w", err) } @@ -308,7 +311,7 @@ func (ps *L2PricingState) ClearMultiGasConstraints() error { return err } for range length { - subStorage, err := ps.multigasConstraints.Pop() + subStorage, err := ps.multiGasConstraints.Pop() if err != nil { return err } diff --git a/arbos/l2pricing/model.go b/arbos/l2pricing/model.go index ea6933e2d3..3e5e09cdcd 100644 --- a/arbos/l2pricing/model.go +++ b/arbos/l2pricing/model.go @@ -123,7 +123,7 @@ func (ps *L2PricingState) updateSingleGasConstraintsBacklogs(op BacklogOperation } func (ps *L2PricingState) updateMultiGasConstraintsBacklogs(op BacklogOperation, _usedGas uint64, usedMultiGas multigas.MultiGas) error { - constraintsLength, err := ps.multigasConstraints.Length() + constraintsLength, err := ps.multiGasConstraints.Length() if err != nil { return err } @@ -161,7 +161,7 @@ func (ps *L2PricingState) BacklogUpdateCost() uint64 { result += storage.StorageReadCost // updateMultiGasConstraintsBacklogs costs - constraintsLength, _ := ps.multigasConstraints.Length() + constraintsLength, _ := ps.multiGasConstraints.Length() if constraintsLength > 0 { result += storage.StorageReadCost // read length to traverse @@ -272,17 +272,20 @@ func (ps *L2PricingState) updatePricingModelMultiConstraints(timePassed uint64) // Calculate exponents per resource kind for all constraints exponentPerKind, _ := ps.CalcMultiGasConstraintsExponents() - // Choose the most congested resource - maxExponent := arbmath.Bips(0) - for _, exp := range exponentPerKind { - if exp > maxExponent { - maxExponent = exp + // Compute base fee per resource kind, store and choose the most congested resource + maxBaseFee, _ := ps.MinBaseFeeWei() + for kind, exp := range exponentPerKind { + baseFee, _ := ps.calcBaseFeeFromExponent(exp) + + // #nosec G115 safe: kind < multigas.NumResourceKind + _ = ps.multiGasBaseFees.Set(multigas.ResourceKind(kind), baseFee) + + if baseFee.Cmp(maxBaseFee) > 0 { + maxBaseFee = baseFee } - } - // Compute base fee - baseFee, _ := ps.calcBaseFeeFromExponent(maxExponent) - _ = ps.SetBaseFeeWei(baseFee) + } + _ = ps.SetBaseFeeWei(maxBaseFee) } // CalcMultiGasConstraintsExponents calculates the exponents for each resource kind @@ -347,3 +350,35 @@ func (ps *L2PricingState) calcBaseFeeFromExponent(exponent arbmath.Bips) (*big.I return minBaseFee, nil } } + +func (ps *L2PricingState) MultiDimensionalPriceForRefund(gasUsed multigas.MultiGas) (*big.Int, error) { + // Base fee is max of per-resource-kind base fees + baseFeeWei, err := ps.BaseFeeWei() + if err != nil { + return nil, err + } + + total := new(big.Int) + for kind := range multigas.ResourceKind(multigas.NumResourceKind) { + baseFee, err := ps.multiGasBaseFees.Get(kind) + if err != nil { + return nil, err + } + // Force L1 calldata (and the unlikely zero-basefee case) to use the max base fee. + if kind == multigas.ResourceKindL1Calldata || baseFee.Cmp(big.NewInt(0)) == 0 { + baseFee = baseFeeWei + } + + amount := gasUsed.Get(kind) + if amount == 0 { + continue + } + + part := new(big.Int).Mul( + new(big.Int).SetUint64(amount), + baseFee, + ) + total.Add(total, part) + } + return total, nil +} diff --git a/arbos/l2pricing/model_test.go b/arbos/l2pricing/model_test.go index 0ba6ad48f2..4f06d042e9 100644 --- a/arbos/l2pricing/model_test.go +++ b/arbos/l2pricing/model_test.go @@ -9,7 +9,10 @@ import ( "slices" "testing" + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/util/arbmath" ) func toGwei(wei *big.Int) string { @@ -124,3 +127,119 @@ func TestCompareSingleGasConstraintsPricingModelWithMultiGasConstraints(t *testi } } } + +func TestCalcMultiGasConstraintsExponents(t *testing.T) { + pricing := PricingForTest(t) + pricing.ArbosVersion = ArbosMultiGasConstraintsVersion + + Require(t, pricing.AddMultiGasConstraint( + 100000, + 10, + 20000, + map[uint8]uint64{ + uint8(multigas.ResourceKindComputation): 1, + uint8(multigas.ResourceKindStorageAccess): 2, + }, + )) + Require(t, pricing.AddMultiGasConstraint( + 50000, + 5, + 15000, + map[uint8]uint64{ + uint8(multigas.ResourceKindStorageGrowth): 1, + }, + )) + + exponents, err := pricing.CalcMultiGasConstraintsExponents() + Require(t, err) + + // From constraint 1: + // exp_comp = floor(20000 * 1 * 10000 / (10 * 100000 * 3)) = 66 + // exp_store = floor(20000 * 2 * 10000 / (10 * 100000 * 3)) = 133 + if got, want := exponents[multigas.ResourceKindComputation], arbmath.Bips(66); got != want { + t.Errorf("unexpected computation exponent: got %v, want %v", got, want) + } + if got, want := exponents[multigas.ResourceKindStorageAccess], arbmath.Bips(133); got != want { + t.Errorf("unexpected storage-access exponent: got %v, want %v", got, want) + } + + // From constraint 2: + // exp_storageGrowth = floor(15000 * 1 * 10000 / (5 * 50000 * 1)) = 600 + if got, want := exponents[multigas.ResourceKindStorageGrowth], arbmath.Bips(600); got != want { + t.Errorf("unexpected storage-growth exponent: got %v, want %v", got, want) + } + + // All other kinds should be zero + if got := exponents[multigas.ResourceKindHistoryGrowth]; got != 0 { + t.Errorf("expected zero history-growth exponent, got %v", got) + } + if got := exponents[multigas.ResourceKindL1Calldata]; got != 0 { + t.Errorf("expected zero L1 calldata exponent, got %v", got) + } + if got := exponents[multigas.ResourceKindL2Calldata]; got != 0 { + t.Errorf("expected zero L2 calldata exponent, got %v", got) + } + if got := exponents[multigas.ResourceKindWasmComputation]; got != 0 { + t.Errorf("expected zero wasm computation exponent, got %v", got) + } +} + +func TestMultiDimensionalPriceForRefund(t *testing.T) { + pricing := PricingForTest(t) + + minPrice, err := pricing.MinBaseFeeWei() + Require(t, err) + + multiGas := multigas.MultiGasFromPairs( + multigas.Pair{Kind: multigas.ResourceKindComputation, Amount: 50000}, + multigas.Pair{Kind: multigas.ResourceKindStorageAccess, Amount: 15000}, + ) + // #nosec G115 + singleGas := big.NewInt(int64(multiGas.SingleGas())) + // Initial price should match minBaseFeeWei * singleGas + expectedPrice := minPrice.Mul(minPrice, singleGas) + Require(t, err) + + pricing.ArbosVersion = ArbosMultiGasConstraintsVersion + + // Initial price check + price, err := pricing.MultiDimensionalPriceForRefund(multiGas) + Require(t, err) + if price.Cmp(expectedPrice) != 0 { + t.Errorf("Unexpected initial price: got %v, want %v", price, expectedPrice) + } + + // updatePricingModelMultiConstraints() should set multi gas base fees + Require(t, pricing.AddMultiGasConstraint( + 100000, + 10, + 20000, + map[uint8]uint64{ + uint8(multigas.ResourceKindComputation): 1, + uint8(multigas.ResourceKindStorageAccess): 2, + }, + )) + Require(t, pricing.AddMultiGasConstraint( + 50000, + 5, + 15000, + map[uint8]uint64{ + uint8(multigas.ResourceKindComputation): 2, + uint8(multigas.ResourceKindStorageAccess): 1, + }, + )) + usedMultiGas := multigas.MultiGasFromPairs( + multigas.Pair{Kind: multigas.ResourceKindComputation, Amount: 500000}, + multigas.Pair{Kind: multigas.ResourceKindStorageAccess, Amount: 1500000}, + ) + err = pricing.GrowBacklog(usedMultiGas.SingleGas(), usedMultiGas) + Require(t, err) + + pricing.updatePricingModelMultiConstraints(10) + + price, err = pricing.MultiDimensionalPriceForRefund(multiGas) + Require(t, err) + if price.Cmp(expectedPrice) <= 0 { + t.Errorf("Price did not increase after backlog growth: got %v, want > %v", price, expectedPrice) + } +} diff --git a/arbos/tx_processor.go b/arbos/tx_processor.go index b7c57e9a9f..3083a97f9e 100644 --- a/arbos/tx_processor.go +++ b/arbos/tx_processor.go @@ -21,6 +21,7 @@ import ( "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/util/arbmath" @@ -534,6 +535,13 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas, } gasUsed := p.msg.GasLimit - gasLeft + var multiDimensionalCost *big.Int + var err error + if p.state.L2PricingState().ArbosVersion >= l2pricing.ArbosMultiGasConstraintsVersion { + multiDimensionalCost, err = p.state.L2PricingState().MultiDimensionalPriceForRefund(usedMultiGas) + p.state.Restrict(err) + } + if underlyingTx != nil && underlyingTx.Type() == types.ArbitrumRetryTxType { inner, _ := underlyingTx.GetInner().(*types.ArbitrumRetryTx) effectiveBaseFee := inner.GasFeeCap @@ -555,6 +563,9 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas, log.Error("Uh oh, Geth didn't refund the user", inner.From, gasRefund) } + singleGasCost := arbmath.BigMulByUint(effectiveBaseFee, gasUsed) + shouldRefundMultiGas := multiDimensionalCost != nil && arbmath.BigGreaterThan(singleGasCost, multiDimensionalCost) + maxRefund := new(big.Int).Set(inner.MaxRefund) refund := func(refundFrom common.Address, amount *big.Int, reason tracing.BalanceChangeReason) { const errLog = "fee address doesn't have enough funds to give user refund" @@ -597,8 +608,12 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas, // The submission fee is still taken from the L1 deposit earlier, even if it's not refunded. takeFunds(maxRefund, inner.SubmissionFeeRefund) } + // Conceptually, the gas charge is taken from the L1 deposit pool if possible. - takeFunds(maxRefund, arbmath.BigMulByUint(effectiveBaseFee, gasUsed)) + // This continues to use the simple-gas price; any multi-gas discount is handled + // as an explicit refund below. + takeFunds(maxRefund, singleGasCost) + // Refund any unused gas, without overdrafting the L1 deposit. networkRefund := gasRefund if p.state.ArbOSVersion() >= params.ArbosVersion_11 { @@ -616,6 +631,18 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas, } refund(networkFeeAccount, networkRefund, tracing.BalanceChangeTransferNetworkRefund) + // Multi-dimensional refund (retryable tx path) + if shouldRefundMultiGas { + amount := arbmath.BigSub(singleGasCost, multiDimensionalCost) + refund(networkFeeAccount, amount, tracing.BalanceChangeMultiGasRefund) + } else if arbmath.BigLessThan(singleGasCost, multiDimensionalCost) { + log.Warn( + "multi dimensional gas price exceeded simple gas price in retryable path", + "simpleGasCost", singleGasCost, + "multiDimensionalCost", multiDimensionalCost, + ) + } + if success { // we don't want to charge for this tracingInfo := util.NewTracingInfo(p.evm, arbosAddress, p.msg.From, scenario) @@ -680,6 +707,24 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas, } } + // Multi-dimensional refund (normal tx path) + if multiDimensionalCost != nil { + amount := new(big.Int).Sub(totalCost, multiDimensionalCost) + if amount.Sign() > 0 { + err := util.TransferBalance( + &networkFeeAccount, + &p.msg.From, + amount, + p.evm, + scenario, + tracing.BalanceChangeMultiGasRefund, + ) + p.state.Restrict(err) + } else if amount.Sign() < 0 { + log.Warn("multi dimensional gas price exceeded simple gas price", "amount", amount) + } + } + if p.msg.GasPrice.Sign() > 0 { // in tests, gas price could be 0 // ArbOS's gas pool is meant to enforce the computational speed-limit. // We don't want to remove from the pool the poster's L1 costs (as expressed in L2 gas in this func) diff --git a/arbos/tx_processor_multigas_test.go b/arbos/tx_processor_multigas_test.go index 5b8e32ac75..e1d673e81f 100644 --- a/arbos/tx_processor_multigas_test.go +++ b/arbos/tx_processor_multigas_test.go @@ -10,11 +10,17 @@ import ( "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/arbitrum/multigas" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/l2pricing" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/cmd/chaininfo" + "github.com/offchainlabs/nitro/util/arbmath" ) func newMockEVMForTestingWithBaseFee(baseFee *big.Int) *vm.EVM { @@ -77,3 +83,187 @@ func TestStartTxHookReturnsMultigas(t *testing.T) { }) } } + +func TestEndTxHookMultiGasRefundNormalTx(t *testing.T) { + const gasLimit uint64 = 1000000 + const gasLeft uint64 = 0 + from := common.HexToAddress("0x1234") + + evm := newMockEVMForTestingWithBaseFee(big.NewInt(l2pricing.InitialBaseFeeWei)) + + msg := &core.Message{ + TxRunContext: core.NewMessageReplayContext(), + From: from, + GasLimit: gasLimit, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(1), + GasTipCap: big.NewInt(0), + } + + txProcessor := NewTxProcessor(evm, msg) + txProcessor.PosterFee = big.NewInt(0) + + initialBalance := evm.StateDB.GetBalance(from) + require.True(t, initialBalance.IsZero()) + + gasUsed := gasLimit - gasLeft + + // Distribute used gas equally between computation and storage access. + usedMultiGas := multigas.MultiGasFromPairs( + multigas.Pair{Kind: multigas.ResourceKindComputation, Amount: gasUsed / 2}, + multigas.Pair{Kind: multigas.ResourceKindStorageAccess, Amount: gasUsed / 2}, + ) + + // Set up multi-gas constraints and spin model to produce different multi-dimensional cost. + txProcessor.state.L2PricingState().ArbosVersion = l2pricing.ArbosMultiGasConstraintsVersion + + Require(t, txProcessor.state.L2PricingState().AddMultiGasConstraint( + 100000, + 10, + 200000000000, + map[uint8]uint64{ + uint8(multigas.ResourceKindComputation): 1, + uint8(multigas.ResourceKindStorageGrowth): 10, + }, + )) + txProcessor.state.L2PricingState().UpdatePricingModel(100) + + baseFee, err := txProcessor.state.L2PricingState().BaseFeeWei() + require.NoError(t, err) + + // Align the EVM block basefee with the pricing state's min basefee. + evm.Context.BaseFee = new(big.Int).Set(baseFee) + + singleGasCost := new(big.Int).Mul(baseFee, new(big.Int).SetUint64(gasUsed)) + + multiDimensionalCost, err := txProcessor.state.L2PricingState().MultiDimensionalPriceForRefund(usedMultiGas) + require.NoError(t, err) + + expectedRefund := new(big.Int).Sub(singleGasCost, multiDimensionalCost) + require.True(t, expectedRefund.Sign() > 0, "expected refund to be positive", expectedRefund) + + txProcessor.EndTxHook(gasLeft, usedMultiGas, true) + + finalBalance := evm.StateDB.GetBalance(from) + require.True( + t, + arbmath.BigEquals(expectedRefund, finalBalance.ToBig()), + "unexpected multi-gas refund amount to sender: got %v, want %v", + finalBalance.ToBig(), + expectedRefund, + ) +} + +func TestEndTxHookMultiGasRefundRetryableTx(t *testing.T) { + const gasLimit uint64 = 1_000_000 + const gasLeft uint64 = 0 + + from := common.HexToAddress("0x1111") + refundTo := common.HexToAddress("0x2222") + + evm := newMockEVMForTestingWithBaseFee(big.NewInt(l2pricing.InitialBaseFeeWei)) + + msg := &core.Message{ + TxRunContext: core.NewMessageReplayContext(), + From: from, + GasLimit: gasLimit, + GasPrice: big.NewInt(0), + GasFeeCap: big.NewInt(1), + GasTipCap: big.NewInt(0), + } + + txProcessor := NewTxProcessor(evm, msg) + txProcessor.PosterFee = big.NewInt(0) + + require.True(t, evm.StateDB.GetBalance(from).IsZero()) + require.True(t, evm.StateDB.GetBalance(refundTo).IsZero()) + + gasUsed := gasLimit - gasLeft + usedMultiGas := multigas.MultiGasFromPairs( + multigas.Pair{Kind: multigas.ResourceKindComputation, Amount: gasUsed / 2}, + multigas.Pair{Kind: multigas.ResourceKindStorageAccess, Amount: gasUsed / 2}, + ) + + // Set up multi-gas constraints and spin model to produce a different multi-dimensional cost. + pricing := txProcessor.state.L2PricingState() + pricing.ArbosVersion = l2pricing.ArbosMultiGasConstraintsVersion + + Require(t, pricing.AddMultiGasConstraint( + 100000, + 10, + 200000000000, + map[uint8]uint64{ + uint8(multigas.ResourceKindComputation): 1, + uint8(multigas.ResourceKindStorageGrowth): 10, + }, + )) + pricing.UpdatePricingModel(100) + + baseFee, err := pricing.BaseFeeWei() + require.NoError(t, err) + + // Align the EVM block basefee with the pricing state's base fee. + evm.Context.BaseFee = new(big.Int).Set(baseFee) + + // For retryables, simple gas price is GasFeeCap. Use the same as BaseFeeWei. + gasFeeCap := new(big.Int).Set(baseFee) + simpleGasCost := new(big.Int).Mul(gasFeeCap, new(big.Int).SetUint64(gasUsed)) + + multiDimensionalCost, err := pricing.MultiDimensionalPriceForRefund(usedMultiGas) + require.NoError(t, err) + + expectedRefund := new(big.Int).Sub(simpleGasCost, multiDimensionalCost) + require.True(t, expectedRefund.Sign() > 0, "expected refund to be positive", expectedRefund) + + // Big MaxRefund so the full multi-gas refund can go to RefundTo. + maxRefund := new(big.Int).Mul(expectedRefund, big.NewInt(10)) + + inner := &types.ArbitrumRetryTx{ + From: from, + RefundTo: refundTo, + GasFeeCap: gasFeeCap, + SubmissionFeeRefund: big.NewInt(0), + MaxRefund: maxRefund, + Value: big.NewInt(0), + TicketId: common.HexToHash("0x01"), + } + retryTx := types.NewTx(inner) + msg.Tx = retryTx + + // Pre-fund network fee account so refund(...) can pay the user. + networkFeeAccount, err := txProcessor.state.NetworkFeeAccount() + require.NoError(t, err) + + util.MintBalance( + &networkFeeAccount, + new(big.Int).Mul(expectedRefund, big.NewInt(2)), // plenty + evm, + util.TracingAfterEVM, + tracing.BalanceIncreaseNetworkFee, + ) + + refundToBefore := evm.StateDB.GetBalance(refundTo).ToBig() + fromBefore := evm.StateDB.GetBalance(from).ToBig() + + // Retryable path check + txProcessor.EndTxHook(gasLeft, usedMultiGas, true) + + refundToAfter := evm.StateDB.GetBalance(refundTo).ToBig() + fromAfter := evm.StateDB.GetBalance(from).ToBig() + + refundToDelta := new(big.Int).Sub(refundToAfter, refundToBefore) + fromDelta := new(big.Int).Sub(fromAfter, fromBefore) + + // Expect: + // - SubmissionFeeRefund = 0 + // - gasLeft = 0 => gasRefund = 0 + // - MaxRefund is large enough, so the entire multi-gas refund goes to RefundTo, none to From. + require.True(t, fromDelta.Sign() == 0, "expected no refund to From, got %v", fromDelta) + require.True( + t, + arbmath.BigEquals(expectedRefund, refundToDelta), + "unexpected multi-gas refund to RefundTo: got %v, want %v", + refundToDelta, + expectedRefund, + ) +} diff --git a/go-ethereum b/go-ethereum index 6b19938827..a50f8438cd 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 6b19938827e8d7684caf6a883765d51ee967f250 +Subproject commit a50f8438cd43e92b16dd17a0298e28920f289852