Skip to content

Commit 028680e

Browse files
committed
feat: RulesHooks.MinimumGasSpend
1 parent 2d94327 commit 028680e

File tree

5 files changed

+186
-4
lines changed

5 files changed

+186
-4
lines changed

core/state_transition.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
446446
// After EIP-3529: refunds are capped to gasUsed / 5
447447
gasRefund = st.refundGas(params.RefundQuotientEIP3529)
448448
}
449+
st.consumeMinimumGas() // libevm: see comment on method re call-site requirements
449450
effectiveTip := msg.GasPrice
450451
if rules.IsLondon {
451452
effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee))

core/state_transition.libevm.go

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,74 @@
1717
package core
1818

1919
import (
20+
"math/big"
21+
22+
"github.com/holiman/uint256"
23+
2024
"github.com/ava-labs/libevm/log"
25+
"github.com/ava-labs/libevm/params"
2126
)
2227

28+
func (st *StateTransition) rulesHooks() params.RulesHooks {
29+
bCtx := st.evm.Context
30+
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
31+
return rules.Hooks()
32+
}
33+
2334
// canExecuteTransaction is a convenience wrapper for calling the
2435
// [params.RulesHooks.CanExecuteTransaction] hook.
2536
func (st *StateTransition) canExecuteTransaction() error {
26-
bCtx := st.evm.Context
27-
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
28-
if err := rules.Hooks().CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
37+
hooks := st.rulesHooks()
38+
if err := hooks.CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
2939
log.Debug(
3040
"Transaction execution blocked by libevm hook",
3141
"from", st.msg.From,
3242
"to", st.msg.To,
33-
"hooks", log.TypeOf(rules.Hooks()),
43+
"hooks", log.TypeOf(hooks),
3444
"reason", err,
3545
)
3646
return err
3747
}
3848
return nil
3949
}
50+
51+
// consumeMinimumGas updates the gas remaining to reflect the value returned by
52+
// [params.RulesHooks.MinimumGasConsumption]. It MUST be called after all code
53+
// that modifies gas consumption; i.e. `st.gasRemaining` MUST remain constant
54+
// after consumeMinimumGas returns.
55+
func (st *StateTransition) consumeMinimumGas() {
56+
limit := st.msg.GasLimit
57+
minConsume := st.rulesHooks().MinimumGasConsumption(st.msg.GasLimit)
58+
if minConsume > limit {
59+
minConsume = limit // as documented in [params.RulesHooks]
60+
}
61+
62+
maxRemaining := limit - minConsume
63+
if st.gasRemaining < maxRemaining {
64+
return
65+
}
66+
67+
diff := st.gasRemaining - maxRemaining
68+
st.gasRemaining -= diff
69+
if err := st.gp.SubGas(diff); err != nil {
70+
// This would mean that the transaction wouldn't have been able to spend
71+
// up to its limit.
72+
log.Crit(
73+
"Broken gas-charging invariant",
74+
"tx limit", limit,
75+
"min consume", minConsume,
76+
"extra consume", diff,
77+
"SubGas() error", err,
78+
)
79+
}
80+
81+
spend := new(big.Int).Mul(st.msg.GasPrice, new(big.Int).SetUint64(diff))
82+
st.state.SubBalance(st.msg.From, uint256.MustFromBig(spend))
83+
84+
log.Debug(
85+
"Consumed extra gas to enforce minimum",
86+
"tx_limit", limit,
87+
"min_consumption", minConsume,
88+
"extra_consumption", diff,
89+
)
90+
}

core/state_transition.libevm_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ package core_test
1717

1818
import (
1919
"fmt"
20+
"math/big"
2021
"testing"
2122

23+
"github.com/google/go-cmp/cmp"
24+
"github.com/holiman/uint256"
2225
"github.com/stretchr/testify/require"
2326

2427
"github.com/ava-labs/libevm/common"
2528
"github.com/ava-labs/libevm/core"
29+
"github.com/ava-labs/libevm/core/types"
30+
"github.com/ava-labs/libevm/crypto"
2631
"github.com/ava-labs/libevm/libevm"
2732
"github.com/ava-labs/libevm/libevm/ethtest"
2833
"github.com/ava-labs/libevm/libevm/hookstest"
34+
"github.com/ava-labs/libevm/params"
2935
)
3036

