-
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.
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
- Loading branch information
Showing
2 changed files
with
272 additions
and
0 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
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 | ||
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 | ||
} |
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,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})) | ||
}) | ||
} |