Commit 32eea1b6 authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: automatic ABI bindings from Go struct (#11497)

parent a81de910
package script
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/core/vm"
)
// CallBackendFn is the function that encoded binding calls get made with.
// The function may return vm.ErrExecutionReverted to revert (revert is ABI decoded from data).
// Or any other error, where the return-data is then ignored.
type CallBackendFn func(data []byte) ([]byte, error)
// MakeBindings turns a struct type with function-typed fields into EVM call bindings
// that are hooked up to the backend function.
// fields annotated with `evm:"-"` are ignored.
func MakeBindings[E any](backendFn CallBackendFn,
checkABI func(abiDef string) bool,
) (*E, error) {
v := new(E)
val := reflect.ValueOf(v)
err := hydrateBindingsStruct(val, backendFn, checkABI)
return v, err
}
// hydrateBindingsStruct initializes a struct with function fields into
// a struct of ABI functions hooked up to the backend.
func hydrateBindingsStruct(
val reflect.Value,
backendFn CallBackendFn,
checkABI func(abiDef string) bool,
) error {
typ := val.Type()
if typ.Kind() == reflect.Pointer {
if val.IsNil() {
return errors.New("cannot hydrate nil pointer value")
}
val = val.Elem()
typ = val.Type()
}
if typ.Kind() != reflect.Struct {
return fmt.Errorf("object is not a struct: %s", typ)
}
// Hydrate each of the fields
fieldCount := val.NumField()
for i := 0; i < fieldCount; i++ {
fieldDef := typ.Field(i)
if !fieldDef.IsExported() { // ignore unexposed fields
continue
}
// ignore fields / embedded structs that are annotated with `evm:"-"`
if v, ok := fieldDef.Tag.Lookup("evm"); ok && v == "-" {
continue
}
if fieldDef.Anonymous { // fields of embedded structs will be hydrated too
if err := hydrateBindingsStruct(val.Field(i), backendFn, checkABI); err != nil {
return fmt.Errorf("failed to hydrate bindings of embedded field %q: %w", fieldDef.Name, err)
}
continue
}
if fieldDef.Type.Kind() != reflect.Func { // We can only hydrate fields with a function type
continue
}
fVal := val.Field(i)
if !fVal.IsNil() {
return fmt.Errorf("cannot hydrate bindings func, field %q is already set", fieldDef.Name)
}
if err := hydrateBindingsField(fVal, fieldDef, backendFn, checkABI); err != nil {
return fmt.Errorf("cannot hydrate bindings field %q: %w", fieldDef.Name, err)
}
}
return nil
}
var ErrABICheck = errors.New("failed ABI check")
// hydrateBindingsField initializes a struct field value to a function that calls the implied ABI function.
func hydrateBindingsField(
fVal reflect.Value,
fieldDef reflect.StructField,
backendFn CallBackendFn,
checkABI func(abiDef string) bool,
) error {
// derive the ABI function name from the field name
abiFunctionName := fieldDef.Name
if len(abiFunctionName) == 0 {
return errors.New("ABI method name must not be empty")
}
if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] {
abiFunctionName = lo + abiFunctionName[1:]
}
// derive the ABI function arguments from the function type
inArgs, err := makeArgs(fieldDef.Type.NumIn(), fieldDef.Type.In)
if err != nil {
return fmt.Errorf("failed to determine ABI types of input args: %w", err)
}
inArgTypes := makeArgTypes(inArgs)
methodSig := fmt.Sprintf("%v(%v)", abiFunctionName, strings.Join(inArgTypes, ","))
// check the ABI, if we can
if checkABI != nil {
if !checkABI(methodSig) {
return fmt.Errorf("function %s with signature %q is invalid: %w", fieldDef.Name, methodSig, ErrABICheck)
}
}
byte4Sig := bytes4(methodSig)
// Define how we encode Go arguments as function calldata, including the function selector
inArgsEncodeFn := func(args []reflect.Value) ([]byte, error) {
vals := make([]interface{}, len(args))
for i := range args {
vals[i] = args[i].Interface()
}
out, err := inArgs.PackValues(vals)
if err != nil {
return nil, fmt.Errorf("failed to encode call data: %w", err)
}
calldata := make([]byte, 0, len(out)+4)
calldata = append(calldata, byte4Sig[:]...)
calldata = append(calldata, out...)
return calldata, nil
}
// Determine how many arguments we decode from ABI, and if we have an error return case.
outArgCount := fieldDef.Type.NumOut()
errReturn := hasTrailingError(outArgCount, fieldDef.Type.Out)
var nilErrValue reflect.Value
if errReturn {
outArgCount -= 1
nilErrValue = reflect.New(fieldDef.Type.Out(outArgCount)).Elem()
}
outArgs, err := makeArgs(outArgCount, fieldDef.Type.Out)
if err != nil {
return fmt.Errorf("failed to determine ABI types of output args: %w", err)
}
outAllocators := makeArgAllocators(outArgCount, fieldDef.Type.Out)
// Helper func to return an error with, where we try to fit it in the returned error value, if there is any.
returnErr := func(err error) []reflect.Value {
if !errReturn {
panic(fmt.Errorf("error, but cannot return as arg: %w", err))
}
out := make([]reflect.Value, outArgCount+1)
for i := 0; i < outArgCount; i++ {
out[i] = reflect.New(fieldDef.Type.Out(i)).Elem()
}
out[outArgCount] = reflect.ValueOf(err)
return out
}
// Decodes the result of the backend into values to return as function, including error/revert handling.
outDecodeFn := func(result []byte, resultErr error) []reflect.Value {
if resultErr != nil {
if errors.Is(resultErr, vm.ErrExecutionReverted) {
msg, err := abi.UnpackRevert(result)
if err != nil {
return returnErr(fmt.Errorf("failed to unpack result args: %w", err))
}
return returnErr(fmt.Errorf("revert: %s", msg))
}
return returnErr(resultErr)
}
out := make([]reflect.Value, outArgCount, outArgCount+1)
err := abiToValues(outArgs, outAllocators, out, result)
if err != nil {
return returnErr(fmt.Errorf("failed to convert output to return values: %w", err))
}
if errReturn { // don't forget the nil error value, to match the expected output arg count
out = append(out, nilErrValue)
}
return out
}
// Construct the actual Go function: it encodes the Go args, turns it into an ABI call, and decodes the results.
f := reflect.MakeFunc(fieldDef.Type, func(args []reflect.Value) (results []reflect.Value) {
input, err := inArgsEncodeFn(args) // ABI encode args
if err != nil {
return returnErr(fmt.Errorf("failed to encode input args: %w", err))
}
result, err := backendFn(input) // call backend func
return outDecodeFn(result, err) // ABI decode result
})
// Now hydrate the field definition with our new Go function
fVal.Set(f)
return nil
}
package script
import (
"errors"
"fmt"
"math/big"
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
)
type EmbeddedBindings struct {
Adder func(a uint8, b uint64) *big.Int
}
type ExampleBindings struct {
DoThing func() error
EmbeddedBindings
Hello func(greeting string, target common.Address) string
}
func TestBindings(t *testing.T) {
var testFn CallBackendFn
backendFn := func(data []byte) ([]byte, error) {
return testFn(data) // indirect call, so we can swap it per test case.
}
bindings, err := MakeBindings[ExampleBindings](backendFn, nil)
require.NoError(t, err)
testFn = func(data []byte) ([]byte, error) {
require.Len(t, data, 4)
require.Equal(t, bytes4("doThing()"), [4]byte(data))
return encodeRevert(errors.New("example revert"))
}
err = bindings.DoThing()
require.ErrorContains(t, err, "example revert")
testFn = func(data []byte) ([]byte, error) {
require.Len(t, data, 4)
require.Equal(t, bytes4("doThing()"), [4]byte(data))
return []byte{}, nil
}
err = bindings.DoThing()
require.NoError(t, err, "no revert")
testFn = func(data []byte) ([]byte, error) {
require.Len(t, data, 4+32+32, "selector and two ABI args")
require.Equal(t, bytes4("adder(uint8,uint64)"), [4]byte(data[:4]))
a := new(big.Int).SetBytes(data[4 : 4+32])
b := new(big.Int).SetBytes(data[4+32:])
return leftPad32(new(big.Int).Add(a, b).Bytes()), nil
}
result := bindings.Adder(42, 0x1337)
require.NoError(t, err)
require.True(t, result.IsUint64())
require.Equal(t, uint64(42+0x1337), result.Uint64())
}
type TestContract struct{}
func (e *TestContract) Hello(greeting string, target common.Address) string {
return fmt.Sprintf("Test says: %s %s!", greeting, target)
}
func TestPrecompileBindings(t *testing.T) {
contract, err := NewPrecompile[*TestContract](&TestContract{})
require.NoError(t, err)
bindings, err := MakeBindings[ExampleBindings](contract.Run, nil)
require.NoError(t, err)
addr := common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
response := bindings.Hello("Hola", addr)
require.Equal(t, fmt.Sprintf("Test says: Hola %s!", addr), response)
}
func TestBindingsABICheck(t *testing.T) {
fn := CallBackendFn(func(data []byte) ([]byte, error) {
panic("should not run")
})
needABI := map[string]struct{}{
"doThing()": {},
"adder(uint8,uint64)": {},
"hello(string,address)": {},
}
gotABI := make(map[string]struct{})
abiCheckFn := func(abiDef string) bool {
_, ok := needABI[abiDef]
gotABI[abiDef] = struct{}{}
return ok
}
_, err := MakeBindings[ExampleBindings](fn, abiCheckFn)
require.NoError(t, err)
require.Equal(t, needABI, gotABI, "checked all ABI methods")
delete(needABI, "doThing()")
_, err = MakeBindings[ExampleBindings](fn, abiCheckFn)
require.ErrorIs(t, err, ErrABICheck)
}
......@@ -99,6 +99,53 @@ func (p *Precompile[E]) setupMethods(val *reflect.Value) error {
return nil
}
// makeArgs infers a list of ABI types, from a list of Go arguments.
func makeArgs(argCount int, getType func(i int) reflect.Type) (abi.Arguments, error) {
out := make(abi.Arguments, argCount)
for i := 0; i < argCount; i++ {
argType := getType(i)
abiTyp, err := goTypeToABIType(argType)
if err != nil {
return nil, fmt.Errorf("failed to determine ABI type of input arg %d: %w", i, err)
}
out[i] = abi.Argument{
Name: fmt.Sprintf("arg_%d", i),
Type: abiTyp,
}
}
return out, nil
}
// makeArgTypes turns a slice of ABI argument types into a slice of ABI stringified types
func makeArgTypes(args abi.Arguments) []string {
out := make([]string, len(args))
for i := 0; i < len(args); i++ {
out[i] = args[i].Type.String()
}
return out
}
// makeArgAllocators returns a lice of Go object allocator functions, for each of the arguments.
func makeArgAllocators(argCount int, getType func(i int) reflect.Type) []func() any {
out := make([]func() interface{}, argCount)
for i := 0; i < argCount; i++ {
argType := getType(i)
out[i] = func() interface{} {
return reflect.New(argType).Elem().Interface()
}
}
return out
}
// hasTrailingError checks if the last returned argument type, if any, is a Go error.
func hasTrailingError(argCount int, getType func(i int) reflect.Type) bool {
if argCount == 0 {
return false
}
lastTyp := getType(argCount - 1)
return lastTyp.Kind() == reflect.Interface && lastTyp.Implements(typeFor[error]())
}
// 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 {
......@@ -124,24 +171,14 @@ func (p *Precompile[E]) setupMethod(selfVal *reflect.Value, methodDef *reflect.M
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()
getInArg := func(i int) reflect.Type {
return methodDef.Type.In(i + 1) // +1 to account for the receiver
}
inArgs, err := makeArgs(inArgCount, getInArg)
if err != nil {
return fmt.Errorf("failed to preserve input args: %w", err)
}
inArgTypes := makeArgTypes(inArgs)
methodSig := fmt.Sprintf("%v(%v)", abiFunctionName, strings.Join(inArgTypes, ","))
byte4Sig := bytes4(methodSig)
if variantSuffix != "" {
......@@ -157,29 +194,18 @@ func (p *Precompile[E]) setupMethod(selfVal *reflect.Value, methodDef *reflect.M
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
}
errReturn := hasTrailingError(outArgCount, methodDef.Type.Out)
if errReturn {
outArgCount -= 1
}
// 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,
}
outArgs, err := makeArgs(outArgCount, methodDef.Type.Out)
if err != nil {
return fmt.Errorf("failed to prepare output arg types: %w", err)
}
inArgAllocators := makeArgAllocators(inArgCount, getInArg)
fn := makeFn(selfVal, &methodDef.Func, errReturn, inArgs, outArgs, inArgAllocators)
p.abiMethods[byte4Sig] = &precompileFunc{
......@@ -190,6 +216,35 @@ func (p *Precompile[E]) setupMethod(selfVal *reflect.Value, methodDef *reflect.M
return nil
}
// abiToValues turns serialized ABI input data into values, which are written to the given dest slice.
// The ABI decoding happens following the given args ABI type definitions.
// Values are allocated with the given respective allocator functions.
func abiToValues(args abi.Arguments, allocators []func() any, dest []reflect.Value, input []byte) error {
// sanity check that we have as many allocators as result destination slots
if len(allocators) != len(dest) {
return fmt.Errorf("have %d allocators, but %d destinations", len(allocators), len(dest))
}
// Unpack the ABI data into default Go types
inVals, err := args.UnpackValues(input)
if err != nil {
return 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(allocators) {
return fmt.Errorf("expected %d args, got %d", len(allocators), len(inVals))
}
for i, inAlloc := range allocators {
argSrc := inVals[i]
argDest := inAlloc()
argDest, err = convertType(argSrc, argDest)
if err != nil {
return fmt.Errorf("failed to convert arg %d from Go type %T to %T: %w", i, argSrc, argDest, err)
}
dest[i] = reflect.ValueOf(argDest)
}
return nil
}
// makeFn is a helper function to perform a method call:
// - ABI decoding of input
// - type conversion of inputs
......@@ -198,26 +253,12 @@ func (p *Precompile[E]) setupMethod(selfVal *reflect.Value, methodDef *reflect.M
// - 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))
callArgs := make([]reflect.Value, 1+len(inArgAllocators))
callArgs[0] = *selfVal
err := abiToValues(inArgs, inArgAllocators, callArgs[1:], input)
if err != nil {
return nil, err
}
// Call the precompile Go function
returnReflectVals := methodVal.Call(callArgs)
......
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