Arbitrary Messaging: EVM to SVM

This tutorial demonstrates how to send arbitrary data from an Ethereum Virtual Machine (EVM) chain to a program on a SVM chain using Chainlink's Cross-Chain Interoperability Protocol (CCIP). You will learn how to configure CCIP messages that trigger a program execution on the destination chain.

Introduction

This tutorial shows you how to send data-only messages from Ethereum Sepolia to a receiver program on Solana devnet.

What You will Build

In this tutorial, you will:

  • Configure a CCIP message for arbitrary data messaging
  • Send data from Ethereum Sepolia to a Solana program
  • Pay for CCIP transaction fees using LINK tokens
  • Verify the data was received and processed by the program

Prerequisites

Before starting EVM to SVM tutorials, ensure you have:

Development Environment

  • Node.js v20 or higher. You can use the nvm package to install and switch between Node.js versions. Once installed, you can verify the installation by running node -v in your terminal:

    node -v
    

    Example output:

    $ node -v
    v23.11.0
    
  • Git for cloning the repository.

  • Yarn for installing dependencies.

  • Anchor and Solana CLI Tools: Install Anchor and Solana CLI Tools following the installation guide. This requires Rust to be installed.

Wallets

  • An Ethereum wallet with a private key. If you use MetaMask, you can follow this guide to export your private key.

RPC URLs

Get an Ethereum RPC URL. You can sign up for a personal endpoint from Alchemy, Infura, or another node provider service.

Setup Instructions

  • Clone the repository:

    git clone https://github.com/smartcontractkit/solana-starter-kit.git && cd solana-starter-kit
    
  • Checkout the ccip branch:

    git checkout ccip
    
  • Install dependencies:

    yarn install
    
  • Create a .env file in the project root based on the provided example:

    EVM_PRIVATE_KEY=your_private_key_here
    EVM_RPC_URL=your_rpc_url_here
    
  • Replace the EVM_PRIVATE_KEY and EVM_RPC_URL values with your own Ethereum private key and RPC URL that you obtained in the previous steps.

Tokens for Testing

You'll need the following tokens on Ethereum Sepolia testnet to complete the tutorials:

TokenPurposeHow to Obtain
ETHTransaction gas feesChainlink Faucet or alternative sources such as the Google Cloud Faucet
LINKCCIP feesChainlink Faucet

Understanding Arbitrary Messaging to SVM

This tutorial focuses on arbitrary messaging from EVM chains to SVM programs. For detailed information about CCIP message structure and parameters, refer to the guide on building CCIP messages from EVM to SVM.

Key Points Specific to Arbitrary Messaging

  • Program Execution: Messages trigger program execution on the destination chain
  • Mandatory Settings:
    • computeUnits must be sufficient for the execution of ccip_receive instruction of the receiver program
    • receiver field should be set to the Solana program ID
    • tokenReceiver is typically set to the default PublicKey (11111111111111111111111111111111)
    • Required accounts must be specified in the accounts array
    • accountIsWritableBitmap must correctly identify which accounts should be writable
    • allowOutOfOrderExecution must be true

For more details on the CCIP message structure, extraArgs, and how the SVM account model works, refer to the guide on building CCIP messages from EVM to SVM.

Key Differences from Token Transfers

Token Transfers:

  • Simply move assets between chains
  • No program execution on destination

Arbitrary Messaging:

  • Sends data for processing by a program
  • Triggers program execution on destination
  • Can update program state or perform complex logic

The CCIP Basic Receiver Program

This tutorial uses the Basic Receiver program deployed on Solana Devnet as the destination program.

Program Interface Overview

The Basic Receiver program implements the CCIP receiver interface, which requires a ccip_receive instruction. This instruction is called by the CCIP offramp when a cross-chain message arrives. Key aspects include:

  • The program verifies the caller (must be the trusted CCIP offramp)
  • It deserializes and validates the incoming message
  • The message data is stored in the program's storage PDA
  • The program maintains a message counter to track received messages
  • Received messages can be accessed using helper methods provided in the program

Understanding Program Derived Addresses (PDAs)

To interact with SVM programs, you need to understand Program Derived Addresses (PDAs):

  • PDAs are deterministic addresses derived from seeds and a program ID
  • They provide onchain storage for Solana programs
  • Each PDA serves a specific purpose
  • When sending messages to a program, you must specify all accounts (including PDAs) that the program requires

