diff --git a/.changeset/four-hats-mate.md b/.changeset/four-hats-mate.md new file mode 100644 index 00000000000..83110978ed5 --- /dev/null +++ b/.changeset/four-hats-mate.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +refactor extradata codec logic and unblock ccip msg optimization using protobuf #added diff --git a/.mockery.yaml b/.mockery.yaml index 8b7d9988ab9..7ad18b97802 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -27,6 +27,9 @@ packages: interfaces: CCIPOracle: OracleCreator: + github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common: + interfaces: + ExtraDataCodec: github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types: interfaces: Dispatcher: diff --git a/core/capabilities/ccip/ccipevm/executecodec.go b/core/capabilities/ccip/ccipevm/executecodec.go index 6a2431d6e81..d8fcb912a01 100644 --- a/core/capabilities/ccip/ccipevm/executecodec.go +++ b/core/capabilities/ccip/ccipevm/executecodec.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -19,9 +20,10 @@ import ( // - "OffRamp 1.6.0" type ExecutePluginCodecV1 struct { executeReportMethodInputs abi.Arguments + extraDataCodec ccipcommon.ExtraDataCodec } -func NewExecutePluginCodecV1() *ExecutePluginCodecV1 { +func NewExecutePluginCodecV1(extraDataCodec ccipcommon.ExtraDataCodec) *ExecutePluginCodecV1 { abiParsed, err := abi.JSON(strings.NewReader(offramp.OffRampABI)) if err != nil { panic(fmt.Errorf("parse multi offramp abi: %s", err)) @@ -33,6 +35,7 @@ func NewExecutePluginCodecV1() *ExecutePluginCodecV1 { return &ExecutePluginCodecV1{ executeReportMethodInputs: methodInputs[:1], + extraDataCodec: extraDataCodec, } } @@ -59,7 +62,12 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec return nil, fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress) } - destGasAmount, err := abiDecodeUint32(tokenAmount.DestExecData) + destExecDataDecodedMap, err := e.extraDataCodec.DecodeTokenAmountDestExecData(tokenAmount.DestExecData, chainReport.SourceChainSelector) + if err != nil { + return nil, fmt.Errorf("failed to decode dest exec data: %w", err) + } + + destGasAmount, err := extractDestGasAmountFromMap(destExecDataDecodedMap) if err != nil { return nil, fmt.Errorf("decode dest gas amount: %w", err) } @@ -82,7 +90,12 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec }) } - gasLimit, err := decodeExtraArgsV1V2(message.ExtraArgs) + decodedExtraArgsMap, err := e.extraDataCodec.DecodeExtraArgs(message.ExtraArgs, chainReport.SourceChainSelector) + if err != nil { + return nil, err + } + + gasLimit, err := parseExtraDataMap(decodedExtraArgsMap) if err != nil { return nil, fmt.Errorf("decode extra args to get gas limit: %w", err) } diff --git a/core/capabilities/ccip/ccipevm/executecodec_test.go b/core/capabilities/ccip/ccipevm/executecodec_test.go index 2e171f04030..7f8f924c740 100644 --- a/core/capabilities/ccip/ccipevm/executecodec_test.go +++ b/core/capabilities/ccip/ccipevm/executecodec_test.go @@ -2,6 +2,7 @@ package ccipevm import ( "encoding/base64" + "math/big" "math/rand" "testing" @@ -10,7 +11,11 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana" + ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -25,7 +30,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) -var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.ExecutePluginReport { +var randomExecuteReport = func(t *testing.T, d *testSetupData, chainSelector uint64, gasLimit *big.Int, destGasAmount uint32) cciptypes.ExecutePluginReport { const numChainReports = 10 const msgsPerReport = 10 const numTokensPerMsg = 3 @@ -39,7 +44,7 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute tokenAmounts := make([]cciptypes.RampTokenAmount, numTokensPerMsg) for z := 0; z < numTokensPerMsg; z++ { - encodedDestExecData, err2 := abiEncodeUint32(rand.Uint32()) + encodedDestExecData, err2 := abiEncodeUint32(destGasAmount) require.NoError(t, err2) tokenAmounts[z] = cciptypes.RampTokenAmount{ @@ -52,7 +57,7 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute } extraArgs, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ - GasLimit: utils.RandUint256(), + GasLimit: gasLimit, }) assert.NoError(t, err) @@ -82,7 +87,7 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute } chainReports[i] = cciptypes.ExecutePluginReportSingleChain{ - SourceChainSelector: cciptypes.ChainSelector(rand.Uint64()), + SourceChainSelector: cciptypes.ChainSelector(chainSelector), Messages: reportMessages, OffchainTokenData: tokenData, Proofs: []cciptypes.Bytes32{utils.RandomBytes32(), utils.RandomBytes32()}, @@ -95,16 +100,41 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute func TestExecutePluginCodecV1(t *testing.T) { d := testSetup(t) + ctx := testutils.Context(t) + mockExtraDataCodec := &mocks.ExtraDataCodec{} + destGasAmount := rand.Uint32() + gasLimit := utils.RandUint256() + mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{ + "destgasamount": destGasAmount, + }, nil) + mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{ + "gasLimit": utils.RandUint256(), + "accountIsWritableBitmap": gasLimit, + }, nil) testCases := []struct { - name string - report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport - expErr bool + name string + report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport + expErr bool + chainSelector uint64 + destGasAmount uint32 + gasLimit *big.Int }{ { - name: "base report", - report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, - expErr: false, + name: "base report", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + chainSelector: 5009297550715157269, // ETH mainnet chain selector + gasLimit: gasLimit, + destGasAmount: destGasAmount, + }, + { + name: "base report", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + chainSelector: 124615329519749607, // Solana mainnet chain selector + gasLimit: gasLimit, + destGasAmount: destGasAmount, }, { name: "reports have empty msgs", @@ -113,7 +143,10 @@ func TestExecutePluginCodecV1(t *testing.T) { report.ChainReports[4].Messages = []cciptypes.Message{} return report }, - expErr: false, + expErr: false, + chainSelector: 5009297550715157269, // ETH mainnet chain selector + gasLimit: gasLimit, + destGasAmount: destGasAmount, }, { name: "reports have empty offchain token data", @@ -122,12 +155,13 @@ func TestExecutePluginCodecV1(t *testing.T) { report.ChainReports[4].OffchainTokenData[1] = [][]byte{} return report }, - expErr: false, + expErr: false, + chainSelector: 5009297550715157269, // ETH mainnet chain selector + gasLimit: gasLimit, + destGasAmount: destGasAmount, }, } - ctx := testutils.Context(t) - // Deploy the contract transactor := evmtestutils.MustNewSimTransactor(t) simulatedBackend := backends.NewSimulatedBackend(core.GenesisAlloc{ @@ -141,8 +175,8 @@ func TestExecutePluginCodecV1(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - codec := NewExecutePluginCodecV1() - report := tc.report(randomExecuteReport(t, d)) + codec := NewExecutePluginCodecV1(mockExtraDataCodec) + report := tc.report(randomExecuteReport(t, d, tc.chainSelector, tc.gasLimit, tc.destGasAmount)) bytes, err := codec.Encode(ctx, report) if tc.expErr { assert.Error(t, err) @@ -183,6 +217,7 @@ func TestExecutePluginCodecV1(t *testing.T) { } func Test_DecodeReport(t *testing.T) { + ExtraDataCodec := ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})) offRampABI, err := offramp.OffRampMetaData.GetAbi() require.NoError(t, err) @@ -201,7 +236,7 @@ func Test_DecodeReport(t *testing.T) { rawReport := *abi.ConvertType(executeInputs[1], new([]byte)).(*[]byte) - codec := NewExecutePluginCodecV1() + codec := NewExecutePluginCodecV1(ExtraDataCodec) decoded, err := codec.Decode(tests.Context(t), rawReport) require.NoError(t, err) diff --git a/core/capabilities/ccip/ccipevm/extradatadecoder.go b/core/capabilities/ccip/ccipevm/extradatadecoder.go index 5f26ba9a54c..be657bfbb8d 100644 --- a/core/capabilities/ccip/ccipevm/extradatadecoder.go +++ b/core/capabilities/ccip/ccipevm/extradatadecoder.go @@ -6,8 +6,12 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) -func DecodeDestExecDataToMap(DestExecData cciptypes.Bytes) (map[string]interface{}, error) { - destGasAmount, err := abiDecodeUint32(DestExecData) +// ExtraDataDecoder is a concrete implementation of ExtraDataDecoder +type ExtraDataDecoder struct{} + +// DecodeDestExecDataToMap reformats bytes into a chain agnostic map[string]interface{} representation for dest exec data +func (d ExtraDataDecoder) DecodeDestExecDataToMap(destExecData cciptypes.Bytes) (map[string]interface{}, error) { + destGasAmount, err := abiDecodeUint32(destExecData) if err != nil { return nil, fmt.Errorf("decode dest gas amount: %w", err) } @@ -17,7 +21,8 @@ func DecodeDestExecDataToMap(DestExecData cciptypes.Bytes) (map[string]interface }, nil } -func DecodeExtraArgsToMap(extraArgs []byte) (map[string]any, error) { +// DecodeExtraArgsToMap reformats bytes into a chain agnostic map[string]any representation for extra args +func (d ExtraDataDecoder) DecodeExtraArgsToMap(extraArgs cciptypes.Bytes) (map[string]any, error) { if len(extraArgs) < 4 { return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs)) } diff --git a/core/capabilities/ccip/ccipevm/extradatadecoder_test.go b/core/capabilities/ccip/ccipevm/extradatadecoder_test.go index f2a7b168618..b213a14331a 100644 --- a/core/capabilities/ccip/ccipevm/extradatadecoder_test.go +++ b/core/capabilities/ccip/ccipevm/extradatadecoder_test.go @@ -14,6 +14,7 @@ import ( func Test_decodeExtraData(t *testing.T) { d := testSetup(t) gasLimit := big.NewInt(rand.Int63()) + extraDataDecoder := &ExtraDataDecoder{} t.Run("decode extra args into map evm v1", func(t *testing.T) { encoded, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ @@ -21,7 +22,7 @@ func Test_decodeExtraData(t *testing.T) { }) require.NoError(t, err) - m, err := DecodeExtraArgsToMap(encoded) + m, err := extraDataDecoder.DecodeExtraArgsToMap(encoded) require.NoError(t, err) require.Len(t, m, 1) @@ -37,7 +38,7 @@ func Test_decodeExtraData(t *testing.T) { }) require.NoError(t, err) - m, err := DecodeExtraArgsToMap(encoded) + m, err := extraDataDecoder.DecodeExtraArgsToMap(encoded) require.NoError(t, err) require.Len(t, m, 2) @@ -65,7 +66,7 @@ func Test_decodeExtraData(t *testing.T) { encoded, err := d.contract.EncodeSVMExtraArgsV1(nil, decoded) require.NoError(t, err) - m, err := DecodeExtraArgsToMap(encoded) + m, err := extraDataDecoder.DecodeExtraArgsToMap(encoded) require.NoError(t, err) require.Len(t, m, 5) @@ -94,7 +95,7 @@ func Test_decodeExtraData(t *testing.T) { destGasAmount := uint32(10000) encoded, err := abiEncodeUint32(destGasAmount) require.NoError(t, err) - m, err := DecodeDestExecDataToMap(encoded) + m, err := extraDataDecoder.DecodeDestExecDataToMap(encoded) require.NoError(t, err) require.Len(t, m, 1) diff --git a/core/capabilities/ccip/ccipevm/helpers_test.go b/core/capabilities/ccip/ccipevm/helpers_test.go index 35615e9644a..f1381ea4cc5 100644 --- a/core/capabilities/ccip/ccipevm/helpers_test.go +++ b/core/capabilities/ccip/ccipevm/helpers_test.go @@ -13,6 +13,7 @@ import ( func Test_decodeExtraArgs(t *testing.T) { d := testSetup(t) gasLimit := big.NewInt(rand.Int63()) + extraDataDecoder := &ExtraDataDecoder{} t.Run("v1", func(t *testing.T) { encoded, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ @@ -45,7 +46,7 @@ func Test_decodeExtraArgs(t *testing.T) { }) require.NoError(t, err) - m, err := DecodeExtraArgsToMap(encoded) + m, err := extraDataDecoder.DecodeExtraArgsToMap(encoded) require.NoError(t, err) require.Len(t, m, 1) @@ -61,7 +62,7 @@ func Test_decodeExtraArgs(t *testing.T) { }) require.NoError(t, err) - m, err := DecodeExtraArgsToMap(encoded) + m, err := extraDataDecoder.DecodeExtraArgsToMap(encoded) require.NoError(t, err) require.Len(t, m, 2) diff --git a/core/capabilities/ccip/ccipevm/msghasher.go b/core/capabilities/ccip/ccipevm/msghasher.go index ac436afb028..e385edebe29 100644 --- a/core/capabilities/ccip/ccipevm/msghasher.go +++ b/core/capabilities/ccip/ccipevm/msghasher.go @@ -2,11 +2,15 @@ package ccipevm import ( "context" + "errors" "fmt" + "math/big" + "strings" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -40,12 +44,14 @@ var ( // Compatible with: // - "OnRamp 1.6.0" type MessageHasherV1 struct { - lggr logger.Logger + lggr logger.Logger + extraDataCodec ccipcommon.ExtraDataCodec } -func NewMessageHasherV1(lggr logger.Logger) *MessageHasherV1 { +func NewMessageHasherV1(lggr logger.Logger, extraDataCodec ccipcommon.ExtraDataCodec) *MessageHasherV1 { return &MessageHasherV1{ - lggr: lggr, + lggr: lggr, + extraDataCodec: extraDataCodec, } } @@ -154,9 +160,15 @@ func (h *MessageHasherV1) Hash(ctx context.Context, msg cciptypes.Message) (ccip // TODO: we assume that extra args is always abi-encoded for now, but we need // to decode according to source chain selector family. We should add a family // lookup API to the chain-selectors library. - gasLimit, err := decodeExtraArgsV1V2(msg.ExtraArgs) + + decodedExtraArgsMap, err := h.extraDataCodec.DecodeExtraArgs(msg.ExtraArgs, msg.Header.SourceChainSelector) + if err != nil { + return [32]byte{}, err + } + + gasLimit, err := parseExtraDataMap(decodedExtraArgsMap) if err != nil { - return [32]byte{}, fmt.Errorf("decode extra args: %w", err) + return [32]byte{}, fmt.Errorf("decode extra args to get gas limit: %w", err) } lggr.Debugw("decoded msg gas limit", "gasLimit", gasLimit) @@ -242,5 +254,44 @@ func abiDecodeAddress(data []byte) (common.Address, error) { return val, nil } +func parseExtraDataMap(input map[string]any) (*big.Int, error) { + var outputGas *big.Int + for fieldName, fieldValue := range input { + lowercase := strings.ToLower(fieldName) + switch lowercase { + case "gaslimit": + // Expect [][32]byte + if val, ok := fieldValue.(*big.Int); ok { + outputGas = val + return outputGas, nil + } else { + return nil, fmt.Errorf("unexpected type for gas limit: %T", fieldValue) + } + default: + // no error here, as we only need the keys to gasLimit, other keys can be skipped without like AllowOutOfOrderExecution etc. + } + } + return outputGas, errors.New("gas limit not found in extra data map") +} + +func extractDestGasAmountFromMap(input map[string]any) (uint32, error) { + // Iterate through the expected fields in the struct + for fieldName, fieldValue := range input { + lowercase := strings.ToLower(fieldName) + switch lowercase { + case "destgasamount": + // Expect uint32 + if val, ok := fieldValue.(uint32); ok { + return val, nil + } else { + return 0, errors.New("invalid type for destgasamount, expected uint32") + } + default: + } + } + + return 0, errors.New("invalid token message, dest gas amount not found in the DestExecDataDecoded map") +} + // Interface compliance check var _ cciptypes.MessageHasher = (*MessageHasherV1)(nil) diff --git a/core/capabilities/ccip/ccipevm/msghasher_test.go b/core/capabilities/ccip/ccipevm/msghasher_test.go index cb399a75573..3528c7635d6 100644 --- a/core/capabilities/ccip/ccipevm/msghasher_test.go +++ b/core/capabilities/ccip/ccipevm/msghasher_test.go @@ -17,6 +17,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana" + ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -31,6 +33,8 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) +var ExtraDataCodec = ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})) + // NOTE: these test cases are only EVM <-> EVM. // Update these cases once we have non-EVM examples. func TestMessageHasher_EVM2EVM(t *testing.T) { @@ -38,19 +42,19 @@ func TestMessageHasher_EVM2EVM(t *testing.T) { d := testSetup(t) testCases := []evmExtraArgs{ - {version: "v1", gasLimit: big.NewInt(rand.Int63())}, - {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: false}, - {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: true}, + {version: "v1", gasLimit: big.NewInt(rand.Int63()), chainSelector: 5009297550715157269}, // ETH mainnet chain selector + {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: false, chainSelector: 5009297550715157269}, // ETH mainnet chain selector + {version: "v2", gasLimit: big.NewInt(rand.Int63()), allowOOO: true, chainSelector: 5009297550715157269}, // ETH mainnet chain selector } for i, tc := range testCases { t.Run(fmt.Sprintf("tc_%d", i), func(tt *testing.T) { - testHasherEVM2EVM(ctx, tt, d, tc) + testHasherEVM2EVM(ctx, tt, d, tc, tc.chainSelector) }) } } -func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmExtraArgs evmExtraArgs) { - ccipMsg := createEVM2EVMMessage(t, d.contract, evmExtraArgs) +func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmExtraArgs evmExtraArgs, sourceChainSelector uint64) { + ccipMsg := createEVM2EVMMessage(t, d.contract, evmExtraArgs, sourceChainSelector) var tokenAmounts []message_hasher.InternalAny2EVMTokenTransfer for _, rta := range ccipMsg.TokenAmounts { @@ -83,7 +87,7 @@ func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmE expectedHash, err := d.contract.Hash(&bind.CallOpts{Context: ctx}, evmMsg, ccipMsg.Header.OnRamp) require.NoError(t, err) - evmMsgHasher := NewMessageHasherV1(logger.Test(t)) + evmMsgHasher := NewMessageHasherV1(logger.Test(t), ExtraDataCodec) actualHash, err := evmMsgHasher.Hash(ctx, ccipMsg) require.NoError(t, err) @@ -91,19 +95,20 @@ func testHasherEVM2EVM(ctx context.Context, t *testing.T, d *testSetupData, evmE } type evmExtraArgs struct { - version string - gasLimit *big.Int - allowOOO bool + version string + gasLimit *big.Int + allowOOO bool + chainSelector uint64 } -func createEVM2EVMMessage(t *testing.T, messageHasher *message_hasher.MessageHasher, evmExtraArgs evmExtraArgs) cciptypes.Message { +func createEVM2EVMMessage(t *testing.T, messageHasher *message_hasher.MessageHasher, evmExtraArgs evmExtraArgs, sourceChainSelector uint64) cciptypes.Message { messageID := utils.RandomBytes32() sourceTokenData := make([]byte, rand.Intn(2048)) _, err := cryptorand.Read(sourceTokenData) require.NoError(t, err) - sourceChain := rand.Uint64() + sourceChain := sourceChainSelector seqNum := rand.Uint64() nonce := rand.Uint64() destChain := rand.Uint64() @@ -257,7 +262,7 @@ func TestMessagerHasher_againstRmnSharedVector(t *testing.T) { }, any2EVMMessage, common.LeftPadBytes(msg.Header.OnRamp, 32)) require.NoError(t, err) - h := NewMessageHasherV1(logger.Test(t)) + h := NewMessageHasherV1(logger.Test(t), ExtraDataCodec) msgH, err := h.Hash(tests.Context(t), msg) require.NoError(t, err) require.Equal(t, expectedMsgHash, msgH.String()) @@ -330,7 +335,7 @@ func TestMessagerHasher_againstRmnSharedVector(t *testing.T) { rmnMsgHash = "0xb6ea678f918293745bfb8db05d79dcf08986c7da3e302ac5f6782618a6f11967" ) - h := NewMessageHasherV1(logger.Test(t)) + h := NewMessageHasherV1(logger.Test(t), ExtraDataCodec) msgH, err := h.Hash(tests.Context(t), msg) require.NoError(t, err) @@ -355,7 +360,7 @@ func TestMessagerHasher_againstRmnSharedVector(t *testing.T) { err = json.Unmarshal(data, &msgs) require.NoError(t, err) - msgHasher := NewMessageHasherV1(logger.Test(t)) + msgHasher := NewMessageHasherV1(logger.Test(t), ExtraDataCodec) for _, msg := range msgs { any2EVMMessage := ccipMsgToAny2EVMMessage(t, msg) diff --git a/core/capabilities/ccip/ccipsolana/executecodec.go b/core/capabilities/ccip/ccipsolana/executecodec.go index dceb0d19a42..ad6efebf04f 100644 --- a/core/capabilities/ccip/ccipsolana/executecodec.go +++ b/core/capabilities/ccip/ccipsolana/executecodec.go @@ -10,6 +10,7 @@ import ( agbinary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" @@ -19,10 +20,13 @@ import ( // Compatible with: // - "OffRamp 1.6.0-dev" type ExecutePluginCodecV1 struct { + extraDataCodec common.ExtraDataCodec } -func NewExecutePluginCodecV1() *ExecutePluginCodecV1 { - return &ExecutePluginCodecV1{} +func NewExecutePluginCodecV1(extraDataCodec common.ExtraDataCodec) *ExecutePluginCodecV1 { + return &ExecutePluginCodecV1{ + extraDataCodec: extraDataCodec, + } } func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) { @@ -50,7 +54,12 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec return nil, fmt.Errorf("invalid destTokenAddress address: %v", tokenAmount.DestTokenAddress) } - destGasAmount, err := extractDestGasAmountFromMap(tokenAmount.DestExecDataDecoded) + destExecDataDecodedMap, err := e.extraDataCodec.DecodeTokenAmountDestExecData(tokenAmount.DestExecData, chainReport.SourceChainSelector) + if err != nil { + return nil, fmt.Errorf("failed to decode dest exec data: %w", err) + } + + destGasAmount, err := extractDestGasAmountFromMap(destExecDataDecodedMap) if err != nil { return nil, err } @@ -64,8 +73,13 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.Exec }) } + extraDataDecodecMap, err := e.extraDataCodec.DecodeExtraArgs(msg.ExtraArgs, chainReport.SourceChainSelector) + if err != nil { + return nil, fmt.Errorf("failed to decode extra args: %w", err) + } + var extraArgs ccip_offramp.Any2SVMRampExtraArgs - extraArgs, _, err := parseExtraArgsMapWithAccounts(msg.ExtraArgsDecoded) + extraArgs, _, err = parseExtraArgsMapWithAccounts(extraDataDecodecMap) if err != nil { return nil, fmt.Errorf("invalid extra args map: %w", err) } @@ -192,25 +206,23 @@ func (e *ExecutePluginCodecV1) Decode(ctx context.Context, encodedReport []byte) } func extractDestGasAmountFromMap(input map[string]any) (uint32, error) { - var out uint32 - - // Iterate through the expected fields in the struct + // Search for the gas fields for fieldName, fieldValue := range input { lowercase := strings.ToLower(fieldName) switch lowercase { case "destgasamount": // Expect uint32 if v, ok := fieldValue.(uint32); ok { - out = v + return v, nil } else { - return out, errors.New("invalid type for destgasamount, expected uint32") + return 0, errors.New("invalid type for destgasamount, expected uint32") } default: - return out, errors.New("invalid token message, dest gas amount not found in the DestExecDataDecoded map") + } } - return out, nil + return 0, errors.New("invalid token message, dest gas amount not found in the DestExecDataDecoded map") } // Ensure ExecutePluginCodec implements the ExecutePluginCodec interface diff --git a/core/capabilities/ccip/ccipsolana/executecodec_test.go b/core/capabilities/ccip/ccipsolana/executecodec_test.go index 833d83126be..927b28f6c04 100644 --- a/core/capabilities/ccip/ccipsolana/executecodec_test.go +++ b/core/capabilities/ccip/ccipsolana/executecodec_test.go @@ -9,6 +9,11 @@ import ( agbinary "github.com/gagliardetto/binary" solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks" + + "github.com/smartcontractkit/chainlink-ccip/mocks/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" @@ -146,10 +151,18 @@ func TestExecutePluginCodecV1(t *testing.T) { } ctx := testutils.Context(t) + mockExtraDataCodec := &mocks.ExtraDataCodec{} + mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{ + "destGasAmount": uint32(10), + }, nil) + mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{ + "ComputeUnits": uint32(1000), + "accountIsWritableBitmap": uint64(2), + }, nil) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cd := NewExecutePluginCodecV1() + cd := NewExecutePluginCodecV1(mockExtraDataCodec) report := tc.report(randomExecuteReport(t, tc.chainSelector)) bytes, err := cd.Encode(ctx, report) if tc.expErr { @@ -181,6 +194,14 @@ func TestExecutePluginCodecV1(t *testing.T) { } func Test_DecodingExecuteReport(t *testing.T) { + mockExtraDataCodec := ccipocr3.NewMockExtraDataCodec(t) + mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{ + "destGasAmount": uint32(10), + }, nil) + mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{ + "ComputeUnits": uint32(1000), + "accountIsWritableBitmap": uint64(2), + }, nil) t.Run("decode on-chain execute report", func(t *testing.T) { chainSel := cciptypes.ChainSelector(rand.Uint64()) onRampAddr, err := solanago.NewRandomPrivateKey() @@ -222,7 +243,7 @@ func Test_DecodingExecuteReport(t *testing.T) { err = onChainReport.MarshalWithEncoder(encoder) require.NoError(t, err) - executeCodec := NewExecutePluginCodecV1() + executeCodec := NewExecutePluginCodecV1(mockExtraDataCodec) decode, err := executeCodec.Decode(testutils.Context(t), buf.Bytes()) require.NoError(t, err) @@ -238,7 +259,7 @@ func Test_DecodingExecuteReport(t *testing.T) { t.Run("decode Borsh encoded execute report", func(t *testing.T) { ocrReport := randomExecuteReport(t, 124615329519749607) - cd := NewExecutePluginCodecV1() + cd := NewExecutePluginCodecV1(mockExtraDataCodec) encodedReport, err := cd.Encode(testutils.Context(t), ocrReport) require.NoError(t, err) diff --git a/core/capabilities/ccip/ccipsolana/extradatadecoder.go b/core/capabilities/ccip/ccipsolana/extradatadecoder.go index b46692d0d98..616a65aa462 100644 --- a/core/capabilities/ccip/ccipsolana/extradatadecoder.go +++ b/core/capabilities/ccip/ccipsolana/extradatadecoder.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" agbinary "github.com/gagliardetto/binary" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" ) @@ -26,8 +27,11 @@ var ( evmExtraArgsV2Tag = hexutil.MustDecode("0x181dcf10") ) -// DecodeExtraArgsToMap is a helper function for converting Borsh encoded extra args bytes into map[string]any, which will be saved in ocr report.message.ExtraArgsDecoded -func DecodeExtraArgsToMap(extraArgs []byte) (map[string]any, error) { +// ExtraDataDecoder is a helper struct for decoding extra data +type ExtraDataDecoder struct{} + +// DecodeExtraArgsToMap is a helper function for converting Borsh encoded extra args bytes into map[string]any +func (d ExtraDataDecoder) DecodeExtraArgsToMap(extraArgs cciptypes.Bytes) (map[string]any, error) { if len(extraArgs) < 4 { return nil, fmt.Errorf("extra args too short: %d, should be at least 4 (i.e the extraArgs tag)", len(extraArgs)) } @@ -67,7 +71,8 @@ func DecodeExtraArgsToMap(extraArgs []byte) (map[string]any, error) { return outputMap, nil } -func DecodeDestExecDataToMap(destExecData []byte) (map[string]any, error) { +// DecodeDestExecDataToMap is a helper function for converting dest exec data bytes into map[string]any +func (d ExtraDataDecoder) DecodeDestExecDataToMap(destExecData cciptypes.Bytes) (map[string]any, error) { return map[string]interface{}{ svmDestExecDataKey: bytesToUint32LE(destExecData), }, nil diff --git a/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go b/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go index a4de8331d24..56de78fd412 100644 --- a/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go +++ b/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go @@ -15,11 +15,12 @@ import ( ) func Test_decodeExtraArgs(t *testing.T) { + extraDataDecoder := &ExtraDataDecoder{} t.Run("decode dest exec data into map svm", func(t *testing.T) { destGasAmount := uint32(10000) encoded := make([]byte, 4) binary.LittleEndian.PutUint32(encoded, destGasAmount) - output, err := DecodeDestExecDataToMap(encoded) + output, err := extraDataDecoder.DecodeDestExecDataToMap(encoded) require.NoError(t, err) decoded, exist := output[svmDestExecDataKey] @@ -46,7 +47,7 @@ func Test_decodeExtraArgs(t *testing.T) { encoder := agbinary.NewBorshEncoder(&buf) err := extraArgs.MarshalWithEncoder(encoder) require.NoError(t, err) - output, err := DecodeExtraArgsToMap(append(svmExtraArgsV1Tag, buf.Bytes()...)) + output, err := extraDataDecoder.DecodeExtraArgsToMap(append(svmExtraArgsV1Tag, buf.Bytes()...)) require.NoError(t, err) require.Len(t, output, 5) @@ -74,7 +75,7 @@ func Test_decodeExtraArgs(t *testing.T) { err := extraArgs.MarshalWithEncoder(encoder) require.NoError(t, err) - output, err := DecodeExtraArgsToMap(append(evmExtraArgsV2Tag, buf.Bytes()...)) + output, err := extraDataDecoder.DecodeExtraArgsToMap(append(evmExtraArgsV2Tag, buf.Bytes()...)) require.NoError(t, err) require.Len(t, output, 2) diff --git a/core/capabilities/ccip/ccipsolana/msghasher.go b/core/capabilities/ccip/ccipsolana/msghasher.go index 7c12de91026..54f5a0eb159 100644 --- a/core/capabilities/ccip/ccipsolana/msghasher.go +++ b/core/capabilities/ccip/ccipsolana/msghasher.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip" @@ -20,12 +21,14 @@ import ( // Compatible with: // - "OnRamp 1.6.0-dev" type MessageHasherV1 struct { - lggr logger.Logger + lggr logger.Logger + extraDataCodec common.ExtraDataCodec } -func NewMessageHasherV1(lggr logger.Logger) *MessageHasherV1 { +func NewMessageHasherV1(lggr logger.Logger, extraDataCodec cciptypes.ExtraDataCodec) *MessageHasherV1 { return &MessageHasherV1{ - lggr: lggr, + lggr: lggr, + extraDataCodec: extraDataCodec, } } @@ -45,7 +48,12 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty anyToSolanaMessage.Sender = msg.Sender anyToSolanaMessage.Data = msg.Data for _, ta := range msg.TokenAmounts { - destGasAmount, err := extractDestGasAmountFromMap(ta.DestExecDataDecoded) + destExecDataDecodedMap, err := h.extraDataCodec.DecodeTokenAmountDestExecData(ta.DestExecData, msg.Header.SourceChainSelector) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to decode dest exec data: %w", err) + } + + destGasAmount, err := extractDestGasAmountFromMap(destExecDataDecodedMap) if err != nil { return [32]byte{}, err } @@ -59,9 +67,13 @@ func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (ccipty }) } - var err error + extraDataDecodecMap, err := h.extraDataCodec.DecodeExtraArgs(msg.ExtraArgs, msg.Header.SourceChainSelector) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to decode extra args: %w", err) + } + var msgAccounts []solana.PublicKey - anyToSolanaMessage.ExtraArgs, msgAccounts, err = parseExtraArgsMapWithAccounts(msg.ExtraArgsDecoded) + anyToSolanaMessage.ExtraArgs, msgAccounts, err = parseExtraArgsMapWithAccounts(extraDataDecodecMap) if err != nil { return [32]byte{}, fmt.Errorf("failed to decode ExtraArgs: %w", err) } @@ -107,7 +119,7 @@ func parseExtraArgsMapWithAccounts(input map[string]any) (ccip_offramp.Any2SVMRa return out, accounts, errors.New("invalid type for Accounts, expected [][32]byte") } default: - // no error here, aswe only need the keys to construct SVMExtraArgs, other keys can be skipped without + // no error here, as we only need the keys to construct SVMExtraArgs, other keys can be skipped without // return errors because there's no guarantee SVMExtraArgs will match with SVMExtraArgsV1 } } diff --git a/core/capabilities/ccip/ccipsolana/msghasher_test.go b/core/capabilities/ccip/ccipsolana/msghasher_test.go index c078b124b5b..8e838689cb5 100644 --- a/core/capabilities/ccip/ccipsolana/msghasher_test.go +++ b/core/capabilities/ccip/ccipsolana/msghasher_test.go @@ -9,8 +9,11 @@ import ( agbinary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip" @@ -25,7 +28,20 @@ import ( func TestMessageHasher_Any2Solana(t *testing.T) { any2AnyMsg, any2SolanaMsg, msgAccounts := createAny2SolanaMessages(t) - msgHasher := NewMessageHasherV1(logger.Test(t)) + mockExtraDataCodec := &mocks.ExtraDataCodec{} + mockExtraDataCodec.On("DecodeTokenAmountDestExecData", mock.Anything, mock.Anything).Return(map[string]any{ + "destGasAmount": uint32(10), + }, nil) + mockExtraDataCodec.On("DecodeExtraArgs", mock.Anything, mock.Anything).Return(map[string]any{ + "ComputeUnits": uint32(1000), + "AccountIsWritableBitmap": uint64(10), + "Accounts": [][32]byte{ + [32]byte(config.CcipLogicReceiver.Bytes()), + [32]byte(config.ReceiverTargetAccountPDA.Bytes()), + [32]byte(solana.SystemProgramID.Bytes()), + }, + }, nil) + msgHasher := NewMessageHasherV1(logger.Test(t), mockExtraDataCodec) actualHash, err := msgHasher.Hash(testutils.Context(t), any2AnyMsg) require.NoError(t, err) expectedHash, err := ccip.HashAnyToSVMMessage(any2SolanaMsg, any2AnyMsg.Header.OnRamp, msgAccounts) diff --git a/core/capabilities/ccip/common/extradatacodec.go b/core/capabilities/ccip/common/extradatacodec.go index 47146ddd5b6..a7e3376035c 100644 --- a/core/capabilities/ccip/common/extradatacodec.go +++ b/core/capabilities/ccip/common/extradatacodec.go @@ -4,19 +4,53 @@ import ( "fmt" chainsel "github.com/smartcontractkit/chain-selectors" - cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana" ) -type ExtraDataCodec struct{} +// ExtraDataCodec is an interface for decoding extra args and dest exec data into a chain-agnostic map[string]any representation +type ExtraDataCodec interface { + // DecodeExtraArgs reformat bytes into a chain agnostic map[string]any representation for extra args + DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) + // DecodeTokenAmountDestExecData reformat bytes to chain-agnostic map[string]any for tokenAmount DestExecData field + DecodeTokenAmountDestExecData(destExecData cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) +} + +// ExtraDataDecoder is an interface for decoding extra args and dest exec data into a map[string]any representation +type ExtraDataDecoder interface { + DecodeExtraArgsToMap(extraArgs cciptypes.Bytes) (map[string]any, error) + DecodeDestExecDataToMap(destExecData cciptypes.Bytes) (map[string]any, error) +} + +// RealExtraDataCodec is a concrete implementation of ExtraDataCodec +type RealExtraDataCodec struct { + EVMExtraDataDecoder ExtraDataDecoder + SolanaExtraDataDecoder ExtraDataDecoder +} + +// ExtraDataCodecParams is a struct that holds the parameters for creating a RealExtraDataCodec +type ExtraDataCodecParams struct { + evmExtraDataDecoder ExtraDataDecoder + solanaExtraDataDecoder ExtraDataDecoder +} + +// NewExtraDataCodecParams is a constructor for ExtraDataCodecParams +func NewExtraDataCodecParams(evmDecoder ExtraDataDecoder, solanaDecoder ExtraDataDecoder) ExtraDataCodecParams { + return ExtraDataCodecParams{ + evmExtraDataDecoder: evmDecoder, + solanaExtraDataDecoder: solanaDecoder, + } +} -func NewExtraDataCodec() ExtraDataCodec { - return ExtraDataCodec{} +// NewExtraDataCodec is a constructor for RealExtraDataCodec +func NewExtraDataCodec(params ExtraDataCodecParams) RealExtraDataCodec { + return RealExtraDataCodec{ + EVMExtraDataDecoder: params.evmExtraDataDecoder, + SolanaExtraDataDecoder: params.solanaExtraDataDecoder, + } } -func (c ExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) { +// DecodeExtraArgs reformats bytes into a chain agnostic map[string]any representation for extra args +func (c RealExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) { if len(extraArgs) == 0 { // return empty map if extraArgs is empty return nil, nil @@ -29,17 +63,18 @@ func (c ExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSe switch family { case chainsel.FamilyEVM: - return ccipevm.DecodeExtraArgsToMap(extraArgs) + return c.EVMExtraDataDecoder.DecodeExtraArgsToMap(extraArgs) case chainsel.FamilySolana: - return ccipsolana.DecodeExtraArgsToMap(extraArgs) + return c.SolanaExtraDataDecoder.DecodeExtraArgsToMap(extraArgs) default: return nil, fmt.Errorf("unsupported family for extra args type %s", family) } } -func (c ExtraDataCodec) DecodeTokenAmountDestExecData(destExecData cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) { +// DecodeTokenAmountDestExecData reformats bytes to chain-agnostic map[string]any for tokenAmount DestExecData field +func (c RealExtraDataCodec) DecodeTokenAmountDestExecData(destExecData cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) { if len(destExecData) == 0 { // return empty map if destExecData is empty return nil, nil @@ -52,10 +87,10 @@ func (c ExtraDataCodec) DecodeTokenAmountDestExecData(destExecData cciptypes.Byt switch family { case chainsel.FamilyEVM: - return ccipevm.DecodeDestExecDataToMap(destExecData) + return c.EVMExtraDataDecoder.DecodeDestExecDataToMap(destExecData) case chainsel.FamilySolana: - return ccipsolana.DecodeDestExecDataToMap(destExecData) + return c.SolanaExtraDataDecoder.DecodeDestExecDataToMap(destExecData) default: return nil, fmt.Errorf("unsupported family for extra args type %s", family) diff --git a/core/capabilities/ccip/common/mocks/extra_data_codec.go b/core/capabilities/ccip/common/mocks/extra_data_codec.go new file mode 100644 index 00000000000..d52d2bd8497 --- /dev/null +++ b/core/capabilities/ccip/common/mocks/extra_data_codec.go @@ -0,0 +1,154 @@ +// Code generated by mockery v2.50.0. DO NOT EDIT. + +package mocks + +import ( + ccipocr3 "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + + mock "github.com/stretchr/testify/mock" +) + +// ExtraDataCodec is an autogenerated mock type for the ExtraDataCodec type +type ExtraDataCodec struct { + mock.Mock +} + +type ExtraDataCodec_Expecter struct { + mock *mock.Mock +} + +func (_m *ExtraDataCodec) EXPECT() *ExtraDataCodec_Expecter { + return &ExtraDataCodec_Expecter{mock: &_m.Mock} +} + +// DecodeExtraArgs provides a mock function with given fields: extraArgs, sourceChainSelector +func (_m *ExtraDataCodec) DecodeExtraArgs(extraArgs ccipocr3.Bytes, sourceChainSelector ccipocr3.ChainSelector) (map[string]interface{}, error) { + ret := _m.Called(extraArgs, sourceChainSelector) + + if len(ret) == 0 { + panic("no return value specified for DecodeExtraArgs") + } + + var r0 map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes, ccipocr3.ChainSelector) (map[string]interface{}, error)); ok { + return rf(extraArgs, sourceChainSelector) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes, ccipocr3.ChainSelector) map[string]interface{}); ok { + r0 = rf(extraArgs, sourceChainSelector) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes, ccipocr3.ChainSelector) error); ok { + r1 = rf(extraArgs, sourceChainSelector) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExtraDataCodec_DecodeExtraArgs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DecodeExtraArgs' +type ExtraDataCodec_DecodeExtraArgs_Call struct { + *mock.Call +} + +// DecodeExtraArgs is a helper method to define mock.On call +// - extraArgs ccipocr3.Bytes +// - sourceChainSelector ccipocr3.ChainSelector +func (_e *ExtraDataCodec_Expecter) DecodeExtraArgs(extraArgs interface{}, sourceChainSelector interface{}) *ExtraDataCodec_DecodeExtraArgs_Call { + return &ExtraDataCodec_DecodeExtraArgs_Call{Call: _e.mock.On("DecodeExtraArgs", extraArgs, sourceChainSelector)} +} + +func (_c *ExtraDataCodec_DecodeExtraArgs_Call) Run(run func(extraArgs ccipocr3.Bytes, sourceChainSelector ccipocr3.ChainSelector)) *ExtraDataCodec_DecodeExtraArgs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes), args[1].(ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *ExtraDataCodec_DecodeExtraArgs_Call) Return(_a0 map[string]interface{}, _a1 error) *ExtraDataCodec_DecodeExtraArgs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ExtraDataCodec_DecodeExtraArgs_Call) RunAndReturn(run func(ccipocr3.Bytes, ccipocr3.ChainSelector) (map[string]interface{}, error)) *ExtraDataCodec_DecodeExtraArgs_Call { + _c.Call.Return(run) + return _c +} + +// DecodeTokenAmountDestExecData provides a mock function with given fields: destExecData, sourceChainSelector +func (_m *ExtraDataCodec) DecodeTokenAmountDestExecData(destExecData ccipocr3.Bytes, sourceChainSelector ccipocr3.ChainSelector) (map[string]interface{}, error) { + ret := _m.Called(destExecData, sourceChainSelector) + + if len(ret) == 0 { + panic("no return value specified for DecodeTokenAmountDestExecData") + } + + var r0 map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes, ccipocr3.ChainSelector) (map[string]interface{}, error)); ok { + return rf(destExecData, sourceChainSelector) + } + if rf, ok := ret.Get(0).(func(ccipocr3.Bytes, ccipocr3.ChainSelector) map[string]interface{}); ok { + r0 = rf(destExecData, sourceChainSelector) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(ccipocr3.Bytes, ccipocr3.ChainSelector) error); ok { + r1 = rf(destExecData, sourceChainSelector) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExtraDataCodec_DecodeTokenAmountDestExecData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DecodeTokenAmountDestExecData' +type ExtraDataCodec_DecodeTokenAmountDestExecData_Call struct { + *mock.Call +} + +// DecodeTokenAmountDestExecData is a helper method to define mock.On call +// - destExecData ccipocr3.Bytes +// - sourceChainSelector ccipocr3.ChainSelector +func (_e *ExtraDataCodec_Expecter) DecodeTokenAmountDestExecData(destExecData interface{}, sourceChainSelector interface{}) *ExtraDataCodec_DecodeTokenAmountDestExecData_Call { + return &ExtraDataCodec_DecodeTokenAmountDestExecData_Call{Call: _e.mock.On("DecodeTokenAmountDestExecData", destExecData, sourceChainSelector)} +} + +func (_c *ExtraDataCodec_DecodeTokenAmountDestExecData_Call) Run(run func(destExecData ccipocr3.Bytes, sourceChainSelector ccipocr3.ChainSelector)) *ExtraDataCodec_DecodeTokenAmountDestExecData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(ccipocr3.Bytes), args[1].(ccipocr3.ChainSelector)) + }) + return _c +} + +func (_c *ExtraDataCodec_DecodeTokenAmountDestExecData_Call) Return(_a0 map[string]interface{}, _a1 error) *ExtraDataCodec_DecodeTokenAmountDestExecData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ExtraDataCodec_DecodeTokenAmountDestExecData_Call) RunAndReturn(run func(ccipocr3.Bytes, ccipocr3.ChainSelector) (map[string]interface{}, error)) *ExtraDataCodec_DecodeTokenAmountDestExecData_Call { + _c.Call.Return(run) + return _c +} + +// NewExtraDataCodec creates a new instance of ExtraDataCodec. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewExtraDataCodec(t interface { + mock.TestingT + Cleanup(func()) +}) *ExtraDataCodec { + mock := &ExtraDataCodec{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/ccip/oraclecreator/plugin.go b/core/capabilities/ccip/oraclecreator/plugin.go index e9cab490893..803b5fed2e5 100644 --- a/core/capabilities/ccip/oraclecreator/plugin.go +++ b/core/capabilities/ccip/oraclecreator/plugin.go @@ -53,12 +53,53 @@ import ( ) var _ cctypes.OracleCreator = &pluginOracleCreator{} +var extraDataCodec = ccipcommon.NewExtraDataCodec( + ccipcommon.NewExtraDataCodecParams( + ccipevm.ExtraDataDecoder{}, + ccipsolana.ExtraDataDecoder{}, + ), +) + +var plugins = map[string]plugin{ + chainsel.FamilyEVM: { + CommitPluginCodec: ccipevm.NewCommitPluginCodecV1(), + ExecutePluginCodec: ccipevm.NewExecutePluginCodecV1(extraDataCodec), + ExtraArgsCodec: extraDataCodec, + MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { + return ccipevm.NewMessageHasherV1(lggr, extraDataCodec) + }, + TokenDataEncoder: ccipevm.NewEVMTokenDataEncoder(), + GasEstimateProvider: ccipevm.NewGasEstimateProvider(), + RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return ccipevm.NewEVMRMNCrypto(lggr) }, + }, + chainsel.FamilySolana: { + CommitPluginCodec: ccipsolana.NewCommitPluginCodecV1(), + ExecutePluginCodec: ccipsolana.NewExecutePluginCodecV1(extraDataCodec), + ExtraArgsCodec: extraDataCodec, + MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { + return ccipsolana.NewMessageHasherV1(lggr, extraDataCodec) + }, + TokenDataEncoder: ccipsolana.NewSolanaTokenDataEncoder(), + GasEstimateProvider: ccipsolana.NewGasEstimateProvider(), + RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return nil }, + }, +} const ( defaultCommitGasLimit = 500_000 defaultExecGasLimit = 6_500_000 ) +type plugin struct { + CommitPluginCodec cciptypes.CommitPluginCodec + ExecutePluginCodec cciptypes.ExecutePluginCodec + ExtraArgsCodec ccipcommon.ExtraDataCodec + MessageHasher func(lggr logger.Logger) cciptypes.MessageHasher + TokenDataEncoder cciptypes.TokenDataEncoder + GasEstimateProvider cciptypes.EstimateProvider + RMNCrypto func(lggr logger.Logger) cciptypes.RMNCrypto +} + // pluginOracleCreator creates oracles that reference plugins running // in the same process as the chainlink node, i.e not LOOPPs. type pluginOracleCreator struct { @@ -238,37 +279,6 @@ func encodeOffRampAddr(addr []byte, chainFamily string, checkSum bool) string { return offRampAddr } -type plugin struct { - CommitPluginCodec cciptypes.CommitPluginCodec - ExecutePluginCodec cciptypes.ExecutePluginCodec - ExtraArgsCodec cciptypes.ExtraDataCodec - MessageHasher func(lggr logger.Logger) cciptypes.MessageHasher - TokenDataEncoder cciptypes.TokenDataEncoder - GasEstimateProvider cciptypes.EstimateProvider - RMNCrypto func(lggr logger.Logger) cciptypes.RMNCrypto -} - -var plugins = map[string]plugin{ - chainsel.FamilyEVM: { - CommitPluginCodec: ccipevm.NewCommitPluginCodecV1(), - ExecutePluginCodec: ccipevm.NewExecutePluginCodecV1(), - ExtraArgsCodec: ccipcommon.NewExtraDataCodec(), - MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { return ccipevm.NewMessageHasherV1(lggr) }, - TokenDataEncoder: ccipevm.NewEVMTokenDataEncoder(), - GasEstimateProvider: ccipevm.NewGasEstimateProvider(), - RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return ccipevm.NewEVMRMNCrypto(lggr) }, - }, - chainsel.FamilySolana: { - CommitPluginCodec: ccipsolana.NewCommitPluginCodecV1(), - ExecutePluginCodec: ccipsolana.NewExecutePluginCodecV1(), - ExtraArgsCodec: ccipcommon.NewExtraDataCodec(), - MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { return ccipsolana.NewMessageHasherV1(lggr) }, - TokenDataEncoder: ccipsolana.NewSolanaTokenDataEncoder(), - GasEstimateProvider: ccipsolana.NewGasEstimateProvider(), - RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return nil }, - }, -} - func (i *pluginOracleCreator) createFactoryAndTransmitter( donID uint32, config cctypes.OCR3ConfigWithMeta, diff --git a/integration-tests/smoke/ccip/ccip_reader_test.go b/integration-tests/smoke/ccip/ccip_reader_test.go index 3ca3ef73316..d68242c1c96 100644 --- a/integration-tests/smoke/ccip/ccip_reader_test.go +++ b/integration-tests/smoke/ccip/ccip_reader_test.go @@ -19,6 +19,9 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana" + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" "github.com/smartcontractkit/chainlink/deployment/environment/memory" @@ -294,7 +297,7 @@ func TestCCIPReader_GetRMNRemoteConfig(t *testing.T) { nil, chainD, rmnRemoteAddr.Bytes(), - ccipcommon.NewExtraDataCodec(), + ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ccipevm.ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})), ) exp, err := rmnRemote.GetVersionedConfig(&bind.CallOpts{ @@ -418,7 +421,7 @@ func TestCCIPReader_GetOffRampConfigDigest(t *testing.T) { nil, chainD, addr.Bytes(), - ccipcommon.NewExtraDataCodec(), + ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ccipevm.ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})), ) ccipReaderCommitDigest, err := reader.GetOffRampConfigDigest(ctx, consts.PluginTypeCommit) @@ -1545,7 +1548,7 @@ func testSetupRealContracts( contractReaders[chain] = cr } contractWriters := make(map[cciptypes.ChainSelector]types.ContractWriter) - edc := ccipcommon.NewExtraDataCodec() + edc := ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ccipevm.ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})) reader := ccipreaderpkg.NewCCIPReaderWithExtendedContractReaders(ctx, lggr, contractReaders, contractWriters, cciptypes.ChainSelector(destChain), nil, edc) return reader @@ -1661,7 +1664,7 @@ func testSetup( contractReaders[chain] = cr } contractWriters := make(map[cciptypes.ChainSelector]types.ContractWriter) - edc := ccipcommon.NewExtraDataCodec() + edc := ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ccipevm.ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})) reader := ccipreaderpkg.NewCCIPReaderWithExtendedContractReaders(ctx, lggr, contractReaders, contractWriters, params.DestChain, nil, edc) t.Cleanup(func() {