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

KS-35: EVM Encoder compatible with consensus capability #12025

Merged
merged 2 commits into from
Feb 16, 2024
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
249 changes: 249 additions & 0 deletions core/chains/evm/abi/selector_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Sourced from https://github.com/ethereum/go-ethereum/blob/fe91d476ba3e29316b6dc99b6efd4a571481d888/accounts/abi/selector_parser.go#L126
bolekk marked this conversation as resolved.
Show resolved Hide resolved
// Modified assembleArgs to retain argument names

// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package abi

import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/accounts/abi"
)

func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}

func isAlpha(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

func isIdentifierSymbol(c byte) bool {
return c == '$' || c == '_'
}

func parseToken(unescapedSelector string, isIdent bool) (string, string, error) {
if len(unescapedSelector) == 0 {
return "", "", errors.New("empty token")
}
firstChar := unescapedSelector[0]
position := 1
if !(isAlpha(firstChar) || (isIdent && isIdentifierSymbol(firstChar))) {
return "", "", fmt.Errorf("invalid token start: %c", firstChar)
}
for position < len(unescapedSelector) {
char := unescapedSelector[position]
if !(isAlpha(char) || isDigit(char) || (isIdent && isIdentifierSymbol(char))) {
break
}
position++
}
return unescapedSelector[:position], unescapedSelector[position:], nil
}

func parseIdentifier(unescapedSelector string) (string, string, error) {
return parseToken(unescapedSelector, true)
}

func parseElementaryType(unescapedSelector string) (string, string, error) {
parsedType, rest, err := parseToken(unescapedSelector, false)
if err != nil {
return "", "", fmt.Errorf("failed to parse elementary type: %v", err)
}
// handle arrays
for len(rest) > 0 && rest[0] == '[' {
parsedType = parsedType + string(rest[0])
rest = rest[1:]
for len(rest) > 0 && isDigit(rest[0]) {
parsedType = parsedType + string(rest[0])
rest = rest[1:]
}
if len(rest) == 0 || rest[0] != ']' {
return "", "", fmt.Errorf("failed to parse array: expected ']', got %c", unescapedSelector[0])
}
parsedType = parsedType + string(rest[0])
rest = rest[1:]
}
return parsedType, rest, nil
}

func parseCompositeType(unescapedSelector string) ([]interface{}, 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])
}
result := []abi.ArgumentMarshaling{}
rest := unescapedSelector[1:]
var parsedType any
var err error
for len(rest) > 0 && rest[0] != ')' {
// parse method name
var name string
name, rest, err = parseIdentifier(rest[:])
if err != nil {
return nil, fmt.Errorf("failed to parse name: %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)
}

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

result = append(result, arg)

for rest[0] == ' ' || rest[0] == ',' {
rest = rest[1:]
}
}
if len(rest) == 0 || rest[0] != ')' {
Copy link
Contributor

Choose a reason for hiding this comment

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

The if statements here are a little awkward. Not going to necessarily request changes, but I would suggest something simpler like if !(len(rest) == 1 && rest[0] == ')')

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)
}
return result, nil
}

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)
}
tupleType := "tuple"
if len(subArgs) != 0 && subArgs[len(subArgs)-1].Type == "[]" {
subArgs = subArgs[:len(subArgs)-1]
tupleType = "tuple[]"
}
return abi.ArgumentMarshaling{Name: name, Type: tupleType, InternalType: tupleType, Components: subArgs, Indexed: false}, nil
}
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) {
Copy link
Collaborator

@DeividasK DeividasK Feb 19, 2024

Choose a reason for hiding this comment

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

What's the difference between ParseSelector and ParseSignature (below)? Seems like ParseSelector is not used anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@DeividasK , @austinborn these two files were copied from geth (see comment above: // Sourced from https://github.com/ethereum/go-ethereum/blob/fe91d476ba3e29316b6dc99b6efd4a571481d888/accounts/abi/selector_parser.go#L126
// Modified assembleArgs to retain argument names)

Blaz modified them slightly to include argument name parsing as geth by default doesn't support that. They happened to be merged with my PR because encoders also depend on this library and were merged earlier.

We can either keep them as close to original as possible or make cleaner/smaller (I already had to do minor tweaks to satisfy Sonar). I'll leave that decision to @archseer and @patrick-dowell .

name, rest, err := parseIdentifier(unescapedSelector)
if err != nil {
return abi.SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
}
args := []interface{}{}
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)
}
}
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this named unescpaedSelector? I can't find an "escaped" version. What does it need to be unescaped from?

