-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Parse evm types to create structs that we can encode/decode
- Loading branch information
Showing
8 changed files
with
430 additions
and
145 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
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 | ||
unwrappedArgs abi.Arguments | ||
encodingPrefix []byte | ||
checkedType reflect.Type | ||
checkedArrayType reflect.Type | ||
arraySize int | ||
nativeType reflect.Type | ||
} | ||
|
||
func (info *CodecEntry) Init() error { | ||
if info.checkedType != nil { | ||
return nil | ||
} | ||
|
||
args := UnwrapArgs(info.Args) | ||
info.unwrappedArgs = args | ||
argLen := len(args) | ||
native := make([]reflect.StructField, argLen) | ||
checked := make([]reflect.StructField, argLen) | ||
|
||
for i, arg := range args { | ||
if len(arg.Name) == 0 { | ||
return relaytypes.InvalidTypeError{} | ||
} | ||
nativeArg, checkedArg, err := getNativeAndCheckedTypes(&arg.Type) | ||
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) | ||
info.checkedArrayType, info.arraySize = getArrayType(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 nil, nil, relaytypes.InvalidTypeError{} | ||
} | ||
|
||
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 | ||
} | ||
|
||
func getArrayType(checked []reflect.StructField) (reflect.Type, int) { | ||
checkedArray := make([]reflect.StructField, len(checked)) | ||
length := 0 | ||
for i, f := range checked { | ||
kind := f.Type.Kind() | ||
if kind == reflect.Slice { | ||
if i == 0 { | ||
length = 0 | ||
} else if length != 0 { | ||
return nil, 0 | ||
} | ||
} else if kind == reflect.Array { | ||
if i == 0 { | ||
length = f.Type.Len() | ||
} else { | ||
if f.Type.Len() != length { | ||
return nil, 0 | ||
} | ||
} | ||
} else { | ||
return nil, 0 | ||
} | ||
|
||
checkedArray[i] = reflect.StructField{Name: f.Name, Type: f.Type.Elem()} | ||
} | ||
return reflect.SliceOf(reflect.StructOf(checkedArray)), length | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package evm | ||
|
||
import ( | ||
"math/big" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/ethereum/go-ethereum/accounts/abi" | ||
relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"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"}, | ||
}) | ||
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})) | ||
}) | ||
} |
Oops, something went wrong.