From e71a16fc8e24cc61e12fbf79b85b1b8eab0fe5e7 Mon Sep 17 00:00:00 2001 From: Bolek Kulbabinski <1416262+bolekk@users.noreply.github.com> Date: Tue, 13 Feb 2024 21:16:39 -0800 Subject: [PATCH] KS-35: EVM Encoder compatible with consensus capability --- core/chains/evm/abi/selector_parser.go | 249 ++++++++++++++++++++ core/chains/evm/abi/selector_parser_test.go | 126 ++++++++++ core/scripts/go.mod | 2 +- core/scripts/go.sum | 4 +- core/services/relay/evm/cap_encoder.go | 97 ++++++++ core/services/relay/evm/cap_encoder_test.go | 58 +++++ go.mod | 2 +- go.sum | 4 +- integration-tests/go.mod | 2 +- integration-tests/go.sum | 4 +- 10 files changed, 539 insertions(+), 9 deletions(-) create mode 100644 core/chains/evm/abi/selector_parser.go create mode 100644 core/chains/evm/abi/selector_parser_test.go create mode 100644 core/services/relay/evm/cap_encoder.go create mode 100644 core/services/relay/evm/cap_encoder_test.go diff --git a/core/chains/evm/abi/selector_parser.go b/core/chains/evm/abi/selector_parser.go new file mode 100644 index 00000000000..30e687ba33a --- /dev/null +++ b/core/chains/evm/abi/selector_parser.go @@ -0,0 +1,249 @@ +// Sourced from https://github.com/ethereum/go-ethereum/blob/fe91d476ba3e29316b6dc99b6efd4a571481d888/accounts/abi/selector_parser.go#L126 +// 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 . + +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] != ')' { + 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) { + 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) { + 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 +} diff --git a/core/chains/evm/abi/selector_parser_test.go b/core/chains/evm/abi/selector_parser_test.go new file mode 100644 index 00000000000..caae3744678 --- /dev/null +++ b/core/chains/evm/abi/selector_parser_test.go @@ -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 . + +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) + } + } +} diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 7c0a41fa366..ae9d592c6ca 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -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 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index b32a445d60a..36569639eac 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -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= diff --git a/core/services/relay/evm/cap_encoder.go b/core/services/relay/evm/cap_encoder.go new file mode 100644 index 00000000000..b6865096af9 --- /dev/null +++ b/core/services/relay/evm/cap_encoder.go @@ -0,0 +1,97 @@ +package evm + +import ( + "context" + "encoding/json" + "fmt" + + consensustypes "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/values" + abiutil "github.com/smartcontractkit/chainlink/v2/core/chains/evm/abi" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +const ( + abiConfigFieldName = "abi" + encoderName = "user" + idLen = 32 +) + +type capEncoder struct { + codec commontypes.RemoteCodec +} + +var _ consensustypes.Encoder = (*capEncoder)(nil) + +func NewEVMEncoder(config *values.Map) (consensustypes.Encoder, error) { + // parse the "inner" encoder config - user-defined fields + wrappedSelector, err := config.Underlying[abiConfigFieldName].Unwrap() + if err != nil { + return nil, err + } + selectorStr, ok := wrappedSelector.(string) + if !ok { + return nil, fmt.Errorf("expected %s to be a string", abiConfigFieldName) + } + selector, err := abiutil.ParseSignature("inner(" + selectorStr + ")") + if err != nil { + return nil, err + } + jsonSelector, err := json.Marshal(selector.Inputs) + if err != nil { + return nil, err + } + + codecConfig := types.CodecConfig{Configs: map[string]types.ChainCodecConfig{ + encoderName: {TypeABI: string(jsonSelector)}, + }} + c, err := NewCodec(codecConfig) + if err != nil { + return nil, err + } + + return &capEncoder{codec: c}, nil +} + +func (c *capEncoder) Encode(ctx context.Context, input values.Map) ([]byte, error) { + unwrappedInput, err := input.Unwrap() + if err != nil { + return nil, err + } + unwrappedMap, ok := unwrappedInput.(map[string]any) + if !ok { + return nil, fmt.Errorf("expected unwrapped input to be a map") + } + userPayload, err := c.codec.Encode(ctx, unwrappedMap, encoderName) + if err != nil { + return nil, err + } + // prepend workflowID and workflowExecutionID to the encoded user data + workflowIDbytes, executionIDBytes, err := extractIDs(unwrappedMap) + if err != nil { + return nil, err + } + return append(append(workflowIDbytes, executionIDBytes...), userPayload...), nil +} + +// extract workflowID and executionID from the input map, validate and align to 32 bytes +// NOTE: consider requiring them to be exactly 32 bytes to avoid issues with padding +func extractIDs(input map[string]any) ([]byte, []byte, error) { + workflowID, ok := input[consensustypes.WorkflowIDFieldName].(string) + if !ok { + return nil, nil, fmt.Errorf("expected %s to be a string", consensustypes.WorkflowIDFieldName) + } + executionID, ok := input[consensustypes.ExecutionIDFieldName].(string) + if !ok { + return nil, nil, fmt.Errorf("expected %s to be a string", consensustypes.ExecutionIDFieldName) + } + if len(workflowID) > 32 || len(executionID) > 32 { + return nil, nil, fmt.Errorf("IDs too long: %d, %d", len(workflowID), len(executionID)) + } + alignedWorkflowID := make([]byte, idLen) + copy(alignedWorkflowID, workflowID) + alignedExecutionID := make([]byte, idLen) + copy(alignedExecutionID, executionID) + return alignedWorkflowID, alignedExecutionID, nil +} diff --git a/core/services/relay/evm/cap_encoder_test.go b/core/services/relay/evm/cap_encoder_test.go new file mode 100644 index 00000000000..1d8b6da4610 --- /dev/null +++ b/core/services/relay/evm/cap_encoder_test.go @@ -0,0 +1,58 @@ +package evm_test + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + consensustypes "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" +) + +var ( + reportA = []byte{0x01, 0x02, 0x03} + reportB = []byte{0xaa, 0xbb, 0xcc, 0xdd} + workflowID = "my_id" + executionID = "my_execution_id" +) + +func TestEVMEncoder(t *testing.T) { + config := map[string]any{ + "abi": "mercury_reports bytes[]", + } + wrapped, err := values.NewMap(config) + require.NoError(t, err) + enc, err := evm.NewEVMEncoder(wrapped) + require.NoError(t, err) + + // output of a DF2.0 aggregator + metadata fields appended by OCR + input := map[string]any{ + "mercury_reports": []any{reportA, reportB}, + consensustypes.WorkflowIDFieldName: workflowID, + consensustypes.ExecutionIDFieldName: executionID, + } + wrapped, err = values.NewMap(input) + require.NoError(t, err) + encoded, err := enc.Encode(testutils.Context(t), *wrapped) + require.NoError(t, err) + + expected := + // start of the outer tuple ((user_fields), workflow_id, workflow_execution_id) + "6d795f6964000000000000000000000000000000000000000000000000000000" + // workflow ID + "6d795f657865637574696f6e5f69640000000000000000000000000000000000" + // execution ID + // start of the inner tuple (user_fields) + "0000000000000000000000000000000000000000000000000000000000000020" + // offset of mercury_reports array + "0000000000000000000000000000000000000000000000000000000000000002" + // length of mercury_reports array + "0000000000000000000000000000000000000000000000000000000000000040" + // offset of reportA + "0000000000000000000000000000000000000000000000000000000000000080" + // offset of reportB + "0000000000000000000000000000000000000000000000000000000000000003" + // length of reportA + "0102030000000000000000000000000000000000000000000000000000000000" + // reportA + "0000000000000000000000000000000000000000000000000000000000000004" + // length of reportB + "aabbccdd00000000000000000000000000000000000000000000000000000000" // reportB + // end of the inner tuple (user_fields) + + require.Equal(t, expected, hex.EncodeToString(encoded)) +} diff --git a/go.mod b/go.mod index 799dbe9b906..e68fe191db2 100644 --- a/go.mod +++ b/go.mod @@ -67,7 +67,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 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-cosmos v0.4.1-0.20240213120401-01a23955f9f8 github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240214203158-47dae5de1336 github.com/smartcontractkit/chainlink-feeds v0.0.0-20240119021347-3c541a78cdb8 diff --git a/go.sum b/go.sum index 097aaea35c9..c9b2aa88574 100644 --- a/go.sum +++ b/go.sum @@ -1164,8 +1164,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= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 0f9df5b63f9..e8d46fd8ab8 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -24,7 +24,7 @@ require ( github.com/segmentio/ksuid v1.0.4 github.com/slack-go/slack v0.12.2 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-testing-framework v1.23.2 github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 275761a080e..9b5c4ad604d 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1503,8 +1503,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=