mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-21 06:04:33 +00:00
## 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 <519948+ARR4N@users.noreply.github.com>
Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
171 lines
5.5 KiB
Go
171 lines
5.5 KiB
Go
// Copyright 2024 the libevm authors.
|
|
//
|
|
// The libevm additions to go-ethereum are free software: you can redistribute
|
|
// them and/or modify them under the terms of the GNU Lesser General Public License
|
|
// as published by the Free Software Foundation, either version 3 of the License,
|
|
// or (at your option) any later version.
|
|
//
|
|
// The libevm additions are distributed in the hope that they will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
|
// General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see
|
|
// <http://www.gnu.org/licenses/>.
|
|
|
|
package vm
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/holiman/uint256"
|
|
|
|
"github.com/ava-labs/libevm/common"
|
|
"github.com/ava-labs/libevm/common/math"
|
|
"github.com/ava-labs/libevm/core/types"
|
|
"github.com/ava-labs/libevm/libevm"
|
|
"github.com/ava-labs/libevm/libevm/options"
|
|
"github.com/ava-labs/libevm/params"
|
|
)
|
|
|
|
var _ PrecompileEnvironment = (*environment)(nil)
|
|
|
|
type environment struct {
|
|
evm *EVM
|
|
self *Contract
|
|
callType CallType
|
|
}
|
|
|
|
func (e *environment) Gas() uint64 { return e.self.Gas }
|
|
func (e *environment) UseGas(gas uint64) bool { return e.self.UseGas(gas) }
|
|
func (e *environment) Value() *uint256.Int { return new(uint256.Int).Set(e.self.Value()) }
|
|
|
|
func (e *environment) ChainConfig() *params.ChainConfig { return e.evm.chainConfig }
|
|
func (e *environment) Rules() params.Rules { return e.evm.chainRules }
|
|
func (e *environment) ReadOnlyState() libevm.StateReader { return e.evm.StateDB }
|
|
func (e *environment) IncomingCallType() CallType { return e.callType }
|
|
func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) }
|
|
func (e *environment) BlockTime() uint64 { return e.evm.Context.Time }
|
|
|
|
func (e *environment) refundGas(add uint64) error {
|
|
gas, overflow := math.SafeAdd(e.self.Gas, add)
|
|
if overflow {
|
|
return ErrGasUintOverflow
|
|
}
|
|
e.self.Gas = gas
|
|
return nil
|
|
}
|
|
|
|
func (e *environment) ReadOnly() bool {
|
|
// A switch statement provides clearer code coverage for difficult-to-test
|
|
// cases.
|
|
switch {
|
|
case e.callType == StaticCall:
|
|
// evm.interpreter.readOnly is only set to true via a call to
|
|
// EVMInterpreter.Run() so, if a precompile is called directly with
|
|
// StaticCall(), then readOnly might not be set yet.
|
|
return true
|
|
case e.evm.interpreter.readOnly:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (e *environment) Addresses() *libevm.AddressContext {
|
|
return &libevm.AddressContext{
|
|
Origin: e.evm.Origin,
|
|
Caller: e.self.CallerAddress,
|
|
Self: e.self.Address(),
|
|
}
|
|
}
|
|
|
|
func (e *environment) StateDB() StateDB {
|
|
if e.ReadOnly() {
|
|
return nil
|
|
}
|
|
return e.evm.StateDB
|
|
}
|
|
|
|
func (e *environment) BlockHeader() (types.Header, error) {
|
|
hdr := e.evm.Context.Header
|
|
if hdr == nil {
|
|
// Although [core.NewEVMBlockContext] sets the field and is in the
|
|
// typical hot path (e.g. miner), there are other ways to create a
|
|
// [vm.BlockContext] (e.g. directly in tests) that may result in no
|
|
// available header.
|
|
return types.Header{}, fmt.Errorf("nil %T in current %T", hdr, e.evm.Context)
|
|
}
|
|
return *hdr, nil
|
|
}
|
|
|
|
func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, error) {
|
|
return e.callContract(Call, addr, input, gas, value, opts...)
|
|
}
|
|
|
|
func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retErr error) {
|
|
// Depth and read-only setting are handled by [EVMInterpreter.Run], which
|
|
// isn't used for precompiles, so we need to do it ourselves to maintain the
|
|
// expected invariants.
|
|
in := e.evm.interpreter
|
|
|
|
in.evm.depth++
|
|
defer func() { in.evm.depth-- }()
|
|
|
|
if e.ReadOnly() && !in.readOnly { // i.e. the precompile was StaticCall()ed
|
|
in.readOnly = true
|
|
defer func() { in.readOnly = false }()
|
|
}
|
|
|
|
var caller ContractRef = e.self
|
|
if options.As[callConfig](opts...).unsafeCallerAddressProxying {
|
|
// Note that, in addition to being unsafe, this breaks an EVM
|
|
// assumption that the caller ContractRef is always a *Contract.
|
|
caller = AccountRef(e.self.CallerAddress)
|
|
if e.callType == DelegateCall {
|
|
// self was created with AsDelegate(), which means that
|
|
// CallerAddress was inherited.
|
|
caller = AccountRef(e.self.Address())
|
|
}
|
|
}
|
|
|
|
if in.readOnly && value != nil && !value.IsZero() {
|
|
return nil, ErrWriteProtection
|
|
}
|
|
if !e.UseGas(gas) {
|
|
return nil, ErrOutOfGas
|
|
}
|
|
|
|
if t := e.evm.Config.Tracer; t != nil {
|
|
var bigVal *big.Int
|
|
if value != nil {
|
|
bigVal = value.ToBig()
|
|
}
|
|
t.CaptureEnter(typ.OpCode(), caller.Address(), addr, input, gas, bigVal)
|
|
|
|
startGas := gas
|
|
defer func() {
|
|
t.CaptureEnd(retData, startGas-e.Gas(), retErr)
|
|
}()
|
|
}
|
|
|
|
switch typ {
|
|
case Call:
|
|
ret, returnGas, callErr := e.evm.Call(caller, addr, input, gas, value)
|
|
if err := e.refundGas(returnGas); err != nil {
|
|
return nil, err
|
|
}
|
|
return ret, callErr
|
|
case CallCode, DelegateCall, StaticCall:
|
|
// TODO(arr4n): these cases should be very similar to CALL, hence the
|
|
// early abstraction, to signal to future maintainers. If implementing
|
|
// them, there's likely no need to honour the
|
|
// [callOptUNSAFECallerAddressProxy] because it's purely for backwards
|
|
// compatibility.
|
|
fallthrough
|
|
default:
|
|
return nil, fmt.Errorf("unimplemented precompile call type %v", typ)
|
|
}
|
|
}
|