Copy link
Contributor

Choose a reason for hiding this comment

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

I modified this verbatim from geth (ParseSelector being the original) so I retained the naming

Copy link
Contributor

Choose a reason for hiding this comment

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

it's mostly just because of naming here so the variable names don't conflict:

https://github.com/ethereum/go-ethereum/blob/5d984796afd4aa7c00c6663f4155488a9df73d0e/signer/fourbyte/abi.go#L77-L80

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
}
126 changes: 126 additions & 0 deletions core/chains/evm/abi/selector_parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Sourced from https://github.com/ethereum/go-ethereum/blob/fe91d476ba3e29316b6dc99b6efd4a571481d888/accounts/abi/selector_parser_test.go

// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package abi

import (
"fmt"
"log"
"reflect"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
)

func TestParseSelector(t *testing.T) {
t.Parallel()
mkType := func(types ...interface{}) []abi.ArgumentMarshaling {
var result []abi.ArgumentMarshaling
for i, typeOrComponents := range types {
name := fmt.Sprintf("name%d", i)
if typeName, ok := typeOrComponents.(string); ok {
result = append(result, abi.ArgumentMarshaling{Name: name, Type: typeName, InternalType: typeName, Components: nil, Indexed: false})
} else if components, ok := typeOrComponents.([]abi.ArgumentMarshaling); ok {
result = append(result, abi.ArgumentMarshaling{Name: name, Type: "tuple", InternalType: "tuple", Components: components, Indexed: false})
} else if components, ok := typeOrComponents.([][]abi.ArgumentMarshaling); ok {
result = append(result, abi.ArgumentMarshaling{Name: name, Type: "tuple[]", InternalType: "tuple[]", Components: components[0], Indexed: false})
} else {
log.Fatalf("unexpected type %T", typeOrComponents)
}
}
return result
}
tests := []struct {
input string
name string
args []abi.ArgumentMarshaling
}{
{"noargs()", "noargs", []abi.ArgumentMarshaling{}},
{"simple(uint256,uint256,uint256)", "simple", mkType("uint256", "uint256", "uint256")},
{"other(uint256,address)", "other", mkType("uint256", "address")},
{"withArray(uint256[],address[2],uint8[4][][5])", "withArray", mkType("uint256[]", "address[2]", "uint8[4][][5]")},
{"singleNest(bytes32,uint8,(uint256,uint256),address)", "singleNest", mkType("bytes32", "uint8", mkType("uint256", "uint256"), "address")},
{"multiNest(address,(uint256[],uint256),((address,bytes32),uint256))", "multiNest",
mkType("address", mkType("uint256[]", "uint256"), mkType(mkType("address", "bytes32"), "uint256"))},
{"arrayNest((uint256,uint256)[],bytes32)", "arrayNest", mkType([][]abi.ArgumentMarshaling{mkType("uint256", "uint256")}, "bytes32")},
{"multiArrayNest((uint256,uint256)[],(uint256,uint256)[])", "multiArrayNest",
mkType([][]abi.ArgumentMarshaling{mkType("uint256", "uint256")}, [][]abi.ArgumentMarshaling{mkType("uint256", "uint256")})},
{"singleArrayNestAndArray((uint256,uint256)[],bytes32[])", "singleArrayNestAndArray",
mkType([][]abi.ArgumentMarshaling{mkType("uint256", "uint256")}, "bytes32[]")},
{"singleArrayNestWithArrayAndArray((uint256[],address[2],uint8[4][][5])[],bytes32[])", "singleArrayNestWithArrayAndArray",
mkType([][]abi.ArgumentMarshaling{mkType("uint256[]", "address[2]", "uint8[4][][5]")}, "bytes32[]")},
}
for i, tt := range tests {
selector, err := ParseSelector(tt.input)
if err != nil {
t.Errorf("test %d: failed to parse selector '%v': %v", i, tt.input, err)
}
if selector.Name != tt.name {
t.Errorf("test %d: unexpected function name: '%s' != '%s'", i, selector.Name, tt.name)
}

if selector.Type != "function" {
t.Errorf("test %d: unexpected type: '%s' != '%s'", i, selector.Type, "function")
}
if !reflect.DeepEqual(selector.Inputs, tt.args) {
t.Errorf("test %d: unexpected args: '%v' != '%v'", i, selector.Inputs, tt.args)
}
}
}

