Skip to content

Commit

Permalink
EVM encoder support for tuples (#13202)
Browse files Browse the repository at this point in the history
credit goes to @archseer
  • Loading branch information
bolekk authored May 22, 2024
1 parent 677abe1 commit eb6b50d
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 119 deletions.
5 changes: 5 additions & 0 deletions .changeset/silent-jars-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

#internal [Keystone] EVM encoder support for tuples
148 changes: 47 additions & 101 deletions core/chains/evm/abi/selector_parser.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Sourced from https://github.com/ethereum/go-ethereum/blob/fe91d476ba3e29316b6dc99b6efd4a571481d888/accounts/abi/selector_parser.go#L126
// Modified assembleArgs to retain argument names
// Originally sourced from https://github.com/ethereum/go-ethereum/blob/fe91d476ba3e29316b6dc99b6efd4a571481d888/accounts/abi/selector_parser.go#L126
// Modified to suppor parsing selectors with argument names

// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
Expand Down Expand Up @@ -83,96 +83,83 @@ func parseElementaryType(unescapedSelector string) (string, string, error) {
return parsedType, rest, nil
}

func parseCompositeType(unescapedSelector string) ([]interface{}, string, error) {
func parseCompositeType(unescapedSelector string) ([]abi.ArgumentMarshaling, string, error) {
if len(unescapedSelector) == 0 || unescapedSelector[0] != '(' {
return nil, "", fmt.Errorf("expected '(', got %c", unescapedSelector[0])
}
parsedType, rest, err := parseType(unescapedSelector[1:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}
result := []interface{}{parsedType}
for len(rest) > 0 && rest[0] != ')' {
parsedType, rest, err = parseType(rest[1:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}
result = append(result, parsedType)
}
if len(rest) == 0 || rest[0] != ')' {
return nil, "", fmt.Errorf("expected ')', got '%s'", rest)
}
if len(rest) >= 3 && rest[1] == '[' && rest[2] == ']' {
return append(result, "[]"), rest[3:], nil
}
return result, rest[1:], nil
}

func parseType(unescapedSelector string) (interface{}, string, error) {
if len(unescapedSelector) == 0 {
return nil, "", errors.New("empty type")
}
if unescapedSelector[0] == '(' {
return parseCompositeType(unescapedSelector)
}
return parseElementaryType(unescapedSelector)
}

func parseArgs(unescapedSelector string) ([]abi.ArgumentMarshaling, error) {
if len(unescapedSelector) == 0 || unescapedSelector[0] != '(' {
return nil, fmt.Errorf("expected '(', got %c", unescapedSelector[0])
}
rest := unescapedSelector[1:] // skip over the opening `(`
result := []abi.ArgumentMarshaling{}
rest := unescapedSelector[1:]
var parsedType any
var err error
i := 0
for len(rest) > 0 && rest[0] != ')' {
// parse method name
var name string
name, rest, err = parseIdentifier(rest[:])
// skip any leading whitespace
for rest[0] == ' ' {
rest = rest[1:]
}

parsedType, rest, err = parseType(rest[0:])
if err != nil {
return nil, fmt.Errorf("failed to parse name: %v", err)
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}

// skip whitespace between name and identifier
for rest[0] == ' ' {
rest = rest[1:]
}

// parse type
parsedType, rest, err = parseType(rest[:])
if err != nil {
return nil, fmt.Errorf("failed to parse type: %v", err)
name := fmt.Sprintf("name%d", i)
// if we're at a delimiter the parameter is unnamed
if !(rest[0] == ',' || rest[0] == ')') {
// attempt to parse name
name, rest, err = parseIdentifier(rest[:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse name: %v", err)
}
}

arg, err := assembleArg(name, parsedType)
if err != nil {
return nil, fmt.Errorf("failed to parse type: %v", err)
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}

result = append(result, arg)
i++

// skip trailing whitespace, consume comma
for rest[0] == ' ' || rest[0] == ',' {
rest = rest[1:]
}
}
if len(rest) == 0 || rest[0] != ')' {
return nil, fmt.Errorf("expected ')', got '%s'", rest)
return nil, "", fmt.Errorf("expected ')', got '%s'", rest)
}
if len(rest) > 1 {
return nil, fmt.Errorf("failed to parse selector '%s': unexpected string '%s'", unescapedSelector, rest)
if len(rest) >= 3 && rest[1] == '[' && rest[2] == ']' {
// emits a sentinel value that later gets removed when assembling
array, err := assembleArg("", "[]")
if err != nil {
panic("unreachable")
}
return append(result, array), rest[3:], nil
}
return result, nil
return result, rest[1:], nil
}

// type-name rule
func parseType(unescapedSelector string) (interface{}, string, error) {
if len(unescapedSelector) == 0 {
return nil, "", errors.New("empty type")
}
if unescapedSelector[0] == '(' {
return parseCompositeType(unescapedSelector)
}
return parseElementaryType(unescapedSelector)
}

func assembleArg(name string, arg any) (abi.ArgumentMarshaling, error) {
if s, ok := arg.(string); ok {
return abi.ArgumentMarshaling{Name: name, Type: s, InternalType: s, Components: nil, Indexed: false}, nil
} else if components, ok := arg.([]interface{}); ok {
subArgs, err := assembleArgs(components)
if err != nil {
return abi.ArgumentMarshaling{}, fmt.Errorf("failed to assemble components: %v", err)
}
} else if subArgs, ok := arg.([]abi.ArgumentMarshaling); ok {
tupleType := "tuple"
if len(subArgs) != 0 && subArgs[len(subArgs)-1].Type == "[]" {
subArgs = subArgs[:len(subArgs)-1]
Expand All @@ -183,67 +170,26 @@ func assembleArg(name string, arg any) (abi.ArgumentMarshaling, error) {
return abi.ArgumentMarshaling{}, fmt.Errorf("failed to assemble args: unexpected type %T", arg)
}

func assembleArgs(args []interface{}) ([]abi.ArgumentMarshaling, error) {
arguments := make([]abi.ArgumentMarshaling, 0)
for i, arg := range args {
// generate dummy name to avoid unmarshal issues
name := fmt.Sprintf("name%d", i)
arg, err := assembleArg(name, arg)
if err != nil {
return nil, err
}
arguments = append(arguments, arg)
}
return arguments, nil
}

// ParseSelector converts a method selector into a struct that can be JSON encoded
// and consumed by other functions in this package.
// Note, although uppercase letters are not part of the ABI spec, this function
// still accepts it as the general format is valid.
func ParseSelector(unescapedSelector string) (abi.SelectorMarshaling, error) {
name, rest, err := parseIdentifier(unescapedSelector)
if err != nil {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector identifier '%s': %v", unescapedSelector, err)
}
args := []interface{}{}
args := []abi.ArgumentMarshaling{}
if len(rest) >= 2 && rest[0] == '(' && rest[1] == ')' {
rest = rest[2:]
} else {
args, rest, err = parseCompositeType(rest)
if err != nil {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector args '%s': %v", unescapedSelector, err)
}
}
if len(rest) > 0 {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': unexpected string '%s'", unescapedSelector, rest)
}

// Reassemble the fake ABI and construct the JSON
fakeArgs, err := assembleArgs(args)
if err != nil {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector: %v", err)
}

return abi.SelectorMarshaling{Name: name, Type: "function", Inputs: fakeArgs}, nil
}

// ParseSelector converts a method selector into a struct that can be JSON encoded
// and consumed by other functions in this package.
// Note, although uppercase letters are not part of the ABI spec, this function
// still accepts it as the general format is valid.
func ParseSignature(unescapedSelector string) (abi.SelectorMarshaling, error) {
name, rest, err := parseIdentifier(unescapedSelector)
if err != nil {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
}
args := []abi.ArgumentMarshaling{}
if len(rest) < 2 || rest[0] != '(' || rest[1] != ')' {
args, err = parseArgs(rest)
if err != nil {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
}
}

return abi.SelectorMarshaling{Name: name, Type: "function", Inputs: args}, nil
}
47 changes: 41 additions & 6 deletions core/chains/evm/abi/selector_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseSelector(t *testing.T) {
Expand Down Expand Up @@ -83,7 +85,7 @@ func TestParseSelector(t *testing.T) {
}
}

func TestParseSignature(t *testing.T) {
func TestParseSelectorWithNames(t *testing.T) {
t.Parallel()
mkType := func(name string, typeOrComponents interface{}) abi.ArgumentMarshaling {
if typeName, ok := typeOrComponents.(string); ok {
Expand All @@ -102,13 +104,14 @@ func TestParseSignature(t *testing.T) {
args []abi.ArgumentMarshaling
}{
{"noargs()", "noargs", []abi.ArgumentMarshaling{}},
{"simple(a uint256, b uint256, c uint256)", "simple", []abi.ArgumentMarshaling{mkType("a", "uint256"), mkType("b", "uint256"), mkType("c", "uint256")}},
{"other(foo uint256, bar address)", "other", []abi.ArgumentMarshaling{mkType("foo", "uint256"), mkType("bar", "address")}},
{"withArray(a uint256[], b address[2], c uint8[4][][5])", "withArray", []abi.ArgumentMarshaling{mkType("a", "uint256[]"), mkType("b", "address[2]"), mkType("c", "uint8[4][][5]")}},
{"singleNest(d bytes32, e uint8, f (uint256,uint256), g address)", "singleNest", []abi.ArgumentMarshaling{mkType("d", "bytes32"), mkType("e", "uint8"), mkType("f", []abi.ArgumentMarshaling{mkType("name0", "uint256"), mkType("name1", "uint256")}), mkType("g", "address")}},
{"simple(uint256 a , uint256 b, uint256 c)", "simple", []abi.ArgumentMarshaling{mkType("a", "uint256"), mkType("b", "uint256"), mkType("c", "uint256")}},
{"other(uint256 foo, address bar )", "other", []abi.ArgumentMarshaling{mkType("foo", "uint256"), mkType("bar", "address")}},
{"withArray(uint256[] a, address[2] b, uint8[4][][5] c)", "withArray", []abi.ArgumentMarshaling{mkType("a", "uint256[]"), mkType("b", "address[2]"), mkType("c", "uint8[4][][5]")}},
{"singleNest(bytes32 d, uint8 e, (uint256,uint256) f, address g)", "singleNest", []abi.ArgumentMarshaling{mkType("d", "bytes32"), mkType("e", "uint8"), mkType("f", []abi.ArgumentMarshaling{mkType("name0", "uint256"), mkType("name1", "uint256")}), mkType("g", "address")}},
{"singleNest(bytes32 d, uint8 e, (uint256 first, uint256 second ) f, address g)", "singleNest", []abi.ArgumentMarshaling{mkType("d", "bytes32"), mkType("e", "uint8"), mkType("f", []abi.ArgumentMarshaling{mkType("first", "uint256"), mkType("second", "uint256")}), mkType("g", "address")}},
}
for i, tt := range tests {
selector, err := ParseSignature(tt.input)
selector, err := ParseSelector(tt.input)
if err != nil {
t.Errorf("test %d: failed to parse selector '%v': %v", i, tt.input, err)
}
Expand All @@ -124,3 +127,35 @@ func TestParseSignature(t *testing.T) {
}
}
}

func TestParseSelectorErrors(t *testing.T) {
type errorTestCases struct {
description string
input string
expectedError string
}

for _, scenario := range []errorTestCases{
{
description: "invalid name",
input: "123()",
expectedError: "failed to parse selector identifier '123()': invalid token start: 1",
},
{
description: "missing closing parenthesis",
input: "noargs(",
expectedError: "failed to parse selector args 'noargs(': expected ')', got ''",
},
{
description: "missing opening parenthesis",
input: "noargs)",
expectedError: "failed to parse selector args 'noargs)': expected '(', got )",
},
} {
t.Run(scenario.description, func(t *testing.T) {
_, err := ParseSelector(scenario.input)
require.Error(t, err)
assert.Equal(t, scenario.expectedError, err.Error())
})
}
}
2 changes: 1 addition & 1 deletion core/services/relay/evm/cap_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func NewEVMEncoder(config *values.Map) (consensustypes.Encoder, error) {
if !ok {
return nil, fmt.Errorf("expected %s to be a string", abiConfigFieldName)
}
selector, err := abiutil.ParseSignature("inner(" + selectorStr + ")")
selector, err := abiutil.ParseSelector("inner(" + selectorStr + ")")
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit eb6b50d

Please sign in to comment.