Skip to content

Commit 44f23c8

Browse files
ARR4Nqdm12
andauthored
refactor: PrecompileEnvironment.{,Use,Refund}Gas() in lieu of args (#73)
## Why this should be merged Aligns precompiled contracts with the pattern used in `vm.EVMInterpreter` for regular contracts and simplifies gas accounting by using existing mechanisms. The most notable simplification occurs when there are multiple error paths that return early and have to account for consumed gas (any local solution would likely just mirror this one). This forms part of a broader, long-term direction of feature parity between precompiled and bytecode contracts, exposed via `vm.PrecompileEnvironment`. The `vm.Contract.Value()` method is also exposed as a natural accompaniment to this change. ## How this works The original signature for `vm.PrecompiledStatefulContract` received the amount of gas available and then returned the amount remaining; this has been removed in lieu of exposing the existing `vm.Contract.UseGas()` method via the `vm.PrecompileEnvironment`. A new `legacy` package wraps the old signature and converts it to a new one so `ava-labs/coreth` doesn't need to be refactored. ```go func oldPrecompile(env vm.PrecompileEnvironment, input []byte, gas uint64) ([]byte, uint64, error) { // ... if err != nil { return nil, gas - gasCost, err // pattern susceptible to bugs; should it be `nil, gas, err` ? } // ... return output, gas - gasCost, nil } func newPrecompile(env vm.PrecompileEnvironment, input []byte) ([]byte, error) { // The original `gas` argument is still available as `env.Gas()` // ... if !env.UseGas(gasCost) { // an explicit point at which gas is consumed return nil, vm.ErrOutOfGas } // ... if err != nil { return nil, err } // ... return output, nil } ``` ## How this was tested Existing unit test modified to use the `legacy` adaptor. --------- Signed-off-by: Arran Schlosberg <[email protected]> Co-authored-by: Quentin McGaw <[email protected]>
1 parent 43878f4 commit 44f23c8

File tree

6 files changed

+141
-52
lines changed

6 files changed

+141
-52
lines changed

core/vm/contracts.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ func (args *evmCallArgs) RunPrecompiledContract(p PrecompiledContract, input []b
177177
return nil, 0, ErrOutOfGas
178178
}
179179
suppliedGas -= gasCost
180-
return args.run(p, input, suppliedGas)
180+
args.gasRemaining = suppliedGas
181+
output, err := args.run(p, input)
182+
return output, args.gasRemaining, err
181183
}
182184

183185
// ECRECOVER implemented as a native contract.

core/vm/contracts.libevm.go

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ type evmCallArgs struct {
4545
callType CallType
4646

4747
// args:start
48-
caller ContractRef
49-
addr common.Address
50-
input []byte
51-
gas uint64
52-
value *uint256.Int
48+
caller ContractRef
49+
addr common.Address
50+
input []byte
51+
gasRemaining uint64
52+
value *uint256.Int
5353
// args:end
5454
}
5555

@@ -89,20 +89,26 @@ func (t CallType) OpCode() OpCode {
8989
}
9090

9191
// run runs the [PrecompiledContract], differentiating between stateful and
92-
// regular types.
93-
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
94-
if p, ok := p.(statefulPrecompile); ok {
95-
return p(args.env(), input, suppliedGas)
92+
// regular types, updating `args.gasRemaining` in the stateful case.
93+
func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, err error) {
94+
switch p := p.(type) {
95+
default:
96+
return p.Run(input)
97+
case statefulPrecompile:
98+
env := args.env()
99+
ret, err := p(env, input)
100+
args.gasRemaining = env.Gas()
101+
return ret, err
96102
}
97-
// Gas consumption for regular precompiles was already handled by the native
98-
// RunPrecompiledContract(), which called this method.
99-
ret, err = p.Run(input)
100-
return ret, suppliedGas, err
101103
}
102104

103105
// PrecompiledStatefulContract is the stateful equivalent of a
104106
// [PrecompiledContract].
105-
type PrecompiledStatefulContract func(env PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error)
107+
//
108+
// Instead of receiving and returning gas arguments, stateful precompiles use
109+
// the respective methods on [PrecompileEnvironment]. If a call to UseGas()
110+
// returns false, a stateful precompile SHOULD return [ErrOutOfGas].
111+
type PrecompiledStatefulContract func(env PrecompileEnvironment, input []byte) (ret []byte, err error)
106112

