Skip to content

Commit

Permalink
Merge pull request #1 from EscanBE/feat/stateful-precompile-contracts
Browse files Browse the repository at this point in the history
feat(precompiled): add custom precompiled contracts, support stateful precompiled contracts
  • Loading branch information
VictorTrustyDev authored Oct 10, 2024
2 parents e5eb32a + fc1317e commit 989f571
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 36 deletions.
126 changes: 126 additions & 0 deletions core/vm/contracts_evermint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package vm

import (
"bytes"
"encoding/hex"
"fmt"
"github.com/ethereum/go-ethereum/common"
)

var _ PrecompiledContract = &CustomPrecompiledContract{}

// CustomPrecompiledContract is a precompiled contract that externally provided.
type CustomPrecompiledContract struct {
name string // name of the contract
address common.Address // address of the contract
methods []CustomPrecompiledContractMethod // methods of the contract
disabled bool // disabled flag indicate the contract is disabled
}

func NewCustomPrecompiledContract(
address common.Address, methods []CustomPrecompiledContractMethod, name string,
) PrecompiledContract {
if address == (common.Address{}) {
panic("invalid address")
}

if len(methods) == 0 {
panic("no methods registered")
}

uniqueSig := make(map[string]struct{})
for _, m := range methods {
hexSig := hex.EncodeToString(m.Method4BytesSignatures)
if err := m.Validate(); err != nil {
panic(fmt.Sprintf("invalid method %s: %s", hexSig, err))
}
if _, exists := uniqueSig[hexSig]; exists {
panic(fmt.Sprintf("duplicate method %s", hexSig))
}
}

return &CustomPrecompiledContract{
address: address,
methods: methods,
name: name,
}
}

func (s CustomPrecompiledContract) RequiredGas(input []byte) uint64 {
sig := input[:4]

for _, method := range s.methods {
if bytes.Equal(method.Method4BytesSignatures, sig) {
return method.RequireGas
}
}

return 0
}

// Run runs the precompiled contract.
func (s CustomPrecompiledContract) Run(_ []byte) ([]byte, error) {
panic("use RunCustom instead")
}

// RunCustom runs the custom precompiled contract with extra arguments supports state-modifying contracts.
func (s CustomPrecompiledContract) RunCustom(caller ContractRef, input []byte, readOnly bool, evm *EVM) ([]byte, error) {
sig := input[:4]

for _, method := range s.methods {
if bytes.Equal(method.Method4BytesSignatures, sig) {
if readOnly && !method.ReadOnly {
return nil, ErrWriteProtection
}

return method.Executor.Execute(caller, s.address, input, evm)
}
}

return nil, ErrExecutionReverted
}

func (s CustomPrecompiledContract) Name() string {
return s.name
}

func (s CustomPrecompiledContract) Address() common.Address {
return s.address
}

func (s *CustomPrecompiledContract) WithDisabled(disabled bool) *CustomPrecompiledContract {
s.disabled = disabled
return s
}

type CustomPrecompiledContractMethod struct {
Method4BytesSignatures []byte
RequireGas uint64
ReadOnly bool
Executor CustomPrecompiledContractMethodExecutorI
}

func (m CustomPrecompiledContractMethod) Validate() error {
if len(m.Method4BytesSignatures) != 4 {
return fmt.Errorf("invalid method signature, expected 4 bytes, got %d", len(m.Method4BytesSignatures))
}

if m.ReadOnly {
// allow any gas requirement for read-only methods
} else {
if m.RequireGas == 0 {
return fmt.Errorf("invalid gas requirement, expected non-zero value")
}
}

if m.Executor == nil {
return fmt.Errorf("missing executor")
}

return nil
}

type CustomPrecompiledContractMethodExecutorI interface {
// Execute executes the method with the given input and returns the output.
Execute(caller ContractRef, contractAddress common.Address, input []byte, evm *EVM) ([]byte, error)
}
17 changes: 13 additions & 4 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
default:
precompiles = PrecompiledContractsHomestead
}

p, ok := precompiles[addr]

if !ok {
p, ok = evm.customPrecompiledContracts[addr]
}

return p, ok
}

Expand Down Expand Up @@ -121,6 +127,9 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64

// customPrecompiledContracts contains the list of custom precompiled contracts
customPrecompiledContracts map[common.Address]PrecompiledContract
}

// NewEVM returns a new EVM. The returned EVM is not thread safe and should
Expand Down Expand Up @@ -212,7 +221,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.interpreter.RunPrecompiledContract(caller, p, input, 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 @@ -275,7 +284,7 @@ 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)
ret, gas, err = evm.interpreter.RunPrecompiledContract(caller, p, input, 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 @@ -316,7 +325,7 @@ 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)
ret, gas, err = evm.interpreter.RunPrecompiledContract(caller, p, input, gas, false)
} else {
addrCopy := addr
// Initialise a new contract and make initialise the delegate values
Expand Down Expand Up @@ -365,7 +374,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
}

if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
ret, gas, err = evm.interpreter.RunPrecompiledContract(caller, p, input, 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
11 changes: 11 additions & 0 deletions core/vm/evm_evermint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package vm

import "github.com/ethereum/go-ethereum/common"

func (evm *EVM) WithCustomPrecompiledContract(precompiles ...PrecompiledContract) *EVM {
evm.customPrecompiledContracts = make(map[common.Address]PrecompiledContract, len(precompiles))
for _, precompile := range precompiles {
evm.customPrecompiledContracts[precompile.(*CustomPrecompiledContract).Address()] = precompile
}
return evm
}
45 changes: 45 additions & 0 deletions core/vm/interpreter_evermint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package vm

import (
"errors"
)

var (
ErrDisabledPrecompile = errors.New("the precompile contract is disabled")
)

// RunPrecompiledContract runs a precompiled contract with the given input and supplied gas.
func (in *EVMInterpreter) RunPrecompiledContract(caller ContractRef, p PrecompiledContract, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
cpc, isCustomPrecompiledContract := p.(*CustomPrecompiledContract)
if !isCustomPrecompiledContract {
return RunPrecompiledContract(p, input, suppliedGas)
}

if cpc.disabled {
return nil, suppliedGas, ErrDisabledPrecompile
}

if len(input) < 4 {
return nil, 0, ErrExecutionReverted
}

gasCost := cpc.RequiredGas(input)
if suppliedGas < gasCost {
return nil, 0, ErrOutOfGas
}
suppliedGas -= gasCost

{
// We do not increase the call depth for precompiled contracts.
}

// 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 && !in.readOnly {
in.readOnly = true
defer func() { in.readOnly = false }()
}

output, err := cpc.RunCustom(caller, input, readOnly, in.evm)
return output, suppliedGas, err
}
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,9 @@ require (
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect
github.com/go-logfmt/logfmt v0.4.0 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
Expand Down
Loading

0 comments on commit 989f571

Please sign in to comment.