For the CCIP Basic Receiver program, we need two main PDAs:

  1. State PDA:

    • Contains essential program settings like the router address and owner
    • Derived using the seed "state"
    • Controlled by the program owner/authority
    • The State PDA is critical for validating incoming messages in the ccip_receive instruction. It stores the trusted router program ID, which is used to authenticate the caller
    • When a message is received, the program checks if the caller is an authorized CCIP router by validating against the router address stored in this PDA
  2. Messages Storage PDA:

    • Contains only the most recent cross-chain message
    • Derived using the seed "messages"
    • Maintains a message counter and last updated timestamp
    • Stores message such as the message ID, type, and data payload

Implementing Arbitrary Messaging

In this section, we'll examine how arbitrary messaging works in the example script from the starter kit repository. This will help you understand how to send messages to the CCIP Basic Receiver program on Solana Devnet.

Deriving Program Derived Addresses

The script first derives the PDAs required by the receiver program. This is a critical step because the program can only process messages if the correct PDAs are provided:

function deriveReceiverPDAs(receiverProgramIdStr: string) {
  const receiverProgramId = new PublicKey(receiverProgramIdStr)

  const STATE_SEED = Buffer.from("state")
  const MESSAGES_STORAGE_SEED = Buffer.from("messages_storage")

  const [statePda] = PublicKey.findProgramAddressSync([STATE_SEED], receiverProgramId)

  const [messagesStoragePda] = PublicKey.findProgramAddressSync([MESSAGES_STORAGE_SEED], receiverProgramId)

  return {
    state: statePda,
    messagesStorage: messagesStoragePda,
  }
}

// Get the receiver program ID for Solana Devnet
const receiverProgramId = getCCIPSVMConfig(ChainId.SOLANA_DEVNET).receiverProgramId.toString()

// Derive the PDAs for the receiver program
const pdas = deriveReceiverPDAs(receiverProgramId)

Configuring the Message

The script then configures the CCIP message with all necessary parameters for arbitrary messaging:

const MESSAGE_CONFIG = {
  // Custom message to send - must be properly encoded as hex with 0x prefix
  // This example encodes "Hello World" to hex
  data: "0x48656c6c6f20576f726c64", // "Hello World" in hex

  // Destination program on Solana that will receive the message
  receiver: receiverProgramId,

  // Fee token to use for CCIP fees
  feeToken: FeeTokenType.LINK,

  // No tokens are transferred with this message
  tokenAmounts: [],

  // Extra configuration for Solana
  extraArgs: {
    // Compute units for Solana execution
    // Needed because message processing requires compute units
    computeUnits: 200000,

    // Allow out-of-order execution - MUST be true for Solana
    allowOutOfOrderExecution: true,

    // Binary 10 (decimal 2) means only the messages storage account is writable
    accountIsWritableBitmap: BigInt(2),

    // Token receiver - for arbitrary messages, this is usually the default PublicKey
    tokenReceiver: PublicKey.default.toString(),

    // Accounts required by the receiver program
    accounts: [pdas.state.toString(), pdas.messagesStorage.toString()],
  },
}

Understanding AccountIsWritableBitmap

When sending messages to Solana programs, you need to specify which accounts can be written to. The accountIsWritableBitmap parameter in the script indicates which accounts in the accounts array should be marked as writable.

For our example:

  • The script provides 2 accounts: [statePda, messagesStoragePda]
  • Only the messagesStoragePda needs to be writable for the program to update it
  • The state PDA is read-only during message processing
  • The bitmap uses binary representation to specify writable permissions:
    • Position 0 (statePda): Not writable → Bit value = 0
    • Position 1 (messagesStoragePda): Writable → Bit value = 1
    • Binary value: 10 (base 2) = 2 (base 10)

Therefore, we set accountIsWritableBitmap: BigInt(2) in our message configuration.

Creating and Sending the CCIP Message

Finally, the script creates and sends the CCIP message using the CCIP SDK:

// Setup client context
const { client, wallet } = await setupClientContext()

// Create the CCIP message request
const ccipMessageRequest = await createCCIPMessageRequest(client, config.destinationChainSelector, MESSAGE_CONFIG)

// Send the CCIP message
const result = await client.sendCCIPMessage(ccipMessageRequest)

This sends the message to the CCIP router, which then forwards it to the Solana program on the destination chain.

