Skip to content

Commit bedfd12

Browse files
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 <[email protected]> Co-authored-by: Stephen Buttolph <[email protected]>
1 parent 2672fbd commit bedfd12

File tree

5 files changed

+195
-4
lines changed

5 files changed

+195
-4
lines changed

core/state_transition.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ func (st *StateTransition) refundGas(refundQuotient uint64) uint64 {
478478
}
479479
st.gasRemaining += refund
480480

481+
st.consumeMinimumGas() // libevm: see comment on method re call-site requirements
482+
481483
// Return ETH for remaining gas, exchanged at the original rate.
482484
remaining := uint256.NewInt(st.gasRemaining)
483485
remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))

core/state_transition.libevm.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,44 @@ package core
1818

1919
import (
2020
"github.com/ava-labs/libevm/log"
21+
"github.com/ava-labs/libevm/params"
2122
)
2223

24+
func (st *StateTransition) rulesHooks() params.RulesHooks {
25+
bCtx := st.evm.Context
26+
rules := st.evm.ChainConfig().Rules(bCtx.BlockNumber, bCtx.Random != nil, bCtx.Time)
27+
return rules.Hooks()
28+
}
29+
2330
// canExecuteTransaction is a convenience wrapper for calling the
2431
// [params.RulesHooks.CanExecuteTransaction] hook.
2532
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 {
33+
hooks := st.rulesHooks()
34+
if err := hooks.CanExecuteTransaction(st.msg.From, st.msg.To, st.state); err != nil {
2935
log.Debug(
3036
"Transaction execution blocked by libevm hook",
3137
"from", st.msg.From,
3238
"to", st.msg.To,
33-
"hooks", log.TypeOf(rules.Hooks()),
39+
"hooks", log.TypeOf(hooks),
3440
"reason", err,
3541
)
3642
return err
3743
}
3844
return nil
3945
}
46+
47+
// consumeMinimumGas updates the gas remaining to reflect the value returned by
48+
// [params.RulesHooks.MinimumGasConsumption]. It MUST be called after all code
49+
// that modifies gas consumption but before the balance is returned for
50+
// remaining gas.
51+
func (st *StateTransition) consumeMinimumGas() {
52+
limit := st.msg.GasLimit
53+
minConsume := min(
54+
limit, // as documented in [params.RulesHooks]
55+
st.rulesHooks().MinimumGasConsumption(limit),
56+
)
57+
st.gasRemaining = min(
58+
st.gasRemaining,
59+
limit-minConsume,
60+
)
61+
}

core/state_transition.libevm_test.go

Lines changed: 146 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/holiman/uint256"
2224
"github.com/stretchr/testify/require"
2325

