Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/capabilities/ccip/ccipsui/executecodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
33 changes: 29 additions & 4 deletions core/capabilities/ccip/ccipsui/msghasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ 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"
)

Expand Down Expand Up @@ -116,7 +117,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)
}
Expand Down Expand Up @@ -288,8 +289,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")
}
Expand All @@ -298,9 +308,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{}
Expand Down
84 changes: 78 additions & 6 deletions core/capabilities/ccip/ccipsui/msghasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ 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
}
Expand All @@ -26,6 +34,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
Expand All @@ -41,6 +50,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
Expand All @@ -56,15 +66,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 {
Expand Down
Loading