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

Create a codec entry with from args, allowing us to do type checking and to have structs to use with abi.Arguments's Pack function #11302

Merged
merged 1 commit into from
Nov 28, 2023
Merged
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
132 changes: 132 additions & 0 deletions core/services/relay/evm/codec_entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package evm

import (
"reflect"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"

commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"

"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
)

type CodecEntry struct {
Args abi.Arguments
checkedType reflect.Type
nativeType reflect.Type
}

func (info *CodecEntry) Init() error {
if info.checkedType != nil {
return nil
}

args := UnwrapArgs(info.Args)
argLen := len(args)
native := make([]reflect.StructField, argLen)
checked := make([]reflect.StructField, argLen)

if len(args) == 1 && args[0].Name == "" {
nativeArg, checkedArg, err := getNativeAndCheckedTypes(&args[0].Type)
if err != nil {
return err
}
info.nativeType = nativeArg
info.checkedType = checkedArg
return nil
}

for i, arg := range args {
tmp := arg.Type
nativeArg, checkedArg, err := getNativeAndCheckedTypes(&tmp)
if err != nil {
return err
}
tag := reflect.StructTag(`json:"` + arg.Name + `"`)
name := strings.ToUpper(arg.Name[:1]) + arg.Name[1:]
native[i] = reflect.StructField{Name: name, Type: nativeArg, Tag: tag}
checked[i] = reflect.StructField{Name: name, Type: checkedArg, Tag: tag}
}

info.nativeType = reflect.StructOf(native)
info.checkedType = reflect.StructOf(checked)
return nil
}

func UnwrapArgs(args abi.Arguments) abi.Arguments {
// Unwrap an unnamed tuple so that callers don't need to wrap it
// Eg: If you have struct Foo { ... } and return an unnamed Foo, you should be able ot decode to a go Foo{} directly
if len(args) != 1 || args[0].Name != "" {
return args
}

elms := args[0].Type.TupleElems
if len(elms) != 0 {
names := args[0].Type.TupleRawNames
args = make(abi.Arguments, len(elms))
for i, elm := range elms {
args[i] = abi.Argument{
Name: names[i],
Type: *elm,
}
}
}
return args
}

func getNativeAndCheckedTypes(curType *abi.Type) (reflect.Type, reflect.Type, error) {
converter := func(t reflect.Type) reflect.Type { return t }
for curType.Elem != nil {
prior := converter
switch curType.GetType().Kind() {
case reflect.Slice:
converter = func(t reflect.Type) reflect.Type {
return prior(reflect.SliceOf(t))
}
curType = curType.Elem
case reflect.Array:
tmp := curType
converter = func(t reflect.Type) reflect.Type {
return prior(reflect.ArrayOf(tmp.Size, t))
}
curType = curType.Elem
default:
return nil, nil, commontypes.ErrInvalidType
}
}
base, ok := types.GetType(curType.String())
if ok {
return converter(base.Native), converter(base.Checked), nil
}

return createTupleType(curType, converter)
}

func createTupleType(curType *abi.Type, converter func(reflect.Type) reflect.Type) (reflect.Type, reflect.Type, error) {
if len(curType.TupleElems) == 0 {
return curType.TupleType, curType.TupleType, nil
}

// Create native type ourselves to assure that it'll always have the exact memory layout of checked types
// Otherwise, the "unsafe" casting that will be done to convert from checked to native won't be safe.
// At the time of writing, the way the TupleType is built it will be the same, but I don't want to rely on that
// If they ever add private fields for internal tracking
// or anything it would break us if we don't build the native type.
// As an example of how it could possibly change in the future, I've seen struct{}
// added with tags to the top of generated structs to allow metadata exploration.
nativeFields := make([]reflect.StructField, len(curType.TupleElems))
checkedFields := make([]reflect.StructField, len(curType.TupleElems))
for i, elm := range curType.TupleElems {
name := curType.TupleRawNames[i]
nativeFields[i].Name = name
checkedFields[i].Name = name
nativeArgType, checkedArgType, err := getNativeAndCheckedTypes(elm)
if err != nil {
return nil, nil, err
}
nativeFields[i].Type = nativeArgType
checkedFields[i].Type = checkedArgType
}
return converter(reflect.StructOf(nativeFields)), converter(reflect.StructOf(checkedFields)), nil
}
139 changes: 139 additions & 0 deletions core/services/relay/evm/codec_entry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package evm

import (
"math/big"
"reflect"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"

"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
)

