refactor!: gas consumption for stateful precompiles (#26)

* refactor!: gas consumption for stateful precompiles

* chore: remove receiver & arg names on `statefulPrecompile.RequiredGas()`

* doc: `vm.statefulPrecompile`
This commit is contained in:
Arran Schlosberg 2024-09-17 12:58:59 -04:00 committed by GitHub
parent 38eaaab96c
commit c5da3ca99e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 27 additions and 33 deletions

View file

@ -174,8 +174,7 @@ func (args *evmCallArgs) RunPrecompiledContract(p PrecompiledContract, input []b
return nil, 0, ErrOutOfGas return nil, 0, ErrOutOfGas
} }
suppliedGas -= gasCost suppliedGas -= gasCost
output, err := args.run(p, input) return args.run(p, input, suppliedGas)
return output, suppliedGas, err
} }
// ECRECOVER implemented as a native contract. // ECRECOVER implemented as a native contract.

View file

@ -50,42 +50,43 @@ const (
// run runs the [PrecompiledContract], differentiating between stateful and // run runs the [PrecompiledContract], differentiating between stateful and
// regular types. // regular types.
func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, err error) { func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
if p, ok := p.(statefulPrecompile); ok { if p, ok := p.(statefulPrecompile); ok {
return p.run(args, input) return p(args, input, suppliedGas)
} }
return p.Run(input) // Gas consumption for regular precompiles was already handled by the native
// RunPrecompiledContract(), which called this method.
ret, err = p.Run(input)
return ret, suppliedGas, err
} }
// PrecompiledStatefulRun is the stateful equivalent of the Run() method of a // PrecompiledStatefulContract is the stateful equivalent of a
// [PrecompiledContract]. // [PrecompiledContract].
type PrecompiledStatefulRun func(env PrecompileEnvironment, input []byte) ([]byte, error) type PrecompiledStatefulContract func(env PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error)
// NewStatefulPrecompile constructs a new PrecompiledContract that can be used // NewStatefulPrecompile constructs a new PrecompiledContract that can be used
// via an [EVM] instance but MUST NOT be called directly; a direct call to Run() // via an [EVM] instance but MUST NOT be called directly; a direct call to Run()
// reserves the right to panic. See other requirements defined in the comments // reserves the right to panic. See other requirements defined in the comments
// on [PrecompiledContract]. // on [PrecompiledContract].
func NewStatefulPrecompile(run PrecompiledStatefulRun, requiredGas func([]byte) uint64) PrecompiledContract { func NewStatefulPrecompile(run PrecompiledStatefulContract) PrecompiledContract {
return statefulPrecompile{ return statefulPrecompile(run)
gas: requiredGas,
run: run,
}
} }
type statefulPrecompile struct { // statefulPrecompile implements the [PrecompiledContract] interface to allow a
gas func([]byte) uint64 // [PrecompiledStatefulContract] to be carried with regular geth plumbing. The
run PrecompiledStatefulRun // methods are defined on this unexported type instead of directly on
} // [PrecompiledStatefulContract] to hide implementation details.
type statefulPrecompile PrecompiledStatefulContract
func (p statefulPrecompile) RequiredGas(input []byte) uint64 { // RequiredGas always returns zero as this gas is consumed by native geth code
return p.gas(input) // before the contract is run.
} func (statefulPrecompile) RequiredGas([]byte) uint64 { return 0 }
func (p statefulPrecompile) Run([]byte) ([]byte, error) { func (p statefulPrecompile) Run([]byte) ([]byte, error) {
// https://google.github.io/styleguide/go/best-practices.html#when-to-panic // https://google.github.io/styleguide/go/best-practices.html#when-to-panic
// This would indicate an API misuse and would occur in tests, not in // This would indicate an API misuse and would occur in tests, not in
// production. // production.
panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T", p, p.run)) panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T itself", p, p))
} }
// A PrecompileEnvironment provides information about the context in which a // A PrecompileEnvironment provides information about the context in which a

View file

@ -93,23 +93,18 @@ func TestNewStatefulPrecompile(t *testing.T) {
caller, self, stateVal, readOnly, input, caller, self, stateVal, readOnly, input,
)) ))
} }
run := func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) { run := func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want { if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want {
return nil, fmt.Errorf("PrecompileEnvironment().StateDB() must be non-nil i.f.f. not read-only; got non-nil? %t; want %t", got, want) return nil, 0, fmt.Errorf("PrecompileEnvironment().StateDB() must be non-nil i.f.f. not read-only; got non-nil? %t; want %t", got, want)
} }
addrs := env.Addresses() addrs := env.Addresses()
val := env.ReadOnlyState().GetState(precompile, slot) val := env.ReadOnlyState().GetState(precompile, slot)
return makeOutput(addrs.Caller, addrs.Self, input, val, env.ReadOnly()), nil return makeOutput(addrs.Caller, addrs.Self, input, val, env.ReadOnly()), suppliedGas - gasCost, nil
} }
hooks := &hookstest.Stub{ hooks := &hookstest.Stub{
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
precompile: vm.NewStatefulPrecompile( precompile: vm.NewStatefulPrecompile(run),
run,
func(b []byte) uint64 {
return gasCost
},
),
}, },
} }
hooks.Register(t) hooks.Register(t)
@ -204,13 +199,12 @@ func TestInheritReadOnly(t *testing.T) {
hooks := &hookstest.Stub{ hooks := &hookstest.Stub{
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
precompile: vm.NewStatefulPrecompile( precompile: vm.NewStatefulPrecompile(
func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) { func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
if env.ReadOnly() { if env.ReadOnly() {
return []byte{ifReadOnly}, nil return []byte{ifReadOnly}, suppliedGas, nil
} }
return []byte{ifNotReadOnly}, nil return []byte{ifNotReadOnly}, suppliedGas, nil
}, },
func([]byte) uint64 { return 0 },
), ),
}, },
} }