Commit b970f823 authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops/script: turn any Go struct/interface into a precompile with ABI...

op-chain-ops/script: turn any Go struct/interface into a precompile with ABI encoding/decoding (#11444)

* op-chain-ops/script: turn any Go struct/interface into a precompile with ABI encoding/decoding

* op-chain-ops: add more comments about precompile auto ABI code
parent db21f4aa
package script
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math/big"
"reflect"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
// precompileFunc is a prepared function to perform a method call / field read with ABI decoding/encoding.
type precompileFunc struct {
goName string
abiSignature string
fn func(input []byte) ([]byte, error)
}
// bytes4 computes a 4-byte method-selector ID of a solidity method signature
func bytes4(sig string) [4]byte {
return [4]byte(crypto.Keccak256([]byte(sig))[:4])
}
// big-endian uint64 to bytes32
func b32(v uint64) []byte {
out := make([]byte, 32)
binary.BigEndian.PutUint64(out[24:], v)
return out
}
// pad to multiple of 32 bytes
func pad32(data []byte) []byte {
out := bytes.Clone(data)
if len(out)%32 == 0 {
return out
}
return append(out, make([]byte, 32-(len(out)%32))...)
}
// Precompile is a wrapper around a Go object, making it a precompile.
type Precompile[E any] struct {
Precompile E
// abiMethods is effectively the jump-table for 4-byte ABI calls to the precompile.
abiMethods map[[4]byte]*precompileFunc
}
var _ vm.PrecompiledContract = (*Precompile[struct{}])(nil)
// NewPrecompile wraps a Go object into a Precompile.
// All exported fields and methods will have a corresponding ABI interface.
// Fields with a tag `evm:"-"` will be ignored.
// Field names and method names are adjusted to start with a lowercase character in the ABI signature.
// Method names may end with a `_X` where X must be the 4byte selector (this is sanity-checked),
// to support multiple variants of the same method with different ABI input parameters.
// Methods may return an error, which will result in a revert, rather than become an ABI encoded arg, if not nil.
// All precompile methods have 0 gas cost.
func NewPrecompile[E any](e E) (*Precompile[E], error) {
out := &Precompile[E]{Precompile: e, abiMethods: make(map[[4]byte]*precompileFunc)}
elemVal := reflect.ValueOf(e)
// setup methods (and if pointer, the indirect methods also)
if err := out.setupMethods(&elemVal); err != nil {
return nil, fmt.Errorf("failed to setup methods of precompile: %w", err)
}
// setup fields and embedded types (if a struct)
if err := out.setupFields(&elemVal); err != nil {
return nil, fmt.Errorf("failed to setup fields of precompile: %w", err)
}
return out, nil
}
// setupMethods iterates through all exposed methods of val, and sets them all up as ABI methods.
func (p *Precompile[E]) setupMethods(val *reflect.Value) error {
typ := val.Type()
methodCount := val.NumMethod()
for i := 0; i < methodCount; i++ {
methodDef := typ.Method(i)
if !methodDef.IsExported() {
continue
}
if err := p.setupMethod(val, &methodDef); err != nil {
return fmt.Errorf("failed to set up call-handler for method %d (%s): %w", i, methodDef.Name, err)
}
}
return nil
}
// setupMethod takes a method definition, attached to selfVal,
// and builds an ABI method to handle the input decoding and output encoding around the inner Go function.
func (p *Precompile[E]) setupMethod(selfVal *reflect.Value, methodDef *reflect.Method) error {
methodName := methodDef.Name
abiFunctionName := methodName
// Solidity allows multiple functions with the same name, but different input params.
// So cut off the suffix after the last "_", to allow the different variants to be defined in Go.
variantSuffixIndex := strings.LastIndexByte(methodName, '_')
variantSuffix := ""
if variantSuffixIndex >= 0 {
abiFunctionName = methodName[:variantSuffixIndex]
variantSuffix = methodName[variantSuffixIndex+1:] // strip out the underscore
}
if len(abiFunctionName) == 0 {
return fmt.Errorf("ABI method name of %s must not be empty", methodDef.Name)
}
if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] {
abiFunctionName = lo + abiFunctionName[1:]
}
// Prepare ABI definitions of call parameters.
inArgCount := methodDef.Type.NumIn() - 1
if inArgCount < 0 {
return errors.New("expected method with receiver as first argument")
}
inArgs := make(abi.Arguments, inArgCount)
inArgTypes := make([]string, inArgCount)
inArgAllocators := make([]func() interface{}, inArgCount)
for i := 0; i < inArgCount; i++ {
argType := methodDef.Type.In(i + 1) // account for receiver
abiTyp, err := goTypeToABIType(argType)
if err != nil {
return fmt.Errorf("failed to determine ABI type of input arg %d: %w", i, err)
}
inArgs[i] = abi.Argument{
Name: fmt.Sprintf("in_%d", i),
Type: abiTyp,
}
inArgAllocators[i] = func() interface{} {
return reflect.New(argType).Elem().Interface()
}
inArgTypes[i] = abiTyp.String()
}
methodSig := fmt.Sprintf("%v(%v)", abiFunctionName, strings.Join(inArgTypes, ","))
byte4Sig := bytes4(methodSig)
if variantSuffix != "" {
if expected := fmt.Sprintf("%x", byte4Sig); expected != variantSuffix {
return fmt.Errorf("expected variant suffix %s for ABI method %s (Go: %s), but got %s",
expected, methodSig, methodDef.Name, variantSuffix)
}
}
if m, ok := p.abiMethods[byte4Sig]; ok {
return fmt.Errorf("method %s conflicts with existing ABI method %s (Go: %s), signature: %x",
methodDef.Name, m.abiSignature, m.goName, byte4Sig)
}
outArgCount := methodDef.Type.NumOut()
// A Go method may return an error, which we do not ABI-encode, but rather forward as revert.
errReturn := false
if outArgCount > 0 {
errIndex := outArgCount - 1
lastTyp := methodDef.Type.Out(errIndex)
if lastTyp.Kind() == reflect.Interface && lastTyp.Implements(typeFor[error]()) {
outArgCount -= 1
errReturn = true
}
}
// Prepare ABI definitions of return parameters.
outArgs := make(abi.Arguments, outArgCount)
for i := 0; i < outArgCount; i++ {
argType := methodDef.Type.Out(i)
abiTyp, err := goTypeToABIType(argType)
if err != nil {
return fmt.Errorf("failed to determine ABI type of output arg %d: %w", i, err)
}
outArgs[i] = abi.Argument{
Name: fmt.Sprintf("out_%d", i),
Type: abiTyp,
}
}
fn := makeFn(selfVal, &methodDef.Func, errReturn, inArgs, outArgs, inArgAllocators)
p.abiMethods[byte4Sig] = &precompileFunc{
goName: methodName,
abiSignature: methodSig,
fn: fn,
}
return nil
}
// makeFn is a helper function to perform a method call:
// - ABI decoding of input
// - type conversion of inputs
// - actual function Go call
// - handling of error return value
// - and ABI encoding of outputs
func makeFn(selfVal, methodVal *reflect.Value, errReturn bool, inArgs, outArgs abi.Arguments, inArgAllocators []func() any) func(input []byte) ([]byte, error) {
return func(input []byte) ([]byte, error) {
// Unpack the ABI data into default Go types
inVals, err := inArgs.UnpackValues(input)
if err != nil {
return nil, fmt.Errorf("failed to decode input: %x\nerr: %w", input, err)
}
// Sanity check that the ABI util returned the expected number of inputs
if len(inVals) != len(inArgAllocators) {
return nil, fmt.Errorf("expected %d args, got %d", len(inArgAllocators), len(inVals))
}
// Convert each default Go type into the expected opinionated Go type
callArgs := make([]reflect.Value, 0, 1+len(inArgAllocators))
callArgs = append(callArgs, *selfVal)
for i, inAlloc := range inArgAllocators {
argSrc := inVals[i]
argDest := inAlloc()
argDest, err = convertType(argSrc, argDest)
if err != nil {
return nil, fmt.Errorf("failed to convert arg %d from Go type %T to %T: %w", i, argSrc, argDest, err)
}
callArgs = append(callArgs, reflect.ValueOf(argDest))
}
// Call the precompile Go function
returnReflectVals := methodVal.Call(callArgs)
// Collect the return values
returnVals := make([]interface{}, len(returnReflectVals))
for i := range returnReflectVals {
returnVals[i] = returnReflectVals[i].Interface()
}
if errReturn {
errIndex := len(returnVals) - 1
if errV := returnVals[errIndex]; errV != nil {
if err, ok := errV.(error); ok {
return nil, err
}
}
returnVals = returnVals[:errIndex]
}
// Encode the return values
out, err := outArgs.PackValues(returnVals)
if err != nil {
return nil, fmt.Errorf("failed to encode return data: %w", err)
}
return out, nil
}
}
// convertType is a helper to run the Geth type conversion util,
// forcing one Go type into another approximately equivalent Go type
// (handling pointers and underlying equivalent types).
func convertType(src, dest any) (out any, err error) {
defer func() {
if rErr := recover(); rErr != nil {
err = fmt.Errorf("ConvertType fail: %v", rErr)
}
}()
out = abi.ConvertType(src, dest) // no error return, just panics if invalid.
return
}
// goTypeToABIType infers the geth ABI type definition from a Go reflect type definition.
func goTypeToABIType(typ reflect.Type) (abi.Type, error) {
solType, internalType, err := goTypeToSolidityType(typ)
if err != nil {
return abi.Type{}, err
}
return abi.NewType(solType, internalType, nil)
}
// ABIInt256 is an alias for big.Int that is represented as int256 in ABI method signature,
// since big.Int interpretation defaults to uint256.
type ABIInt256 big.Int
var abiInt256Type = typeFor[ABIInt256]()
// goTypeToSolidityType converts a Go type to the solidity ABI type definition.
// The "internalType" is a quirk of the Geth ABI utils, for nested structures.
// Unfortunately we have to convert to string, not directly to ABI type structure,
// as it is the only way to initialize Geth ABI types.
func goTypeToSolidityType(typ reflect.Type) (typeDef, internalType string, err error) {
switch typ.Kind() {
case reflect.Int, reflect.Uint:
return "", "", fmt.Errorf("ints must have explicit size, type not valid: %s", typ)
case reflect.Bool, reflect.String, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strings.ToLower(typ.Kind().String()), "", nil
case reflect.Array:
if typ.Elem().Kind() == reflect.Uint8 {
if typ.Len() == 20 && typ.Name() == "Address" {
return "address", "", nil
}
if typ.Len() > 32 {
return "", "", fmt.Errorf("byte array too large: %d", typ.Len())
}
return fmt.Sprintf("bytes%d", typ.Len()), "", nil
}
elemTyp, internalTyp, err := goTypeToSolidityType(typ.Elem())
if err != nil {
return "", "", fmt.Errorf("unrecognized slice-elem type: %w", err)
}
if internalTyp != "" {
return "", "", fmt.Errorf("nested internal types not supported: %w", err)
}
return fmt.Sprintf("%s[%d]", elemTyp, typ.Len()), "", nil
case reflect.Slice:
if typ.Elem().Kind() == reflect.Uint8 {
return "bytes", "", nil
}
elemABITyp, internalTyp, err := goTypeToSolidityType(typ.Elem())
if err != nil {
return "", "", fmt.Errorf("unrecognized slice-elem type: %w", err)
}
if internalTyp != "" {
return "", "", fmt.Errorf("nested internal types not supported: %w", err)
}
return elemABITyp + "[]", "", nil
case reflect.Struct:
if typ.AssignableTo(abiInt256Type) {
return "int256", "", nil
}
if typ.ConvertibleTo(typeFor[big.Int]()) {
return "uint256", "", nil
}
// We can parse into abi.TupleTy in the future, if necessary
return "", "", fmt.Errorf("structs are not supported, cannot handle type %s", typ)
case reflect.Pointer:
elemABITyp, internalTyp, err := goTypeToSolidityType(typ.Elem())
if err != nil {
return "", "", fmt.Errorf("unrecognized pointer-elem type: %w", err)
}
return elemABITyp, internalTyp, nil
default:
return "", "", fmt.Errorf("unrecognized typ: %s", typ)
}
}
// setupFields registers all exported non-ignored fields as public ABI getters.
// Fields and methods of embedded structs are registered along the way.
func (p *Precompile[E]) setupFields(val *reflect.Value) error {
if val.Kind() == reflect.Pointer {
if val.IsNil() {
return fmt.Errorf("cannot setupFields of nil value (type: %s)", val.Type())
}
inner := val.Elem()
if err := p.setupFields(&inner); err != nil {
return fmt.Errorf("failed to setupFields of inner pointer type: %w", err)
}
return nil
}
if val.Kind() != reflect.Struct {
return nil // ignore non-struct types
}
typ := val.Type()
fieldCount := val.NumField()
for i := 0; i < fieldCount; i++ {
fieldTyp := typ.Field(i)
if !fieldTyp.IsExported() {
continue
}
// With the "evm" struct tag set to "-", exposed fields can explicitly be ignored,
// and will not be translated into getter functions on the precompile or further exposed.
if tag, ok := fieldTyp.Tag.Lookup("evm"); ok && tag == "-" {
continue
}
fieldVal := val.Field(i)
if fieldTyp.Anonymous {
// process methods and inner fields of embedded fields
if err := p.setupMethods(&fieldVal); err != nil {
return fmt.Errorf("failed to setup methods of embedded field %s (type: %s): %w",
fieldTyp.Name, fieldTyp.Type, err)
}
if err := p.setupFields(&fieldVal); err != nil {
return fmt.Errorf("failed to setup fields of embedded field %s (type %s): %w",
fieldTyp.Name, fieldTyp.Type, err)
}
continue
}
if err := p.setupStructField(&fieldTyp, &fieldVal); err != nil {
return fmt.Errorf("failed to setup struct field %s (type %s): %w", fieldTyp.Name, fieldTyp.Type, err)
}
}
return nil
}
// setupStructField registers a struct field as a public-getter ABI method.
func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal *reflect.Value) error {
abiFunctionName := fieldDef.Name
if len(abiFunctionName) == 0 {
return fmt.Errorf("ABI name of %s must not be empty", fieldDef.Name)
}
if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] {
abiFunctionName = lo + abiFunctionName[1:]
}
// The ABI signature of public fields in solidity is simply a getter function of the same name.
// The return type is not part of the ABI signature. So we just append "()" to turn it into a function.
methodSig := abiFunctionName + "()"
byte4Sig := bytes4(methodSig)
if m, ok := p.abiMethods[byte4Sig]; ok {
return fmt.Errorf("struct field %s conflicts with existing ABI method %s (Go: %s), signature: %x",
fieldDef.Name, m.abiSignature, m.goName, byte4Sig)
}
// Determine the type to ABI-encode the Go field value into
abiTyp, err := goTypeToABIType(fieldDef.Type)
if err != nil {
return fmt.Errorf("failed to determine ABI type of struct field of type %s: %w", fieldDef.Type, err)
}
outArgs := abi.Arguments{
{
Name: abiFunctionName,
Type: abiTyp,
},
}
// Create the getter ABI method, that will take the field value, encode it, and return it.
fn := func(input []byte) ([]byte, error) {
if len(input) != 0 { // 4 byte selector is already trimmed
return nil, fmt.Errorf("unexpected input: %x", input)
}
outData, err := outArgs.PackValues([]any{fieldVal.Interface()})
if err != nil {
return nil, fmt.Errorf("method %s failed to pack return data: %w", methodSig, err)
}
return outData, nil
}
p.abiMethods[byte4Sig] = &precompileFunc{
goName: fieldDef.Name,
abiSignature: methodSig,
fn: fn,
}
return nil
}
// RequiredGas is part of the vm.PrecompiledContract interface, and all system precompiles use 0 gas.
func (p *Precompile[E]) RequiredGas(input []byte) uint64 {
return 0
}
// Run implements the vm.PrecompiledContract interface.
// This takes the ABI calldata, finds the applicable method by selector, and then runs that method with the data.
func (p *Precompile[E]) Run(input []byte) ([]byte, error) {
if len(input) < 4 {
return encodeRevert(fmt.Errorf("expected at least 4 bytes, but got '%x'", input))
}
sig := [4]byte(input[:4])
params := input[4:]
fn, ok := p.abiMethods[sig]
if !ok {
return encodeRevert(fmt.Errorf("unrecognized 4 byte signature: %x", sig))
}
out, err := fn.fn(params)
if err != nil {
return encodeRevert(fmt.Errorf("failed to run %s, ABI: %q, err: %w", fn.goName, fn.abiSignature, err))
}
return out, nil
}
// revertSelector is the ABI signature of a default error type in solidity.
var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4]
func encodeRevert(outErr error) ([]byte, error) {
outErrStr := []byte(outErr.Error())
out := make([]byte, 0, 4+32*2+len(outErrStr)+32)
out = append(out, revertSelector...) // selector
out = append(out, b32(0x20)...) // offset to string
out = append(out, b32(uint64(len(outErrStr)))...) // length of string
out = append(out, pad32(outErrStr)...) // the error message string
return out, vm.ErrExecutionReverted // Geth EVM will pick this up as a revert with return-data
}
// typeFor returns the [Type] that represents the type argument T.
// Note: not available yet in Go 1.21, but part of std-lib later.
func typeFor[T any]() reflect.Type {
return reflect.TypeOf((*T)(nil)).Elem()
}
package script
import (
"errors"
"math/big"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
type EmbeddedExample struct {
Foo uint64
}
func (e *EmbeddedExample) TwoFoo() uint64 {
e.Foo *= 2
return e.Foo
}
type ExamplePrecompile struct {
EmbeddedExample
Bar *big.Int
hello string
helloFrom string
}
var testErr = errors.New("test err")
func (e *ExamplePrecompile) Greet(name string) (string, error) {
if name == "mallory" {
return "", testErr
}
e.helloFrom = name
return e.hello + " " + name + "!", nil
}
func (e *ExamplePrecompile) Things() (bar *big.Int, hello string, seen string) {
return e.Bar, e.hello, e.helloFrom
}
func (e *ExamplePrecompile) AddAndMul(a, b, c uint64, x uint8) uint64 {
return (a + b + c) * uint64(x)
}
func TestPrecompile(t *testing.T) {
e := &ExamplePrecompile{hello: "Hola", EmbeddedExample: EmbeddedExample{Foo: 42}, Bar: big.NewInt(123)}
p, err := NewPrecompile[*ExamplePrecompile](e)
require.NoError(t, err)
for k, v := range p.abiMethods {
t.Logf("4byte: %x ABI: %s Go: %s", k, v.abiSignature, v.goName)
}
// input/output
input := crypto.Keccak256([]byte("greet(string)"))[:4]
input = append(input, b32(0x20)...) // offset
input = append(input, b32(uint64(len("alice")))...) // length
input = append(input, "alice"...)
out, err := p.Run(input)
require.NoError(t, err)
require.Equal(t, e.helloFrom, "alice")
require.Equal(t, out[:32], b32(0x20))
require.Equal(t, out[32:32*2], b32(uint64(len("Hola alice!"))))
require.Equal(t, out[32*2:32*3], pad32([]byte("Hola alice!")))
// error handling
input = crypto.Keccak256([]byte("greet(string)"))[:4]
input = append(input, b32(0x20)...) // offset
input = append(input, b32(uint64(len("mallory")))...) // length
input = append(input, "mallory"...)
out, err = p.Run(input)
require.Equal(t, err, vm.ErrExecutionReverted)
msg, err := abi.UnpackRevert(out)
require.NoError(t, err, "must unpack revert data")
require.True(t, strings.HasSuffix(msg, testErr.Error()), "revert data must end with the inner error")
// field reads
input = crypto.Keccak256([]byte("foo()"))[:4]
out, err = p.Run(input)
require.NoError(t, err)
require.Equal(t, out, b32(42))
input = crypto.Keccak256([]byte("twoFoo()"))[:4]
out, err = p.Run(input)
require.NoError(t, err)
require.Equal(t, out, b32(42*2))
// persistent state changes
input = crypto.Keccak256([]byte("twoFoo()"))[:4]
out, err = p.Run(input)
require.NoError(t, err)
require.Equal(t, out, b32(42*2*2))
// multi-output
input = crypto.Keccak256([]byte("things()"))[:4]
out, err = p.Run(input)
require.NoError(t, err)
require.Equal(t, b32(123), out[:32])
require.Equal(t, b32(32*3), out[32*1:32*2]) // offset of hello
require.Equal(t, b32(32*5), out[32*2:32*3]) // offset of seen
require.Equal(t, b32(uint64(len("Hola"))), out[32*3:32*4]) // length of hello
require.Equal(t, pad32([]byte("Hola")), out[32*4:32*5]) // hello content
require.Equal(t, b32(uint64(len("alice"))), out[32*5:32*6]) // length of seen
require.Equal(t, pad32([]byte("alice")), out[32*6:32*7]) // seen content
// multi-input
input = crypto.Keccak256([]byte("addAndMul(uint64,uint64,uint64,uint8)"))[:4]
input = append(input, b32(42)...)
input = append(input, b32(100)...)
input = append(input, b32(7)...)
input = append(input, b32(3)...)
out, err = p.Run(input)
require.NoError(t, err)
require.Equal(t, b32((42+100+7)*3), out)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment