Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions arbos/tx_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,10 @@ func (p *TxProcessor) GasChargingHook(gasRemaining *uint64, intrinsicGas uint64)
p.state.Restrict(p.state.L1PricingState().AddToUnitsSinceUpdate(calldataUnits))
}
p.posterGas = GetPosterGas(p.state, actualGasPrice, p.msg.TxRunContext, posterCost)
if p.hasRelaxedBaseFee() {
// Free calls still consume poster gas, but cannot pay the fallback base fee.
actualGasPrice = p.GetPaidGasPrice()
}
p.PosterFee = arbmath.BigMulByUint(actualGasPrice, p.posterGas) // round down
gasNeededToStartEVM = p.posterGas
}
Expand Down Expand Up @@ -618,6 +622,10 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, usedMultiGas multigas.MultiGas,
} else {
basefee = p.evm.Context.BaseFee
}
if p.hasRelaxedBaseFee() {
// Fee recipients cannot receive more base fee than this transaction paid.
basefee = arbmath.BigMin(basefee, p.GetPaidGasPrice())
}

var multiDimensionalCost *big.Int
if p.state.L2PricingState().ArbosVersion >= params.ArbosVersion_MultiGasConstraintsVersion {
Expand Down Expand Up @@ -923,10 +931,19 @@ func (p *TxProcessor) GetPaidGasPrice() *big.Int {
if p.CollectTips() {
return p.evm.GasPrice
}
if p.hasRelaxedBaseFee() {
return arbmath.BigMin(p.evm.Context.BaseFee, p.msg.GasPrice)
}
// p.evm.Context.BaseFee is already lowered to 0 when vm runs with NoBaseFee flag and 0 gas price
return p.evm.Context.BaseFee
}

func (p *TxProcessor) hasRelaxedBaseFee() bool {
// Native Arbitrum transactions have separate funding and refund rules.
return p.evm.Config.DisableBaseFeeCheck &&
(p.msg.Tx == nil || p.msg.Tx.Type() < types.ArbitrumDepositTxType)
}

func (p *TxProcessor) GasPriceOp(evm *vm.EVM) *big.Int {
if p.state.ArbOSVersion() >= params.ArbosVersion_3 {
return p.GetPaidGasPrice()
Expand Down
182 changes: 182 additions & 0 deletions arbos/tx_processor_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package arbos

import (
"math/big"
"testing"

"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"github.com/tenderly/net-nitro/arbos/arbosState"
"github.com/tenderly/net-nitro/arbos/l1pricing"
"github.com/tenderly/net-nitro/cmd/chaininfo"
"github.com/tenderly/net-nitro/go-ethereum/arbitrum/multigas"
"github.com/tenderly/net-nitro/go-ethereum/common"
"github.com/tenderly/net-nitro/go-ethereum/core"
"github.com/tenderly/net-nitro/go-ethereum/core/tracing"
"github.com/tenderly/net-nitro/go-ethereum/core/types"
"github.com/tenderly/net-nitro/go-ethereum/core/vm"
)

func TestExecutionValidationPriorityFeeBeforeDroppingTips(t *testing.T) {
for _, tc := range []struct {
name string
relaxBaseFee bool
relaxPriorityFee bool
widePriorityFee bool
wantErr error
}{
{name: "native tip dropping unchanged"},
{name: "mining checks submitted priority fee", relaxBaseFee: true, wantErr: core.ErrTipAboveFeeCap},
{name: "simulation allows high priority fee", relaxBaseFee: true, relaxPriorityFee: true},
{name: "simulation keeps priority fee width check", relaxBaseFee: true, relaxPriorityFee: true, widePriorityFee: true, wantErr: core.ErrTipVeryHigh},
} {
t.Run(tc.name, func(t *testing.T) {
processor := newValidationTxProcessor(t, tc.relaxBaseFee, 20)
evm, msg := processor.evm, processor.msg
evm.Config.DisablePriorityFeeCheck = tc.relaxPriorityFee
evm.ProcessingHook = processor
msg.GasTipCap.SetInt64(30)
if tc.widePriorityFee {
msg.GasTipCap.Lsh(big.NewInt(1), 256)
}
msg.Tx = types.NewTx(&types.DynamicFeeTx{
ChainID: evm.ChainConfig().ChainID, Gas: msg.GasLimit, To: msg.To,
GasFeeCap: msg.GasFeeCap, GasTipCap: msg.GasTipCap, Value: msg.Value,
})
evm.StateDB.AddBalance(msg.From, uint256.NewInt(3_000_000), tracing.BalanceChangeUnspecified)
require.NoError(t, processor.state.L1PricingState().SetPricePerUnit(big.NewInt(1)))
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
require.ErrorIs(t, err, tc.wantErr)
})
}
}

func newValidationTxProcessor(t *testing.T, disableBaseFeeCheck bool, gasPrice int64) *TxProcessor {
t.Helper()
chainConfig := chaininfo.ArbitrumDevTestChainConfig()
_, statedb := arbosState.NewArbosMemoryBackedArbOSStateWithConfig(chainConfig)
header := &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(1), BaseFee: big.NewInt(10), GasLimit: 1_000_000}
types.HeaderInfo{ArbOSFormatVersion: chainConfig.ArbitrumChainParams.InitialArbOSVersion}.UpdateHeaderWithInfo(header)
evm := vm.NewEVM(core.NewEVMBlockContext(header, nil, &l1pricing.BatchPosterAddress), statedb, chainConfig, vm.Config{DisableBaseFeeCheck: disableBaseFeeCheck})
to := common.Address{2}
msg := &core.Message{
From: common.Address{1}, To: &to, Value: new(big.Int), GasLimit: 100_000,
GasPrice: big.NewInt(gasPrice), GasFeeCap: big.NewInt(gasPrice), GasTipCap: new(big.Int),
TxRunContext: core.NewMessageReplayContext(),
}
evm.SetTxContext(core.NewEVMTxContext(msg))
processor := NewTxProcessor(evm, msg)
require.NoError(t, processor.state.L2PricingState().SetMinBaseFeeWei(big.NewInt(3)))
return processor
}

func TestExecutionValidationPaidGasPrice(t *testing.T) {
for _, tc := range []struct {
name string
disable bool
gasPrice int64
systemTx bool
want int64
}{
{name: "native no-tip price", gasPrice: 7, want: 10},
{name: "relaxed underpriced transaction", disable: true, gasPrice: 7, want: 7},
{name: "relaxed free transaction", disable: true, want: 0},
{name: "native tip dropping unchanged", disable: true, gasPrice: 20, want: 10},
{name: "retryable price unchanged", disable: true, gasPrice: 7, systemTx: true, want: 10},
} {
t.Run(tc.name, func(t *testing.T) {
processor := newValidationTxProcessor(t, tc.disable, tc.gasPrice)
if tc.systemTx {
processor.msg.Tx = types.NewTx(&types.ArbitrumRetryTx{})
}
require.Equal(t, tc.want, processor.GetPaidGasPrice().Int64())
require.Equal(t, tc.want, processor.GasPriceOp(processor.evm).Int64())
require.Equal(t, int64(10), processor.evm.Context.BaseFee.Int64())
require.Equal(t, tc.gasPrice, processor.msg.GasPrice.Int64())
})
}
}

func TestExecutionValidationArbosFeeRecipients(t *testing.T) {
for _, disabled := range []bool{false, true} {
name := "native"
if disabled {
name = "relaxed"
}
t.Run(name, func(t *testing.T) {
processor := newValidationTxProcessor(t, disabled, 7)
network, infra := common.Address{3}, common.Address{4}
require.NoError(t, processor.state.SetNetworkFeeAccount(network))
require.NoError(t, processor.state.SetInfraFeeAccount(infra))
const gasUsed, posterGas = uint64(21_000), uint64(1_000)
price := int64(10)
if disabled {
price = 7
}
processor.posterGas = posterGas
processor.PosterFee = big.NewInt(int64(posterGas) * price)
processor.EndTxHook(processor.msg.GasLimit-gasUsed, multigas.SingleDimGas(gasUsed), true)
db := processor.evm.StateDB
require.Equal(t, uint64(3)*(gasUsed-posterGas), db.GetBalance(infra).Uint64())
require.Equal(t, uint64(price-3)*(gasUsed-posterGas), db.GetBalance(network).Uint64())
require.Equal(t, uint64(price)*posterGas, db.GetBalance(types.L1PricerFundsPoolAddress).Uint64())
total := new(big.Int).Add(db.GetBalance(infra).ToBig(), db.GetBalance(network).ToBig())
total.Add(total, db.GetBalance(types.L1PricerFundsPoolAddress).ToBig())
require.Equal(t, big.NewInt(int64(gasUsed)*price), total)
})
}
}

func TestExecutionValidationZeroPriceRetainsPosterGas(t *testing.T) {
processor := newValidationTxProcessor(t, true, 0)
require.NoError(t, processor.state.L1PricingState().SetPricePerUnit(big.NewInt(1)))
gasRemaining := processor.msg.GasLimit
_, _, err := processor.GasChargingHook(&gasRemaining, 0)
require.NoError(t, err)
require.Positive(t, processor.PosterGas())
require.Less(t, gasRemaining, processor.msg.GasLimit)
require.Zero(t, processor.PosterFee.Sign())
processor.EndTxHook(gasRemaining, multigas.SingleDimGas(processor.PosterGas()), true)
require.True(t, processor.evm.StateDB.GetBalance(types.L1PricerFundsPoolAddress).IsZero())
}

func TestExecutionValidationArbosApplyMessage(t *testing.T) {
for _, gasPrice := range []int64{0, 1, 7} {
t.Run(big.NewInt(gasPrice).String(), func(t *testing.T) {
processor := newValidationTxProcessor(t, true, gasPrice)
evm, msg := processor.evm, processor.msg
evm.Config.DisableBalanceCheck = true
evm.ProcessingHook = processor
msg.Value.SetInt64(7)
msg.Tx = types.NewTx(&types.DynamicFeeTx{
ChainID: evm.ChainConfig().ChainID, Gas: msg.GasLimit, To: msg.To,
GasFeeCap: msg.GasFeeCap, GasTipCap: msg.GasTipCap, Value: msg.Value,
})
evm.StateDB.SetCode(*msg.To, []byte{
byte(vm.GASPRICE), byte(vm.PUSH1), 0, byte(vm.MSTORE),
byte(vm.BASEFEE), byte(vm.PUSH1), 32, byte(vm.MSTORE),
byte(vm.PUSH1), 64, byte(vm.PUSH1), 0, byte(vm.RETURN),
}, tracing.CodeChangeUnspecified)
network, infra := common.Address{3}, common.Address{4}
require.NoError(t, processor.state.SetNetworkFeeAccount(network))
require.NoError(t, processor.state.SetInfraFeeAccount(infra))
require.NoError(t, processor.state.L1PricingState().SetPricePerUnit(big.NewInt(1)))

result, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
require.NoError(t, err)
require.NoError(t, result.Err)
require.Len(t, result.ReturnData, 64)
require.Equal(t, gasPrice, new(big.Int).SetBytes(result.ReturnData[:32]).Int64())
require.Equal(t, int64(10), new(big.Int).SetBytes(result.ReturnData[32:]).Int64())
require.Positive(t, processor.PosterGas())
require.Equal(t, uint64(7), evm.StateDB.GetBalance(*msg.To).Uint64())
require.Equal(t, (msg.GasLimit-result.UsedGas)*uint64(gasPrice), evm.StateDB.GetBalance(msg.From).Uint64())
totalFees := new(big.Int).Add(evm.StateDB.GetBalance(network).ToBig(), evm.StateDB.GetBalance(infra).ToBig())
totalFees.Add(totalFees, evm.StateDB.GetBalance(types.L1PricerFundsPoolAddress).ToBig())
require.Equal(t, result.UsedGas*uint64(gasPrice), totalFees.Uint64())
require.Equal(t, gasPrice, msg.GasPrice.Int64())
require.Equal(t, gasPrice, msg.GasFeeCap.Int64())
require.Zero(t, msg.GasTipCap.Sign())
})
}
}
33 changes: 27 additions & 6 deletions go-ethereum/core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"math"
"math/big"

"github.com/holiman/uint256"
"github.com/tenderly/net-nitro/go-ethereum/arbitrum/multigas"
"github.com/tenderly/net-nitro/go-ethereum/common"
"github.com/tenderly/net-nitro/go-ethereum/core/rawdb"
Expand All @@ -30,7 +31,6 @@ import (
"github.com/tenderly/net-nitro/go-ethereum/core/vm"
"github.com/tenderly/net-nitro/go-ethereum/crypto/kzg4844"
"github.com/tenderly/net-nitro/go-ethereum/params"
"github.com/holiman/uint256"
)

// ExecutionResult includes all output after executing given evm
Expand Down Expand Up @@ -494,7 +494,7 @@ func (st *stateTransition) buyGas() error {
if overflow {
return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
}
if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 {
if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; !st.evm.Config.DisableBalanceCheck && have.Cmp(want) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want)
}
if err := st.gp.SubGas(st.msg.GasLimit); err != nil {
Expand All @@ -508,11 +508,24 @@ func (st *stateTransition) buyGas() error {

st.initialGas = st.msg.GasLimit
mgvalU256, _ := uint256.FromBig(mgval)
if st.evm.Config.DisableBalanceCheck {
// Simulations charge gas with saturating subtraction and keep the full refund.
if balance := st.state.GetBalance(st.msg.From); mgvalU256.Cmp(balance) > 0 {
mgvalU256.Set(balance)
}
}
st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy)
if st.evm.Config.DisableBalanceCheck {
// Fund only the top-level value transfer; nested calls keep their balance checks.
value := uint256.MustFromBig(st.msg.Value)
if balance := st.state.GetBalance(st.msg.From); balance.Cmp(value) < 0 {
st.state.AddBalance(st.msg.From, new(uint256.Int).Sub(value, balance), tracing.BalanceChangeUnspecified)
}
}

// Arbitrum: record fee payment
if tracer := st.evm.Config.Tracer; tracer != nil && tracer.CaptureArbitrumTransfer != nil {
tracer.CaptureArbitrumTransfer(&st.msg.From, nil, mgval, true, tracing.BalanceDecreaseGasBuy)
tracer.CaptureArbitrumTransfer(&st.msg.From, nil, mgvalU256.ToBig(), true, tracing.BalanceDecreaseGasBuy)
}

return nil
Expand Down Expand Up @@ -562,13 +575,13 @@ func (st *stateTransition) preCheck() error {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh,
msg.From.Hex(), l)
}
if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 {
if !st.evm.Config.DisablePriorityFeeCheck && msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap,
msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap)
}
// This will panic if baseFee is nil, but basefee presence is verified
// as part of header validation.
if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
if !st.evm.Config.DisableBaseFeeCheck && msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow,
msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee)
}
Expand Down Expand Up @@ -659,7 +672,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// Arbitrum: drop tip when tip collection is not enabled
if !st.evm.ProcessingHook.CollectTips() && st.msg.GasPrice.Cmp(st.evm.Context.BaseFee) > 0 {
st.msg.GasPrice = st.evm.Context.BaseFee
st.msg.GasTipCap = common.Big0
// Opt-in checks validate the submitted priority fee even when ArbOS
// does not collect it. Reducing the paid price must not bypass them.
if !st.evm.Config.DisableBaseFeeCheck && !st.evm.Config.DisablePriorityFeeCheck && !st.evm.Config.DisableBalanceCheck {
st.msg.GasTipCap = common.Big0
}
}

// Check clauses 1-3, buy gas if everything is correct
Expand Down Expand Up @@ -811,6 +828,10 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
effectiveTip := msg.GasPrice
if rules.IsLondon {
effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee)
if st.evm.Config.DisableBaseFeeCheck && effectiveTip.Sign() < 0 {
// Underpriced transactions pay no tip; do not convert a negative value to uint256.
effectiveTip.SetUint64(0)
}
}
effectiveTipU256, _ := uint256.FromBig(effectiveTip)
gasUsed := st.gasUsed()
Expand Down
Loading
Loading