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 -vin your terminal:node -vExample 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 -
Install dependencies:
yarn install -
Create a
.envfile 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_KEYandEVM_RPC_URLvalues 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:
| Token | Purpose | How to Obtain |
|---|---|---|
| ETH | Transaction gas fees | Chainlink Faucet or alternative sources such as the Google Cloud Faucet |
| LINK | CCIP fees | Chainlink 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:
computeUnitsmust be sufficient for the execution ofccip_receiveinstruction of the receiver programreceiverfield should be set to the Solana program IDtokenReceiveris typically set to the default PublicKey (11111111111111111111111111111111)- Required accounts must be specified in the
accountsarray accountIsWritableBitmapmust correctly identify which accounts should be writableallowOutOfOrderExecutionmust betrue
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
Token Transfers:
computeUnitsmust be 0receiveris default PublicKeytokenReceiveris the recipient wallet- No accounts required
Arbitrary Messaging:
computeUnitsmust be sufficient to execute theccip_receiveinstruction of the receiver programreceiveris a Solana program IDtokenReceiveris typically default PublicKey- Requires specific accounts and proper bitmap
Token Transfers:
- Tokens arrive in a wallet or PDA of a receiver program
- No program execution happens
Arbitrary Messaging:
- Data arrives at a Solana program
- Program executes to process the data
- Program can update its state accounts
Token Transfers:
- Check token balance in wallet
- Use Solana Explorer to verify balances
Arbitrary Messaging:
- Verify program state changes
- Use program-specific methods to retrieve data
- Check transaction logs for program execution
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:
-
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_receiveinstruction. 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
-
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 programcreateCCIPMessageRequest(): Builds the properly formatted CCIP message with all required fields- Hex encoding utilities: Convert string data to properly formatted hex with
0xprefix accountIsWritableBitmapcalculation: 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.tsccip-lib/evm/utils/solana.tsccip-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.