Skip to content

Commit

Permalink
Parse evm types to create structs that we can encode/decode
Browse files Browse the repository at this point in the history
  • Loading branch information
nolag committed Nov 10, 2023
1 parent a29c9e7 commit cc85045
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 145 deletions.
4 changes: 2 additions & 2 deletions core/services/ocr2/plugins/median/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ func NewMedianServices(ctx context.Context,
CreatedAt: time.Now(),
}, lggr)

if medianProvider.ChainReader() != nil {
/*if medianProvider.ChainReader() != nil {
medianProvider = medianProviderWrapper{
medianProvider, // attach newer MedianContract which uses ChainReader
newMedianContract(provider.ChainReader(), common.HexToAddress(spec.ContractID)),
}
}
}*/

if cmdName := env.MedianPluginCmd.Get(); cmdName != "" {

Expand Down
150 changes: 150 additions & 0 deletions core/services/relay/evm/codec_entry.go
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
}
137 changes: 137 additions & 0 deletions core/services/relay/evm/codec_entry_test.go
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}))
})
}
Loading

0 comments on commit cc85045

Please sign in to comment.