Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Craetes a function to get the max size for abi.Argumetns, given any outermost slices have N elements #11259

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions core/services/relay/evm/codec_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ type CodecEntry struct {
nativeType reflect.Type
}

func (info *CodecEntry) Init() error {
if info.checkedType != nil {
func (entry *CodecEntry) Init() error {
if entry.checkedType != nil {
return nil
}

args := UnwrapArgs(info.Args)
args := UnwrapArgs(entry.Args)
argLen := len(args)
native := make([]reflect.StructField, argLen)
checked := make([]reflect.StructField, argLen)
Expand All @@ -32,8 +32,8 @@ func (info *CodecEntry) Init() error {
if err != nil {
return err
}
info.nativeType = nativeArg
info.checkedType = checkedArg
entry.nativeType = nativeArg
entry.checkedType = checkedArg
return nil
}

Expand All @@ -49,11 +49,18 @@ func (info *CodecEntry) Init() error {
checked[i] = reflect.StructField{Name: name, Type: checkedArg, Tag: tag}
}

info.nativeType = reflect.StructOf(native)
info.checkedType = reflect.StructOf(checked)
entry.nativeType = reflect.StructOf(native)
entry.checkedType = reflect.StructOf(checked)
return nil
}

func (entry *CodecEntry) GetMaxSize(n int) (int, error) {
if entry == nil {
return 0, commontypes.ErrInvalidType
}
return GetMaxSize(n, entry.Args)
}

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
Expand Down
65 changes: 65 additions & 0 deletions core/services/relay/evm/size_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package evm

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

commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
)

func GetMaxSize(n int, args abi.Arguments) (int, error) {
size := 0
for _, arg := range args {
tmp := arg.Type
argSize, _, err := getTypeSize(n, &tmp, true, false)
if err != nil {
return 0, err
}
size += argSize
}

return size, nil
}

func getTypeSize(n int, t *abi.Type, dynamicTypeAllowed bool, isNested bool) (int, bool, error) {
Copy link
Contributor

@ilija42 ilija42 Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, why is abi.Type a pointer? Can't we just make this not a ptr and then not use tmp var in GetMaxSize?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's often returned as one, so I figured why bother having the struct copied by value

Copy link
Contributor

@ilija42 ilija42 Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym, why would you copy it by value? If you pass a dereferenced arg.Type into getTypeSize() you get the same effect as if you passed &tmp in. Non pointer attributes inside of the struct won't get changed and attributes with a pointer would get changed either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below, t.Elm is passed in on line 27, it's naturally a pointer. When you pass a struct directly, you force a copy of that struct to be made.

// See https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding
switch t.T {
case abi.ArrayTy:
elmSize, _, err := getTypeSize(n, t.Elem, false, true)
return t.Size * elmSize, false, err
case abi.SliceTy:
if !dynamicTypeAllowed {
return 0, false, commontypes.ErrInvalidType
}
elmSize, _, err := getTypeSize(n, t.Elem, false, true)
return 32 /*header*/ + 32 /*footer*/ + elmSize*n, true, err
case abi.BytesTy, abi.StringTy:
if !dynamicTypeAllowed {
return 0, false, commontypes.ErrInvalidType
}
totalSize := (n + 31) / 32 * 32 // strings and bytes are padded to 32 bytes
return 32 /*header*/ + 32 /*footer*/ + totalSize, true, nil
case abi.TupleTy:
// No header or footer, because if the tuple is dynamically sized we would need to know the inner slice sizes
// so it would return error for that element.
size := 0
dynamic := false
for _, elm := range t.TupleElems {
argSize, dynamicArg, err := getTypeSize(n, elm, !isNested, true)
if err != nil {
return 0, false, err
}
dynamic = dynamic || dynamicArg
size += argSize
}

if dynamic {
// offset for the element needs to be included there are dynamic elements
size += 32
}

return size, dynamic, nil
default:
// types are padded to 32 bytes
return 32, false, nil
}
}
263 changes: 263 additions & 0 deletions core/services/relay/evm/size_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package evm_test

import (
"math/big"
"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"
)

const anyNumElements = 10

func TestGetMaxSize(t *testing.T) {
t.Run("Basic types all encode to 32 bytes", func(t *testing.T) {
args := abi.Arguments{
{Name: "I8", Type: mustType(t, "int8")},
{Name: "I80", Type: mustType(t, "int80")},
{Name: "I256", Type: mustType(t, "int256")},
{Name: "B3", Type: mustType(t, "bytes3")},
{Name: "B32", Type: mustType(t, "bytes32")},
{Name: "TF", Type: mustType(t, "bool")},
}

runSizeTest(t, anyNumElements, args, int8(9), big.NewInt(3), big.NewInt(200), [3]byte{1, 3, 4}, make32Bytes(1), true)
})

t.Run("Slices of basic types all encode to 32 bytes each + header and footer", func(t *testing.T) {
args := abi.Arguments{
{Name: "I8", Type: mustType(t, "int8[]")},
{Name: "I80", Type: mustType(t, "int80[]")},
{Name: "I256", Type: mustType(t, "int256[]")},
{Name: "B3", Type: mustType(t, "bytes3[]")},
{Name: "B32", Type: mustType(t, "bytes32[]")},
{Name: "TF", Type: mustType(t, "bool[]")},
}

i8 := []int8{9, 2, 1, 3, 5, 6, 2, 1, 2, 3}
i80 := []*big.Int{big.NewInt(9), big.NewInt(2), big.NewInt(1), big.NewInt(3), big.NewInt(5), big.NewInt(6), big.NewInt(2), big.NewInt(1), big.NewInt(2), big.NewInt(3)}
i256 := []*big.Int{big.NewInt(119), big.NewInt(112), big.NewInt(1), big.NewInt(3), big.NewInt(5), big.NewInt(6), big.NewInt(2), big.NewInt(1), big.NewInt(2), big.NewInt(3)}
b3 := [][3]byte{{1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}, {1, 2, 3}}
b32 := [][32]byte{make32Bytes(1), make32Bytes(2), make32Bytes(3), make32Bytes(4), make32Bytes(5), make32Bytes(6), make32Bytes(7), make32Bytes(8), make32Bytes(9), make32Bytes(10)}
tf := []bool{true, false, true, false, true, false, true, false, true, false}
runSizeTest(t, anyNumElements, args, i8, i80, i256, b3, b32, tf)
})

t.Run("Arrays of basic types all encode to 32 bytes each", func(t *testing.T) {
args := abi.Arguments{
{Name: "I8", Type: mustType(t, "int8[3]")},
{Name: "I80", Type: mustType(t, "int80[3]")},
{Name: "I256", Type: mustType(t, "int256[3]")},
{Name: "B3", Type: mustType(t, "bytes3[3]")},
{Name: "B32", Type: mustType(t, "bytes32[3]")},
{Name: "TF", Type: mustType(t, "bool[3]")},
}

i8 := [3]int8{9, 2, 1}
i80 := [3]*big.Int{big.NewInt(9), big.NewInt(2), big.NewInt(1)}
i256 := [3]*big.Int{big.NewInt(119), big.NewInt(112), big.NewInt(1)}
b3 := [3][3]byte{{1, 2, 3}, {1, 2, 3}, {1, 2, 3}}
b32 := [3][32]byte{make32Bytes(1), make32Bytes(2), make32Bytes(3)}
tf := [3]bool{true, false, true}
runSizeTest(t, anyNumElements, args, i8, i80, i256, b3, b32, tf)
})

t.Run("Tuples are a sum of their elements", func(t *testing.T) {
tuple1 := []abi.ArgumentMarshaling{
{Name: "I8", Type: "int8"},
{Name: "I80", Type: "int80"},
{Name: "I256", Type: "int256"},
{Name: "B3", Type: "bytes3"},
{Name: "B32", Type: "bytes32"},
{Name: "TF", Type: "bool"},
}
t1, err := abi.NewType("tuple", "", tuple1)
require.NoError(t, err)

tuple2 := []abi.ArgumentMarshaling{
{Name: "I80", Type: "int80"},
{Name: "TF", Type: "bool"},
}
t2, err := abi.NewType("tuple", "", tuple2)
require.NoError(t, err)

args := abi.Arguments{
{Name: "t1", Type: t1},
{Name: "t2", Type: t2},
}
arg1 := struct {
I8 int8
I80 *big.Int
I256 *big.Int
B3 [3]byte
B32 [32]byte
TF bool
}{
int8(9), big.NewInt(3), big.NewInt(200), [3]byte{1, 3, 4}, make32Bytes(1), true,
}

arg2 := struct {
I80 *big.Int
TF bool
}{
big.NewInt(3), true,
}
runSizeTest(t, anyNumElements, args, arg1, arg2)
})

t.Run("Slices of tuples are a sum of their elements with header and footer", func(t *testing.T) {
tuple1 := []abi.ArgumentMarshaling{
{Name: "I80", Type: "int80"},
{Name: "TF", Type: "bool"},
}
t1, err := abi.NewType("tuple[]", "", tuple1)
require.NoError(t, err)

args := abi.Arguments{
{Name: "t1", Type: t1},
}
arg1 := []struct {
I80 *big.Int
TF bool
}{
{big.NewInt(1), true},
{big.NewInt(2), true},
{big.NewInt(3), true},
{big.NewInt(4), false},
{big.NewInt(5), true},
{big.NewInt(6), true},
{big.NewInt(7), true},
{big.NewInt(8), false},
{big.NewInt(9), true},
{big.NewInt(10), true},
}
runSizeTest(t, anyNumElements, args, arg1)
})

t.Run("Arrays of tuples are a sum of their elements", func(t *testing.T) {
tuple1 := []abi.ArgumentMarshaling{
{Name: "I80", Type: "int80"},
{Name: "TF", Type: "bool"},
}
t1, err := abi.NewType("tuple[3]", "", tuple1)
require.NoError(t, err)

args := abi.Arguments{
{Name: "t1", Type: t1},
}
arg1 := []struct {
I80 *big.Int
TF bool
}{
{big.NewInt(1), true},
{big.NewInt(2), true},
{big.NewInt(3), true},
}
runSizeTest(t, anyNumElements, args, arg1)

})

t.Run("Bytes pack themselves", func(t *testing.T) {
args := abi.Arguments{{Name: "B", Type: mustType(t, "bytes")}}
t.Run("No padding needed", func(t *testing.T) {
padded := []byte("12345789022345678903234567890412345678905123456789061234")
runSizeTest(t, 64, args, padded)
})
t.Run("Padding needed", func(t *testing.T) {
needsPadding := []byte("12345789022345678903234567890412345678905123456")
runSizeTest(t, 56, args, needsPadding)
})
})

t.Run("Strings pack themselves", func(t *testing.T) {
args := abi.Arguments{{Name: "B", Type: mustType(t, "string")}}
t.Run("No padding needed", func(t *testing.T) {
padded := "12345789022345678903234567890412345678905123456789061234"
runSizeTest(t, 64, args, padded)
})
t.Run("Padding needed", func(t *testing.T) {
needsPadding := "12345789022345678903234567890412345678905123456"
runSizeTest(t, 56, args, needsPadding)
})
})

t.Run("Nested dynamic types return errors", func(t *testing.T) {
t.Run("Slice in slice", func(t *testing.T) {
args := abi.Arguments{{Name: "B", Type: mustType(t, "int32[][]")}}
_, err := evm.GetMaxSize(anyNumElements, args)
assert.IsType(t, commontypes.ErrInvalidType, err)
})
t.Run("Slice in array", func(t *testing.T) {
args := abi.Arguments{{Name: "B", Type: mustType(t, "int32[][2]")}}
_, err := evm.GetMaxSize(anyNumElements, args)
assert.IsType(t, commontypes.ErrInvalidType, err)
})
})

t.Run("Slices in a top level tuple works as-if they are the sized element", func(t *testing.T) {
tuple1 := []abi.ArgumentMarshaling{
{Name: "I80", Type: "int80[]"},
{Name: "TF", Type: "bool[]"},
}
t1, err := abi.NewType("tuple", "", tuple1)
require.NoError(t, err)
args := abi.Arguments{{Name: "tuple", Type: t1}}

arg1 := struct {
I80 []*big.Int
TF []bool
}{
I80: []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3), big.NewInt(4), big.NewInt(5), big.NewInt(6), big.NewInt(7), big.NewInt(8), big.NewInt(9), big.NewInt(10)},
TF: []bool{true, true, true, false, true, true, true, false, true, true},
}

runSizeTest(t, anyNumElements, args, arg1)
})

