Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stateful precompiles #42

Open
wants to merge 9 commits into
base: release/1.13
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ profile.cov
logs/

tests/spec-tests/

precompile/out/
32 changes: 24 additions & 8 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/precompile/contracts/nativeminter"
"github.com/holiman/uint256"
)

Expand Down Expand Up @@ -121,6 +122,8 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
// stateful precompiles
precompileManager PrecompileManager
}

// NewEVM returns a new EVM. The returned EVM is not thread safe and should
Expand All @@ -146,6 +149,19 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
}
evm.interpreter = NewEVMInterpreter(evm)
evm.precompileManager = NewPrecompileManager(evm)

// register precompiles here

// e.g. register native minter to 0x0000000000000000000000000000000000001000
evm.precompileManager.Register(
common.HexToAddress("0x1000"),
nativeminter.NewNativeMinter(),
)
Comment on lines +156 to +160
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to have this functionality here? probably would be better to move to an example branch

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it def needs removed, at least registering one. Could probably just be moved to a precompile doc that explains a bit and shows examples


// evm.precompileManager.Register(common.HexToAddress("0x1001"), compress.NewCompress())
// evm.precompileManager.Register(common.HexToAddress("0x1002"), jsonutil.NewJsonUtil())
Comment on lines +162 to +163
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove


return evm
}

Expand Down Expand Up @@ -186,7 +202,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
return nil, gas, ErrInsufficientBalance
}
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)
isPrecompile := evm.precompileManager.IsPrecompile(addr)
debug := evm.Config.Tracer != nil

if !evm.StateDB.Exist(addr) {
Expand Down Expand Up @@ -224,7 +240,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
}

if isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), value, gas, false)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
Expand Down Expand Up @@ -286,8 +302,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
}

// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile {
ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), value, gas, false)
} else {
addrCopy := addr
// Initialise a new contract and set the code that is to be used by the EVM.
Expand Down Expand Up @@ -331,8 +347,8 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
}

// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile {
ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), nil, gas, false)
} else {
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
Expand Down Expand Up @@ -380,8 +396,8 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
}(gas)
}

if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
if isPrecompile := evm.precompileManager.IsPrecompile(addr); isPrecompile {
ret, gas, err = evm.precompileManager.Run(addr, input, caller.Address(), new(big.Int), gas, true)
} else {
// At this point, we use a copy of address. If we don't, the go compiler will
// leak the 'contract' to the outer scope, and make allocation for 'contract'
Expand Down
8 changes: 8 additions & 0 deletions core/vm/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/precompile"
)

// StateDB is an EVM database for full state querying.
Expand Down Expand Up @@ -92,3 +93,10 @@ type CallContext interface {
// Create creates a new contract
Create(env *EVM, me ContractRef, data []byte, gas, value *big.Int) ([]byte, common.Address, error)
}

// PrecompileManager registers and runs stateful precompiles
type PrecompileManager interface {
IsPrecompile(addr common.Address) bool
Run(addr common.Address, input []byte, caller common.Address, value *big.Int, suppliedGas uint64, readonly bool) (ret []byte, remainingGas uint64, err error)
Register(addr common.Address, p precompile.StatefulPrecompiledContract) error
}
Comment on lines +97 to +102
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need the interface? are there multiple implementations of this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are not. I don't recall why I made the interface. Will remove.

183 changes: 183 additions & 0 deletions core/vm/precompile_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package vm

import (
"fmt"
"math/big"
"reflect"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/precompile"
)

type methodID [4]byte

type statefulMethod struct {
abiMethod abi.Method
reflectMethod reflect.Method
}

type precompileMethods map[methodID]*statefulMethod

type precompileManager struct {
evm *EVM
precompiles map[common.Address]precompile.StatefulPrecompiledContract
pMethods map[common.Address]precompileMethods
}

func NewPrecompileManager(evm *EVM) PrecompileManager {
precompiles := make(map[common.Address]precompile.StatefulPrecompiledContract)
pMethods := make(map[common.Address]precompileMethods)
return &precompileManager{
evm: evm,
precompiles: precompiles,
pMethods: pMethods,
}
}

func (pm *precompileManager) IsPrecompile(addr common.Address) bool {
_, isEvmPrecompile := pm.evm.precompile(addr)
if isEvmPrecompile {
return true
}

_, isStatefulPrecompile := pm.precompiles[addr]
return isStatefulPrecompile
}

