Skip to content
2 changes: 2 additions & 0 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ func (st *StateTransition) refundGas(refundQuotient uint64) uint64 {
}
st.gasRemaining += refund

st.consumeMinimumGas() // libevm: see comment on method re call-site requirements

// Return ETH for remaining gas, exchanged at the original rate.
remaining := uint256.NewInt(st.gasRemaining)
remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
Expand Down
30 changes: 26 additions & 4 deletions core/state_transition.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,44 @@ package core

import (
"github.com/ava-labs/libevm/log"
"github.com/ava-labs/libevm/params"
)

func (st *StateTransition) rulesHooks() params.RulesHooks {
bCtx := st.evm.Context
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
return rules.Hooks()
}

// canExecuteTransaction is a convenience wrapper for calling the
// [params.RulesHooks.CanExecuteTransaction] hook.
func (st *StateTransition) canExecuteTransaction() error {
bCtx := st.evm.Context
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
if err := rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
hooks := st.rulesHooks()
if err := hooks.CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
log.Debug(
"Transaction execution blocked by libevm hook",
"from", st.msg.From,
"to", st.msg.To,
"hooks", log.TypeOf(rules.Hooks()),
"hooks", log.TypeOf(hooks),
"reason", err,
)
return err
}
return nil
}

// consumeMinimumGas updates the gas remaining to reflect the value returned by
// [params.RulesHooks.MinimumGasConsumption]. It MUST be called after all code
// that modifies gas consumption but before the balance is returned for
// remaining gas.
func (st *StateTransition) consumeMinimumGas() {
limit := st.msg.GasLimit
minConsume := min(
limit, // as documented in [params.RulesHooks]
st.rulesHooks().MinimumGasConsumption(limit),
)
st.gasRemaining = min(
st.gasRemaining,
limit-minConsume,
)
}
146 changes: 146 additions & 0 deletions core/state_transition.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@ package core_test

import (
"fmt"
"math/big"
"testing"

"github.com/holiman/uint256"
"github.com/stretchr/testify/require"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/core"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/core/vm"
"github.com/ava-labs/libevm/crypto"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/ethtest"
"github.com/ava-labs/libevm/libevm/hookstest"
"github.com/ava-labs/libevm/params"
)

func TestCanExecuteTransaction(t *testing.T) {
Expand Down Expand Up @@ -54,3 +60,143 @@ func TestCanExecuteTransaction(t *testing.T) {
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(30e6))
require.EqualError(t, err, makeErr(msg.From, msg.To, value).Error())
}