func TestCodecEntry(t *testing.T) {
t.Run("basic types", func(t *testing.T) {
type1, err := abi.NewType("uint16", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
type2, err := abi.NewType("string", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
type3, err := abi.NewType("uint24", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
type4, err := abi.NewType("int24", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
entry := CodecEntry{
Args: abi.Arguments{
{Name: "Field1", Type: type1},
{Name: "Field2", Type: type2},
{Name: "Field3", Type: type3},
{Name: "Field4", Type: type4},
},
}
require.NoError(t, entry.Init())
native := reflect.New(entry.nativeType)
iNative := reflect.Indirect(native)
iNative.FieldByName("Field1").Set(reflect.ValueOf(uint16(2)))
iNative.FieldByName("Field2").Set(reflect.ValueOf("any string"))
iNative.FieldByName("Field3").Set(reflect.ValueOf(big.NewInt( /*2^24 - 1*/ 16777215)))
iNative.FieldByName("Field4").Set(reflect.ValueOf(big.NewInt( /*2^23 - 1*/ 8388607)))
// native and checked point to the same item, even though they have different "types"
// they have the same memory layout so this is safe per unsafe casting rules, see unsafe.Pointer for details
checked := reflect.NewAt(entry.checkedType, native.UnsafePointer())
iChecked := reflect.Indirect(checked)
checkedField := iChecked.FieldByName("Field3").Interface()

sbi, ok := checkedField.(types.SizedBigInt)
require.True(t, ok)
assert.NoError(t, sbi.Verify())
bi, ok := iNative.FieldByName("Field3").Interface().(*big.Int)
require.True(t, ok)
bi.Add(bi, big.NewInt(1))
assert.IsType(t, commontypes.ErrInvalidType, sbi.Verify())
bi, ok = iNative.FieldByName("Field4").Interface().(*big.Int)
require.True(t, ok)
bi.Add(bi, big.NewInt(1))
assert.IsType(t, commontypes.ErrInvalidType, sbi.Verify())
})

t.Run("tuples", func(t *testing.T) {
type1, err := abi.NewType("uint16", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
tupleType, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{
{Name: "Field3", Type: "uint24"},
{Name: "Field4", Type: "int24"},
})
require.NoError(t, err)
entry := CodecEntry{
Args: abi.Arguments{
{Name: "Field1", Type: type1},
{Name: "Field2", Type: tupleType},
},
}
require.NoError(t, entry.Init())
native := reflect.New(entry.nativeType)
iNative := reflect.Indirect(native)
iNative.FieldByName("Field1").Set(reflect.ValueOf(uint16(2)))
f2 := iNative.FieldByName("Field2")
f2.FieldByName("Field3").Set(reflect.ValueOf(big.NewInt( /*2^24 - 1*/ 16777215)))
f2.FieldByName("Field4").Set(reflect.ValueOf(big.NewInt( /*2^23 - 1*/ 8388607)))
// native and checked point to the same item, even though they have different "types"
// they have the same memory layout so this is safe per unsafe casting rules, see unsafe.Pointer for details
checked := reflect.NewAt(entry.checkedType, native.UnsafePointer())
tuple := reflect.Indirect(checked).FieldByName("Field2")
checkedField := tuple.FieldByName("Field3").Interface()

sbi, ok := checkedField.(types.SizedBigInt)
require.True(t, ok)
assert.NoError(t, sbi.Verify())
bi, ok := f2.FieldByName("Field3").Interface().(*big.Int)
require.True(t, ok)
bi.Add(bi, big.NewInt(1))
assert.IsType(t, commontypes.ErrInvalidType, sbi.Verify())
bi, ok = f2.FieldByName("Field4").Interface().(*big.Int)
require.True(t, ok)
bi.Add(bi, big.NewInt(1))
assert.IsType(t, commontypes.ErrInvalidType, sbi.Verify())
})

t.Run("unwrapped types", func(t *testing.T) {
// This exists to allow you to decode single returned values without naming the parameter
wrappedTuple, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{
{Name: "Field1", Type: "int16"},
})
require.NoError(t, err)
entry := CodecEntry{
Args: abi.Arguments{{Name: "", Type: wrappedTuple}},
}
require.NoError(t, entry.Init())
native := reflect.New(entry.nativeType)
iNative := reflect.Indirect(native)
iNative.FieldByName("Field1").Set(reflect.ValueOf(int16(2)))
})

t.Run("slice types", func(t *testing.T) {
type1, err := abi.NewType("int16[]", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
entry := CodecEntry{
Args: abi.Arguments{{Name: "Field1", Type: type1}},
}
require.NoError(t, entry.Init())
native := reflect.New(entry.nativeType)
iNative := reflect.Indirect(native)
iNative.FieldByName("Field1").Set(reflect.ValueOf([]int16{2, 3}))
})

t.Run("array types", func(t *testing.T) {
type1, err := abi.NewType("int16[3]", "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
entry := CodecEntry{
Args: abi.Arguments{{Name: "Field1", Type: type1}},
}
require.NoError(t, entry.Init())
native := reflect.New(entry.nativeType)
iNative := reflect.Indirect(native)
iNative.FieldByName("Field1").Set(reflect.ValueOf([3]int16{2, 3, 30}))
})
}