diff --git a/examples/oapp-solana/contracts/MyOApp.sol b/examples/oapp-solana/contracts/MyOApp.sol index a06e429273..c9cb3f75c2 100644 --- a/examples/oapp-solana/contracts/MyOApp.sol +++ b/examples/oapp-solana/contracts/MyOApp.sol @@ -28,9 +28,9 @@ contract MyOApp is OApp, OAppOptionsType3 { string calldata _string, bytes calldata _options ) external payable returns (MessagingReceipt memory receipt) { - bytes memory _payload = abi.encodePacked(abi.encode(uint256(bytes(_string).length)), bytes(_string)); + bytes memory _message = abi.encodePacked(abi.encode(uint256(bytes(_string).length)), bytes(_string)); bytes memory options = combineOptions(_dstEid, StringMsgCodec.VANILLA_TYPE, _options); - receipt = _lzSend(_dstEid, _payload, options, MessagingFee(msg.value, 0), payable(msg.sender)); + receipt = _lzSend(_dstEid, _message, options, MessagingFee(msg.value, 0), payable(msg.sender)); } /** diff --git a/examples/oapp-solana/docs/compose-implementation.md b/examples/oapp-solana/docs/compose-implementation.md index ab17d0b17b..b80e5cbf2a 100644 --- a/examples/oapp-solana/docs/compose-implementation.md +++ b/examples/oapp-solana/docs/compose-implementation.md @@ -117,7 +117,6 @@ Handle compose_msg in `lz_receive`: pub use lz_receive_types::*; pub use quote_send::*; pub use set_peer_config::*; - pub use set_peer_config::*; ``` ### Modify `programs/my_oapp/src/instructions/quote_send.rs` to take in `compose_msg` @@ -324,13 +323,13 @@ and appended it to the payload: ```solidity function send( uint32 _dstEid, - string calldata _message, + string calldata _string, + bytes calldata _composeMsg, bytes calldata _options ) external payable returns (MessagingReceipt memory receipt) { - bytes memory _payload = abi.encodePacked( - abi.encode(uint256(bytes(_message).length)), - bytes(_message), + bytes memory _message = abi.encodePacked( + abi.encode(uint256(bytes(_string).length)), + bytes(_string), + _composeMsg ); + uint8 msgType = _composeMsg.length > 0 ? StringMsgCodec.COMPOSED_TYPE : StringMsgCodec.VANILLA_TYPE; @@ -339,18 +338,17 @@ and appended it to the payload: // ... function quote( uint32 _dstEid, - string calldata _message, + string calldata _string, + bytes calldata _composeMsg, bytes calldata _options, bool _payInLzToken ) public view returns (MessagingFee memory fee) { - bytes memory payload = abi.encodePacked( - abi.encode(uint256(bytes(_message).length)), - bytes(_message), - bytes(_message) + bytes memory _message = abi.encodePacked( + abi.encode(uint256(bytes(_string).length)), + bytes(_string), + _composeMsg ); -+ uint8 msgType = _composeMsg.length > 0 ? StringMsgCodec.COMPOSED_TYPE : StringMsgCodec.VANILLA_TYPE; ++ uint8 msgType = _composeMsg.length > 0 ? StringMsgCodec.COMPOSED_TYPE : StringMsgCodec.VANILLA_TYPE; - bytes memory options = combineOptions(_dstEid, StringMsgCodec.VANILLA_TYPE, _options); + bytes memory options = combineOptions(_dstEid, msgType, _options); // ... @@ -361,10 +359,11 @@ and appended it to the payload: address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { -_ (string memory stringValue) = StringMsgCodec.decode(payload); -+ (string memory stringValue, ) = StringMsgCodec.decode(payload); // TODO: use the last value from .decode() +_ (string memory stringValue) = StringMsgCodec.decode(payload); ++ (string memory stringValue, bytes memory composeMsg) = StringMsgCodec.decode(payload); data = stringValue; -+ // TODO: process _composeMsg ++ // if necessary, encode the composeMsg further ++ endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); } ``` diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs index a71cb7f55f..7fa831edf7 100644 --- a/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/init_store.rs @@ -10,7 +10,9 @@ pub struct InitStore<'info> { init, payer = payer, space = Store::SIZE, - seeds = [STORE_SEED], + seeds = [STORE_SEED], // You can namespace this further if your program manages multiple stores. + // e.g. If there can be a store for each user, you can use something like: + // seeds = [STORE_SEED, &user.key().as_ref()] bump )] pub store: Account<'info, Store>, @@ -33,12 +35,16 @@ impl InitStore<'_> { ctx.accounts.store.admin = params.admin; ctx.accounts.store.bump = ctx.bumps.store; ctx.accounts.store.endpoint_program = params.endpoint; - ctx.accounts.store.string = "Nothing received yet.".to_string(); - ctx.accounts.lz_receive_types_accounts.store = ctx.accounts.store.key(); + // the above lines are required for all OApp implementations + + // the line below is specific to this string-passing example + ctx.accounts.store.string = "Nothing received yet.".to_string(); - // calling endpoint cpi + // Prepare the delegate address for the OApp registration. let register_params = RegisterOAppParams { delegate: ctx.accounts.store.admin }; + + // The Store PDA 'signs' CPI to the Endpoint program to register the OApp. let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; oapp::endpoint_cpi::register_oapp( ENDPOINT_ID, diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs index fc1553db65..41552f85f3 100644 --- a/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive.rs @@ -12,8 +12,12 @@ use oapp::{ #[derive(Accounts)] #[instruction(params: LzReceiveParams)] pub struct LzReceive<'info> { + /// OApp Store PDA. This account represents the "address" of your OApp on + /// Solana and can contain any state relevant to your application. + /// Customize the fields in `Store` as needed. #[account(mut, seeds = [STORE_SEED], bump = store.bump)] pub store: Account<'info, Store>, + /// Peer config PDA for the sending chain. Ensures `params.sender` can only be the allowed peer from that remote chain. #[account( seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.src_eid.to_be_bytes()], bump = peer.bump, @@ -24,10 +28,15 @@ pub struct LzReceive<'info> { impl LzReceive<'_> { pub fn apply(ctx: &mut Context, params: &LzReceiveParams) -> Result<()> { - let seeds: &[&[u8]] = - &[STORE_SEED, &[ctx.accounts.store.bump]]; - // the first 9 accounts are for clear() + // The OApp Store PDA is used to sign the CPI to the Endpoint program. + let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; + + // The first Clear::MIN_ACCOUNTS_LEN accounts were returned by + // `lz_receive_types` and are required for Endpoint::clear let accounts_for_clear = &ctx.remaining_accounts[0..Clear::MIN_ACCOUNTS_LEN]; + // Call the Endpoint::clear CPI to clear the message from the Endpoint program. + // This is necessary to ensure the message is processed only once and to + // prevent replays. let _ = oapp::endpoint_cpi::clear( ENDPOINT_ID, ctx.accounts.store.key(), @@ -43,6 +52,7 @@ impl LzReceive<'_> { }, )?; + // From here on, you can process the message as needed by your use case. let string_value = msg_codec::decode(¶ms.message)?; let store = &mut ctx.accounts.store; store.string = string_value; diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs index 6584938f85..adcaffbc65 100644 --- a/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/lz_receive_types.rs @@ -2,9 +2,13 @@ use crate::*; use oapp::endpoint_cpi::{get_accounts_for_clear, LzAccount}; use oapp::{endpoint::ID as ENDPOINT_ID, LzReceiveParams}; -/// LzReceiveTypes instruction provides a list of accounts that are used in the LzReceive -/// instruction. The list of accounts required by this LzReceiveTypes instruction can be found -/// from the specific PDA account that is generated by the LZ_RECEIVE_TYPES_SEED. +/// `lz_receive_types` is queried off-chain by the Executor before calling +/// `lz_receive`. It must return **every** account that will be touched by the +/// actual `lz_receive` instruction as well as the accounts required by +/// `Endpoint::clear`. +/// +/// The return order must match exactly what `lz_receive` expects or the +/// cross-program invocation will fail. #[derive(Accounts)] pub struct LzReceiveTypes<'info> { #[account(seeds = [STORE_SEED], bump = store.bump)] @@ -12,33 +16,28 @@ pub struct LzReceiveTypes<'info> { } impl LzReceiveTypes<'_> { - /// The list of accounts should follow the rules below: - /// 1. Include all the accounts that are used in the LzReceive instruction, including the - /// accounts that are used by the Endpoint program. - /// 2. Set the account is a signer with ZERO address if the LzReceive instruction needs a payer - /// to pay fee, like rent. - /// 3. Set the account is writable if the LzReceive instruction needs to modify the account. pub fn apply( ctx: &Context, params: &LzReceiveParams, ) -> Result> { - // There are two accounts that are used in the LzReceive instruction, - // except those accounts for endpoint program. - // The first account is the store account, that is the fixed one. + // 1. The store PDA is always the first account and is mutable. If your + // program derives the store PDA with additional seeds, ensure the same + // seeds are used when providing the store account. let store = ctx.accounts.store.key(); - // The second account is the peer account, we find it by the params.src_eid. + // 2. The peer PDA for the remote chain needs to be retrieved, for later verification of the `params.sender`. let peer_seeds = [PEER_SEED, &store.to_bytes(), ¶ms.src_eid.to_be_bytes()]; let (peer, _) = Pubkey::find_program_address(&peer_seeds, ctx.program_id); + // Accounts used directly by `lz_receive` let mut accounts = vec![ - // count + // store (mutable) LzAccount { pubkey: store, is_signer: false, is_writable: true }, - // peer + // peer (read-only) LzAccount { pubkey: peer, is_signer: false, is_writable: false } ]; - // append the accounts for the clear ix + // Append the additional accounts required for `Endpoint::clear` let accounts_for_clear = get_accounts_for_clear( ENDPOINT_ID, &store, diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs index e7a2effef2..7c0af2f14c 100644 --- a/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/quote_send.rs @@ -22,11 +22,13 @@ pub struct QuoteSend<'info> { #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump, seeds::program = ENDPOINT_ID)] pub endpoint: Account<'info, EndpointSettings>, } + impl<'info> QuoteSend<'info> { pub fn apply(ctx: &Context, params: &QuoteSendParams) -> Result { + // Encode the payload for quoting let message = msg_codec::encode(¶ms.message); - // calling endpoint cpi + // Ask the Endpoint how much a send would cost let quote_params = QuoteParams { sender: ctx.accounts.store.key(), dst_eid: params.dst_eid, diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs index fb663c7718..9c428245e9 100644 --- a/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/send.rs @@ -15,18 +15,23 @@ pub struct Send<'info> { ], bump = peer.bump )] + /// Configuration for the destination chain. Holds the peer address and any + /// enforced messaging options. pub peer: Account<'info, PeerConfig>, #[account(seeds = [STORE_SEED], bump = store.bump)] + /// OApp Store PDA that signs the send instruction pub store: Account<'info, Store>, #[account(seeds = [ENDPOINT_SEED], bump = endpoint.bump, seeds::program = ENDPOINT_ID)] pub endpoint: Account<'info, EndpointSettings>, } impl<'info> Send<'info> { pub fn apply(ctx: &mut Context, params: &SendMessageParams) -> Result<()> { + // Serialize the message according to our codec let message = msg_codec::encode(¶ms.message); + // Prepare the seeds for the OApp Store PDA, which is used to sign the CPI call to the Endpoint program. let seeds: &[&[u8]] = &[STORE_SEED, &[ctx.accounts.store.bump]]; - // calling endpoint cpi + // Prepare the SendParams for the Endpoint::send CPI call. let send_params = SendParams { dst_eid: params.dst_eid, receiver: ctx.accounts.peer.peer_address, @@ -39,9 +44,10 @@ impl<'info> Send<'info> { native_fee: params.native_fee, lz_token_fee: params.lz_token_fee, }; + // Call the Endpoint::send CPI to send the message. oapp::endpoint_cpi::send( ENDPOINT_ID, - ctx.accounts.store.key(), + ctx.accounts.store.key(), // payer/signer derived from seeds ctx.remaining_accounts, seeds, send_params, diff --git a/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs b/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs index 484ae0caf3..d5b7f5a164 100644 --- a/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs +++ b/examples/oapp-solana/programs/my_oapp/src/instructions/set_peer_config.rs @@ -1,10 +1,15 @@ use crate::*; use anchor_lang::prelude::*; +// PeerConfig PDAs are used to store configuration for each remote chain +// For each remote chain, a PeerConfig PDA is created with the remote EID as part of the seed +// The PDA holds the peer address and any enforced options for messaging + #[derive(Accounts)] #[instruction(params: SetPeerConfigParams)] pub struct SetPeerConfig<'info> { #[account(mut, address = store.admin)] + /// Admin of the OApp store pub admin: Signer<'info>, #[account( init_if_needed, @@ -13,14 +18,17 @@ pub struct SetPeerConfig<'info> { seeds = [PEER_SEED, &store.key().to_bytes(), ¶ms.remote_eid.to_be_bytes()], bump )] + /// Peer configuration PDA for a specific remote chain pub peer: Account<'info, PeerConfig>, #[account(seeds = [STORE_SEED], bump = store.bump)] + /// Store PDA of this OApp pub store: Account<'info, Store>, pub system_program: Program<'info, System>, } impl SetPeerConfig<'_> { pub fn apply(ctx: &mut Context, params: &SetPeerConfigParams) -> Result<()> { + // Update or create the peer config PDA match params.config.clone() { PeerConfigParam::PeerAddress(peer_address) => { ctx.accounts.peer.peer_address = peer_address; @@ -32,6 +40,7 @@ impl SetPeerConfig<'_> { ctx.accounts.peer.enforced_options.send_and_call = send_and_call; }, } + // Store the PDA bump for later validation ctx.accounts.peer.bump = ctx.bumps.peer; Ok(()) } @@ -46,5 +55,6 @@ pub struct SetPeerConfigParams { #[derive(Clone, AnchorSerialize, AnchorDeserialize)] pub enum PeerConfigParam { PeerAddress([u8; 32]), + /// Optionally enforce specific send options for this peer EnforcedOptions { send: Vec, send_and_call: Vec }, } \ No newline at end of file diff --git a/examples/oapp-solana/programs/my_oapp/src/lib.rs b/examples/oapp-solana/programs/my_oapp/src/lib.rs index 3742dfd34f..1a72c9ec90 100644 --- a/examples/oapp-solana/programs/my_oapp/src/lib.rs +++ b/examples/oapp-solana/programs/my_oapp/src/lib.rs @@ -9,25 +9,31 @@ use oapp::{endpoint::MessagingFee, endpoint_cpi::LzAccount, LzReceiveParams}; use solana_helper::program_id_from_env; use state::*; +// to build in verifiable mode and using environment variable (what the README instructs), run: +// anchor build -v -e MYOAPP_ID= +// to build in normal mode and using environment, run: +// MYOAPP_ID=$PROGRAM_ID anchor build declare_id!(anchor_lang::solana_program::pubkey::Pubkey::new_from_array(program_id_from_env!( "MYOAPP_ID", - "41NCdrEvXhQ4mZgyJkmqYxL6A1uEmnraGj31UJ6PsXd3" + "41NCdrEvXhQ4mZgyJkmqYxL6A1uEmnraGj31UJ6PsXd3" // It's not necessary to change the ID here if you are building using environment variable ))); -const LZ_RECEIVE_TYPES_SEED: &[u8] = b"LzReceiveTypes"; -const STORE_SEED: &[u8] = b"Store"; -const PEER_SEED: &[u8] = b"Peer"; +const LZ_RECEIVE_TYPES_SEED: &[u8] = b"LzReceiveTypes"; // The Executor relies on this exact seed to derive the LzReceiveTypes PDA. Keep it the same. +const STORE_SEED: &[u8] = b"Store"; // You are free to edit this seed. +const PEER_SEED: &[u8] = b"Peer"; // The Executor relies on this exact seed to derive the LzReceiveTypes PDA. Keep it the same. #[program] pub mod my_oapp { use super::*; + // ============================== Initializers ============================== + // In this example, init_store can be called by anyone and can be called only once. Ensure you implement your own access control logic if needed. pub fn init_store(mut ctx: Context, params: InitStoreParams) -> Result<()> { InitStore::apply(&mut ctx, ¶ms) } // ============================== Admin ============================== - + // admin instruction to set or update cross-chain peer configuration parameters. pub fn set_peer_config( mut ctx: Context, params: SetPeerConfigParams, @@ -36,19 +42,22 @@ pub mod my_oapp { } // ============================== Public ============================== - + // public instruction returning the estimated MessagingFee for sending a message. pub fn quote_send(ctx: Context, params: QuoteSendParams) -> Result { QuoteSend::apply(&ctx, ¶ms) } + // public instruction to send a message to a cross-chain peer. pub fn send(mut ctx: Context, params: SendMessageParams) -> Result<()> { Send::apply(&mut ctx, ¶ms) } + // handler for processing incoming cross-chain messages and executing the LzReceive logic pub fn lz_receive(mut ctx: Context, params: LzReceiveParams) -> Result<()> { LzReceive::apply(&mut ctx, ¶ms) } + // handler that returns the list of accounts required to execute lz_receive pub fn lz_receive_types( ctx: Context, params: LzReceiveParams, diff --git a/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs b/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs index e6571b3dad..8b03b0d0f8 100644 --- a/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs +++ b/examples/oapp-solana/programs/my_oapp/src/msg_codec.rs @@ -1,10 +1,25 @@ use anchor_lang::prelude::error_code; -use std::str; +use std::str; -pub const VANILLA_TYPE: u8 = 1; +// ----------------------------------------------------------------------------- +// This file defines how the example program encodes and decodes its messages. +// Each OApp can implement its own layout as long as the sending and receiving +// chains agree. Here we simply prefix a UTF-8 string with a 32 byte length +// header. In this example, the EVM-side equivalant is in `contracts/libs/StringMsgCodec.sol` +// ----------------------------------------------------------------------------- -// Just like OFT, we don't need an explicit MSG_TYPE param -// Instead, we'll check whether there's data after the string ends + +// The message is a UTF-8 encoded string prefixed with a 32 byte header. +// The following is the layout of the message: +// Offset → +// 0 28 32 32+N +// |---------------------|------|----------------------------> +// | 28 bytes | 4B | N bytes | +// | zero padding | len | UTF-8 encoded string | +// |---------------------|------|----------------------------| + + +// We prefix the encoded string with a 32 byte length header. pub const LENGTH_OFFSET: usize = 0; pub const STRING_OFFSET: usize = 32; @@ -18,23 +33,27 @@ pub enum MsgCodecError { InvalidUtf8, } +/// Extract the string length fn decode_string_len(buf: &[u8]) -> Result { + // Header not long enough if buf.len() < STRING_OFFSET { return Err(MsgCodecError::InvalidLength); } let mut string_len_bytes = [0u8;32]; string_len_bytes.copy_from_slice(&buf[LENGTH_OFFSET..LENGTH_OFFSET+32]); + // The length is stored in the last 4 bytes (big endian) Ok(u32::from_be_bytes(string_len_bytes[28..32].try_into().unwrap()) as usize) } +// Encode a UTF-8 string into a message format with a 32 byte header pub fn encode(string: &str) -> Vec { let string_bytes = string.as_bytes(); let mut msg = Vec::with_capacity( - STRING_OFFSET + // length word - string_bytes.len() // string + STRING_OFFSET + // header length + string_bytes.len() // string bytes ); - // 4-byte length + // 4 byte length stored at the end of the 32 byte header msg.extend(std::iter::repeat(0).take(28)); // padding msg.extend_from_slice(&(string_bytes.len() as u32).to_be_bytes()); @@ -44,6 +63,8 @@ pub fn encode(string: &str) -> Vec { msg } +// Decode a message format with a 32 byte header into a UTF-8 string +// Returns an error if the message is malformed or not valid UTF-8 pub fn decode(message: &[u8]) -> Result { // Read the declared payload length from the header let string_len = decode_string_len(message)?; @@ -61,7 +82,7 @@ pub fn decode(message: &[u8]) -> Result { // Slice out the payload bytes let payload = &message[start..end]; - // Attempt to convert to &str, returning an error if invalid UTF-8 + // Attempt to convert the bytes into a Rust string match str::from_utf8(payload) { Ok(s) => Ok(s.to_string()), Err(_) => Err(MsgCodecError::InvalidUtf8), diff --git a/examples/oapp-solana/programs/my_oapp/src/state/store.rs b/examples/oapp-solana/programs/my_oapp/src/state/store.rs index 41edbf111f..89263751ec 100644 --- a/examples/oapp-solana/programs/my_oapp/src/state/store.rs +++ b/examples/oapp-solana/programs/my_oapp/src/state/store.rs @@ -2,10 +2,11 @@ use crate::*; #[account] pub struct Store { - pub admin: Pubkey, - pub bump: u8, - pub endpoint_program: Pubkey, - pub string: String, + pub admin: Pubkey, // This is required and should be consistent. + pub bump: u8, // This is required and should be consistent. + pub endpoint_program: Pubkey, // This is required and should be consistent. + pub string: String, // This is specific to this string-passing example. + // You can add more fields as needed for your OApp implementation. } impl Store { @@ -13,10 +14,10 @@ impl Store { pub const SIZE: usize = 8 + std::mem::size_of::() + Self::MAX_STRING_LENGTH; } -/// LzReceiveTypesAccounts includes accounts that are used in the LzReceiveTypes instruction. +// The LzReceiveTypesAccounts PDA is used by the Executor as a prerequisite to calling `lz_receive`. #[account] pub struct LzReceiveTypesAccounts { - pub store: Pubkey, + pub store: Pubkey, // This is required and should be consistent. } impl LzReceiveTypesAccounts {