-
Notifications
You must be signed in to change notification settings - Fork 2
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
base: release/1.13
Are you sure you want to change the base?
Changes from 8 commits
f422517
1a7c6b7
9fd3f64
55166c9
a1d82ee
870b51c
72ca087
dbb333c
53cf1bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,3 +50,5 @@ profile.cov | |
logs/ | ||
|
||
tests/spec-tests/ | ||
|
||
precompile/out/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
) | ||
|
||
|
@@ -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 | ||
|
@@ -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(), | ||
) | ||
|
||
// evm.precompileManager.Register(common.HexToAddress("0x1001"), compress.NewCompress()) | ||
// evm.precompileManager.Register(common.HexToAddress("0x1002"), jsonutil.NewJsonUtil()) | ||
Comment on lines
+162
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove |
||
|
||
return evm | ||
} | ||
|
||
|
@@ -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) { | ||
|
@@ -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. | ||
|
@@ -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. | ||
|
@@ -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 | ||
|
@@ -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' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need the interface? are there multiple implementations of this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,177 @@ | ||||||
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)) | ||||||
} | ||||||
|
||||||
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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
// | ||||||
// 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
} |
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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