mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 05:41:35 +00:00
feat: RulesHooks.MinimumGasConsumption (#185)
## Why this should be merged Required for [ACP-194](https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution#gas-charged) $\lambda$ bound on gas consumption. ## How this works Hook into `core.StateTransition.TransitionDb()` as this is the bottom of all execution paths (e.g. `core.ApplyTransaction()` as used in SAE, `core.StateProcessor.Process(*Block,...)`, etc.). Once consumed gas is no longer changing (i.e. after all spends and refunds), the transaction limit is passed to the hook to determine the minimum consumption, which is applied. ## How this was tested Unit test via `core.ApplyTransaction()` as this is our entry point in SAE. --------- Signed-off-by: Arran Schlosberg <519948+ARR4N@users.noreply.github.com> Co-authored-by: Stephen Buttolph <stephen@avalabs.org>
This commit is contained in:
parent
2672fbd7cd
commit
bedfd12e2d
5 changed files with 195 additions and 4 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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…
Reference in a new issue