107113
// NewStatefulPrecompile constructs a new PrecompiledContract that can be used
108114
// via an [EVM] instance but MUST NOT be called directly; a direct call to Run()
@@ -135,13 +141,18 @@ func (p statefulPrecompile) Run([]byte) ([]byte, error) {
135141
type PrecompileEnvironment interface {
136142
ChainConfig() *params.ChainConfig
137143
Rules() params.Rules
138-
ReadOnly() bool
139144
// StateDB will be non-nil i.f.f !ReadOnly().
140145
StateDB() StateDB
141146
// ReadOnlyState will always be non-nil.
142147
ReadOnlyState() libevm.StateReader
143-
Addresses() *libevm.AddressContext
148+
144149
IncomingCallType() CallType
150+
Addresses() *libevm.AddressContext
151+
ReadOnly() bool
152+
// Equivalent to respective methods on [Contract].
153+
Gas() uint64
154+
UseGas(uint64) (hasEnoughGas bool)
155+
Value() *uint256.Int
145156

146157
BlockHeader() (types.Header, error)
147158
BlockNumber() *big.Int
@@ -150,7 +161,7 @@ type PrecompileEnvironment interface {
150161
// Call is equivalent to [EVM.Call] except that the `caller` argument is
151162
// removed and automatically determined according to the type of call that
152163
// invoked the precompile.
153-
Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, gasRemaining uint64, _ error)
164+
Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, _ error)
154165
}
155166

156167
func (args *evmCallArgs) env() *environment {
@@ -174,7 +185,7 @@ func (args *evmCallArgs) env() *environment {
174185

175186
// This is equivalent to the `contract` variables created by evm.*Call*()
176187
// methods, for non precompiles, to pass to [EVMInterpreter.Run].
177-
contract := NewContract(args.caller, AccountRef(self), value, args.gas)
188+
contract := NewContract(args.caller, AccountRef(self), value, args.gasRemaining)
178189
if args.callType == DelegateCall {
179190
contract = contract.AsDelegate()
180191
}

core/vm/contracts.libevm_test.go

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/ava-labs/libevm/libevm"
4040
"github.com/ava-labs/libevm/libevm/ethtest"
4141
"github.com/ava-labs/libevm/libevm/hookstest"
42+
"github.com/ava-labs/libevm/libevm/legacy"
4243
"github.com/ava-labs/libevm/params"
4344
)
4445

@@ -106,6 +107,7 @@ type statefulPrecompileOutput struct {
106107
ChainID *big.Int
107108
Addresses *libevm.AddressContext
108109
StateValue common.Hash
110+
ValueReceived *uint256.Int
109111
ReadOnly bool
110112
BlockNumber, Difficulty *big.Int
111113
BlockTime uint64
@@ -159,6 +161,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
159161
ChainID: env.ChainConfig().ChainID,
160162
Addresses: env.Addresses(),
161163
StateValue: env.ReadOnlyState().GetState(precompile, slot),
164+
ValueReceived: env.Value(),
162165
ReadOnly: env.ReadOnly(),
163166
BlockNumber: env.BlockNumber(),
164167
BlockTime: env.BlockTime(),
@@ -170,7 +173,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
170173
}
171174
hooks := &hookstest.Stub{
172175
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
173-
precompile: vm.NewStatefulPrecompile(run),
176+
precompile: vm.NewStatefulPrecompile(
177+
// In production, the new function signature should be used, but
178+
// this just exercises the converter.
179+
legacy.PrecompiledStatefulContract(run).Upgrade(),
180+
),
174181
},
175182
}
176183
hooks.Register(t)
@@ -181,7 +188,8 @@ func TestNewStatefulPrecompile(t *testing.T) {
181188
Difficulty: rng.BigUint64(),
182189
}
183190
input := rng.Bytes(8)
184-
value := rng.Hash()
191+
stateValue := rng.Hash()
192+
transferValue := rng.Uint256()
185193
chainID := rng.BigUint64()
186194

187195
caller := common.HexToAddress("CA11E12") // caller of the precompile
@@ -197,13 +205,15 @@ func TestNewStatefulPrecompile(t *testing.T) {
197205
&params.ChainConfig{ChainID: chainID},
198206
),
199207
)
200-
state.SetState(precompile, slot, value)
208+
state.SetState(precompile, slot, stateValue)
209+
state.SetBalance(caller, new(uint256.Int).Not(uint256.NewInt(0)))
201210
evm.Origin = eoa
202211

