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=