t.Run("Nested dynamic tuples return errors", func(t *testing.T) {
tuple1 := []abi.ArgumentMarshaling{
{Name: "I8", Type: "int8"},
{Name: "I80", Type: "int80"},
{Name: "I256", Type: "int256"},
{Name: "B3", Type: "bytes3"},
{Name: "B32", Type: "bytes32"},
{Name: "TF", Type: "bool[]"},
}

tuple2 := []abi.ArgumentMarshaling{
{Name: "I80", Type: "int80"},
{Name: "T1", Type: "tuple", Components: tuple1},
}
t2, err := abi.NewType("tuple", "", tuple2)
require.NoError(t, err)

args := abi.Arguments{{Name: "t2", Type: t2}}
_, err = evm.GetMaxSize(anyNumElements, args)
assert.IsType(t, commontypes.ErrInvalidType, err)
})
}

func runSizeTest(t *testing.T, n int, args abi.Arguments, params ...any) {

actual, err := evm.GetMaxSize(n, args)
require.NoError(t, err)

expected, err := args.Pack(params...)
require.NoError(t, err)
assert.Equal(t, len(expected), actual)
}

func mustType(t *testing.T, name string) abi.Type {
aType, err := abi.NewType(name, "", []abi.ArgumentMarshaling{})
require.NoError(t, err)
return aType
}

func make32Bytes(firstByte byte) [32]byte {
return [32]byte{firstByte, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3}
}
Loading