203212
tests := []struct {
204-
name string
205-
call func() ([]byte, uint64, error)
206-
wantAddresses *libevm.AddressContext
213+
name string
214+
call func() ([]byte, uint64, error)
215+
wantAddresses *libevm.AddressContext
216+
wantTransferValue *uint256.Int
207217
// Note that this only covers evm.readOnly being true because of the
208218
// precompile's call. See TestInheritReadOnly for alternate case.
209219
wantReadOnly bool
@@ -212,28 +222,30 @@ func TestNewStatefulPrecompile(t *testing.T) {
212222
{
213223
name: "EVM.Call()",
214224
call: func() ([]byte, uint64, error) {
215-
return evm.Call(callerContract, precompile, input, gasLimit, uint256.NewInt(0))
225+
return evm.Call(callerContract, precompile, input, gasLimit, transferValue)
216226
},
217227
wantAddresses: &libevm.AddressContext{
218228
Origin: eoa,
219229
Caller: caller,
220230
Self: precompile,
221231
},
222-
wantReadOnly: false,
223-
wantCallType: vm.Call,
232+
wantReadOnly: false,
233+
wantTransferValue: transferValue,
234+
wantCallType: vm.Call,
224235
},
225236
{
226237
name: "EVM.CallCode()",
227238
call: func() ([]byte, uint64, error) {
228-
return evm.CallCode(callerContract, precompile, input, gasLimit, uint256.NewInt(0))
239+
return evm.CallCode(callerContract, precompile, input, gasLimit, transferValue)
229240
},
230241
wantAddresses: &libevm.AddressContext{
231242
Origin: eoa,
232243
Caller: caller,
233244
Self: caller,
234245
},
235-
wantReadOnly: false,
236-
wantCallType: vm.CallCode,
246+
wantReadOnly: false,
247+
wantTransferValue: transferValue,
248+
wantCallType: vm.CallCode,
237249
},
238250
{
239251
name: "EVM.DelegateCall()",
@@ -245,8 +257,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
245257
Caller: eoa, // inherited from caller
246258
Self: caller,
247259
},
248-
wantReadOnly: false,
249-
wantCallType: vm.DelegateCall,
260+
wantReadOnly: false,
261+
wantTransferValue: uint256.NewInt(0),
262+
wantCallType: vm.DelegateCall,
250263
},
251264
{
252265
name: "EVM.StaticCall()",
@@ -258,8 +271,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
258271
Caller: caller,
259272
Self: precompile,
260273
},
261-
wantReadOnly: true,
262-
wantCallType: vm.StaticCall,
274+
wantReadOnly: true,
275+
wantTransferValue: uint256.NewInt(0),
276+
wantCallType: vm.StaticCall,
263277
},
264278
}
265279

@@ -268,7 +282,8 @@ func TestNewStatefulPrecompile(t *testing.T) {
268282
wantOutput := statefulPrecompileOutput{
269283
ChainID: chainID,
270284
Addresses: tt.wantAddresses,
271-
StateValue: value,
285+
StateValue: stateValue,
286+
ValueReceived: tt.wantTransferValue,
272287
ReadOnly: tt.wantReadOnly,
273288
BlockNumber: header.Number,
274289
BlockTime: header.Time,
@@ -318,11 +333,11 @@ func TestInheritReadOnly(t *testing.T) {
318333
hooks := &hookstest.Stub{
319334
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
320335
precompile: vm.NewStatefulPrecompile(
321-
func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
336+
func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
322337
if env.ReadOnly() {
323-
return []byte{ifReadOnly}, suppliedGas, nil
338+
return []byte{ifReadOnly}, nil
324339
}
325-
return []byte{ifNotReadOnly}, suppliedGas, nil
340+
return []byte{ifNotReadOnly}, nil
326341
},
327342
),
328343
},
@@ -535,21 +550,21 @@ func TestPrecompileMakeCall(t *testing.T) {
535550

536551
hooks := &hookstest.Stub{
537552
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
538-
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
553+
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
539554
var opts []vm.CallOption
540555
if bytes.Equal(input, unsafeCallerProxyOptSentinel) {
541556
opts = append(opts, vm.WithUNSAFECallerAddressProxying())
542557
}
543558
// We are ultimately testing env.Call(), hence why this is the SUT.
544-
return env.Call(dest, precompileCallData, suppliedGas, uint256.NewInt(0), opts...)
559+
return env.Call(dest, precompileCallData, env.Gas(), uint256.NewInt(0), opts...)
545560
}),
546-
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
561+
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
547562
out := &statefulPrecompileOutput{
548563
Addresses: env.Addresses(),
549564
ReadOnly: env.ReadOnly(),
550565
Input: input, // expected to be callData
551566
}
552-
return out.Bytes(), suppliedGas, nil
567+
return out.Bytes(), nil
553568
}),
554569
},
555570
}
@@ -696,8 +711,8 @@ func TestPrecompileCallWithTracer(t *testing.T) {
696711

697712
hooks := &hookstest.Stub{
698713
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
699-
precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
700-
return env.Call(contract, nil, suppliedGas, uint256.NewInt(0))
714+
precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
715+
return env.Call(contract, nil, env.Gas(), uint256.NewInt(0))
701716
}),
702717
},
703718
}