func TestMinimumGasConsumption(t *testing.T) {
// All transactions will be basic transfers so consume [params.TxGas] by
// default.
tests := []struct {
name string
gasLimit uint64
refund uint64
minConsumption uint64
wantUsed uint64
}{
{
name: "consume_extra",
gasLimit: 1e6,
minConsumption: 5e5,
wantUsed: 5e5,
},
{
name: "consume_extra",
gasLimit: 1e6,
minConsumption: 4e5,
wantUsed: 4e5,
},
{
name: "no_extra_consumption",
gasLimit: 50_000,
minConsumption: params.TxGas - 1,
wantUsed: params.TxGas,
},
{
name: "zero_min",
gasLimit: 50_000,
minConsumption: 0,
wantUsed: params.TxGas,
},
{
name: "consume_extra_by_one",
gasLimit: 1e6,
minConsumption: params.TxGas + 1,
wantUsed: params.TxGas + 1,
},
{
name: "min_capped_at_limit",
gasLimit: 1e6,
minConsumption: 2e6,
wantUsed: 1e6,
},
{
// Although this doesn't test minimum consumption, it demonstrates
// the expected outcome for comparison with the next test.
name: "refund_without_min_consumption",
gasLimit: 1e6,
refund: 1,
wantUsed: params.TxGas - 1,
},
{
name: "refund_with_min_consumption",
gasLimit: 1e6,
refund: 1,
minConsumption: params.TxGas,
wantUsed: params.TxGas,
},
}

// Very low gas price so we can calculate the expected balance in a uint64,
// but not 1 otherwise tests would pass without multiplying extra
// consumption by the price.
const gasPrice = 3

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hooks := &hookstest.Stub{
MinimumGasConsumptionFn: func(limit uint64) uint64 {
require.Equal(t, tt.gasLimit, limit)
return tt.minConsumption
},
}
hooks.Register(t)

key, err := crypto.GenerateKey()
require.NoError(t, err, "libevm/crypto.GenerateKey()")

stateDB, evm := ethtest.NewZeroEVM(t)
signer := types.LatestSigner(evm.ChainConfig())
tx := types.MustSignNewTx(
key, signer,
&types.LegacyTx{
GasPrice: big.NewInt(gasPrice),
Gas: tt.gasLimit,
To: &common.Address{},
Value: big.NewInt(0),
},
)

const startingBalance = 10 * params.Ether
from := crypto.PubkeyToAddress(key.PublicKey)
stateDB.SetNonce(from, 0)
stateDB.SetBalance(from, uint256.NewInt(startingBalance))
stateDB.AddRefund(tt.refund)

var (
// Both variables are passed as pointers to
// [core.ApplyTransaction], which will modify them.
gotUsed uint64
gotPool = core.GasPool(1e9)
)
wantPool := gotPool - core.GasPool(tt.wantUsed)

receipt, err := core.ApplyTransaction(
evm.ChainConfig(), nil, &common.Address{}, &gotPool, stateDB,
&types.Header{
BaseFee: big.NewInt(gasPrice),
// Required but irrelevant fields
Number: big.NewInt(0),
Difficulty: big.NewInt(0),
},
tx, &gotUsed, vm.Config{},
)
require.NoError(t, err, "core.ApplyTransaction(...)")

for desc, got := range map[string]uint64{
"receipt.GasUsed": receipt.GasUsed,
"receipt.CumulativeGasUsed": receipt.CumulativeGasUsed,
"core.ApplyTransaction(..., usedGas *uint64, ...)": gotUsed,
} {
if got != tt.wantUsed {
t.Errorf("%s got %d; want %d", desc, got, tt.wantUsed)
}
}
if gotPool != wantPool {
t.Errorf("After core.ApplyMessage(..., *%T); got %[1]T = %[1]d; want %d", gotPool, wantPool)
}

wantBalance := uint256.NewInt(startingBalance - tt.wantUsed*gasPrice)
if got := stateDB.GetBalance(from); !got.Eq(wantBalance) {
t.Errorf("got remaining balance %d; want %d", got, wantBalance)
}
})
}
}
10 changes: 10 additions & 0 deletions libevm/hookstest/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Stub struct {
ActivePrecompilesFn func([]common.Address) []common.Address
CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error
CanCreateContractFn func(*libevm.AddressContext, uint64, libevm.StateReader) (uint64, error)
MinimumGasConsumptionFn func(txGasLimit uint64) uint64
}

// Register is a convenience wrapper for registering s as both the
Expand Down Expand Up @@ -122,6 +123,15 @@ func (s Stub) CanCreateContract(cc *libevm.AddressContext, gas uint64, sr libevm
return gas, nil
}

// MinimumGasConsumption proxies arguments to the s.MinimumGasConsumptionFn
// function if non-nil, otherwise it acts as a noop.
func (s Stub) MinimumGasConsumption(limit uint64) uint64 {
if f := s.MinimumGasConsumptionFn; f != nil {
return f(limit)
}
return 0
}

var _ interface {
params.ChainConfigHooks
params.RulesHooks
Expand Down
11 changes: 11 additions & 0 deletions params/hooks.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ type RulesHooks interface {
// received slice. The value it returns MUST be consistent with the
// behaviour of the PrecompileOverride hook.
ActivePrecompiles([]common.Address) []common.Address
// MinimumGasConsumption receives a transaction's gas limit and returns the
// minimum quantity of gas units to be charged for said transaction. If the
// returned value is greater than the transaction's limit, the minimum spend
// will be capped at the limit. The minimum spend will be applied _after_
// refunds, if any.
MinimumGasConsumption(txGasLimit uint64) (gas uint64)
}

// RulesAllowlistHooks are a subset of [RulesHooks] that gate actions, signalled
Expand Down Expand Up @@ -132,3 +138,8 @@ func (NOOPHooks) PrecompileOverride(common.Address) (libevm.PrecompiledContract,
func (NOOPHooks) ActivePrecompiles(active []common.Address) []common.Address {
return active
}

// MinimumGasConsumption always returns 0.
func (NOOPHooks) MinimumGasConsumption(uint64) uint64 {
return 0
}
Loading