Skip to content

Commit

Permalink
Create a codec entry with from args, allowing us to do type checking …
Browse files Browse the repository at this point in the history
…and to have structs to use with abi.Arguments's Pack function
  • Loading branch information
nolag committed Nov 21, 2023
1 parent 63087bc commit 0389024
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 0 deletions.
133 changes: 133 additions & 0 deletions core/services/relay/evm/codec_entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package evm

import (
"reflect"
"strings"

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

relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types"

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

type CodecEntry struct {
Args abi.Arguments
encodingPrefix []byte

Check failure on line 16 in core/services/relay/evm/codec_entry.go

View workflow job for this annotation

GitHub Actions / lint

field `encodingPrefix` is unused (unused)
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, relaytypes.InvalidTypeError{}
}
}
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"

relaytypes "github.com/smartcontractkit/chainlink-relay/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, relaytypes.InvalidTypeError{}, sbi.Verify())
bi, ok = iNative.FieldByName("Field4").Interface().(*big.Int)
require.True(t, ok)
bi.Add(bi, big.NewInt(1))
assert.IsType(t, relaytypes.InvalidTypeError{}, 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, relaytypes.InvalidTypeError{}, sbi.Verify())
bi, ok = f2.FieldByName("Field4").Interface().(*big.Int)
require.True(t, ok)
bi.Add(bi, big.NewInt(1))
assert.IsType(t, relaytypes.InvalidTypeError{}, 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}))
})
}

0 comments on commit 0389024

Please sign in to comment.