core/vm/environment.libevm.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/holiman/uint256"
2424

2525
"github.com/ava-labs/libevm/common"
26+
"github.com/ava-labs/libevm/common/math"
2627
"github.com/ava-labs/libevm/core/types"
2728
"github.com/ava-labs/libevm/libevm"
2829
"github.com/ava-labs/libevm/libevm/options"
@@ -37,13 +38,26 @@ type environment struct {
3738
callType CallType
3839
}
3940

41+
func (e *environment) Gas() uint64 { return e.self.Gas }
42+
func (e *environment) UseGas(gas uint64) bool { return e.self.UseGas(gas) }
43+
func (e *environment) Value() *uint256.Int { return new(uint256.Int).Set(e.self.Value()) }
44+
4045
func (e *environment) ChainConfig() *params.ChainConfig { return e.evm.chainConfig }
4146
func (e *environment) Rules() params.Rules { return e.evm.chainRules }
4247
func (e *environment) ReadOnlyState() libevm.StateReader { return e.evm.StateDB }
4348
func (e *environment) IncomingCallType() CallType { return e.callType }
4449
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
4550
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
4651

52+
func (e *environment) refundGas(add uint64) error {
53+
gas, overflow := math.SafeAdd(e.self.Gas, add)
54+
if overflow {
55+
return ErrGasUintOverflow
56+
}
57+
e.self.Gas = gas
58+
return nil
59+
}
60+
4761
func (e *environment) ReadOnly() bool {
4862
// A switch statement provides clearer code coverage for difficult-to-test
4963
// cases.
@@ -87,11 +101,11 @@ func (e *environment) BlockHeader() (types.Header, error) {
87101
return *hdr, nil
88102
}
89103

90-
func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
104+
func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, error) {
91105
return e.callContract(Call, addr, input, gas, value, opts...)
92106
}
93107

94-
func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retGas uint64, retErr error) {
108+
func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retErr error) {
95109
// Depth and read-only setting are handled by [EVMInterpreter.Run], which
96110
// isn't used for precompiles, so we need to do it ourselves to maintain the
97111
// expected invariants.
@@ -118,8 +132,12 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by
118132
}
119133

120134
if in.readOnly && value != nil && !value.IsZero() {
121-
return nil, gas, ErrWriteProtection
135+
return nil, ErrWriteProtection
122136
}
137+
if !e.UseGas(gas) {
138+
return nil, ErrOutOfGas
139+
}
140+
123141
if t := e.evm.Config.Tracer; t != nil {
124142
var bigVal *big.Int
125143
if value != nil {
@@ -129,13 +147,17 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by
129147

130148
startGas := gas
131149
defer func() {
132-
t.CaptureEnd(retData, startGas-retGas, retErr)
150+
t.CaptureEnd(retData, startGas-e.Gas(), retErr)
133151
}()
134152
}
135153

136154
switch typ {
137155
case Call:
138-
return e.evm.Call(caller, addr, input, gas, value)
156+
ret, returnGas, callErr := e.evm.Call(caller, addr, input, gas, value)
157+
if err := e.refundGas(returnGas); err != nil {
158+
return nil, err
159+
}
160+
return ret, callErr
139161
case CallCode, DelegateCall, StaticCall:
140162
// TODO(arr4n): these cases should be very similar to CALL, hence the
141163
// early abstraction, to signal to future maintainers. If implementing
@@ -144,6 +166,6 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by
144166
// compatibility.
145167
fallthrough
146168
default:
147-
return nil, gas, fmt.Errorf("unimplemented precompile call type %v", typ)
169+
return nil, fmt.Errorf("unimplemented precompile call type %v", typ)
148170
}
149171
}

core/vm/libevm_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ package vm
1818
// The original RunPrecompiledContract was migrated to being a method on
1919
// [evmCallArgs]. We need to replace it for use by regular geth tests.
2020
func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
21-
return (*evmCallArgs)(nil).RunPrecompiledContract(p, input, suppliedGas)
21+
return new(evmCallArgs).RunPrecompiledContract(p, input, suppliedGas)
2222
}

0 commit comments

Comments
 (0)