The script includes detailed comments that explain its implementation and follows a straightforward flow. You can review the source code at ccip-scripts/evm/router/2_arbitrary-messaging.ts in the starter kit repository.

Running the Arbitrary Messaging Script

Run the Script

Now that you understand how the arbitrary messaging script works, let's execute it to send a message:

yarn evm:arbitrary-messaging

Expected Output

When you run the script, it will display detailed information about the message being sent. You should see output similar to this:

==== Environment Information ====
chainId ethereum-sepolia
[arbitrary-messaging] [INFO] Router Address: 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
chainId ethereum-sepolia
[ccip-messenger] [INFO] Creating client for chain: Ethereum Sepolia (ethereum-sepolia)
[arbitrary-messaging] [INFO] Wallet Address: 0x9d087fC03ae39b088326b67fA3C788236645b717
[arbitrary-messaging] [INFO] Native Balance: 600.023603232108193848 ETH
[arbitrary-messaging] [INFO] Creating CCIP message request
[arbitrary-messaging] [INFO] Using fee token: 0x779877A7B0D9E8603169DdbD7836e478b4624789
[arbitrary-messaging] [INFO]
==== Transfer Summary ====
[arbitrary-messaging] [INFO] Source Chain: Ethereum Sepolia
[arbitrary-messaging] [INFO] Destination Chain: Solana Devnet (16423721717087811551)
[arbitrary-messaging] [INFO] Sender: 0x9d087fC03ae39b088326b67fA3C788236645b717
[arbitrary-messaging] [INFO] Receiver: BqmcnLFSbKwyMEgi7VhVeJCis1wW26VySztF34CJrKFq
[arbitrary-messaging] [INFO] Token Receiver: 11111111111111111111111111111111
[arbitrary-messaging] [INFO] Fee Token: 0x779877A7B0D9E8603169DdbD7836e478b4624789
[arbitrary-messaging] [INFO] No tokens being transferred
[arbitrary-messaging] [INFO]
Message Data: 0x48656c6c6f20576f726c64
[arbitrary-messaging] [INFO] Message Data Size: 11 bytes
[arbitrary-messaging] [INFO] Message Data (decoded): Hello World
[arbitrary-messaging] [INFO]
Extra Args: Solana-specific, 292 bytes
[arbitrary-messaging] [INFO] Additional Accounts: 9XDoTJ5mYNnxqdtWV5dA583VCiGUhmL3oEMWirqys3tF, CB7ptrDkY9EgwqHoJwa3TF8u4rhwYmTob2YqzaSpPMtE
[arbitrary-messaging] [INFO] Account Is Writable Bitmap: 2
[arbitrary-messaging] [INFO]
Sending CCIP message...
[ccip-messenger] [INFO] Estimated fee: 12417653034565940
[ccip-messenger] [INFO] Approving 0.014901183641479128 LINK for CCIP Router
[ccip-messenger] [INFO] This token is being used as the fee token with a 20% buffer included
[ccip-messenger] [INFO] Approving 14901183641479128 tokens for 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
[ccip-messenger] [INFO] LINK approved for CCIP Router
[ccip-messenger] [INFO] ✅ Verified on-chain allowance for LINK: 0.014901183641479128 (required: 0.014901183641479128)
[ccip-messenger] [INFO] Sending CCIP message...
[ccip-messenger] [INFO] Sending CCIP message...
[ccip-messenger] [INFO] Transaction sent: 0x6181d2d3d033698149f8dd439df5a5b6753c741e5378761567d013746845213f
[ccip-messenger] [INFO] Transaction sent: 0x6181d2d3d033698149f8dd439df5a5b6753c741e5378761567d013746845213f
[ccip-messenger] [INFO] Message ID: 0x1ebd6f5b23b23613e6ba277c1b4e8ce2a04c6c03e8db20474e97c040a01b3f0b
[arbitrary-messaging] [INFO]
==== Transfer Results ====
[arbitrary-messaging] [INFO] Transaction Hash: 0x6181d2d3d033698149f8dd439df5a5b6753c741e5378761567d013746845213f
[arbitrary-messaging] [INFO] Transaction URL: https://sepolia.etherscan.io/tx/0x6181d2d3d033698149f8dd439df5a5b6753c741e5378761567d013746845213f
[arbitrary-messaging] [INFO] Message ID: 0x1ebd6f5b23b23613e6ba277c1b4e8ce2a04c6c03e8db20474e97c040a01b3f0b
[arbitrary-messaging] [INFO] CCIP Explorer: https://ccip.chain.link/msg/0x1ebd6f5b23b23613e6ba277c1b4e8ce2a04c6c03e8db20474e97c040a01b3f0b
[arbitrary-messaging] [INFO] Destination Chain Selector: 16423721717087811551
[arbitrary-messaging] [INFO] Sequence Number: 13
[arbitrary-messaging] [INFO]
Message tracking for Solana destinations:
[arbitrary-messaging] [INFO] Please check the CCIP Explorer link to monitor your message status.
[arbitrary-messaging] [INFO]
✅ Transaction completed on the source chain