3137
func TestCanExecuteTransaction(t *testing.T) {
@@ -54,3 +60,106 @@ func TestCanExecuteTransaction(t *testing.T) {
5460
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(30e6))
5561
require.EqualError(t, err, makeErr(msg.From, msg.To, value).Error())
5662
}
63+
64+
func TestMinimumGasConsumption(t *testing.T) {
65+
// All transactions will be basic transfers so consume [params.TxGas] by
66+
// default.
67+
tests := []struct {
68+
name string
69+
gasLimit, minConsumption uint64
70+
wantUsed uint64
71+
}{
72+
{
73+
name: "consume_extra",
74+
gasLimit: 1e6,
75+
minConsumption: 5e5,
76+
wantUsed: 5e5,
77+
},
78+
{
79+
name: "consume_extra",
80+
gasLimit: 1e6,
81+
minConsumption: 4e5,
82+
wantUsed: 4e5,
83+
},
84+
{
85+
name: "no_extra_consumption",
86+
gasLimit: 50_000,
87+
minConsumption: params.TxGas - 1,
88+
wantUsed: params.TxGas,
89+
},
90+
{
91+
name: "zero_min",
92+
gasLimit: 50_000,
93+
minConsumption: 0,
94+
wantUsed: params.TxGas,
95+
},
96+
{
97+
name: "consume_extra_by_one",
98+
gasLimit: 1e6,
99+
minConsumption: params.TxGas + 1,
100+
wantUsed: params.TxGas + 1,
101+
},
102+
{
103+
name: "min_capped_at_limit",
104+
gasLimit: 1e6,
105+
minConsumption: 2e6,
106+
wantUsed: 1e6,
107+
},
108+
}
109+
110+
const gasPrice = params.Wei
111+
112+
for _, tt := range tests {
113+
t.Run(tt.name, func(t *testing.T) {
114+
hooks := &hookstest.Stub{
115+
MinimumGasConsumptionFn: func(limit uint64) uint64 {
116+
require.Equal(t, limit, tt.gasLimit)
117+
return tt.minConsumption
118+
},
119+
}
120+
hooks.Register(t)
121+
122+
key, err := crypto.GenerateKey()
123+
require.NoError(t, err, "libevm/crypto.GenerateKey()")
124+
125+
stateDB, evm := ethtest.NewZeroEVM(t)
126+
signer := types.LatestSigner(evm.ChainConfig())
127+
tx := types.MustSignNewTx(
128+
key, signer,
129+
&types.LegacyTx{
130+
GasPrice: big.NewInt(gasPrice),
131+
Gas: tt.gasLimit,
132+
To: &common.Address{},
133+
Value: big.NewInt(0),
134+
},
135+
)
136+
msg, err := core.TransactionToMessage(tx, signer, big.NewInt(gasPrice))
137+
require.NoError(t, err, "core.TransactionToMessage(types.MustSignNewTx(...))")
138+
139+
const startingBalance = 10 * params.Ether
140+
stateDB.SetNonce(msg.From, 0)
141+
stateDB.SetBalance(msg.From, uint256.NewInt(startingBalance))
142+
143+
gotPool := core.GasPool(1e9) // modified when passed as pointer
144+
wantPool := gotPool - core.GasPool(tt.wantUsed)
145+
146+
got, err := core.ApplyMessage(evm, msg, &gotPool)
147+
require.NoError(t, err, "core.ApplyMessage()")
148+
149+
want := &core.ExecutionResult{
150+
UsedGas: tt.wantUsed,
151+
}
152+
if diff := cmp.Diff(want, got); diff != "" {
153+
t.Errorf("core.ApplyMessage(...) diff (-want +got):\n%s", diff)
154+
}
155+
if gotPool != wantPool {
156+
t.Errorf("After core.ApplyMessage(..., *%T); got %[1]T = %[1]d; want %d", gotPool, wantPool)
157+
}
158+
159+
wantBalance := startingBalance - tt.wantUsed*gasPrice
160+
if got := stateDB.GetBalance(msg.From); !got.IsUint64() || got.Uint64() != wantBalance {
161+
t.Errorf("got remaining balance %s; want %d", got.String(), wantBalance)
162+
}
163+
})
164+
}
165+
}

libevm/hookstest/stub.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type Stub struct {
4848
ActivePrecompilesFn func([]common.Address) []common.Address
4949
CanExecuteTransactionFn func(common.Address, *common.Address, libevm.StateReader) error
5050
CanCreateContractFn func(*libevm.AddressContext, uint64, libevm.StateReader) (uint64, error)
51+
MinimumGasConsumptionFn func(txGasLimit uint64) uint64
5152
}
5253

5354
// 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
122123
return gas, nil
123124
}
124125

126+
// MinimumGasConsumption proxies arguments to the s.MinimumGasConsumptionFn
127+
// function if non-nil, otherwise it acts as a noop.
128+
func (s Stub) MinimumGasConsumption(limit uint64) uint64 {
129+
if f := s.MinimumGasConsumptionFn; f != nil {
130+
return f(limit)
131+
}
132+
return 0
133+
}
134+
125135
var _ interface {
126136
params.ChainConfigHooks
127137
params.RulesHooks

params/hooks.libevm.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ type RulesHooks interface {
5555
// received slice. The value it returns MUST be consistent with the
5656
// behaviour of the PrecompileOverride hook.
5757
ActivePrecompiles([]common.Address) []common.Address
58+
// MinimumGasConsumption receives a transaction's gas limit and returns the
59+
// minimum quantity of gas units to be charged for said transaction. If the
60+
// returned value is greater than the transaction's limit, the minimum spend
61+
// will be capped at the limit. The minimum spend will be applied _after_
62+
// refunds, if any.
63+
MinimumGasConsumption(txGasLimit uint64) (gas uint64)
5864
}
5965

6066
// RulesAllowlistHooks are a subset of [RulesHooks] that gate actions, signalled
@@ -132,3 +138,8 @@ func (NOOPHooks) PrecompileOverride(common.Address) (libevm.PrecompiledContract,
132138
func (NOOPHooks) ActivePrecompiles(active []common.Address) []common.Address {
133139
return active
134140
}
141+
142+
// MinimumGasConsumption always returns 0.
143+
func (NOOPHooks) MinimumGasConsumption(uint64) uint64 {
144+
return 0
145+
}

0 commit comments

Comments
 (0)