2426
"github.com/ava-labs/libevm/common"
2527
"github.com/ava-labs/libevm/core"
28+
"github.com/ava-labs/libevm/core/types"
29+
"github.com/ava-labs/libevm/core/vm"
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,143 @@ 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 uint64
70+
refund uint64
71+
minConsumption uint64
72+
wantUsed uint64
73+
}{
74+
{
75+
name: "consume_extra",
76+
gasLimit: 1e6,
77+
minConsumption: 5e5,
78+
wantUsed: 5e5,
79+
},
80+
{
81+
name: "consume_extra",
82+
gasLimit: 1e6,
83+
minConsumption: 4e5,
84+
wantUsed: 4e5,
85+
},
86+
{
87+
name: "no_extra_consumption",
88+
gasLimit: 50_000,
89+
minConsumption: params.TxGas - 1,
90+
wantUsed: params.TxGas,
91+
},
92+
{
93+
name: "zero_min",
94+
gasLimit: 50_000,
95+
minConsumption: 0,
96+
wantUsed: params.TxGas,
97+
},
98+
{
99+
name: "consume_extra_by_one",
100+
gasLimit: 1e6,
101+
minConsumption: params.TxGas + 1,
102+
wantUsed: params.TxGas + 1,
103+
},
104+
{
105+
name: "min_capped_at_limit",
106+
gasLimit: 1e6,
107+
minConsumption: 2e6,
108+
wantUsed: 1e6,
109+
},
110+
{
111+
// Although this doesn't test minimum consumption, it demonstrates
112+
// the expected outcome for comparison with the next test.
113+
name: "refund_without_min_consumption",
114+
gasLimit: 1e6,
115+
refund: 1,
116+
wantUsed: params.TxGas - 1,
117+
},
118+
{
119+
name: "refund_with_min_consumption",
120+
gasLimit: 1e6,
121+
refund: 1,
122+
minConsumption: params.TxGas,
123+
wantUsed: params.TxGas,
124+
},
125+
}
126+
127+
// Very low gas price so we can calculate the expected balance in a uint64,
128+
// but not 1 otherwise tests would pass without multiplying extra
129+
// consumption by the price.
130+
const gasPrice = 3
131+
132+
for _, tt := range tests {
133+
t.Run(tt.name, func(t *testing.T) {
134+
hooks := &hookstest.Stub{
135+
MinimumGasConsumptionFn: func(limit uint64) uint64 {
136+
require.Equal(t, tt.gasLimit, limit)
137+
return tt.minConsumption
138+
},
139+
}
140+
hooks.Register(t)
141+
142+
key, err := crypto.GenerateKey()
143+
require.NoError(t, err, "libevm/crypto.GenerateKey()")
144+
145+
stateDB, evm := ethtest.NewZeroEVM(t)
146+
signer := types.LatestSigner(evm.ChainConfig())
147+
tx := types.MustSignNewTx(
148+
key, signer,
149+
&types.LegacyTx{
150+
GasPrice: big.NewInt(gasPrice),
151+
Gas: tt.gasLimit,
152+
To: &common.Address{},
153+
Value: big.NewInt(0),
154+
},
155+
)
156+
157+
const startingBalance = 10 * params.Ether
158+
from := crypto.PubkeyToAddress(key.PublicKey)
159+
stateDB.SetNonce(from, 0)
160+
stateDB.SetBalance(from, uint256.NewInt(startingBalance))
161+
stateDB.AddRefund(tt.refund)
162+
163+
var (
164+
// Both variables are passed as pointers to
165+
// [core.ApplyTransaction], which will modify them.
166+
gotUsed uint64
167+
gotPool = core.GasPool(1e9)
168+
)
169+
wantPool := gotPool - core.GasPool(tt.wantUsed)
170+
171+
receipt, err := core.ApplyTransaction(
172+
evm.ChainConfig(), nil, &common.Address{}, &gotPool, stateDB,
173+
&types.Header{
174+
BaseFee: big.NewInt(gasPrice),
175+
// Required but irrelevant fields
176+
Number: big.NewInt(0),
177+
Difficulty: big.NewInt(0),
178+
},
179+
tx, &gotUsed, vm.Config{},
180+
)
181+
require.NoError(t, err, "core.ApplyTransaction(...)")
182+
183+
for desc, got := range map[string]uint64{
184+
"receipt.GasUsed": receipt.GasUsed,
185+
"receipt.CumulativeGasUsed": receipt.CumulativeGasUsed,
186+
"core.ApplyTransaction(..., usedGas *uint64, ...)": gotUsed,
187+
} {
188+
if got != tt.wantUsed {
189+
t.Errorf("%s got %d; want %d", desc, got, tt.wantUsed)
190+
}
191+
}
192+
if gotPool != wantPool {
193+
t.Errorf("After core.ApplyMessage(..., *%T); got %[1]T = %[1]d; want %d", gotPool, wantPool)
194+
}
195+
196+
wantBalance := uint256.NewInt(startingBalance - tt.wantUsed*gasPrice)
197+
if got := stateDB.GetBalance(from); !got.Eq(wantBalance) {
198+
t.Errorf("got remaining balance %d; want %d", got, wantBalance)
199+
}
200+
})
201+
}
202+
}

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)