Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solana MessageHasher #15911

Merged
merged 13 commits into from
Jan 30, 2025
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
5 changes: 5 additions & 0 deletions .changeset/rich-frogs-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

Adding solana message hasher #added
142 changes: 142 additions & 0 deletions core/capabilities/ccip/ccipsolana/msghasher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package ccipsolana

import (
"context"
"errors"
"fmt"
"strings"

"github.com/gagliardetto/solana-go"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens"
"github.com/smartcontractkit/chainlink-common/pkg/logger"

cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

// MessageHasherV1 implements the MessageHasher interface.
// Compatible with:
// - "OnRamp 1.6.0-dev"
type MessageHasherV1 struct {
lggr logger.Logger
}

func NewMessageHasherV1(lggr logger.Logger) *MessageHasherV1 {
return &MessageHasherV1{
lggr: lggr,
}
}

// Hash implements the MessageHasher interface.
func (h *MessageHasherV1) Hash(_ context.Context, msg cciptypes.Message) (cciptypes.Bytes32, error) {
h.lggr.Debugw("hashing message", "msg", msg)

anyToSolanaMessage := ccip_router.Any2SVMRampMessage{}
anyToSolanaMessage.Header = ccip_router.RampMessageHeader{
SourceChainSelector: uint64(msg.Header.SourceChainSelector),
DestChainSelector: uint64(msg.Header.DestChainSelector),
SequenceNumber: uint64(msg.Header.SequenceNumber),
MessageId: msg.Header.MessageID,
Nonce: msg.Header.Nonce,
}
anyToSolanaMessage.TokenReceiver = solana.PublicKeyFromBytes(msg.Receiver)
anyToSolanaMessage.Sender = msg.Sender
anyToSolanaMessage.Data = msg.Data
for _, ta := range msg.TokenAmounts {
destGasAmount, err := extractDestGasAmountFromMap(ta.DestExecDataDecoded)
if err != nil {
return [32]byte{}, err
}

anyToSolanaMessage.TokenAmounts = append(anyToSolanaMessage.TokenAmounts, ccip_router.Any2SVMTokenTransfer{
SourcePoolAddress: ta.SourcePoolAddress,
DestTokenAddress: solana.PublicKeyFromBytes(ta.DestTokenAddress),
ExtraData: ta.ExtraData,
DestGasAmount: destGasAmount,
Amount: ccip_router.CrossChainAmount{LeBytes: tokens.ToLittleEndianU256(ta.Amount.Int.Uint64())},
prashantkumar1982 marked this conversation as resolved.
Show resolved Hide resolved
})
}

var err error
var msgAccounts []solana.PublicKey
anyToSolanaMessage.ExtraArgs, msgAccounts, err = parseExtraArgsMapWithAccounts(msg.ExtraArgsDecoded)
if err != nil {
return [32]byte{}, fmt.Errorf("failed to decode ExtraArgs: %w", err)
}

hash, err := ccip.HashAnyToSVMMessage(anyToSolanaMessage, msg.Header.OnRamp, msgAccounts)
return [32]byte(hash), err
}

// TODO remove extractDestGasAmountFromMap once https://github.com/smartcontractkit/chainlink/pull/15816 merged
func extractDestGasAmountFromMap(input map[string]any) (uint32, error) {
var out uint32

// Iterate through the expected fields in the struct
for fieldName, fieldValue := range input {
lowercase := strings.ToLower(fieldName)
switch lowercase {
case "destgasamount":
// Expect uint32
if v, ok := fieldValue.(uint32); ok {
out = v
} else {
return out, 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
}

// TODO combine parseExtraArgsMapWithAccounts with parseExtraArgsMap once https://github.com/smartcontractkit/chainlink/pull/15816 merged
func parseExtraArgsMapWithAccounts(input map[string]any) (ccip_router.Any2SVMRampExtraArgs, []solana.PublicKey, error) {
// Parse input map into SolanaExtraArgs
var out ccip_router.Any2SVMRampExtraArgs
var accounts []solana.PublicKey

// Iterate through the expected fields in the struct
// the field name should match with the one in SVMExtraArgsV1
// https://github.com/smartcontractkit/chainlink/blob/33c0bda696b0ed97f587a46eacd5c65bed9fb2c1/contracts/src/v0.8/ccip/libraries/Client.sol#L57
for fieldName, fieldValue := range input {
lowercase := strings.ToLower(fieldName)
switch lowercase {
case "computeunits":
// Expect uint32
if v, ok := fieldValue.(uint32); ok {
out.ComputeUnits = v
} else {
return out, accounts, errors.New("invalid type for ComputeUnits, expected uint32")
}
case "accountiswritablebitmap":
// Expect uint64
if v, ok := fieldValue.(uint64); ok {
out.IsWritableBitmap = v
} else {
return out, accounts, errors.New("invalid type for IsWritableBitmap, expected uint64")
}
case "accounts":
// Expect [][32]byte
if v, ok := fieldValue.([][32]byte); ok {
a := make([]solana.PublicKey, len(v))
for i, val := range v {
a[i] = solana.PublicKeyFromBytes(val[:])
}
accounts = a
} else {
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
// return errors because there's no guarantee SVMExtraArgs will match with SVMExtraArgsV1
}
}
return out, accounts, nil
}

// Interface compliance check
var _ cciptypes.MessageHasher = (*MessageHasherV1)(nil)
155 changes: 155 additions & 0 deletions core/capabilities/ccip/ccipsolana/msghasher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package ccipsolana

import (
"bytes"
cryptorand "crypto/rand"
"math/big"
"math/rand"
"testing"

agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-ccip/chains/solana/contracts/tests/config"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/ccip"

"github.com/smartcontractkit/chainlink-common/pkg/logger"

"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/evm/utils"

cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

func TestMessageHasher_Any2Solana(t *testing.T) {
any2AnyMsg, any2SolanaMsg, msgAccounts := createAny2SolanaMessages(t)
msgHasher := NewMessageHasherV1(logger.Test(t))
actualHash, err := msgHasher.Hash(testutils.Context(t), any2AnyMsg)
require.NoError(t, err)
expectedHash, err := ccip.HashAnyToSVMMessage(any2SolanaMsg, any2AnyMsg.Header.OnRamp, msgAccounts)
require.NoError(t, err)
require.Equal(t, expectedHash, actualHash[:32])
}

func createAny2SolanaMessages(t *testing.T) (cciptypes.Message, ccip_router.Any2SVMRampMessage, []solana.PublicKey) {
messageID := utils.RandomBytes32()

sourceChain := rand.Uint64()
seqNum := rand.Uint64()
nonce := rand.Uint64()
destChain := rand.Uint64()

messageData := make([]byte, rand.Intn(2048))
_, err := cryptorand.Read(messageData)
require.NoError(t, err)

sender := abiEncodedAddress(t)
receiver := solana.MustPublicKeyFromBase58("DS2tt4BX7YwCw7yrDNwbAdnYrxjeCPeGJbHmZEYC8RTb")
computeUnit := uint32(1000)
bitmap := uint64(10)

extraArgs := ccip_router.Any2SVMRampExtraArgs{
ComputeUnits: computeUnit,
IsWritableBitmap: bitmap,
}
var buf bytes.Buffer
encoder := agbinary.NewBorshEncoder(&buf)
err = extraArgs.MarshalWithEncoder(encoder)
require.NoError(t, err)
tokenAmount := cciptypes.NewBigInt(big.NewInt(rand.Int63()))

ccipTokenAmounts := make([]cciptypes.RampTokenAmount, 5)
for z := 0; z < 5; z++ {
ccipTokenAmounts[z] = cciptypes.RampTokenAmount{
SourcePoolAddress: cciptypes.UnknownAddress("DS2tt4BX7YwCw7yrDNwbAdnYrxjeCPeGJbHmZEYC8RTb"),
DestTokenAddress: receiver.Bytes(),
Amount: tokenAmount,
DestExecDataDecoded: map[string]any{
"destGasAmount": uint32(10),
},
}
}

solTokenAmounts := make([]ccip_router.Any2SVMTokenTransfer, 5)
for z := 0; z < 5; z++ {
solTokenAmounts[z] = ccip_router.Any2SVMTokenTransfer{
SourcePoolAddress: cciptypes.UnknownAddress("DS2tt4BX7YwCw7yrDNwbAdnYrxjeCPeGJbHmZEYC8RTb"),
DestTokenAddress: receiver,
Amount: ccip_router.CrossChainAmount{LeBytes: [32]uint8(encodeBigIntToFixedLengthLE(tokenAmount.Int, 32))},
DestGasAmount: uint32(10),
}
}

any2SolanaMsg := ccip_router.Any2SVMRampMessage{
Header: ccip_router.RampMessageHeader{
MessageId: messageID,
SourceChainSelector: sourceChain,
DestChainSelector: destChain,
SequenceNumber: seqNum,
Nonce: nonce,
},
Sender: sender,
TokenReceiver: receiver,
Data: messageData,
TokenAmounts: solTokenAmounts,
ExtraArgs: extraArgs,
}
any2AnyMsg := cciptypes.Message{
Header: cciptypes.RampMessageHeader{
MessageID: messageID,
SourceChainSelector: cciptypes.ChainSelector(sourceChain),
DestChainSelector: cciptypes.ChainSelector(destChain),
SequenceNumber: cciptypes.SeqNum(seqNum),
Nonce: nonce,
OnRamp: abiEncodedAddress(t),
},
Sender: sender,
Receiver: receiver.Bytes(),
Data: messageData,
TokenAmounts: ccipTokenAmounts,
FeeToken: []byte{},
FeeTokenAmount: cciptypes.NewBigIntFromInt64(0),
ExtraArgs: buf.Bytes(),
ExtraArgsDecoded: map[string]any{
"ComputeUnits": computeUnit,
"AccountIsWritableBitmap": bitmap,
"Accounts": [][32]byte{
[32]byte(config.CcipLogicReceiver.Bytes()),
[32]byte(config.ReceiverTargetAccountPDA.Bytes()),
[32]byte(solana.SystemProgramID.Bytes()),
},
},
}

msgAccounts := []solana.PublicKey{
config.CcipLogicReceiver,
config.ReceiverTargetAccountPDA,
solana.SystemProgramID,
}
return any2AnyMsg, any2SolanaMsg, msgAccounts
}

func abiEncodedAddress(t *testing.T) []byte {
addr := utils.RandomAddress()
encoded, err := utils.ABIEncode(`[{"type": "address"}]`, addr)
require.NoError(t, err)
return encoded
}

// TODO remove encodeBigIntToFixedLengthLE once https://github.com/smartcontractkit/chainlink/pull/15816 merged
func encodeBigIntToFixedLengthLE(bi *big.Int, length int) []byte {
// Create a fixed-length byte array
paddedBytes := make([]byte, length)

// Use FillBytes to fill the array with big-endian data, zero-padded
bi.FillBytes(paddedBytes)

// Reverse the array for little-endian encoding
for i, j := 0, len(paddedBytes)-1; i < j; i, j = i+1, j-1 {
paddedBytes[i], paddedBytes[j] = paddedBytes[j], paddedBytes[i]
}

return paddedBytes
}
3 changes: 2 additions & 1 deletion core/capabilities/ccip/oraclecreator/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper"

"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana"
evmconfig "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm"
solanaconfig "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/solana"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ocrimpls"
Expand Down Expand Up @@ -257,7 +258,7 @@ var plugins = map[string]plugin{
CommitPluginCodec: nil,
ExecutePluginCodec: nil,
ExtraArgsCodec: ccipcommon.NewExtraDataCodec(),
MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { return nil },
MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { return ccipsolana.NewMessageHasherV1(lggr) },
TokenDataEncoder: nil,
GasEstimateProvider: nil,
RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return nil },
Expand Down
2 changes: 1 addition & 1 deletion core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ require (
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-salt-fix // indirect
github.com/smartcontractkit/chain-selectors v1.0.36 // indirect
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128193522-bdbfcc588847 // indirect
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250114180313-3ba6bac6203a // indirect
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b // indirect
github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250121210000-2a9675d7a1b4 // indirect
github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect
github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250121195549-294ec6a40b92 // indirect
Expand Down
4 changes: 2 additions & 2 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1170,8 +1170,8 @@ github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgB
github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08=
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128193522-bdbfcc588847 h1:kCcrM/osIQFmHx7ZOxeGIeYAMkSmTxkOXcmqHNlXQXQ=
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128193522-bdbfcc588847/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250114180313-3ba6bac6203a h1:BuKTz6TpCQiLRmdT/Vp3OZj4MEVSEpNfY2nL8RYRbg8=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250114180313-3ba6bac6203a/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b h1:eNsqumP7VVJudA7gEcTKVFofealwbPJRinUw24uEmII=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36 h1:dytZPggag6auyzmbhpIDmkHu7KrflIBEhLLec4/xFIk=
github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68=
github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250121210000-2a9675d7a1b4 h1:w7w42ml8MOxdoyAZ9+og0342UkiH3deRM1V0Pj5JR5g=
Expand Down
6 changes: 2 additions & 4 deletions deployment/ccip/changeset/cs_deploy_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,14 +625,12 @@ func deployChainContractsSolana(
return fmt.Errorf("failed to get solana router program data: %w", err)
}

defaultGasLimit := solBinary.Uint128{Lo: 3000, Hi: 0, Endianness: nil}

instruction, err := ccip_router.NewInitializeInstruction(
chain.Selector, // chain selector
defaultGasLimit, // default gas limit
true, // allow out of order execution
EnableExecutionAfter, // period to wait before allowing manual execution
solana.PublicKey{},
solana.PublicKey{},
solBinary.Uint128{Lo: 300000000, Hi: 0, Endianness: nil},
GetRouterConfigPDA(ccipRouterProgram),
GetRouterStatePDA(ccipRouterProgram),
chain.DeployerKey.PublicKey(),
Expand Down
2 changes: 1 addition & 1 deletion deployment/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ require (
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-salt-fix
github.com/smartcontractkit/chain-selectors v1.0.36
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128193522-bdbfcc588847
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250114180313-3ba6bac6203a
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b
github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36
github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250121205514-f73e2f86c23b
github.com/smartcontractkit/chainlink-protos/job-distributor v0.6.0
Expand Down
4 changes: 2 additions & 2 deletions deployment/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1396,8 +1396,8 @@ github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgB
github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08=
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128193522-bdbfcc588847 h1:kCcrM/osIQFmHx7ZOxeGIeYAMkSmTxkOXcmqHNlXQXQ=
github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128193522-bdbfcc588847/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250114180313-3ba6bac6203a h1:BuKTz6TpCQiLRmdT/Vp3OZj4MEVSEpNfY2nL8RYRbg8=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250114180313-3ba6bac6203a/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b h1:eNsqumP7VVJudA7gEcTKVFofealwbPJRinUw24uEmII=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36 h1:dytZPggag6auyzmbhpIDmkHu7KrflIBEhLLec4/xFIk=
github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68=
github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250121210000-2a9675d7a1b4 h1:w7w42ml8MOxdoyAZ9+og0342UkiH3deRM1V0Pj5JR5g=
Expand Down
Loading
Loading