diff --git a/.changeset/mighty-waves-change.md b/.changeset/mighty-waves-change.md new file mode 100644 index 00000000000..f84a1932bb2 --- /dev/null +++ b/.changeset/mighty-waves-change.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +add solana chain reader config support to plugin creator, remove evm specific code #added diff --git a/.changeset/old-pets-divide.md b/.changeset/old-pets-divide.md new file mode 100644 index 00000000000..9586d283f42 --- /dev/null +++ b/.changeset/old-pets-divide.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Add solana chain writer config initialization #added diff --git a/.changeset/rich-frogs-work.md b/.changeset/rich-frogs-work.md new file mode 100644 index 00000000000..427f04e18b8 --- /dev/null +++ b/.changeset/rich-frogs-work.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Adding solana message hasher #added diff --git a/.changeset/twelve-games-sneeze.md b/.changeset/twelve-games-sneeze.md new file mode 100644 index 00000000000..ccee0e508c9 --- /dev/null +++ b/.changeset/twelve-games-sneeze.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +add Solana estimate provider as a no-op #added diff --git a/.changeset/violet-bears-try.md b/.changeset/violet-bears-try.md new file mode 100644 index 00000000000..187527757c1 --- /dev/null +++ b/.changeset/violet-bears-try.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Solana CCIP plugin codec support for both commit and execute report #added diff --git a/.changeset/wise-items-brush.md b/.changeset/wise-items-brush.md new file mode 100644 index 00000000000..715f08f6ccc --- /dev/null +++ b/.changeset/wise-items-brush.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Add solana support for contract transmitter and remove evm depdendency for address encoding #added diff --git a/.github/actions/setup-solana/build-contracts/action.yml b/.github/actions/setup-solana/build-contracts/action.yml index b1c94345abf..5b4be8a2e82 100644 --- a/.github/actions/setup-solana/build-contracts/action.yml +++ b/.github/actions/setup-solana/build-contracts/action.yml @@ -31,14 +31,14 @@ runs: echo "ANCHOR_VERSION=${anchor}" >> $GITHUB_ENV - name: cache docker build image id: cache-image - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@v4 # v4.0.2 with: lookup-only: true path: chains/solana/contracts/docker-build.tar key: ${{ runner.os }}-solana-build-${{ env.ANCHOR_VERSION }}-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo target dir id: cache-target - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@v4 # v4.0.2 with: lookup-only: true path: chains/solana/contracts/target diff --git a/core/capabilities/ccip/ccipevm/extradatadecoder.go b/core/capabilities/ccip/ccipevm/extradatadecoder.go new file mode 100644 index 00000000000..5f26ba9a54c --- /dev/null +++ b/core/capabilities/ccip/ccipevm/extradatadecoder.go @@ -0,0 +1,56 @@ +package ccipevm + +import ( + "fmt" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +func 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) + } + + return map[string]interface{}{ + evmDestExecDataKey: destGasAmount, + }, nil +} + +func DecodeExtraArgsToMap(extraArgs []byte) (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)) + } + + var method string + var extraByteOffset int + switch string(extraArgs[:4]) { + case string(evmExtraArgsV1Tag): + // for EVMExtraArgs, the first four bytes is the method name + method = evmV1DecodeName + extraByteOffset = 4 + case string(evmExtraArgsV2Tag): + method = evmV2DecodeName + extraByteOffset = 4 + case string(svmExtraArgsV1Tag): + // for SVMExtraArgs there's the four bytes plus another 32 bytes padding for the dynamic array + // TODO this is a temporary solution, the evm on-chain side will fix it, so the offset should just be 4 instead of 36 + // https://smartcontract-it.atlassian.net/browse/BCI-4180 + method = svmV1DecodeName + extraByteOffset = 36 + default: + return nil, fmt.Errorf("unknown extra args tag: %x", extraArgs) + } + + output := make(map[string]any) + args := make(map[string]interface{}) + err := messageHasherABI.Methods[method].Inputs.UnpackIntoMap(args, extraArgs[extraByteOffset:]) + if err != nil { + return nil, fmt.Errorf("abi decode extra args %v: %w", method, err) + } + + for k, val := range args { + output[k] = val + } + return output, nil +} diff --git a/core/capabilities/ccip/ccipevm/extradatadecoder_test.go b/core/capabilities/ccip/ccipevm/extradatadecoder_test.go new file mode 100644 index 00000000000..e0387ed9040 --- /dev/null +++ b/core/capabilities/ccip/ccipevm/extradatadecoder_test.go @@ -0,0 +1,105 @@ +package ccipevm + +import ( + "math/big" + "math/rand" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/message_hasher" +) + +func Test_decodeExtraData(t *testing.T) { + d := testSetup(t) + gasLimit := big.NewInt(rand.Int63()) + + t.Run("decode extra args into map evm v1", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: gasLimit, + }) + require.NoError(t, err) + + m, err := DecodeExtraArgsToMap(encoded) + require.NoError(t, err) + require.Len(t, m, 1) + + gl, exist := m["gasLimit"] + require.True(t, exist) + require.Equal(t, gl, gasLimit) + }) + + t.Run("decode extra args into map evm v2", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV2(nil, message_hasher.ClientEVMExtraArgsV2{ + GasLimit: gasLimit, + AllowOutOfOrderExecution: true, + }) + require.NoError(t, err) + + m, err := DecodeExtraArgsToMap(encoded) + require.NoError(t, err) + require.Len(t, m, 2) + + gl, exist := m["gasLimit"] + require.True(t, exist) + require.Equal(t, gl, gasLimit) + + ooe, exist := m["allowOutOfOrderExecution"] + require.True(t, exist) + require.Equal(t, true, ooe) + }) + + t.Run("decode extra args into map svm", func(t *testing.T) { + key, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + cu := uint32(10000) + bitmap := uint64(4) + ooe := false + tokenReceiver := [32]byte(key.PublicKey().Bytes()) + accounts := [][32]byte{[32]byte(key.PublicKey().Bytes())} + decoded, err := d.contract.DecodeSVMExtraArgsV1(nil, cu, bitmap, ooe, tokenReceiver, accounts) + if err != nil { + return + } + encoded, err := d.contract.EncodeSVMExtraArgsV1(nil, decoded) + require.NoError(t, err) + + m, err := DecodeExtraArgsToMap(encoded) + require.NoError(t, err) + require.Len(t, m, 5) + + cuDecoded, exist := m["computeUnits"] + require.True(t, exist) + require.Equal(t, cuDecoded, cu) + + bitmapDecoded, exist := m["accountIsWritableBitmap"] + require.True(t, exist) + require.Equal(t, bitmapDecoded, bitmap) + + ooeDecoded, exist := m["allowOutOfOrderExecution"] + require.True(t, exist) + require.Equal(t, ooeDecoded, ooe) + + tokenReceiverDecoded, exist := m["tokenReceiver"] + require.True(t, exist) + require.Equal(t, tokenReceiverDecoded, tokenReceiver) + + accountsDecoded, exist := m["accounts"] + require.True(t, exist) + require.Equal(t, accountsDecoded, accounts) + }) + + t.Run("decode dest exec data into map", func(t *testing.T) { + destGasAmount := uint32(10000) + encoded, err := abiEncodeUint32(destGasAmount) + require.NoError(t, err) + m, err := DecodeDestExecDataToMap(encoded) + require.NoError(t, err) + require.Len(t, m, 1) + + decoded, exist := m[evmDestExecDataKey] + require.True(t, exist) + require.Equal(t, destGasAmount, decoded) + }) +} diff --git a/core/capabilities/ccip/ccipevm/helpers.go b/core/capabilities/ccip/ccipevm/helpers.go index 3406421b915..13e635ecbdf 100644 --- a/core/capabilities/ccip/ccipevm/helpers.go +++ b/core/capabilities/ccip/ccipevm/helpers.go @@ -8,6 +8,13 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" ) +const ( + svmV1DecodeName = "decodeSVMExtraArgsV1" + evmV1DecodeName = "decodeEVMExtraArgsV1" + evmV2DecodeName = "decodeEVMExtraArgsV2" + evmDestExecDataKey = "destGasAmount" +) + var ( abiUint32 = ABITypeOrPanic("uint32") TokenDestGasOverheadABI = abi.Arguments{ @@ -24,9 +31,9 @@ func decodeExtraArgsV1V2(extraArgs []byte) (gasLimit *big.Int, err error) { var method string if bytes.Equal(extraArgs[:4], evmExtraArgsV1Tag) { - method = "decodeEVMExtraArgsV1" + method = evmV1DecodeName } else if bytes.Equal(extraArgs[:4], evmExtraArgsV2Tag) { - method = "decodeEVMExtraArgsV2" + method = evmV2DecodeName } else { return nil, fmt.Errorf("unknown extra args tag: %x", extraArgs) } diff --git a/core/capabilities/ccip/ccipevm/helpers_test.go b/core/capabilities/ccip/ccipevm/helpers_test.go index e0de0572226..89580c4d6f7 100644 --- a/core/capabilities/ccip/ccipevm/helpers_test.go +++ b/core/capabilities/ccip/ccipevm/helpers_test.go @@ -38,4 +38,39 @@ func Test_decodeExtraArgs(t *testing.T) { require.Equal(t, gasLimit, decodedGasLimit) }) + + t.Run("decode extra args into map evm v1", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV1(nil, message_hasher.ClientEVMExtraArgsV1{ + GasLimit: gasLimit, + }) + require.NoError(t, err) + + m, err := DecodeExtraArgsToMap(encoded) + require.NoError(t, err) + require.Len(t, m, 1) + + gl, exist := m["gasLimit"] + require.True(t, exist) + require.Equal(t, gl, gasLimit) + }) + + t.Run("decode extra args into map evm v2", func(t *testing.T) { + encoded, err := d.contract.EncodeEVMExtraArgsV2(nil, message_hasher.ClientEVMExtraArgsV2{ + GasLimit: gasLimit, + AllowOutOfOrderExecution: true, + }) + require.NoError(t, err) + + m, err := DecodeExtraArgsToMap(encoded) + require.NoError(t, err) + require.Len(t, m, 2) + + gl, exist := m["gasLimit"] + require.True(t, exist) + require.Equal(t, gl, gasLimit) + + ooe, exist := m["allowOutOfOrderExecution"] + require.True(t, exist) + require.Equal(t, true, ooe) + }) } diff --git a/core/capabilities/ccip/ccipevm/msghasher.go b/core/capabilities/ccip/ccipevm/msghasher.go index efc18a20829..3c9cea1147e 100644 --- a/core/capabilities/ccip/ccipevm/msghasher.go +++ b/core/capabilities/ccip/ccipevm/msghasher.go @@ -30,6 +30,9 @@ var ( // bytes4 public constant EVM_EXTRA_ARGS_V2_TAG = 0x181dcf10; evmExtraArgsV2Tag = hexutil.MustDecode("0x181dcf10") + + // bytes4 public constant SVM_EXTRA_EXTRA_ARGS_V1_TAG = 0x1f3b3aba + svmExtraArgsV1Tag = hexutil.MustDecode("0x1f3b3aba") ) // MessageHasherV1 implements the MessageHasher interface. diff --git a/core/capabilities/ccip/ccipsolana/commitcodec.go b/core/capabilities/ccip/ccipsolana/commitcodec.go new file mode 100644 index 00000000000..3868d82d8e1 --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/commitcodec.go @@ -0,0 +1,159 @@ +package ccipsolana + +import ( + "bytes" + "context" + "fmt" + "math/big" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +// CommitPluginCodecV1 is a codec for encoding and decoding commit plugin reports. +// Compatible with: +// - "OffRamp 1.6.0-dev" +type CommitPluginCodecV1 struct{} + +func NewCommitPluginCodecV1() *CommitPluginCodecV1 { + return &CommitPluginCodecV1{} +} + +func (c *CommitPluginCodecV1) Encode(ctx context.Context, report cciptypes.CommitPluginReport) ([]byte, error) { + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + if len(report.MerkleRoots) != 1 { + return nil, fmt.Errorf("unexpected merkle root length in report: %d", len(report.MerkleRoots)) + } + + mr := ccip_router.MerkleRoot{ + SourceChainSelector: uint64(report.MerkleRoots[0].ChainSel), + OnRampAddress: report.MerkleRoots[0].OnRampAddress, + MinSeqNr: uint64(report.MerkleRoots[0].SeqNumsRange.Start()), + MaxSeqNr: uint64(report.MerkleRoots[0].SeqNumsRange.End()), + MerkleRoot: report.MerkleRoots[0].MerkleRoot, + } + + tpu := make([]ccip_router.TokenPriceUpdate, 0, len(report.PriceUpdates.TokenPriceUpdates)) + for _, update := range report.PriceUpdates.TokenPriceUpdates { + token, err := solana.PublicKeyFromBase58(string(update.TokenID)) + if err != nil { + return nil, fmt.Errorf("invalid token address: %s, %w", update.TokenID, err) + } + if update.Price.IsEmpty() { + return nil, fmt.Errorf("empty price for token: %s", update.TokenID) + } + tpu = append(tpu, ccip_router.TokenPriceUpdate{ + SourceToken: token, + UsdPerToken: [28]uint8(encodeBigIntToFixedLengthLE(update.Price.Int, 28)), + }) + } + + gpu := make([]ccip_router.GasPriceUpdate, 0, len(report.PriceUpdates.GasPriceUpdates)) + for _, update := range report.PriceUpdates.GasPriceUpdates { + if update.GasPrice.IsEmpty() { + return nil, fmt.Errorf("empty gas price for chain: %d", update.ChainSel) + } + + gpu = append(gpu, ccip_router.GasPriceUpdate{ + DestChainSelector: uint64(update.ChainSel), + UsdPerUnitGas: [28]uint8(encodeBigIntToFixedLengthLE(update.GasPrice.Int, 28)), + }) + } + + commit := ccip_router.CommitInput{ + MerkleRoot: mr, + PriceUpdates: ccip_router.PriceUpdates{ + TokenPriceUpdates: tpu, + GasPriceUpdates: gpu, + }, + } + + err := commit.MarshalWithEncoder(encoder) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (c *CommitPluginCodecV1) Decode(ctx context.Context, bytes []byte) (cciptypes.CommitPluginReport, error) { + decoder := agbinary.NewBorshDecoder(bytes) + commitReport := ccip_router.CommitInput{} + err := commitReport.UnmarshalWithDecoder(decoder) + if err != nil { + return cciptypes.CommitPluginReport{}, err + } + + merkleRoots := []cciptypes.MerkleRootChain{ + { + ChainSel: cciptypes.ChainSelector(commitReport.MerkleRoot.SourceChainSelector), + OnRampAddress: commitReport.MerkleRoot.OnRampAddress, + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(commitReport.MerkleRoot.MinSeqNr), + cciptypes.SeqNum(commitReport.MerkleRoot.MaxSeqNr), + ), + MerkleRoot: commitReport.MerkleRoot.MerkleRoot, + }, + } + + tokenPriceUpdates := make([]cciptypes.TokenPrice, 0, len(commitReport.PriceUpdates.TokenPriceUpdates)) + for _, update := range commitReport.PriceUpdates.TokenPriceUpdates { + tokenPriceUpdates = append(tokenPriceUpdates, cciptypes.TokenPrice{ + TokenID: cciptypes.UnknownEncodedAddress(update.SourceToken.String()), + Price: decodeLEToBigInt(update.UsdPerToken[:]), + }) + } + + gasPriceUpdates := make([]cciptypes.GasPriceChain, 0, len(commitReport.PriceUpdates.GasPriceUpdates)) + for _, update := range commitReport.PriceUpdates.GasPriceUpdates { + gasPriceUpdates = append(gasPriceUpdates, cciptypes.GasPriceChain{ + GasPrice: decodeLEToBigInt(update.UsdPerUnitGas[:]), + ChainSel: cciptypes.ChainSelector(update.DestChainSelector), + }) + } + + return cciptypes.CommitPluginReport{ + MerkleRoots: merkleRoots, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: tokenPriceUpdates, + GasPriceUpdates: gasPriceUpdates, + }, + }, nil +} + +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 +} + +func decodeLEToBigInt(data []byte) cciptypes.BigInt { + // Reverse the byte array to convert it from little-endian to big-endian + for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { + data[i], data[j] = data[j], data[i] + } + + // Use big.Int.SetBytes to construct the big.Int + bi := new(big.Int).SetBytes(data) + if bi.Int64() == 0 { + return cciptypes.NewBigInt(big.NewInt(0)) + } + + return cciptypes.NewBigInt(bi) +} + +// Ensure CommitPluginCodec implements the CommitPluginCodec interface +var _ cciptypes.CommitPluginCodec = (*CommitPluginCodecV1)(nil) diff --git a/core/capabilities/ccip/ccipsolana/commitcodec_test.go b/core/capabilities/ccip/ccipsolana/commitcodec_test.go new file mode 100644 index 00000000000..3f5916d6450 --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/commitcodec_test.go @@ -0,0 +1,251 @@ +package ccipsolana + +import ( + "bytes" + "math/big" + "math/rand" + "strconv" + "testing" + + agbinary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-integrations/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +var randomCommitReport = func() cciptypes.CommitPluginReport { + pubkey, err := solanago.NewRandomPrivateKey() + if err != nil { + panic(err) + } + + return cciptypes.CommitPluginReport{ + MerkleRoots: []cciptypes.MerkleRootChain{ + { + OnRampAddress: cciptypes.UnknownAddress(pubkey.PublicKey().String()), + ChainSel: cciptypes.ChainSelector(rand.Uint64()), + SeqNumsRange: cciptypes.NewSeqNumRange( + cciptypes.SeqNum(rand.Uint64()), + cciptypes.SeqNum(rand.Uint64()), + ), + MerkleRoot: utils.RandomBytes32(), + }, + }, + PriceUpdates: cciptypes.PriceUpdates{ + TokenPriceUpdates: []cciptypes.TokenPrice{ + { + TokenID: "C8WSPj3yyus1YN3yNB6YA5zStYtbjQWtpmKadmvyUXq8", + Price: cciptypes.NewBigInt(big.NewInt(rand.Int63())), + }, + }, + GasPriceUpdates: []cciptypes.GasPriceChain{ + {GasPrice: cciptypes.NewBigInt(big.NewInt(rand.Int63())), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(big.NewInt(rand.Int63())), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + {GasPrice: cciptypes.NewBigInt(big.NewInt(rand.Int63())), ChainSel: cciptypes.ChainSelector(rand.Uint64())}, + }, + }, + } +} + +func TestCommitPluginCodecV1(t *testing.T) { + testCases := []struct { + name string + report func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport + expErr bool + }{ + { + name: "base report", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + return report + }, + }, + { + name: "empty token address", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].TokenID = "" + return report + }, + expErr: true, + }, + { + name: "empty merkle root", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.MerkleRoots[0].MerkleRoot = cciptypes.Bytes32{} + return report + }, + }, + { + name: "zero token price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.TokenPriceUpdates[0].Price = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + { + name: "zero gas price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(big.NewInt(0)) + return report + }, + }, + { + name: "empty gas price", + report: func(report cciptypes.CommitPluginReport) cciptypes.CommitPluginReport { + report.PriceUpdates.GasPriceUpdates[0].GasPrice = cciptypes.NewBigInt(nil) + return report + }, + expErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + report := tc.report(randomCommitReport()) + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(t) + encodedReport, err := commitCodec.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + decodedReport, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(t, err) + require.Equal(t, report, decodedReport) + }) + } +} + +func BenchmarkCommitPluginCodecV1_Encode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + + rep := randomCommitReport() + for i := 0; i < b.N; i++ { + _, err := commitCodec.Encode(ctx, rep) + require.NoError(b, err) + } +} + +func BenchmarkCommitPluginCodecV1_Decode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + encodedReport, err := commitCodec.Encode(ctx, randomCommitReport()) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(b, err) + } +} + +func BenchmarkCommitPluginCodecV1_Encode_Decode(b *testing.B) { + commitCodec := NewCommitPluginCodecV1() + ctx := testutils.Context(b) + + rep := randomCommitReport() + for i := 0; i < b.N; i++ { + encodedReport, err := commitCodec.Encode(ctx, rep) + require.NoError(b, err) + decodedReport, err := commitCodec.Decode(ctx, encodedReport) + require.NoError(b, err) + require.Equal(b, rep, decodedReport) + } +} + +func Test_DecodingCommitReport(t *testing.T) { + t.Run("decode on-chain commit report", func(t *testing.T) { + chainSel := cciptypes.ChainSelector(rand.Uint64()) + minSeqNr := rand.Uint64() + maxSeqNr := minSeqNr + 10 + onRampAddr, err := solanago.NewRandomPrivateKey() + require.NoError(t, err) + + tokenSource := solanago.MustPublicKeyFromBase58("C8WSPj3yyus1YN3yNB6YA5zStYtbjQWtpmKadmvyUXq8") + tokenPrice := encodeBigIntToFixedLengthLE(big.NewInt(rand.Int63()), 28) + gasPrice := encodeBigIntToFixedLengthLE(big.NewInt(rand.Int63()), 28) + merkleRoot := utils.RandomBytes32() + + tpu := []ccip_router.TokenPriceUpdate{ + { + SourceToken: tokenSource, + UsdPerToken: [28]uint8(tokenPrice), + }, + } + + gpu := []ccip_router.GasPriceUpdate{ + {UsdPerUnitGas: [28]uint8(gasPrice), DestChainSelector: uint64(chainSel)}, + {UsdPerUnitGas: [28]uint8(gasPrice), DestChainSelector: uint64(chainSel)}, + {UsdPerUnitGas: [28]uint8(gasPrice), DestChainSelector: uint64(chainSel)}, + } + + onChainReport := ccip_router.CommitInput{ + MerkleRoot: ccip_router.MerkleRoot{ + SourceChainSelector: uint64(chainSel), + OnRampAddress: onRampAddr.PublicKey().Bytes(), + MinSeqNr: minSeqNr, + MaxSeqNr: maxSeqNr, + MerkleRoot: merkleRoot, + }, + PriceUpdates: ccip_router.PriceUpdates{ + TokenPriceUpdates: tpu, + GasPriceUpdates: gpu, + }, + } + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err = onChainReport.MarshalWithEncoder(encoder) + require.NoError(t, err) + + commitCodec := NewCommitPluginCodecV1() + decode, err := commitCodec.Decode(testutils.Context(t), buf.Bytes()) + require.NoError(t, err) + mr := decode.MerkleRoots[0] + + // check decoded ocr report merkle root matches with on-chain report + require.Equal(t, strconv.FormatUint(minSeqNr, 10), mr.SeqNumsRange.Start().String()) + require.Equal(t, strconv.FormatUint(maxSeqNr, 10), mr.SeqNumsRange.End().String()) + require.Equal(t, cciptypes.UnknownAddress(onRampAddr.PublicKey().Bytes()), mr.OnRampAddress) + require.Equal(t, cciptypes.Bytes32(merkleRoot), mr.MerkleRoot) + + // check decoded ocr report token price update matches with on-chain report + pu := decode.PriceUpdates.TokenPriceUpdates[0] + require.Equal(t, decodeLEToBigInt(tokenPrice), pu.Price) + require.Equal(t, cciptypes.UnknownEncodedAddress(tokenSource.String()), pu.TokenID) + + // check decoded ocr report gas price update matches with on-chain report + gu := decode.PriceUpdates.GasPriceUpdates[0] + require.Equal(t, decodeLEToBigInt(gasPrice), gu.GasPrice) + require.Equal(t, chainSel, gu.ChainSel) + }) + + t.Run("decode Borsh encoded commit report", func(t *testing.T) { + rep := randomCommitReport() + commitCodec := NewCommitPluginCodecV1() + decode, err := commitCodec.Encode(testutils.Context(t), rep) + require.NoError(t, err) + + decoder := agbinary.NewBorshDecoder(decode) + decodedReport := ccip_router.CommitInput{} + err = decodedReport.UnmarshalWithDecoder(decoder) + require.NoError(t, err) + + reportMerkleRoot := rep.MerkleRoots[0] + require.Equal(t, reportMerkleRoot.MerkleRoot, cciptypes.Bytes32(decodedReport.MerkleRoot.MerkleRoot)) + + tu := rep.PriceUpdates.TokenPriceUpdates[0] + require.Equal(t, tu.TokenID, cciptypes.UnknownEncodedAddress(decodedReport.PriceUpdates.TokenPriceUpdates[0].SourceToken.String())) + require.Equal(t, tu.Price, decodeLEToBigInt(decodedReport.PriceUpdates.TokenPriceUpdates[0].UsdPerToken[:])) + + gu := rep.PriceUpdates.GasPriceUpdates[0] + require.Equal(t, gu.ChainSel, cciptypes.ChainSelector(decodedReport.PriceUpdates.GasPriceUpdates[0].DestChainSelector)) + require.Equal(t, gu.GasPrice, decodeLEToBigInt(decodedReport.PriceUpdates.GasPriceUpdates[0].UsdPerUnitGas[:])) + }) +} diff --git a/core/capabilities/ccip/ccipsolana/executecodec.go b/core/capabilities/ccip/ccipsolana/executecodec.go new file mode 100644 index 00000000000..0cf05e2df13 --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/executecodec.go @@ -0,0 +1,217 @@ +package ccipsolana + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "strings" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +// ExecutePluginCodecV1 is a codec for encoding and decoding execute plugin reports. +// Compatible with: +// - "OffRamp 1.6.0-dev" +type ExecutePluginCodecV1 struct { +} + +func NewExecutePluginCodecV1() *ExecutePluginCodecV1 { + return &ExecutePluginCodecV1{} +} + +func (e *ExecutePluginCodecV1) Encode(ctx context.Context, report cciptypes.ExecutePluginReport) ([]byte, error) { + if len(report.ChainReports) != 1 { + return nil, fmt.Errorf("unexpected chain report length: %d", len(report.ChainReports)) + } + + chainReport := report.ChainReports[0] + if len(chainReport.Messages) > 1 { + return nil, fmt.Errorf("unexpected report message length: %d", len(chainReport.Messages)) + } + + var message ccip_router.Any2SVMRampMessage + var offChainTokenData [][]byte + if len(chainReport.Messages) > 0 { + // currently only allow executing one message at a time + msg := chainReport.Messages[0] + tokenAmounts := make([]ccip_router.Any2SVMTokenTransfer, 0, len(msg.TokenAmounts)) + for _, tokenAmount := range msg.TokenAmounts { + if tokenAmount.Amount.IsEmpty() { + return nil, fmt.Errorf("empty amount for token: %s", tokenAmount.DestTokenAddress) + } + + if len(tokenAmount.DestTokenAddress) != solana.PublicKeyLength { + return nil, fmt.Errorf("invalid destTokenAddress address: %v", tokenAmount.DestTokenAddress) + } + + destGasAmount, err := extractDestGasAmountFromMap(tokenAmount.DestExecDataDecoded) + if err != nil { + return nil, err + } + + tokenAmounts = append(tokenAmounts, ccip_router.Any2SVMTokenTransfer{ + SourcePoolAddress: tokenAmount.SourcePoolAddress, + DestTokenAddress: solana.PublicKeyFromBytes(tokenAmount.DestTokenAddress), + ExtraData: tokenAmount.ExtraData, + Amount: ccip_router.CrossChainAmount{LeBytes: [32]uint8(encodeBigIntToFixedLengthLE(tokenAmount.Amount.Int, 32))}, + DestGasAmount: destGasAmount, + }) + } + + var extraArgs ccip_router.Any2SVMRampExtraArgs + extraArgs, _, err := parseExtraArgsMapWithAccounts(msg.ExtraArgsDecoded) + if err != nil { + return nil, fmt.Errorf("invalid extra args map: %w", err) + } + + if len(msg.Receiver) != solana.PublicKeyLength { + return nil, fmt.Errorf("invalid receiver address: %v", msg.Receiver) + } + + message = ccip_router.Any2SVMRampMessage{ + Header: ccip_router.RampMessageHeader{ + MessageId: msg.Header.MessageID, + SourceChainSelector: uint64(msg.Header.SourceChainSelector), + DestChainSelector: uint64(msg.Header.DestChainSelector), + SequenceNumber: uint64(msg.Header.SequenceNumber), + Nonce: msg.Header.Nonce, + }, + Sender: msg.Sender, + Data: msg.Data, + TokenReceiver: solana.PublicKeyFromBytes(msg.Receiver), + TokenAmounts: tokenAmounts, + ExtraArgs: extraArgs, + } + + // should only have an offchain token data if there are tokens as part of the message + if len(chainReport.OffchainTokenData) > 0 { + offChainTokenData = chainReport.OffchainTokenData[0] + } + } + + solanaProofs := make([][32]byte, 0, len(chainReport.Proofs)) + for _, proof := range chainReport.Proofs { + solanaProofs = append(solanaProofs, proof) + } + + solanaReport := ccip_router.ExecutionReportSingleChain{ + SourceChainSelector: uint64(chainReport.SourceChainSelector), + Message: message, + OffchainTokenData: offChainTokenData, + Proofs: solanaProofs, + } + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err := solanaReport.MarshalWithEncoder(encoder) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (e *ExecutePluginCodecV1) Decode(ctx context.Context, encodedReport []byte) (cciptypes.ExecutePluginReport, error) { + decoder := agbinary.NewBorshDecoder(encodedReport) + executeReport := ccip_router.ExecutionReportSingleChain{} + err := executeReport.UnmarshalWithDecoder(decoder) + if err != nil { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("unpack encoded report: %w", err) + } + + tokenAmounts := make([]cciptypes.RampTokenAmount, 0, len(executeReport.Message.TokenAmounts)) + for _, tokenAmount := range executeReport.Message.TokenAmounts { + destData := make([]byte, 4) + binary.LittleEndian.PutUint32(destData, tokenAmount.DestGasAmount) + + tokenAmounts = append(tokenAmounts, cciptypes.RampTokenAmount{ + SourcePoolAddress: tokenAmount.SourcePoolAddress, + DestTokenAddress: tokenAmount.DestTokenAddress.Bytes(), + ExtraData: tokenAmount.ExtraData, + Amount: decodeLEToBigInt(tokenAmount.Amount.LeBytes[:]), + DestExecData: destData, + }) + } + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err = executeReport.Message.ExtraArgs.MarshalWithEncoder(encoder) + if err != nil { + return cciptypes.ExecutePluginReport{}, fmt.Errorf("unpack encoded report: %w", err) + } + + messages := []cciptypes.Message{ + { + Header: cciptypes.RampMessageHeader{ + MessageID: executeReport.Message.Header.MessageId, + SourceChainSelector: cciptypes.ChainSelector(executeReport.Message.Header.SourceChainSelector), + DestChainSelector: cciptypes.ChainSelector(executeReport.Message.Header.DestChainSelector), + SequenceNumber: cciptypes.SeqNum(executeReport.Message.Header.SequenceNumber), + Nonce: executeReport.Message.Header.Nonce, + MsgHash: cciptypes.Bytes32{}, // todo: info not available, but not required atm + OnRamp: cciptypes.UnknownAddress{}, // todo: info not available, but not required atm + }, + Sender: executeReport.Message.Sender, + Data: executeReport.Message.Data, + Receiver: executeReport.Message.TokenReceiver.Bytes(), + ExtraArgs: buf.Bytes(), + FeeToken: cciptypes.UnknownAddress{}, // <-- todo: info not available, but not required atm + FeeTokenAmount: cciptypes.BigInt{}, // <-- todo: info not available, but not required atm + TokenAmounts: tokenAmounts, + }, + } + + offchainTokenData := make([][][]byte, 0, 1) + if executeReport.OffchainTokenData != nil { + offchainTokenData = append(offchainTokenData, executeReport.OffchainTokenData) + } + + proofs := make([]cciptypes.Bytes32, 0, len(executeReport.Proofs)) + for _, proof := range executeReport.Proofs { + proofs = append(proofs, proof) + } + + chainReport := cciptypes.ExecutePluginReportSingleChain{ + SourceChainSelector: cciptypes.ChainSelector(executeReport.SourceChainSelector), + Messages: messages, + OffchainTokenData: offchainTokenData, + Proofs: proofs, + } + + report := cciptypes.ExecutePluginReport{ + ChainReports: []cciptypes.ExecutePluginReportSingleChain{chainReport}, + } + + return report, nil +} + +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 +} + +// Ensure ExecutePluginCodec implements the ExecutePluginCodec interface +var _ cciptypes.ExecutePluginCodec = (*ExecutePluginCodecV1)(nil) diff --git a/core/capabilities/ccip/ccipsolana/executecodec_test.go b/core/capabilities/ccip/ccipsolana/executecodec_test.go new file mode 100644 index 00000000000..f36918cd077 --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/executecodec_test.go @@ -0,0 +1,270 @@ +package ccipsolana + +import ( + "bytes" + "encoding/binary" + "math/big" + "math/rand" + "testing" + + agbinary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/smartcontractkit/chainlink-integrations/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var randomExecuteReport = func(t *testing.T, sourceChainSelector uint64) cciptypes.ExecutePluginReport { + const numChainReports = 1 + const msgsPerReport = 1 + const numTokensPerMsg = 1 + + chainReports := make([]cciptypes.ExecutePluginReportSingleChain, numChainReports) + for i := 0; i < numChainReports; i++ { + reportMessages := make([]cciptypes.Message, msgsPerReport) + for j := 0; j < msgsPerReport; j++ { + key, err := solanago.NewRandomPrivateKey() + if err != nil { + panic(err) + } + extraData, err := cciptypes.NewBytesFromString("0x1234") + require.NoError(t, err) + + destGasAmount := uint32(10) + destExecData := make([]byte, 4) + binary.LittleEndian.PutUint32(destExecData, destGasAmount) + + tokenAmounts := make([]cciptypes.RampTokenAmount, numTokensPerMsg) + for z := 0; z < numTokensPerMsg; z++ { + tokenAmounts[z] = cciptypes.RampTokenAmount{ + SourcePoolAddress: cciptypes.UnknownAddress(key.PublicKey().String()), + DestTokenAddress: key.PublicKey().Bytes(), + ExtraData: extraData, + Amount: cciptypes.NewBigInt(big.NewInt(rand.Int63())), + DestExecData: destExecData, + DestExecDataDecoded: map[string]any{ + "destGasAmount": uint32(10), + }, + } + } + + extraArgs := ccip_router.Any2SVMRampExtraArgs{ + ComputeUnits: 1000, + IsWritableBitmap: 2, + } + + extraArgsMap := map[string]any{ + "ComputeUnits": uint32(1000), + "accountIsWritableBitmap": uint64(2), + } + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err = extraArgs.MarshalWithEncoder(encoder) + require.NoError(t, err) + + reportMessages[j] = cciptypes.Message{ + Header: cciptypes.RampMessageHeader{ + MessageID: utils.RandomBytes32(), + SourceChainSelector: cciptypes.ChainSelector(sourceChainSelector), + DestChainSelector: cciptypes.ChainSelector(rand.Uint64()), + SequenceNumber: cciptypes.SeqNum(rand.Uint64()), + Nonce: rand.Uint64(), + MsgHash: utils.RandomBytes32(), + OnRamp: cciptypes.UnknownAddress(key.PublicKey().String()), + }, + Sender: cciptypes.UnknownAddress(key.PublicKey().String()), + Data: extraData, + Receiver: key.PublicKey().Bytes(), + ExtraArgs: buf.Bytes(), + FeeToken: cciptypes.UnknownAddress(key.PublicKey().String()), + FeeTokenAmount: cciptypes.NewBigInt(big.NewInt(rand.Int63())), + TokenAmounts: tokenAmounts, + ExtraArgsDecoded: extraArgsMap, + } + } + + tokenData := make([][][]byte, numTokensPerMsg) + for j := 0; j < numTokensPerMsg; j++ { + tokenData[j] = [][]byte{{0x1}, {0x2, 0x3}} + } + + chainReports[i] = cciptypes.ExecutePluginReportSingleChain{ + SourceChainSelector: cciptypes.ChainSelector(sourceChainSelector), + Messages: reportMessages, + OffchainTokenData: tokenData, + Proofs: []cciptypes.Bytes32{utils.RandomBytes32(), utils.RandomBytes32()}, + } + } + + return cciptypes.ExecutePluginReport{ChainReports: chainReports} +} + +func TestExecutePluginCodecV1(t *testing.T) { + testCases := []struct { + name string + report func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport + expErr bool + chainSelector uint64 + }{ + { + name: "base report with Solana as source chain", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + chainSelector: 124615329519749607, // Solana mainnet chain selector + }, + { + name: "base report with EVM as source chain", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { return report }, + expErr: false, + chainSelector: 5009297550715157269, // ETH mainnet chain selector + }, + // TODO: check if empty msg if necessary since there is only single msg in solana execute report + // { + // name: "reports have empty msgs", + // report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + // report.ChainReports[0].Messages = []cciptypes.Message{} + // return report + // }, + // expErr: false, + // }, + { + name: "reports have empty offchain token data", + report: func(report cciptypes.ExecutePluginReport) cciptypes.ExecutePluginReport { + report.ChainReports[0].OffchainTokenData = [][][]byte{} + return report + }, + expErr: false, + chainSelector: 124615329519749607, // Solana mainnet chain selector + }, + } + + ctx := testutils.Context(t) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cd := NewExecutePluginCodecV1() + report := tc.report(randomExecuteReport(t, tc.chainSelector)) + bytes, err := cd.Encode(ctx, report) + if tc.expErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + // ignore msg hash, extraArgsDecoded map and DestExecDataDecoded map in comparison + for i := range report.ChainReports { + for j := range report.ChainReports[i].Messages { + report.ChainReports[i].Messages[j].Header.MsgHash = cciptypes.Bytes32{} + report.ChainReports[i].Messages[j].Header.OnRamp = cciptypes.UnknownAddress{} + report.ChainReports[i].Messages[j].FeeToken = cciptypes.UnknownAddress{} + report.ChainReports[i].Messages[j].FeeTokenAmount = cciptypes.BigInt{} + report.ChainReports[i].Messages[j].ExtraArgsDecoded = nil + for k := range report.ChainReports[i].Messages[j].TokenAmounts { + report.ChainReports[i].Messages[j].TokenAmounts[k].DestExecDataDecoded = nil + } + } + } + + // decode using the codec + codecDecoded, err := cd.Decode(ctx, bytes) + require.NoError(t, err) + assert.Equal(t, report, codecDecoded) + }) + } +} + +func Test_DecodingExecuteReport(t *testing.T) { + t.Run("decode on-chain execute report", func(t *testing.T) { + chainSel := cciptypes.ChainSelector(rand.Uint64()) + onRampAddr, err := solanago.NewRandomPrivateKey() + require.NoError(t, err) + + destGasAmount := uint32(10) + tokenAmount := big.NewInt(rand.Int63()) + tokenReceiver := solanago.MustPublicKeyFromBase58("C8WSPj3yyus1YN3yNB6YA5zStYtbjQWtpmKadmvyUXq8") + extraArgs := ccip_router.Any2SVMRampExtraArgs{ + ComputeUnits: 1000, + IsWritableBitmap: 2, + } + + onChainReport := ccip_router.ExecutionReportSingleChain{ + SourceChainSelector: uint64(chainSel), + Message: ccip_router.Any2SVMRampMessage{ + Header: ccip_router.RampMessageHeader{ + SourceChainSelector: uint64(chainSel), + }, + TokenReceiver: tokenReceiver, + ExtraArgs: extraArgs, + TokenAmounts: []ccip_router.Any2SVMTokenTransfer{ + { + Amount: ccip_router.CrossChainAmount{LeBytes: [32]uint8(encodeBigIntToFixedLengthLE(tokenAmount, 32))}, + DestGasAmount: destGasAmount, + }, + }, + OnRampAddress: onRampAddr.PublicKey().Bytes(), + }, + } + + var extraArgsBuf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&extraArgsBuf) + err = extraArgs.MarshalWithEncoder(encoder) + require.NoError(t, err) + + var buf bytes.Buffer + encoder = agbinary.NewBorshEncoder(&buf) + err = onChainReport.MarshalWithEncoder(encoder) + require.NoError(t, err) + + executeCodec := NewExecutePluginCodecV1() + decode, err := executeCodec.Decode(testutils.Context(t), buf.Bytes()) + require.NoError(t, err) + + report := decode.ChainReports[0] + require.Equal(t, chainSel, report.SourceChainSelector) + + msg := report.Messages[0] + require.Equal(t, cciptypes.UnknownAddress(tokenReceiver.Bytes()), msg.Receiver) + require.Equal(t, cciptypes.Bytes(extraArgsBuf.Bytes()), msg.ExtraArgs) + require.Equal(t, tokenAmount, msg.TokenAmounts[0].Amount.Int) + require.Equal(t, destGasAmount, bytesToUint32LE(msg.TokenAmounts[0].DestExecData)) + }) + + t.Run("decode Borsh encoded execute report", func(t *testing.T) { + ocrReport := randomExecuteReport(t, 124615329519749607) + cd := NewExecutePluginCodecV1() + encodedReport, err := cd.Encode(testutils.Context(t), ocrReport) + require.NoError(t, err) + + decoder := agbinary.NewBorshDecoder(encodedReport) + executeReport := ccip_router.ExecutionReportSingleChain{} + err = executeReport.UnmarshalWithDecoder(decoder) + require.NoError(t, err) + + originReport := ocrReport.ChainReports[0] + require.Equal(t, originReport.SourceChainSelector, cciptypes.ChainSelector(executeReport.SourceChainSelector)) + + originMsg := originReport.Messages[0] + require.Equal(t, originMsg.Header.MessageID, cciptypes.Bytes32(executeReport.Message.Header.MessageId)) + require.Equal(t, originMsg.Header.DestChainSelector, cciptypes.ChainSelector(executeReport.Message.Header.DestChainSelector)) + require.Equal(t, originMsg.Header.SourceChainSelector, cciptypes.ChainSelector(executeReport.Message.Header.SourceChainSelector)) + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err = executeReport.Message.ExtraArgs.MarshalWithEncoder(encoder) + require.NoError(t, err) + require.Equal(t, originMsg.ExtraArgs, cciptypes.Bytes(buf.Bytes())) + + originTokenAmount := originMsg.TokenAmounts[0] + require.Equal(t, originTokenAmount.Amount, decodeLEToBigInt(executeReport.Message.TokenAmounts[0].Amount.LeBytes[:])) + require.Equal(t, originTokenAmount.DestTokenAddress, cciptypes.UnknownAddress(executeReport.Message.TokenAmounts[0].DestTokenAddress.Bytes())) + require.Equal(t, bytesToUint32LE(originTokenAmount.DestExecData), executeReport.Message.TokenAmounts[0].DestGasAmount) + require.Equal(t, originMsg.Sender, cciptypes.UnknownAddress(executeReport.Message.Sender)) + }) +} diff --git a/core/capabilities/ccip/ccipsolana/extradatadecoder.go b/core/capabilities/ccip/ccipsolana/extradatadecoder.go new file mode 100644 index 00000000000..4b2e72ef34e --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/extradatadecoder.go @@ -0,0 +1,84 @@ +package ccipsolana + +import ( + "encoding/binary" + "fmt" + "reflect" + + "github.com/ethereum/go-ethereum/common/hexutil" + agbinary "github.com/gagliardetto/binary" + + "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_router" +) + +const ( + svmDestExecDataKey = "destGasAmount" +) + +var ( + // tag definition https://github.com/smartcontractkit/chainlink-ccip/blob/1b2ee24da54bddef8f3943dc84102686f2890f87/chains/solana/contracts/programs/ccip-router/src/extra_args.rs#L8C21-L11C45 + // this should be moved to msghasher.go once merged + + // bytes4(keccak256("CCIP SVMExtraArgsV1")); + svmExtraArgsV1Tag = hexutil.MustDecode("0x1f3b3aba") + + // bytes4(keccak256("CCIP EVMExtraArgsV2")); + 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) { + 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)) + } + + var val reflect.Value + var typ reflect.Type + outputMap := make(map[string]any) + switch string(extraArgs[:4]) { + case string(evmExtraArgsV2Tag): + var args ccip_router.EVMExtraArgsV2 + decoder := agbinary.NewBorshDecoder(extraArgs[4:]) + err := args.UnmarshalWithDecoder(decoder) + if err != nil { + return outputMap, fmt.Errorf("failed to decode extra args: %w", err) + } + val = reflect.ValueOf(args) + typ = reflect.TypeOf(args) + case string(svmExtraArgsV1Tag): + var args ccip_router.SVMExtraArgsV1 + decoder := agbinary.NewBorshDecoder(extraArgs[4:]) + err := args.UnmarshalWithDecoder(decoder) + if err != nil { + return outputMap, fmt.Errorf("failed to decode extra args: %w", err) + } + val = reflect.ValueOf(args) + typ = reflect.TypeOf(args) + default: + return nil, fmt.Errorf("unknown extra args tag: %x", extraArgs) + } + + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + fieldValue := val.Field(i).Interface() + outputMap[field.Name] = fieldValue + } + + return outputMap, nil +} + +func DecodeDestExecDataToMap(destExecData []byte) (map[string]any, error) { + return map[string]interface{}{ + svmDestExecDataKey: bytesToUint32LE(destExecData), + }, nil +} + +func bytesToUint32LE(b []byte) uint32 { + if len(b) < 4 { + var padded [4]byte + copy(padded[:len(b)], b) // Pad from the right for little-endian + return binary.LittleEndian.Uint32(padded[:]) + } + + return binary.LittleEndian.Uint32(b) +} diff --git a/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go b/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go new file mode 100644 index 00000000000..8d39bd0cd7a --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/extradatadecoder_test.go @@ -0,0 +1,89 @@ +package ccipsolana + +import ( + "bytes" + "encoding/binary" + "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" +) + +func Test_decodeExtraArgs(t *testing.T) { + 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) + require.NoError(t, err) + + decoded, exist := output[svmDestExecDataKey] + require.True(t, exist) + require.Equal(t, destGasAmount, decoded) + }) + + t.Run("decode extra args into map svm", func(t *testing.T) { + destGasAmount := uint32(10000) + bitmap := uint64(0) + extraArgs := ccip_router.SVMExtraArgsV1{ + ComputeUnits: destGasAmount, + AccountIsWritableBitmap: bitmap, + AllowOutOfOrderExecution: false, + TokenReceiver: config.CcipLogicReceiver, + Accounts: [][32]byte{ + [32]byte(config.CcipLogicReceiver.Bytes()), + [32]byte(config.ReceiverTargetAccountPDA.Bytes()), + [32]byte(solana.SystemProgramID.Bytes()), + }, + } + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err := extraArgs.MarshalWithEncoder(encoder) + require.NoError(t, err) + output, err := DecodeExtraArgsToMap(append(svmExtraArgsV1Tag, buf.Bytes()...)) + require.NoError(t, err) + require.Len(t, output, 5) + + gasLimit, exist := output["ComputeUnits"] + require.True(t, exist) + require.Equal(t, destGasAmount, gasLimit) + + writableBitmap, exist := output["AccountIsWritableBitmap"] + require.True(t, exist) + require.Equal(t, bitmap, writableBitmap) + + ooe, exist := output["AllowOutOfOrderExecution"] + require.True(t, exist) + require.Equal(t, false, ooe) + }) + + t.Run("decode extra args into map evm", func(t *testing.T) { + extraArgs := ccip_router.EVMExtraArgsV2{ + GasLimit: agbinary.Uint128{Lo: 5000, Hi: 0}, + AllowOutOfOrderExecution: false, + } + + var buf bytes.Buffer + encoder := agbinary.NewBorshEncoder(&buf) + err := extraArgs.MarshalWithEncoder(encoder) + require.NoError(t, err) + + output, err := DecodeExtraArgsToMap(append(evmExtraArgsV2Tag, buf.Bytes()...)) + require.NoError(t, err) + require.Len(t, output, 2) + + gasLimit, exist := output["GasLimit"] + require.True(t, exist) + require.Equal(t, agbinary.Uint128{Lo: 5000, Hi: 0}, gasLimit) + + ooe, exist := output["AllowOutOfOrderExecution"] + require.True(t, exist) + require.Equal(t, false, ooe) + }) +} diff --git a/core/capabilities/ccip/ccipsolana/gas_helpers.go b/core/capabilities/ccip/ccipsolana/gas_helpers.go new file mode 100644 index 00000000000..59ab4ff9dcb --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/gas_helpers.go @@ -0,0 +1,22 @@ +package ccipsolana + +import ( + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +func NewGasEstimateProvider() EstimateProvider { + return EstimateProvider{} +} + +type EstimateProvider struct { +} + +// CalculateMerkleTreeGas is not implemented +func (gp EstimateProvider) CalculateMerkleTreeGas(numRequests int) uint64 { + return 0 +} + +// CalculateMessageMaxGas is not implemented. +func (gp EstimateProvider) CalculateMessageMaxGas(msg cciptypes.Message) uint64 { + return 0 +} diff --git a/core/capabilities/ccip/ccipsolana/msghasher.go b/core/capabilities/ccip/ccipsolana/msghasher.go new file mode 100644 index 00000000000..ff595b83848 --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/msghasher.go @@ -0,0 +1,118 @@ +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())}, + }) + } + + 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 +} + +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) diff --git a/core/capabilities/ccip/ccipsolana/msghasher_test.go b/core/capabilities/ccip/ccipsolana/msghasher_test.go new file mode 100644 index 00000000000..9ee22890d5c --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/msghasher_test.go @@ -0,0 +1,139 @@ +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-integrations/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + + 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 +} diff --git a/core/capabilities/ccip/ccipsolana/rmncrypto.go b/core/capabilities/ccip/ccipsolana/rmncrypto.go new file mode 100644 index 00000000000..95987eb09ff --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/rmncrypto.go @@ -0,0 +1,20 @@ +package ccipsolana + +import ( + "context" + "errors" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +// SolanaRMNCrypto is the RMNCrypto implementation for Solana chains. +type SolanaRMNCrypto struct{} + +func (r *SolanaRMNCrypto) VerifyReportSignatures( + _ context.Context, + _ []cciptypes.RMNECDSASignature, + _ cciptypes.RMNReport, + _ []cciptypes.UnknownAddress, +) error { + return errors.New("not implemented") +} diff --git a/core/capabilities/ccip/ccipsolana/tokendata.go b/core/capabilities/ccip/ccipsolana/tokendata.go new file mode 100644 index 00000000000..009b66aedb4 --- /dev/null +++ b/core/capabilities/ccip/ccipsolana/tokendata.go @@ -0,0 +1,18 @@ +package ccipsolana + +import ( + "context" + "errors" + + cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" +) + +type SolanaTokenDataEncoder struct{} + +func NewSolanaTokenDataEncoder() SolanaTokenDataEncoder { + return SolanaTokenDataEncoder{} +} + +func (e SolanaTokenDataEncoder) EncodeUSDC(_ context.Context, message cciptypes.Bytes, attestation cciptypes.Bytes) (cciptypes.Bytes, error) { + return nil, errors.New("not implemented") +} diff --git a/core/capabilities/ccip/common/extradatacodec.go b/core/capabilities/ccip/common/extradatacodec.go index e003a91691d..47146ddd5b6 100644 --- a/core/capabilities/ccip/common/extradatacodec.go +++ b/core/capabilities/ccip/common/extradatacodec.go @@ -1,7 +1,13 @@ package common 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{} @@ -11,11 +17,47 @@ func NewExtraDataCodec() ExtraDataCodec { } func (c ExtraDataCodec) DecodeExtraArgs(extraArgs cciptypes.Bytes, sourceChainSelector cciptypes.ChainSelector) (map[string]any, error) { - // Not implemented and not return error - return nil, nil + if len(extraArgs) == 0 { + // return empty map if extraArgs is empty + return nil, nil + } + + family, err := chainsel.GetSelectorFamily(uint64(sourceChainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get chain family for selector %d: %w", sourceChainSelector, err) + } + + switch family { + case chainsel.FamilyEVM: + return ccipevm.DecodeExtraArgsToMap(extraArgs) + + case chainsel.FamilySolana: + return ccipsolana.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) { - // Not implemented and not return error - return nil, nil + if len(destExecData) == 0 { + // return empty map if destExecData is empty + return nil, nil + } + + family, err := chainsel.GetSelectorFamily(uint64(sourceChainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get chain family for selector %d: %w", sourceChainSelector, err) + } + + switch family { + case chainsel.FamilyEVM: + return ccipevm.DecodeDestExecDataToMap(destExecData) + + case chainsel.FamilySolana: + return ccipsolana.DecodeDestExecDataToMap(destExecData) + + default: + return nil, fmt.Errorf("unsupported family for extra args type %s", family) + } } diff --git a/core/capabilities/ccip/configs/solana/chain_writer.go b/core/capabilities/ccip/configs/solana/chain_writer.go new file mode 100644 index 00000000000..941c04f7c68 --- /dev/null +++ b/core/capabilities/ccip/configs/solana/chain_writer.go @@ -0,0 +1,342 @@ +package solana + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + + idl "github.com/smartcontractkit/chainlink-ccip/chains/solana" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + solanacodec "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" +) + +var ccipRouterIDL = idl.FetchCCIPRouterIDL() + +const ( + destChainSelectorPath = "Info.AbstractReports.Messages.Header.DestChainSelector" + destTokenAddress = "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress" + merkleRootChainSelector = "Info.MerkleRoots.ChainSel" +) + +func getCommitMethodConfig(fromAddress string, routerProgramAddress string, commonAddressesLookupTable solana.PublicKey) chainwriter.MethodConfig { + sysvarInstructionsAddress := solana.SysVarInstructionsPubkey.String() + return chainwriter.MethodConfig{ + FromAddress: fromAddress, + InputModifications: []codec.ModifierConfig{ + &codec.RenameModifierConfig{ + Fields: map[string]string{"ReportContextByteWords": "ReportContext"}, + }, + &codec.RenameModifierConfig{ + Fields: map[string]string{"RawReport": "Report"}, + }, + }, + ChainSpecificName: "commit", + LookupTables: chainwriter.LookupTables{ + StaticLookupTables: []solana.PublicKey{ + commonAddressesLookupTable, + }, + }, + Accounts: []chainwriter.Lookup{ + getRouterAccountConfig(routerProgramAddress), + chainwriter.PDALookups{ + Name: "SourceChainState", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("source_chain_state")}, + {Dynamic: chainwriter.AccountLookup{Location: merkleRootChainSelector}}, + }, + IsSigner: false, + IsWritable: true, + }, + chainwriter.PDALookups{ + Name: "RouterReportAccount", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("commit_report")}, + {Dynamic: chainwriter.AccountLookup{Location: merkleRootChainSelector}}, + {Dynamic: chainwriter.AccountLookup{ + Location: "Info.MerkleRoots.MerkleRoot", + }}, + }, + IsSigner: false, + IsWritable: false, + }, + getAuthorityAccountConstant(fromAddress), + getSystemProgramConstant(), + chainwriter.AccountConstant{ + Name: "SysvarInstructions", + Address: sysvarInstructionsAddress, + IsSigner: true, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "GlobalState", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("state")}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "BillingTokenConfig", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("fee_billing_token_config")}, + {Dynamic: chainwriter.AccountLookup{Location: "Info.TokenPrices.TokenID"}}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "ChainConfigGasPrice", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("dest_chain_state")}, + {Dynamic: chainwriter.AccountLookup{Location: merkleRootChainSelector}}, + }, + IsSigner: false, + IsWritable: false, + }, + }, + DebugIDLocation: "", + } +} + +func getExecuteMethodConfig(fromAddress string, routerProgramAddress string, commonAddressesLookupTable solana.PublicKey) chainwriter.MethodConfig { + sysvarInstructionsAddress := solana.SysVarInstructionsPubkey.String() + return chainwriter.MethodConfig{ + FromAddress: fromAddress, + InputModifications: []codec.ModifierConfig{ + &codec.RenameModifierConfig{ + Fields: map[string]string{"ReportContextByteWords": "ReportContext"}, + }, + &codec.RenameModifierConfig{ + Fields: map[string]string{"RawExecutionReport": "Report"}, + }, + }, + ChainSpecificName: "execute", + ArgsTransform: "CCIP", + LookupTables: chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "PoolLookupTable", + Accounts: chainwriter.PDALookups{ + Name: "TokenAdminRegistry", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Location: destTokenAddress}}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + TypeName: "TokenAdminRegistry", + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: []solana.PublicKey{ + commonAddressesLookupTable, + }, + }, + Accounts: []chainwriter.Lookup{ + getRouterAccountConfig(routerProgramAddress), + chainwriter.PDALookups{ + Name: "SourceChainState", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("source_chain_state")}, + {Dynamic: chainwriter.AccountLookup{Location: destChainSelectorPath}}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "CommitReport", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("external_execution_config")}, + {Dynamic: chainwriter.AccountLookup{Location: destChainSelectorPath}}, + {Dynamic: chainwriter.AccountLookup{ + // The seed is the merkle root of the report, as passed into the input params. + Location: "Info.MerkleRoots.MerkleRoot", + }}, + }, + IsSigner: false, + IsWritable: true, + }, + chainwriter.PDALookups{ + Name: "ExternalExecutionConfig", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("external_execution_config")}, + }, + IsSigner: false, + IsWritable: false, + }, + getAuthorityAccountConstant(fromAddress), + getSystemProgramConstant(), + chainwriter.AccountConstant{ + Name: "SysvarInstructions", + Address: sysvarInstructionsAddress, + IsSigner: true, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "ExternalTokenPoolsSigner", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("external_token_pools_signer")}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountLookup{ + Name: "UserAccounts", + Location: "Info.AbstractReports.Message.ExtraArgsDecoded.Accounts", + IsWritable: chainwriter.MetaBool{BitmapLocation: "Info.AbstractReports.Message.ExtraArgsDecoded.IsWritableBitmap"}, + IsSigner: chainwriter.MetaBool{Value: false}, + }, + chainwriter.PDALookups{ + Name: "ReceiverAssociatedTokenAccount", + PublicKey: chainwriter.AccountConstant{ + Address: solana.SPLAssociatedTokenAccountProgramID.String(), + }, + Seeds: []chainwriter.Seed{ + {Static: []byte(fromAddress)}, + {Dynamic: chainwriter.AccountLookup{Location: "Info.AbstractReports.Messages.Receiver"}}, + {Dynamic: chainwriter.AccountsFromLookupTable{ + LookupTableName: "PoolLookupTable", + IncludeIndexes: []int{6}, + }}, + {Dynamic: chainwriter.AccountLookup{Location: destTokenAddress}}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "PerChainTokenConfig", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("ccip_tokenpool_billing")}, + {Dynamic: chainwriter.AccountLookup{Location: destTokenAddress}}, + {Dynamic: chainwriter.AccountLookup{Location: destChainSelectorPath}}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "PoolChainConfig", + PublicKey: chainwriter.AccountsFromLookupTable{ + LookupTableName: "PoolLookupTable", + IncludeIndexes: []int{2}, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("ccip_tokenpool_billing")}, + {Dynamic: chainwriter.AccountLookup{Location: destTokenAddress}}, + {Dynamic: chainwriter.AccountLookup{Location: destChainSelectorPath}}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountsFromLookupTable{ + LookupTableName: "PoolLookupTable", + IncludeIndexes: []int{}, + }, + }, + DebugIDLocation: "AbstractReport.Message.MessageID", + } +} + +func GetSolanaChainWriterConfig(routerProgramAddress string, commonAddressesLookupTable solana.PublicKey, fromAddress string) (chainwriter.ChainWriterConfig, error) { + // check fromAddress + pk, err := solana.PublicKeyFromBase58(fromAddress) + if err != nil { + return chainwriter.ChainWriterConfig{}, fmt.Errorf("invalid from address %s: %w", fromAddress, err) + } + + if pk.IsZero() { + return chainwriter.ChainWriterConfig{}, errors.New("from address cannot be empty") + } + + // validate CCIP Router IDL, errors not expected + var idl solanacodec.IDL + if err = json.Unmarshal([]byte(ccipRouterIDL), &idl); err != nil { + return chainwriter.ChainWriterConfig{}, fmt.Errorf("unexpected error: invalid CCIP Router IDL, error: %w", err) + } + + // solConfig references the ccip_example_config.go from github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter, which is currently subject to change + solConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "ccip-router": { + Methods: map[string]chainwriter.MethodConfig{ + "execute": getExecuteMethodConfig(fromAddress, routerProgramAddress, commonAddressesLookupTable), + "commit": getCommitMethodConfig(fromAddress, routerProgramAddress, commonAddressesLookupTable), + }, + IDL: ccipRouterIDL}, + }, + } + + return solConfig, nil +} + +func getRouterAccountConfig(routerProgramAddress string) chainwriter.PDALookups { + return chainwriter.PDALookups{ + Name: "RouterAccountConfig", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("config")}, + }, + IsSigner: false, + IsWritable: false, + } +} + +func getAuthorityAccountConstant(fromAddress string) chainwriter.AccountConstant { + return chainwriter.AccountConstant{ + Name: "Authority", + Address: fromAddress, + IsSigner: true, + IsWritable: true, + } +} + +func getSystemProgramConstant() chainwriter.AccountConstant { + return chainwriter.AccountConstant{ + Name: "SystemProgram", + Address: solana.SystemProgramID.String(), + IsSigner: false, + IsWritable: false, + } +} diff --git a/core/capabilities/ccip/configs/solana/chain_writer_test.go b/core/capabilities/ccip/configs/solana/chain_writer_test.go new file mode 100644 index 00000000000..3915185ab06 --- /dev/null +++ b/core/capabilities/ccip/configs/solana/chain_writer_test.go @@ -0,0 +1,39 @@ +package solana + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" +) + +func TestChainWriterConfigRaw(t *testing.T) { + tests := []struct { + name string + fromAddress string + expectedError string + }{ + { + name: "valid input", + fromAddress: "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6A", + expectedError: "", + }, + { + name: "zero fromAddress", + fromAddress: "", + expectedError: "invalid from address : decode: zero length string", + }, + } + + commonAddressesLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := GetSolanaChainWriterConfig("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H", commonAddressesLookupTable, tt.fromAddress) + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/core/capabilities/ccip/configs/solana/contract_reader.go b/core/capabilities/ccip/configs/solana/contract_reader.go new file mode 100644 index 00000000000..39d9efbfbf7 --- /dev/null +++ b/core/capabilities/ccip/configs/solana/contract_reader.go @@ -0,0 +1,19 @@ +package solana + +import ( + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" +) + +var DestReaderConfig = config.ContractReader{} // TODO update the Dest chain reader configuration +var SourceReaderConfig = config.ContractReader{} // TODO update the Source chain reader configuration + +func MergeReaderConfigs(configs ...config.ContractReader) config.ContractReader { + allNamespaces := make(map[string]config.ChainContractReader) + for _, c := range configs { + for namespace, method := range c.Namespaces { + allNamespaces[namespace] = method + } + } + + return config.ContractReader{Namespaces: allNamespaces} +} diff --git a/core/capabilities/ccip/delegate.go b/core/capabilities/ccip/delegate.go index 30f2e740f84..02d5688868c 100644 --- a/core/capabilities/ccip/delegate.go +++ b/core/capabilities/ccip/delegate.go @@ -51,6 +51,10 @@ type RelayGetter interface { GetIDToRelayerMap() (map[types.RelayID]loop.Relayer, error) } +type Keystore[K keystore.Key] interface { + GetAll() ([]K, error) +} + type Delegate struct { lggr logger.Logger registrarConfig plugins.RegistrarConfig @@ -280,46 +284,67 @@ func (d *Delegate) getOCRKeys(ocrKeyBundleIDs job.JSONConfig) (map[string]ocr2ke return ocrKeys, nil } +func getKeys[K keystore.Key](ks Keystore[K]) ([]string, error) { + result := make([]string, 0) + + keys, err := ks.GetAll() + if err != nil { + return nil, fmt.Errorf("error getting all keys: %w", err) + } + + for _, key := range keys { + result = append(result, key.ID()) + } + + return result, nil +} + func (d *Delegate) getTransmitterKeys(ctx context.Context, relayIDs []types.RelayID) (map[types.RelayID][]string, error) { transmitterKeys := make(map[types.RelayID][]string) for _, relayID := range relayIDs { + chainID, ok := new(big.Int).SetString(relayID.ChainID, 10) + if !ok { + return nil, fmt.Errorf("error parsing chain ID, expected big int: %s", relayID.ChainID) + } + + var keys []string + var err error switch relayID.Network { - case relay.NetworkSolana: - solKeys, err := d.keystore.Solana().GetAll() - if err != nil { - return nil, fmt.Errorf("error getting enabled addresses for chain: %s %w", relayID.ChainID, err) - } - - transmitterKeys[relayID] = func() (r []string) { - for _, key := range solKeys { - r = append(r, key.PublicKey().String()) - } - return - }() case relay.NetworkEVM: - chainID, ok := new(big.Int).SetString(relayID.ChainID, 10) - if !ok { - return nil, fmt.Errorf("error parsing chain ID, expected big int: %s", relayID.ChainID) - } - - ethKeys, err := d.keystore.Eth().EnabledAddressesForChain(ctx, chainID) - if err != nil { - return nil, fmt.Errorf("error getting enabled addresses for chain: %s %w", chainID.String(), err) - } - - transmitterKeys[relayID] = func() (r []string) { - for _, key := range ethKeys { - r = append(r, key.Hex()) - } - return - }() + keys, err = d.getEVMKeys(ctx, chainID) + case relay.NetworkSolana: + keys, err = getKeys(d.keystore.Solana()) + case relay.NetworkAptos: + keys, err = getKeys(d.keystore.Aptos()) + case relay.NetworkCosmos: + keys, err = getKeys(d.keystore.Cosmos()) + case relay.NetworkStarkNet: + keys, err = getKeys(d.keystore.StarkNet()) default: return nil, fmt.Errorf("unsupported network: %s", relayID.Network) } + + if err != nil { + return nil, err + } + transmitterKeys[relayID] = keys } return transmitterKeys, nil } +func (d *Delegate) getEVMKeys(ctx context.Context, chainID *big.Int) ([]string, error) { + result := make([]string, 0) + ethKeys, err := d.keystore.Eth().EnabledAddressesForChain(ctx, chainID) + if err != nil { + return result, fmt.Errorf("error getting enabled addresses for chain: %s %w", chainID.String(), err) + } + + for _, key := range ethKeys { + result = append(result, key.Hex()) + } + return result, nil +} + func (d *Delegate) getHomeChainContractReader( ctx context.Context, homeChainRelayer loop.Relayer, diff --git a/core/capabilities/ccip/oraclecreator/plugin.go b/core/capabilities/ccip/oraclecreator/plugin.go index b779e82e493..0c577e7ef62 100644 --- a/core/capabilities/ccip/oraclecreator/plugin.go +++ b/core/capabilities/ccip/oraclecreator/plugin.go @@ -9,16 +9,24 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/gagliardetto/solana-go" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink/v2/core/services/relay" + "github.com/smartcontractkit/libocr/commontypes" libocr3 "github.com/smartcontractkit/libocr/offchainreporting2plus" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipsolana" + solanaconfig "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/solana" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/promwrapper" commitocr3 "github.com/smartcontractkit/chainlink-ccip/commit" "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn" @@ -29,8 +37,8 @@ import ( "github.com/smartcontractkit/chainlink-ccip/pluginconfig" "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/types" - - "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" evmconfig "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/configs/evm" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ocrimpls" @@ -38,7 +46,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr3/promwrapper" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" evmrelaytypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" @@ -166,7 +173,7 @@ func (i *pluginOracleCreator) Create(ctx context.Context, donID uint32, config c // TODO: Extract the correct transmitter address from the destsFromAccount factory, transmitter, err := i.createFactoryAndTransmitter( - donID, config, destRelayID, contractReaders, chainWriters, destChainWriter, destFromAccounts, publicConfig) + donID, config, destRelayID, contractReaders, chainWriters, destChainWriter, destFromAccounts, publicConfig, destChainFamily) if err != nil { return nil, fmt.Errorf("failed to create factory and transmitter: %w", err) } @@ -184,7 +191,7 @@ func (i *pluginOracleCreator) Create(ctx context.Context, donID uint32, config c i.lggr. Named(fmt.Sprintf("CCIP%sOCR3", pluginType.String())). Named(destRelayID.String()). - Named(hexutil.Encode(config.Config.OfframpAddress)), + Named(encodeOffRampAddr(config.Config.OfframpAddress, destChainFamily, false)), false, func(ctx context.Context, msg string) {}), MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"name": fmt.Sprintf("commit-%d", config.Config.ChainSelector)}, prometheus.DefaultRegisterer), @@ -214,6 +221,23 @@ func (i *pluginOracleCreator) Create(ctx context.Context, donID uint32, config c return newWrappedOracle(oracle, closers), nil } +func encodeOffRampAddr(addr []byte, chainFamily string, checkSum bool) string { + var offRampAddr string + switch chainFamily { + case relay.NetworkEVM: + offRampAddr = common.BytesToAddress(addr).Hex() + if !checkSum { + offRampAddr = hexutil.Encode(addr) + } + case relay.NetworkSolana: + offRampAddr = solana.PublicKeyFromBytes(addr).String() + default: + panic(fmt.Errorf("unsupported chain family: %s", chainFamily)) + } + + return offRampAddr +} + type plugin struct { CommitPluginCodec cciptypes.CommitPluginCodec ExecutePluginCodec cciptypes.ExecutePluginCodec @@ -235,12 +259,12 @@ var plugins = map[string]plugin{ RMNCrypto: func(lggr logger.Logger) cciptypes.RMNCrypto { return ccipevm.NewEVMRMNCrypto(lggr) }, }, chainsel.FamilySolana: { - CommitPluginCodec: nil, - ExecutePluginCodec: nil, + CommitPluginCodec: ccipsolana.NewCommitPluginCodecV1(), + ExecutePluginCodec: ccipsolana.NewExecutePluginCodecV1(), ExtraArgsCodec: ccipcommon.NewExtraDataCodec(), - MessageHasher: func(lggr logger.Logger) cciptypes.MessageHasher { return nil }, - TokenDataEncoder: nil, - GasEstimateProvider: nil, + 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 }, }, } @@ -254,6 +278,7 @@ func (i *pluginOracleCreator) createFactoryAndTransmitter( destChainWriter types.ContractWriter, destFromAccounts []string, publicConfig ocr3confighelper.PublicConfig, + destChainFamily string, ) (ocr3types.ReportingPluginFactory[[]byte], ocr3types.ContractTransmitter[[]byte], error) { var factory ocr3types.ReportingPluginFactory[[]byte] var transmitter ocr3types.ContractTransmitter[[]byte] @@ -289,14 +314,13 @@ func (i *pluginOracleCreator) createFactoryAndTransmitter( ) rmnCrypto := plugin.RMNCrypto(i.lggr.Named(chainFamily).Named("RMNCrypto")) - factory = commitocr3.NewCommitPluginFactory( commitocr3.CommitPluginFactoryParams{ Lggr: i.lggr. Named("CCIPCommitPlugin"). Named(destRelayID.String()). Named(fmt.Sprintf("%d", config.Config.ChainSelector)). - Named(hexutil.Encode(config.Config.OfframpAddress)), + Named(encodeOffRampAddr(config.Config.OfframpAddress, destChainFamily, false)), DonID: donID, OcrConfig: ccipreaderpkg.OCR3ConfigWithMeta(config), CommitCodec: plugin.CommitPluginCodec, @@ -307,12 +331,11 @@ func (i *pluginOracleCreator) createFactoryAndTransmitter( ContractReaders: contractReaders, ContractWriters: chainWriters, RmnPeerClient: rmnPeerClient, - RmnCrypto: rmnCrypto, - }) + RmnCrypto: rmnCrypto}) factory = promwrapper.NewReportingPluginFactory[[]byte](factory, i.lggr, chainID, "CCIPCommit") transmitter = ocrimpls.NewCommitContractTransmitter(destChainWriter, ocrtypes.Account(destFromAccounts[0]), - hexutil.Encode(config.Config.OfframpAddress), // TODO: this works for evm only, how about non-evm? + encodeOffRampAddr(config.Config.OfframpAddress, destChainFamily, false), ) } else if config.Config.PluginType == uint8(cctypes.PluginTypeCCIPExec) { factory = execocr3.NewExecutePluginFactory( @@ -320,7 +343,7 @@ func (i *pluginOracleCreator) createFactoryAndTransmitter( Lggr: i.lggr. Named("CCIPExecPlugin"). Named(destRelayID.String()). - Named(hexutil.Encode(config.Config.OfframpAddress)), + Named(encodeOffRampAddr(config.Config.OfframpAddress, destChainFamily, false)), DonID: donID, OcrConfig: ccipreaderpkg.OCR3ConfigWithMeta(config), ExecCodec: plugin.ExecutePluginCodec, @@ -335,7 +358,7 @@ func (i *pluginOracleCreator) createFactoryAndTransmitter( factory = promwrapper.NewReportingPluginFactory[[]byte](factory, i.lggr, chainID, "CCIPExec") transmitter = ocrimpls.NewExecContractTransmitter(destChainWriter, ocrtypes.Account(destFromAccounts[0]), - hexutil.Encode(config.Config.OfframpAddress), // TODO: this works for evm only, how about non-evm? + encodeOffRampAddr(config.Config.OfframpAddress, destChainFamily, false), ) } else { return nil, nil, fmt.Errorf("unsupported plugin type %d", config.Config.PluginType) @@ -385,7 +408,7 @@ func (i *pluginOracleCreator) createReadersAndWriters( return nil, nil, fmt.Errorf("failed to get chain selector from chain ID %s: %w", chainID, err1) } - chainReaderConfig, err1 := getChainReaderConfig(i.lggr, chainID, destChainID, homeChainID, ofc, chainSelector) + chainReaderConfig, err1 := getChainReaderConfig(i.lggr, chainID, destChainID, homeChainID, ofc, chainSelector, destChainFamily) if err1 != nil { return nil, nil, fmt.Errorf("failed to get chain reader config: %w", err1) } @@ -396,15 +419,15 @@ func (i *pluginOracleCreator) createReadersAndWriters( } if chainID == destChainID && destChainFamily == relayChainFamily { - offrampAddressHex := common.BytesToAddress(config.Config.OfframpAddress).Hex() + offrampAddress := encodeOffRampAddr(config.Config.OfframpAddress, relayChainFamily, true) err2 := cr.Bind(ctx, []types.BoundContract{ { - Address: offrampAddressHex, + Address: offrampAddress, Name: consts.ContractNameOffRamp, }, }) if err2 != nil { - return nil, nil, fmt.Errorf("failed to bind chain reader for dest chain %s's offramp at %s: %w", chainID, offrampAddressHex, err) + return nil, nil, fmt.Errorf("failed to bind chain reader for dest chain %s's offramp at %s: %w", chainID, offrampAddress, err) } } @@ -470,35 +493,57 @@ func getChainReaderConfig( homeChainID string, ofc offChainConfig, chainSelector cciptypes.ChainSelector, + chainFamily string, ) ([]byte, error) { - var chainReaderConfig evmrelaytypes.ChainReaderConfig - if chainID == destChainID { - chainReaderConfig = evmconfig.DestReaderConfig - } else { - chainReaderConfig = evmconfig.SourceReaderConfig - } + // TODO: create a chain writer constructor interface and define family specific implementations in oraclecreator.plugin + switch chainFamily { + case relay.NetworkEVM: + var chainReaderConfig evmrelaytypes.ChainReaderConfig + if chainID == destChainID { + chainReaderConfig = evmconfig.DestReaderConfig + } else { + chainReaderConfig = evmconfig.SourceReaderConfig + } - if !ofc.commitEmpty() && ofc.commit().PriceFeedChainSelector == chainSelector { - lggr.Debugw("Adding feed reader config", "chainID", chainID) - chainReaderConfig = evmconfig.MergeReaderConfigs(chainReaderConfig, evmconfig.FeedReaderConfig) - } + if !ofc.commitEmpty() && ofc.commit().PriceFeedChainSelector == chainSelector { + lggr.Debugw("Adding feed reader config", "chainID", chainID) + chainReaderConfig = evmconfig.MergeReaderConfigs(chainReaderConfig, evmconfig.FeedReaderConfig) + } - if isUSDCEnabled(ofc) { - lggr.Debugw("Adding USDC reader config", "chainID", chainID) - chainReaderConfig = evmconfig.MergeReaderConfigs(chainReaderConfig, evmconfig.USDCReaderConfig) - } + if isUSDCEnabled(ofc) { + lggr.Debugw("Adding USDC reader config", "chainID", chainID) + chainReaderConfig = evmconfig.MergeReaderConfigs(chainReaderConfig, evmconfig.USDCReaderConfig) + } - if chainID == homeChainID { - lggr.Debugw("Adding home chain reader config", "chainID", chainID) - chainReaderConfig = evmconfig.MergeReaderConfigs(chainReaderConfig, evmconfig.HomeChainReaderConfigRaw) - } + if chainID == homeChainID { + lggr.Debugw("Adding home chain reader config", "chainID", chainID) + chainReaderConfig = evmconfig.MergeReaderConfigs(chainReaderConfig, evmconfig.HomeChainReaderConfigRaw) + } - marshaledConfig, err := json.Marshal(chainReaderConfig) - if err != nil { - return nil, fmt.Errorf("failed to marshal chain reader config: %w", err) - } + marshaledConfig, err := json.Marshal(chainReaderConfig) + if err != nil { + return nil, fmt.Errorf("failed to marshal chain reader config: %w", err) + } - return marshaledConfig, nil + return marshaledConfig, nil + case relay.NetworkSolana: + // TODO update chain reader config in contract_reader.go + var cfg config.ContractReader + if chainID == destChainID { + cfg = solanaconfig.DestReaderConfig + } else { + cfg = solanaconfig.SourceReaderConfig + } + + marshaledConfig, err := json.Marshal(cfg) + if err != nil { + return nil, fmt.Errorf("failed to marshal chain reader config: %w", err) + } + + return marshaledConfig, nil + default: + return nil, fmt.Errorf("unsupported chain family %s", chainFamily) + } } func isUSDCEnabled(ofc offChainConfig) bool { @@ -517,25 +562,37 @@ func createChainWriter( execBatchGasLimit uint64, chainFamily string, ) (types.ContractWriter, error) { - var fromAddress common.Address + var err error + var chainWriterConfig []byte transmitter, ok := transmitters[types.NewRelayID(chainFamily, chainID)] - if ok { - // TODO: remove EVM-specific stuff - fromAddress = common.HexToAddress(transmitter[0]) - } - - chainWriterRawConfig, err := evmconfig.ChainWriterConfigRaw( - fromAddress, - defaultCommitGasLimit, - execBatchGasLimit, - ) - if err != nil { - return nil, fmt.Errorf("failed to create chain writer config: %w", err) - } - - chainWriterConfig, err := json.Marshal(chainWriterRawConfig) - if err != nil { - return nil, fmt.Errorf("failed to marshal chain writer config: %w", err) + // TODO: create a chain writer constructor interface and define family specific implementations in oraclecreator.plugin + switch chainFamily { + case relay.NetworkSolana: + var solConfig chainwriter.ChainWriterConfig + // TODO once on-chain account lookup address are available, the routerProgramAddress and commonAddressesLookupTable should be provided from tooling config, and populated here for the params. + if solConfig, err = solanaconfig.GetSolanaChainWriterConfig("", solana.PublicKey{}, transmitter[0]); err == nil { + return nil, fmt.Errorf("failed to get Solana chain writer config: %w", err) + } + if chainWriterConfig, err = json.Marshal(solConfig); err != nil { + return nil, fmt.Errorf("failed to marshal Solana chain writer config: %w", err) + } + case relay.NetworkEVM: + var evmConfig evmrelaytypes.ChainWriterConfig + fromAddress := common.Address{} + if ok { + fromAddress = common.HexToAddress(transmitter[0]) + } + if evmConfig, err = evmconfig.ChainWriterConfigRaw( + fromAddress, + defaultCommitGasLimit, + execBatchGasLimit); err != nil { + return nil, fmt.Errorf("failed to create EVM chain writer config: %w", err) + } + if chainWriterConfig, err = json.Marshal(evmConfig); err != nil { + return nil, fmt.Errorf("failed to marshal EVM chain writer config: %w", err) + } + default: + return nil, fmt.Errorf("unknown chain family %s", chainFamily) } cw, err := relayer.NewContractWriter(ctx, chainWriterConfig) diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 1a749a6fc0d..5a6fd3fcd6e 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -317,7 +317,7 @@ require ( github.com/smartcontractkit/ccip-owner-contracts v0.0.0-salt-fix // indirect github.com/smartcontractkit/chain-selectors v1.0.40 // indirect github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b // indirect github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc // indirect github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250205171936-649f95193678 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 22157171f21..d5db091c64a 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1240,8 +1240,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-20250205140756-e0f1a86dfdb3 h1:1cStGzG8OalVpo2EGPMzrWgTdt1gJbEzQiHknRTRAYQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 h1:dw2d6UyvnCGFym2cRYsJfGdHNPa/iGKkrC+ppB3mKWo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847/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.20250130202959-6f1f48342e36 h1:bS51NFGHVjkCy7yu9L2Ss4sBsCW6jpa5GuhRAdWWxzM= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68= github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc h1:WZERXv2hTYRA0NpWg79ci/ZZSxucmvkty39iUOV8d7I= diff --git a/deployment/ccip/changeset/cs_deploy_chain.go b/deployment/ccip/changeset/cs_deploy_chain.go index 1118e7093c2..2aba98478e1 100644 --- a/deployment/ccip/changeset/cs_deploy_chain.go +++ b/deployment/ccip/changeset/cs_deploy_chain.go @@ -569,8 +569,6 @@ func initializeRouter(e deployment.Environment, chain deployment.SolChain, ccipR instruction, err := solRouter.NewInitializeInstruction( chain.Selector, // chain selector - deployment.SolDefaultGasLimit, // default gas limit - true, // allow out of order execution EnableExecutionAfter, // period to wait before allowing manual execution solana.PublicKey{}, // fee aggregator (TODO: changeset to set the fee aggregator) linkTokenAddress, // link token mint diff --git a/deployment/go.mod b/deployment/go.mod index 86600ed49ba..e8223c44ee1 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -32,7 +32,7 @@ require ( github.com/smartcontractkit/ccip-owner-contracts v0.0.0-salt-fix github.com/smartcontractkit/chain-selectors v1.0.40 github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3 - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205171936-649f95193678 github.com/smartcontractkit/chainlink-integrations/evm v0.0.0-20250206144234-88579df97ecd diff --git a/deployment/go.sum b/deployment/go.sum index 2890ff56eac..5cde6957e12 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1238,8 +1238,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-20250205140756-e0f1a86dfdb3 h1:1cStGzG8OalVpo2EGPMzrWgTdt1gJbEzQiHknRTRAYQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 h1:dw2d6UyvnCGFym2cRYsJfGdHNPa/iGKkrC+ppB3mKWo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847/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.20250130202959-6f1f48342e36 h1:bS51NFGHVjkCy7yu9L2Ss4sBsCW6jpa5GuhRAdWWxzM= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68= github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc h1:WZERXv2hTYRA0NpWg79ci/ZZSxucmvkty39iUOV8d7I= diff --git a/go.mod b/go.mod index 59f328e69f2..4b47cdc2788 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/ethereum/go-ethereum v1.14.11 github.com/fatih/color v1.17.0 github.com/fxamacker/cbor/v2 v2.7.0 + github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/solana-go v1.12.0 github.com/getsentry/sentry-go v0.27.0 github.com/gin-contrib/cors v1.7.2 @@ -78,6 +79,7 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.40 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3 + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36 github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 @@ -127,6 +129,8 @@ require ( k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 ) +require github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 // indirect + require ( cel.dev/expr v0.17.0 // indirect cloud.google.com/go/auth v0.9.9 // indirect @@ -161,7 +165,6 @@ require ( github.com/blendle/zapdriver v1.3.1 // indirect github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 // indirect github.com/bytedance/sonic v1.12.3 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect @@ -206,7 +209,6 @@ require ( github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.6 // indirect - github.com/gagliardetto/binary v0.8.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gagliardetto/utilz v0.1.1 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect @@ -326,7 +328,6 @@ require ( github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/sethvargo/go-retry v0.2.4 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a // indirect github.com/smartcontractkit/chainlink-protos/svr v0.0.0-20250123084029-58cce9b32112 github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index 95488c18818..9ae05423f0a 100644 --- a/go.sum +++ b/go.sum @@ -1117,8 +1117,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-20250205140756-e0f1a86dfdb3 h1:1cStGzG8OalVpo2EGPMzrWgTdt1gJbEzQiHknRTRAYQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a h1:1MrD2OiP/CRfyBSwTQE66R1+gLWBgWcU/SYl/+DmZ/Y= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a/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.20250130202959-6f1f48342e36 h1:bS51NFGHVjkCy7yu9L2Ss4sBsCW6jpa5GuhRAdWWxzM= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68= github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc h1:WZERXv2hTYRA0NpWg79ci/ZZSxucmvkty39iUOV8d7I= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 88d294df11c..5296bf8c9af 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -437,7 +437,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.0.0-salt-fix // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b // indirect github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc // indirect github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 15d5992fe75..6a8c456dca1 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1435,8 +1435,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-20250205140756-e0f1a86dfdb3 h1:1cStGzG8OalVpo2EGPMzrWgTdt1gJbEzQiHknRTRAYQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 h1:dw2d6UyvnCGFym2cRYsJfGdHNPa/iGKkrC+ppB3mKWo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847/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.20250130202959-6f1f48342e36 h1:bS51NFGHVjkCy7yu9L2Ss4sBsCW6jpa5GuhRAdWWxzM= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68= github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc h1:WZERXv2hTYRA0NpWg79ci/ZZSxucmvkty39iUOV8d7I= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index c831eb7cf7d..f4e8efc4df3 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -421,7 +421,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.0.0-salt-fix // indirect github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250130162116-1b2ee24da54b // indirect github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc // indirect github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 9b5ae5a5c6a..97425850903 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1422,8 +1422,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-20250205140756-e0f1a86dfdb3 h1:1cStGzG8OalVpo2EGPMzrWgTdt1gJbEzQiHknRTRAYQ= github.com/smartcontractkit/chainlink-ccip v0.0.0-20250205140756-e0f1a86dfdb3/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847 h1:dw2d6UyvnCGFym2cRYsJfGdHNPa/iGKkrC+ppB3mKWo= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128193522-bdbfcc588847/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.20250130202959-6f1f48342e36 h1:bS51NFGHVjkCy7yu9L2Ss4sBsCW6jpa5GuhRAdWWxzM= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68= github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc h1:WZERXv2hTYRA0NpWg79ci/ZZSxucmvkty39iUOV8d7I= diff --git a/integration-tests/smoke/ccip/ccip_reader_test.go b/integration-tests/smoke/ccip/ccip_reader_test.go index 38238adc803..b7f80289eaa 100644 --- a/integration-tests/smoke/ccip/ccip_reader_test.go +++ b/integration-tests/smoke/ccip/ccip_reader_test.go @@ -20,8 +20,10 @@ import ( ccipcommon "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/common" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/ccip/changeset/testhelpers" + "github.com/smartcontractkit/chainlink/v2/core/utils/testutils/heavyweight" + + "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/environment/memory" "github.com/smartcontractkit/chainlink/integration-tests/utils/pgtest" @@ -52,14 +54,14 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/utils/testutils/heavyweight" ) const ( - chainS1 = cciptypes.ChainSelector(1) - chainS2 = cciptypes.ChainSelector(2) - chainS3 = cciptypes.ChainSelector(3) - chainD = cciptypes.ChainSelector(4) + chainS1 = cciptypes.ChainSelector(1) + chainS2 = cciptypes.ChainSelector(2) + chainS3 = cciptypes.ChainSelector(3) + chainD = cciptypes.ChainSelector(4) + chainSEVM = cciptypes.ChainSelector(5009297550715157269) ) var ( @@ -116,10 +118,10 @@ func setupExecutedMessagesTest(ctx context.Context, t testing.TB, useHeavyDB boo }) } -func setupMsgsBetweenSeqNumsTest(ctx context.Context, t testing.TB, useHeavyDB bool) *testSetupData { +func setupMsgsBetweenSeqNumsTest(ctx context.Context, t testing.TB, useHeavyDB bool, sourceChainSel cciptypes.ChainSelector) *testSetupData { sb, auth := setupSimulatedBackendAndAuth(t) return testSetup(ctx, t, testSetupParams{ - ReaderChain: chainS1, + ReaderChain: sourceChainSel, DestChain: chainD, OnChainSeqNums: nil, Cfg: evmconfig.SourceReaderConfig, @@ -569,11 +571,11 @@ func TestCCIPReader_MsgsBetweenSeqNums(t *testing.T) { t.Parallel() ctx := tests.Context(t) - s := setupMsgsBetweenSeqNumsTest(ctx, t, false) + s := setupMsgsBetweenSeqNumsTest(ctx, t, false, chainSEVM) _, err := s.contract.EmitCCIPMessageSent(s.auth, uint64(chainD), ccip_reader_tester.InternalEVM2AnyRampMessage{ Header: ccip_reader_tester.InternalRampMessageHeader{ MessageId: [32]byte{1, 0, 0, 0, 0}, - SourceChainSelector: uint64(chainS1), + SourceChainSelector: uint64(chainSEVM), DestChainSelector: uint64(chainD), SequenceNumber: 10, }, @@ -591,7 +593,7 @@ func TestCCIPReader_MsgsBetweenSeqNums(t *testing.T) { _, err = s.contract.EmitCCIPMessageSent(s.auth, uint64(chainD), ccip_reader_tester.InternalEVM2AnyRampMessage{ Header: ccip_reader_tester.InternalRampMessageHeader{ MessageId: [32]byte{1, 0, 0, 0, 1}, - SourceChainSelector: uint64(chainS1), + SourceChainSelector: uint64(chainSEVM), DestChainSelector: uint64(chainD), SequenceNumber: 15, }, @@ -616,7 +618,7 @@ func TestCCIPReader_MsgsBetweenSeqNums(t *testing.T) { require.Eventually(t, func() bool { msgs, err = s.reader.MsgsBetweenSeqNums( ctx, - chainS1, + chainSEVM, cciptypes.NewSeqNumRange(5, 20), ) require.NoError(t, err) @@ -641,7 +643,7 @@ func TestCCIPReader_MsgsBetweenSeqNums(t *testing.T) { require.Equal(t, int64(4), msgs[1].TokenAmounts[1].Amount.Int64()) for _, msg := range msgs { - require.Equal(t, chainS1, msg.Header.SourceChainSelector) + require.Equal(t, chainSEVM, msg.Header.SourceChainSelector) require.Equal(t, chainD, msg.Header.DestChainSelector) } } @@ -1274,7 +1276,7 @@ func Benchmark_CCIPReader_MessageSentRanges(b *testing.B) { func benchmarkMessageSentRanges(b *testing.B, logsInserted int, startSeqNum, endSeqNum cciptypes.SeqNum) { // Initialize test setup ctx := tests.Context(b) - s := setupMsgsBetweenSeqNumsTest(ctx, b, true) + s := setupMsgsBetweenSeqNumsTest(ctx, b, true, chainS1) expectedRangeLen := calculateExpectedRangeLen(logsInserted, startSeqNum, endSeqNum) err := s.extendedCR.Bind(ctx, []types.BoundContract{