func TestParseSignature(t *testing.T) {
t.Parallel()
mkType := func(name string, typeOrComponents interface{}) abi.ArgumentMarshaling {
if typeName, ok := typeOrComponents.(string); ok {
return abi.ArgumentMarshaling{Name: name, Type: typeName, InternalType: typeName, Components: nil, Indexed: false}
} else if components, ok := typeOrComponents.([]abi.ArgumentMarshaling); ok {
return abi.ArgumentMarshaling{Name: name, Type: "tuple", InternalType: "tuple", Components: components, Indexed: false}
} else if components, ok := typeOrComponents.([][]abi.ArgumentMarshaling); ok {
return abi.ArgumentMarshaling{Name: name, Type: "tuple[]", InternalType: "tuple[]", Components: components[0], Indexed: false}
}
log.Fatalf("unexpected type %T", typeOrComponents)
return abi.ArgumentMarshaling{}
}
tests := []struct {
input string
name string
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")}},
}
for i, tt := range tests {
selector, err := ParseSignature(tt.input)
if err != nil {
t.Errorf("test %d: failed to parse selector '%v': %v", i, tt.input, err)
}
if selector.Name != tt.name {
t.Errorf("test %d: unexpected function name: '%s' != '%s'", i, selector.Name, tt.name)
}

if selector.Type != "function" {
t.Errorf("test %d: unexpected type: '%s' != '%s'", i, selector.Type, "function")
}
if !reflect.DeepEqual(selector.Inputs, tt.args) {
t.Errorf("test %d: unexpected args: '%v' != '%v'", i, selector.Inputs, tt.args)
}
}
}
2 changes: 1 addition & 1 deletion core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/pelletier/go-toml/v2 v2.1.1
github.com/shopspring/decimal v1.3.1
github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240215194703-6ab175aa7290
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240215221559-8a726e745417
github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868
github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000
github.com/smartcontractkit/libocr v0.0.0-20240215150045-fe2ba71b2f0a
Expand Down
4 changes: 2 additions & 2 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1169,8 +1169,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv
github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M=
github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww=
github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240215194703-6ab175aa7290 h1:VgsaJqVkTfcRv/s4EWPPmgL8o2mjftKZSqRGluZro7M=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240215194703-6ab175aa7290/go.mod h1:yKWUC5vRyIB+yQdmpOAf2y2A0hJ43uENKVgljk5Ve3g=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240215221559-8a726e745417 h1:1IeZowwqz3Uql9UqH8KP3C0J48wd/W0bVPMF5D+wDdA=
github.com/smartcontractkit/chainlink-common v0.1.7-0.20240215221559-8a726e745417/go.mod h1:yKWUC5vRyIB+yQdmpOAf2y2A0hJ43uENKVgljk5Ve3g=
github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8 h1:I326nw5GwHQHsLKHwtu5Sb9EBLylC8CfUd7BFAS0jtg=
github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8/go.mod h1:a65NtrK4xZb01mf0dDNghPkN2wXgcqFQ55ADthVBgMc=
github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240214203158-47dae5de1336 h1:j00D0/EqE9HRu+63v7KwUOe4ZxLc4AN5SOJFiinkkH0=
Expand Down
Loading
Loading