From f1faab00776d6508c5b6de69f8994555458264a1 Mon Sep 17 00:00:00 2001 From: Joe Huang Date: Fri, 14 Feb 2025 14:56:08 -0600 Subject: [PATCH] support Solana->EVM --- .../capabilities/ccip/ccipevm/executecodec.go | 19 +++++- .../ccip/ccipevm/executecodec_test.go | 34 +++++++---- core/capabilities/ccip/ccipevm/msghasher.go | 61 +++++++++++++++++-- .../ccip/ccipevm/msghasher_test.go | 35 ++++++----- .../ccip/ccipsolana/executecodec.go | 12 ++-- .../ccip/ccipsolana/executecodec_test.go | 3 +- .../capabilities/ccip/ccipsolana/msghasher.go | 2 +- .../ccip/ccipsolana/msghasher_test.go | 3 +- .../capabilities/ccip/oraclecreator/plugin.go | 2 +- 9 files changed, 124 insertions(+), 47 deletions(-) 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..54b7422a6a8 100644 --- a/core/capabilities/ccip/ccipevm/executecodec_test.go +++ b/core/capabilities/ccip/ccipevm/executecodec_test.go @@ -10,6 +10,8 @@ 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/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,7 +27,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) cciptypes.ExecutePluginReport { const numChainReports = 10 const msgsPerReport = 10 const numTokensPerMsg = 3 @@ -82,7 +84,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 +97,19 @@ var randomExecuteReport = func(t *testing.T, d *testSetupData) cciptypes.Execute func TestExecutePluginCodecV1(t *testing.T) { d := testSetup(t) + ExtraData := ccipcommon.NewExtraDataCodec(ccipcommon.NewExtraDataCodecParams(ExtraDataDecoder{}, ccipsolana.ExtraDataDecoder{})) 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 }{ { - 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 }, { name: "reports have empty msgs", @@ -113,7 +118,8 @@ func TestExecutePluginCodecV1(t *testing.T) { report.ChainReports[4].Messages = []cciptypes.Message{} return report }, - expErr: false, + expErr: false, + chainSelector: 5009297550715157269, // ETH mainnet chain selector }, { name: "reports have empty offchain token data", @@ -122,7 +128,8 @@ func TestExecutePluginCodecV1(t *testing.T) { report.ChainReports[4].OffchainTokenData[1] = [][]byte{} return report }, - expErr: false, + expErr: false, + chainSelector: 5009297550715157269, // ETH mainnet chain selector }, } @@ -141,8 +148,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(ExtraData) + report := tc.report(randomExecuteReport(t, d, tc.chainSelector)) bytes, err := codec.Encode(ctx, report) if tc.expErr { assert.Error(t, err) @@ -183,6 +190,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 +209,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/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 7c0986e4efd..ad6efebf04f 100644 --- a/core/capabilities/ccip/ccipsolana/executecodec.go +++ b/core/capabilities/ccip/ccipsolana/executecodec.go @@ -206,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 47ac394969e..927b28f6c04 100644 --- a/core/capabilities/ccip/ccipsolana/executecodec_test.go +++ b/core/capabilities/ccip/ccipsolana/executecodec_test.go @@ -9,9 +9,10 @@ import ( agbinary "github.com/gagliardetto/binary" solanago "github.com/gagliardetto/solana-go" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks" "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" diff --git a/core/capabilities/ccip/ccipsolana/msghasher.go b/core/capabilities/ccip/ccipsolana/msghasher.go index eef0c595dc3..54f5a0eb159 100644 --- a/core/capabilities/ccip/ccipsolana/msghasher.go +++ b/core/capabilities/ccip/ccipsolana/msghasher.go @@ -119,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 6374f288c4c..8e838689cb5 100644 --- a/core/capabilities/ccip/ccipsolana/msghasher_test.go +++ b/core/capabilities/ccip/ccipsolana/msghasher_test.go @@ -9,10 +9,11 @@ import ( agbinary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common/mocks" "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" diff --git a/core/capabilities/ccip/oraclecreator/plugin.go b/core/capabilities/ccip/oraclecreator/plugin.go index 850d8559ffb..21ebc6029e7 100644 --- a/core/capabilities/ccip/oraclecreator/plugin.go +++ b/core/capabilities/ccip/oraclecreator/plugin.go @@ -63,7 +63,7 @@ var extraDataCodec = ccipcommon.NewExtraDataCodec( var plugins = map[string]plugin{ chainsel.FamilyEVM: { CommitPluginCodec: ccipevm.NewCommitPluginCodecV1(), - ExecutePluginCodec: ccipevm.NewExecutePluginCodecV1(), + ExecutePluginCodec: ccipevm.NewExecutePluginCodecV1(extraDataCodec), ExtraArgsCodec: extraDataCodec, MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { return ccipevm.NewMessageHasherV1(lggr) }, TokenDataEncoder: ccipevm.NewEVMTokenDataEncoder(),