mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-05 04:31:16 +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.gasRemaining += refund
|
||||||
|
|
||||||
|
st.consumeMinimumGas() // libevm: see comment on method re call-site requirements
|
||||||
|
|
||||||
// Return ETH for remaining gas, exchanged at the original rate.
|
// Return ETH for remaining gas, exchanged at the original rate.
|
||||||
remaining := uint256.NewInt(st.gasRemaining)
|
remaining := uint256.NewInt(st.gasRemaining)
|
||||||
remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
|
remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,44 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ava-labs/libevm/log"
|
"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
|
// canExecuteTransaction is a convenience wrapper for calling the
|
||||||
// [params.RulesHooks.CanExecuteTransaction] hook.
|
// [params.RulesHooks.CanExecuteTransaction] hook.
|
||||||
func (st *StateTransition) canExecuteTransaction() error {
|
func (st *StateTransition) canExecuteTransaction() error {
|
||||||
bCtx := st.evm.Context
|
hooks := st.rulesHooks()
|
||||||
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
|
if err := hooks.CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
|
||||||
if err := rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
|
|
||||||
log.Debug(
|
log.Debug(
|
||||||
"Transaction execution blocked by libevm hook",
|
"Transaction execution blocked by libevm hook",
|
||||||
"from", st.msg.From,
|
"from", st.msg.From,
|
||||||
"to", st.msg.To,
|
"to", st.msg.To,
|
||||||
"hooks", log.TypeOf(rules.Hooks()),
|
"hooks", log.TypeOf(hooks),
|
||||||
"reason", err,
|
"reason", err,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/holiman/uint256"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ava-labs/libevm/common"
|
"github.com/ava-labs/libevm/common"
|
||||||
"github.com/ava-labs/libevm/core"
|
"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"
|
||||||
"github.com/ava-labs/libevm/libevm/ethtest"
|
"github.com/ava-labs/libevm/libevm/ethtest"
|
||||||
"github.com/ava-labs/libevm/libevm/hookstest"
|
"github.com/ava-labs/libevm/libevm/hookstest"
|
||||||
|
"github.com/ava-labs/libevm/params"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCanExecuteTransaction(t *testing.T) {
|
func TestCanExecuteTransaction(t *testing.T) {
|
||||||
|
|
@ -54,3 +60,143 @@ func TestCanExecuteTransaction(t *testing.T) {
|
||||||
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(30e6))
|
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(30e6))
|
||||||
require.EqualError(t, err, makeErr(msg.From, msg.To, value).Error())
|
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
|
ActivePrecompilesFn func([]common.Address) []common.Address
|
||||||
CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error
|
CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error
|
||||||
CanCreateContractFn func(*libevm.AddressContext, uint64, libevm.StateReader) (uint64, 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
|
// 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
|
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 {
|
var _ interface {
|
||||||
params.ChainConfigHooks
|
params.ChainConfigHooks
|
||||||
params.RulesHooks
|
params.RulesHooks
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,12 @@ type RulesHooks interface {
|
||||||
// received slice. The value it returns MUST be consistent with the
|
// received slice. The value it returns MUST be consistent with the
|
||||||
// behaviour of the PrecompileOverride hook.
|
// behaviour of the PrecompileOverride hook.
|
||||||
ActivePrecompiles([]common.Address) []common.Address
|
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
|
// 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 {
|
func (NOOPHooks) ActivePrecompiles(active []common.Address) []common.Address {
|
||||||
return active
|
return active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MinimumGasConsumption always returns 0.
|
||||||
|
func (NOOPHooks) MinimumGasConsumption(uint64) uint64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue