From f8a40259939ff3e2cb75d50051019efd38116342 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Wed, 5 Aug 2026 16:46:57 -0400 Subject: [PATCH 1/2] add sol sui messaging support (#23341) * add sol sui support * fix * fix --- .../capabilities/ccip/ccipsui/executecodec.go | 2 +- core/capabilities/ccip/ccipsui/msghasher.go | 32 ++++++- .../ccip/ccipsui/msghasher_test.go | 83 +++++++++++++++++-- 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/core/capabilities/ccip/ccipsui/executecodec.go b/core/capabilities/ccip/ccipsui/executecodec.go index b85428cc325..27f7bc3336f 100644 --- a/core/capabilities/ccip/ccipsui/executecodec.go +++ b/core/capabilities/ccip/ccipsui/executecodec.go @@ -89,7 +89,7 @@ func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report ccipocr3.Execu if err != nil { return nil, fmt.Errorf("failed to decode ExtraArgs: %w", err) } - gasLimit, tokenReceiver, err := parseExtraDataMap(decodedExtraArgsMap) // Use a helper to extract the gas limit + gasLimit, tokenReceiver, err := parseExtraDataMap(decodedExtraArgsMap, chainReport.SourceChainSelector) // Use a helper to extract the gas limit if err != nil { return nil, fmt.Errorf("failed to extract values from decoded ExtraArgs map: %w", err) } diff --git a/core/capabilities/ccip/ccipsui/msghasher.go b/core/capabilities/ccip/ccipsui/msghasher.go index a5411c205f1..558a98e6405 100644 --- a/core/capabilities/ccip/ccipsui/msghasher.go +++ b/core/capabilities/ccip/ccipsui/msghasher.go @@ -13,10 +13,10 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" "github.com/smartcontractkit/chainlink-common/pkg/logger" ccipocr3common "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" - "github.com/smartcontractkit/chainlink-evm/pkg/utils" ) @@ -116,7 +116,7 @@ func (h *MessageHasherV1) Hash(ctx context.Context, msg ccipocr3common.Message) return [32]byte{}, err } - gasLimit, tokenReceiver, err := parseExtraDataMap(decodedExtraArgsMap) + gasLimit, tokenReceiver, err := parseExtraDataMap(decodedExtraArgsMap, msg.Header.SourceChainSelector) if err != nil { return [32]byte{}, fmt.Errorf("decode extra args to get gas limit: %w", err) } @@ -288,8 +288,17 @@ func encodeBytes(b []byte) []byte { return result } -func parseExtraDataMap(input map[string]any) (*big.Int, [32]byte, error) { +func parseExtraDataMap(input map[string]any, sourceChainSelector ccipocr3common.ChainSelector) (*big.Int, [32]byte, error) { + // gasLimit key differs by source family: + // - SuiExtraArgsV1 (EVM/Sui sources, ABI/BCS) uses "gasLimit" (lowercase). + // - Solana GenericExtraArgsV2 (Borsh, tag 0x181dcf10) uses "GasLimit" — the Borsh + // struct field name emitted by ccipsolana.ExtraDataDecoder. Solana sends + // GenericExtraArgsV2 to non-SVM dests (the Solana fee-quoter has no SuiExtraArgsV1). + // Casing tolerance here is only to locate the gas limit; it carries no behavioral risk. outputGas, ok := input["gasLimit"] + if !ok { + outputGas, ok = input["GasLimit"] + } if !ok { return nil, [32]byte{}, errors.New("gas limit not found in extra data map") } @@ -298,9 +307,24 @@ func parseExtraDataMap(input map[string]any) (*big.Int, [32]byte, error) { return nil, [32]byte{}, errors.New("gas limit not a *big.Int") } + // Detect a Solana source by chain family (covers devnet, mainnet, any future Solana chain). + // A lookup error is treated as non-Solana so a bad selector never silently defaults. + sourceFamily, err := chainsel.GetSelectorFamily(uint64(sourceChainSelector)) + isSolanaSource := err == nil && sourceFamily == chainsel.FamilySolana + + // tokenReceiver is carried by SuiExtraArgsV1 but NOT by Solana GenericExtraArgsV2. + // For Solana→Sui the on-chain Solana message cannot convey a separate token receiver, so + // the off-chain Any2SuiRampMessage is built with tokenReceiver = 0 (matching the EVM→Sui + // message-only convention). Default to zero ONLY for Solana sources: SuiExtraArgsV1 + // (EVM/Sui sources) must carry tokenReceiver, so a missing key there is a malformed/partial + // decode and must error rather than silently produce a wrong hash. Token Solana→Sui + // transfers are not supported by this path (Solana has no SuiExtraArgsV1). tokenReceiver, ok := input["tokenReceiver"] if !ok { - return nil, [32]byte{}, errors.New("token receiver not found in extra data map") + if !isSolanaSource { + return nil, [32]byte{}, errors.New("token receiver not found in extra data map") + } + return outputGasInt, [32]byte{}, nil } tokenReceiverBytes := [32]byte{} diff --git a/core/capabilities/ccip/ccipsui/msghasher_test.go b/core/capabilities/ccip/ccipsui/msghasher_test.go index cc7fcfd54ca..768bb98737a 100644 --- a/core/capabilities/ccip/ccipsui/msghasher_test.go +++ b/core/capabilities/ccip/ccipsui/msghasher_test.go @@ -6,15 +6,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + chainsel "github.com/smartcontractkit/chain-selectors" + ccipocr3common "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" ) func TestParseExtraDataMap(t *testing.T) { tokenReceiverExample := [32]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20} + solanaSelector := ccipocr3common.ChainSelector(chainsel.SOLANA_DEVNET.Selector) + nonSolanaSelector := ccipocr3common.ChainSelector(chainsel.SUI_TESTNET.Selector) + tests := []struct { - name string - input map[string]any - want *struct { + name string + input map[string]any + sourceSelector ccipocr3common.ChainSelector + want *struct { gasLimit *big.Int tokenReceiver [32]byte } @@ -26,6 +33,7 @@ func TestParseExtraDataMap(t *testing.T) { "gasLimit": new(big.Int).SetInt64(500000), "tokenReceiver": [32]byte{0x01}, }, + sourceSelector: nonSolanaSelector, want: &struct { gasLimit *big.Int tokenReceiver [32]byte @@ -41,6 +49,7 @@ func TestParseExtraDataMap(t *testing.T) { "gasLimit": new(big.Int).SetInt64(500000), "tokenReceiver": tokenReceiverExample[:], // convert to slice for input }, + sourceSelector: nonSolanaSelector, want: &struct { gasLimit *big.Int tokenReceiver [32]byte @@ -56,15 +65,77 @@ func TestParseExtraDataMap(t *testing.T) { "gasLimit": new(big.Int).SetInt64(500000), "tokenReceiver": tokenReceiverExample[:16], // 16 bytes, we expect an error due to length }, - want: nil, - expectErr: true, + sourceSelector: nonSolanaSelector, + want: nil, + expectErr: true, + }, + { + // Solana source emits GenericExtraArgsV2 (Borsh); ccipsolana.ExtraDataDecoder + // surfaces the gas limit under the struct field name "GasLimit" (capital) and + // does not include a tokenReceiver (GenericExtraArgsV2 has no token_receiver). + // parseExtraDataMap accepts the capital key and, for a Solana source, defaults + // tokenReceiver to zero. + name: "Solana GenericExtraArgsV2: capital GasLimit, no tokenReceiver defaults to zero", + input: map[string]any{ + "GasLimit": new(big.Int).SetInt64(1000000), + "AllowOutOfOrderExecution": true, + }, + sourceSelector: solanaSelector, + want: &struct { + gasLimit *big.Int + tokenReceiver [32]byte + }{ + gasLimit: new(big.Int).SetInt64(1000000), + tokenReceiver: [32]byte{}, + }, + expectErr: false, + }, + { + // A Solana source that does carry a tokenReceiver uses it as-is; the zero default + // only applies when the key is absent. + name: "Solana source with tokenReceiver present uses it", + input: map[string]any{ + "GasLimit": new(big.Int).SetInt64(1000000), + "tokenReceiver": [32]byte{0x0A}, + }, + sourceSelector: solanaSelector, + want: &struct { + gasLimit *big.Int + tokenReceiver [32]byte + }{ + gasLimit: new(big.Int).SetInt64(1000000), + tokenReceiver: [32]byte{0x0A}, + }, + expectErr: false, + }, + { + name: "no gas limit key of either casing", + input: map[string]any{ + "tokenReceiver": [32]byte{0x01}, + }, + sourceSelector: nonSolanaSelector, + want: nil, + expectErr: true, + }, + { + // SuiExtraArgsV1 (EVM/Sui sources) uses lowercase "gasLimit" and MUST carry + // tokenReceiver. A missing tokenReceiver on a non-Solana source is a + // malformed/partial decode and must error — the zero default is scoped to Solana + // sources only, so a bad EVM/Sui decode is surfaced rather than silently hashed. + name: "non-Solana source missing tokenReceiver errors (masking guard)", + input: map[string]any{ + "gasLimit": new(big.Int).SetInt64(500000), + }, + sourceSelector: nonSolanaSelector, + want: nil, + expectErr: true, }, } // Run tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gasLimit, tokenReceiver, err := parseExtraDataMap(tt.input) + gasLimit, tokenReceiver, err := parseExtraDataMap(tt.input, tt.sourceSelector) if tt.expectErr { require.Error(t, err) } else { From 3a363fb2f5e82d18d096ab75d41ed08783338cdc Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Thu, 6 Aug 2026 07:34:04 -0400 Subject: [PATCH 2/2] fix lint --- core/capabilities/ccip/ccipsui/msghasher.go | 1 + core/capabilities/ccip/ccipsui/msghasher_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/core/capabilities/ccip/ccipsui/msghasher.go b/core/capabilities/ccip/ccipsui/msghasher.go index 558a98e6405..8ad1615f4f2 100644 --- a/core/capabilities/ccip/ccipsui/msghasher.go +++ b/core/capabilities/ccip/ccipsui/msghasher.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-ccip/pkg/logutil" "github.com/smartcontractkit/chainlink-common/pkg/logger" ccipocr3common "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" diff --git a/core/capabilities/ccip/ccipsui/msghasher_test.go b/core/capabilities/ccip/ccipsui/msghasher_test.go index 768bb98737a..c27e9dc84ed 100644 --- a/core/capabilities/ccip/ccipsui/msghasher_test.go +++ b/core/capabilities/ccip/ccipsui/msghasher_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" chainsel "github.com/smartcontractkit/chain-selectors" + ccipocr3common "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" )