func (pm *precompileManager) Run(
addr common.Address,
input []byte,
caller common.Address,
value *big.Int,
suppliedGas uint64,
readOnly bool,
) (ret []byte, remainingGas uint64, err error) {

// run core evm precompile
p, isEvmPrecompile := pm.evm.precompile(addr)
if isEvmPrecompile {
return RunPrecompiledContract(p, input, suppliedGas)
}

contract, ok := pm.precompiles[addr]
if !ok {
return nil, 0, fmt.Errorf("no precompiled contract at address %v", addr.Hex())
}

// Extract the method ID from the input
methodId := methodID(input)
// Try to get the method from the precompiled contracts using the method ID
method, exists := pm.pMethods[addr][methodId]
if !exists {
return nil, 0, fmt.Errorf("no method with id %v in precompiled contract at address %v", methodId, addr.Hex())
}

gasCost := contract.RequiredGas(input)
if gasCost > suppliedGas {
return nil, 0, ErrOutOfGas
}

// Unpack the input arguments using the ABI method's inputs
unpackedArgs, err := method.abiMethod.Inputs.Unpack(input[4:])
if err != nil {
return nil, 0, err
}

// Convert the unpacked args to reflect values.
reflectedUnpackedArgs := make([]reflect.Value, 0, len(unpackedArgs))
for _, unpacked := range unpackedArgs {
reflectedUnpackedArgs = append(reflectedUnpackedArgs, reflect.ValueOf(unpacked))
}

// set precompile nonce to 1 to avoid state deletion for being considered an empty account
// this conforms precompile contracts to EIP-161
if pm.evm.StateDB.GetNonce(addr) == 0 {
pm.evm.StateDB.SetNonce(addr, 1)
}

ctx := precompile.NewStatefulContext(pm.evm.StateDB, addr, caller, value)

// Make sure the readOnly is only set if we aren't in readOnly yet.
// This also makes sure that the readOnly flag isn't removed for child calls.
if readOnly && !ctx.IsReadOnly() {
ctx.SetReadOnly(true)
defer func() { ctx.SetReadOnly(false) }()
}

results := method.reflectMethod.Func.Call(append(
[]reflect.Value{
reflect.ValueOf(contract),
reflect.ValueOf(ctx),
},
reflectedUnpackedArgs...,
))

// check if precompile returned an error
if len(results) > 0 {
Copy link

@noot noot Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we check that the result parameters (length, type) are what are expected based on the method signature?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, will add

if err, ok := results[len(results)-1].Interface().(error); ok && err != nil {
return nil, 0, err
}
}

// Pack the result
var output []byte
if len(results) > 1 {
interfaceArgs := make([]interface{}, len(results)-1)
for i, v := range results[:len(results)-1] {
interfaceArgs[i] = v.Interface()
}
output, err = method.abiMethod.Outputs.Pack(interfaceArgs...)
if err != nil {
return nil, 0, err
}
Comment on lines +131 to +134
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to above - if the number/type of args aren't correct, this will probably error?

}

suppliedGas -= gasCost
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the caller check if this went negative? probably but just want to confirm

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a check above, Line 77,

	gasCost := contract.RequiredGas(input)
	if gasCost > suppliedGas {
		return nil, 0, ErrOutOfGas
	}

return output, suppliedGas, nil
}

func (pm *precompileManager) Register(addr common.Address, p precompile.StatefulPrecompiledContract) error {
if _, isEvmPrecompile := pm.evm.precompile(addr); isEvmPrecompile {
return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex())
}

if _, exists := pm.precompiles[addr]; exists {
return fmt.Errorf("precompiled contract already exists at address %v", addr.Hex())
}

// niaeve implementation; parsed abi method names must match precompile method names 1:1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// niaeve implementation; parsed abi method names must match precompile method names 1:1
// naive implementation; parsed abi method names must match precompile method names 1:1

//
// Note on method naming:
// Method name is the abi method name used for internal representation. It's derived from
// the abi raw name and a suffix will be added in the case of a function overload.
//
// e.g.
// These are two functions that have the same name:
// * foo(int,int)
// * foo(uint,uint)
// The method name of the first one will be resolved as Foo while the second one
// will be resolved as Foo0.
//
// Alternatively could require each precompile to define the func mapping instead of doing this magic
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is a lot better - the solidity/abigen stuff is unused apart from the abi, so we should remove it and just have the native code define the mapping.

abiMethods := p.GetABI().Methods
contractType := reflect.ValueOf(p).Type()
precompileMethods := make(precompileMethods)
for _, abiMethod := range abiMethods {
mName := strings.ToUpper(string(abiMethod.Name[0])) + abiMethod.Name[1:]
reflectMethod, exists := contractType.MethodByName(mName)
if !exists {
return fmt.Errorf("precompiled contract does not implement abi method %s with signature %s", abiMethod.Name, abiMethod.RawName)
}
mID := methodID(abiMethod.ID)
precompileMethods[mID] = &statefulMethod{
abiMethod: abiMethod,
reflectMethod: reflectMethod,
}
}

pm.precompiles[addr] = p
pm.pMethods[addr] = precompileMethods
return nil
}
8 changes: 7 additions & 1 deletion genesis.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
"difficulty": "10000000",
"gasLimit": "8000000",
"alloc": {
"0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102": { "balance": "300000000000000000000" }
"0x46B77EFDFB20979E1C29ec98DcE73e3eCbF64102": { "balance": "300000000000000000000" },
"0x0000000000000000000000000000000000001000": {
"balance": "0",
"storage": {
"0x00": "0x00000000000000000000000046B77EFDFB20979E1C29ec98DcE73e3eCbF64102"
}
}
},
"astriaOverrideGenesisExtraData": true,
"astriaSequencerInitialHeight": 1,
Expand Down
9 changes: 9 additions & 0 deletions precompile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Writing a Precompile Contract

1. Create Solidity interface in `contracts/interfaces`, e.g, IExampleContract.sol

2. Generate bindings with `./gen.sh`

3. Implement the precompile in Go. The struct should implement the `StatefulPrecompiledContract` interface and methods defined in the Solidity interface.

See NativeMinter as an example implementation
Loading