From 17fe12329b5b78262bdf19f193bce0a7861a9895 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 15 Nov 2023 13:31:14 -0500 Subject: [PATCH] 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 --- core/services/relay/evm/codec_entry.go | 132 +++++++++++++++++++ core/services/relay/evm/codec_entry_test.go | 139 ++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 core/services/relay/evm/codec_entry.go create mode 100644 core/services/relay/evm/codec_entry_test.go diff --git a/core/services/relay/evm/codec_entry.go b/core/services/relay/evm/codec_entry.go new file mode 100644 index 00000000000..e0c318a0495 --- /dev/null +++ b/core/services/relay/evm/codec_entry.go @@ -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 +} diff --git a/core/services/relay/evm/codec_entry_test.go b/core/services/relay/evm/codec_entry_test.go new file mode 100644 index 00000000000..17491a12f4b --- /dev/null +++ b/core/services/relay/evm/codec_entry_test.go @@ -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})) + }) +}