Check the Message Status

The output indicates that the transaction was completed on the source chain. In this example, the message ID is 0x1ebd6f5b23b23613e6ba277c1b4e8ce2a04c6c03e8db20474e97c040a01b3f0b. You can use the CCIP Explorer to monitor the message status.

Wait for the message to be shown as successful on the CCIP Explorer. This may take several minutes to complete.

Verification: Retrieving the Message

Make sure to wait for the message to be shown as successful on the CCIP Explorer before retrieving the message.

Query the Receiver Program

After sending your message, you'll want to verify that it was received and processed by the receiver program. The CCIP Basic Receiver program stores messages, allowing you to retrieve them:

yarn svm:receiver:get-message

This script queries the program's message storage PDA, deserializes the data, and displays the content of the most recently received cross-chain message.

Expected Verification Output

When you run the verification script, you'll see output that confirms your message was received:

[2025-05-01T22:20:48.307Z] CCIP Basic Receiver - Get Latest Message
[2025-05-01T22:20:48.310Z] Program ID: BqmcnLFSbKwyMEgi7VhVeJCis1wW26VySztF34CJrKFq
[2025-05-01T22:20:48.337Z] Wallet public key: EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB
[2025-05-01T22:20:48.343Z] Messages Storage PDA: CB7ptrDkY9EgwqHoJwa3TF8u4rhwYmTob2YqzaSpPMtE
[2025-05-01T22:20:52.088Z] Fetching latest message...
[2025-05-01T22:20:52.693Z]
======== LATEST MESSAGE ========
[2025-05-01T22:20:52.693Z] Message ID: 0x1ebd6f5b23b23613e6ba277c1b4e8ce2a04c6c03e8db20474e97c040a01b3f0b
[2025-05-01T22:20:52.695Z] Source Chain Selector: 16015286601757825753
[2025-05-01T22:20:52.695Z] Sender: 0x9d087fc03ae39b088326b67fa3c788236645b717
[2025-05-01T22:20:52.695Z] Message Type: Arbitrary Messaging
[2025-05-01T22:20:52.696Z] Received Timestamp: 2025-05-01T22:20:09.000Z
[2025-05-01T22:20:52.696Z] Data (hex): 0x48656c6c6f20576f726c64
[2025-05-01T22:20:52.696Z] Data (text): Hello World
[2025-05-01T22:20:52.696Z] No tokens transferred in this message

Behind the Scenes: Message and Account Encoding

When sending arbitrary messages to SVM programs, proper encoding of both the message data and account structures is critical. The script handles several complex encoding tasks that would need to be implemented in your own applications:

Key utilities used include:

  • deriveReceiverPDAs(): Calculates Program Derived Addresses using the same seeds as the SVM program
  • createCCIPMessageRequest(): Builds the properly formatted CCIP message with all required fields
  • Hex encoding utilities: Convert string data to properly formatted hex with 0x prefix
  • accountIsWritableBitmap calculation: Determines which accounts can be written to by the program

These utilities handle essential tasks such as:

  • Converting between different address formats (Ethereum addresses vs. SVM public keys)
  • Properly encoding message data with correct prefixes
  • Setting up the required accounts array with properly formatted SVM addresses
  • Calculating the correct bitmap values for account permissions

If you're building your own implementation, you should review these key files:

  • ccip-scripts/evm/utils/message-utils.ts
  • ccip-lib/evm/utils/solana.ts
  • ccip-lib/evm/core/models.ts

Understanding these encoding details becomes especially important when working with your own SVM programs, as each program will have its own PDA structure and account requirements.

Get the latest Chainlink content straight to your inbox.