Skip to content

Commit c09287a

Browse files
authored
Feat(Solana): Implement LzReceiveTypes V2 with Multi-Instruction and ALT Support (#152)
* feat(solana): implement LzReceiveTypes V2 with multi-instruction model and ALT support * chore: add lz_receive_v2 module and define ErrorCode for address lookup table validation * refact: introduce common module and refactor LzReceiveTypes to support multi-instruction execution * feat: add lz_compose_types_v2 module and update nonce_account writability in lz_receive_types_v2 * chore: sync with `solana/programs/libs/oapp/src` * clean up * clean up
1 parent 4645b57 commit c09287a

File tree

10 files changed

+919
-2
lines changed

10 files changed

+919
-2
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use std::collections::HashMap;
2+
3+
use anchor_lang::prelude::*;
4+
use anchor_lang::solana_program::address_lookup_table::state::AddressLookupTable;
5+
use anchor_lang::Discriminator;
6+
7+
pub const EXECUTION_CONTEXT_SEED: &[u8] = b"ExecutionContext";
8+
pub const EXECUTION_CONTEXT_VERSION_1: u8 = 1;
9+
10+
/// Execution context data structure that contains metadata for cross-chain execution
11+
/// This provides execution limits and tracking for LayerZero operations
12+
#[derive(InitSpace, AnchorSerialize, AnchorDeserialize, Clone)]
13+
pub struct ExecutionContextV1 {
14+
pub initial_payer_balance: u64,
15+
/// The maximum total lamports allowed to be used by all instructions in this execution.
16+
/// This is a hard cap for the sum of lamports consumed by all instructions in the execution
17+
/// batch.
18+
pub fee_limit: u64,
19+
}
20+
21+
impl anchor_lang::Discriminator for ExecutionContextV1 {
22+
// let discriminator_preimage = "account:ExecutionContextV1";
23+
// let hash = anchor_lang::solana_program::hash::hash(discriminator_preimage.as_bytes());
24+
const DISCRIMINATOR: &'static [u8] = &[132, 92, 176, 59, 141, 186, 141, 137];
25+
}
26+
27+
impl anchor_lang::AccountDeserialize for ExecutionContextV1 {
28+
fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
29+
if buf.len() < Self::DISCRIMINATOR.len() {
30+
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
31+
}
32+
let given_disc = &buf[..8];
33+
if Self::DISCRIMINATOR != given_disc {
34+
return Err(anchor_lang::error!(
35+
anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch
36+
)
37+
.with_account_name("ExecutionContextV1"));
38+
}
39+
Self::try_deserialize_unchecked(buf)
40+
}
41+
42+
fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
43+
let mut data: &[u8] = &buf[8..];
44+
anchor_lang::AnchorDeserialize::deserialize(&mut data)
45+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
46+
}
47+
}
48+
49+
/// A generic account locator used in LZ execution planning for V2.
50+
/// Can reference the address directly, via ALT, or as a placeholder.
51+
///
52+
/// This enum enables the compact account referencing design of V2, supporting:
53+
/// - OApps to request multiple signer accounts, not just a single Executor EOA
54+
/// - Dynamic creation of writable EOA-based data accounts
55+
/// - Efficient encoding of addresses via ALTs, reducing account list size
56+
///
57+
/// The legacy is_signer flag is removed. Instead, signer roles are explicitly
58+
/// declared through Payer and indexed Signer(u8) variants.
59+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
60+
pub enum AddressLocator {
61+
/// Directly supplied public key - standard address reference
62+
Address(Pubkey),
63+
/// Indexed address from a specific Address Lookup Table (ALT)
64+
/// Format: (ALT list index, address index within ALT)
65+
/// Enables efficient account list compression via Solana's ALT mechanism
66+
AltIndex(u8, u8),
67+
/// Executor's fee payer - substituted by the Executor's EOA
68+
/// This is the primary signer and fee payer for the transaction
69+
Payer,
70+
/// Additional signer accounts - substituted by EOAs provided by the Executor
71+
/// The u8 index identifies each signer's position in the signer list,
72+
/// allowing the OApp to reference multiple distinct signers for dynamic account creation
73+
Signer(u8),
74+
/// A context account provided by the Executor containing execution
75+
/// metadata, such as SOL spend limits.
76+
Context,
77+
// Append more address placeholders in the future.
78+
}
79+
80+
impl From<Pubkey> for AddressLocator {
81+
fn from(pubkey: Pubkey) -> Self {
82+
AddressLocator::Address(pubkey)
83+
}
84+
}
85+
86+
/// Account metadata for V2 execution planning.
87+
/// Used by the Executor to construct the final transaction.
88+
///
89+
/// V2 removes the legacy is_signer flag from V1's AccountMeta.
90+
/// Instead, signer roles are explicitly declared through AddressLocator variants.
91+
/// This provides clearer semantics and enables multiple signer support.
92+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
93+
pub struct AccountMetaRef {
94+
/// The account address locator - supports multiple resolution strategies
95+
pub pubkey: AddressLocator,
96+
/// Whether the account should be writable in the final transaction
97+
pub is_writable: bool,
98+
}
99+
100+
pub fn compact_accounts_with_alts(
101+
alt_accounts: &[AccountInfo],
102+
instruction_accounts: Vec<AccountMetaRef>,
103+
) -> Result<Vec<AccountMetaRef>> {
104+
// Build address lookup table mapping from remaining_accounts
105+
// This enables efficient account referencing via ALT indices
106+
let address_to_alt_index_map = build_address_to_alt_index_map(alt_accounts)?;
107+
108+
// Convert accounts to use ALT indices where possible
109+
let compacted_accounts = instruction_accounts
110+
.into_iter()
111+
.map(|mut account_meta| {
112+
if let AddressLocator::Address(pubkey) = account_meta.pubkey {
113+
account_meta.pubkey = to_address_locator(&address_to_alt_index_map, pubkey);
114+
}
115+
account_meta
116+
})
117+
.collect();
118+
119+
Ok(compacted_accounts)
120+
}
121+
122+
pub fn to_address_locator(
123+
address_to_alt_index_map: &HashMap<Pubkey, (u8, u8)>,
124+
key: Pubkey,
125+
) -> AddressLocator {
126+
address_to_alt_index_map
127+
.get(&key)
128+
.map(|alt_index| AddressLocator::AltIndex(alt_index.0, alt_index.1))
129+
.unwrap_or(AddressLocator::Address(key))
130+
}
131+
132+
/// Helper function to deserialize AddressLookupTable data
133+
pub fn deserialize_alt(alt: &AccountInfo) -> Result<Vec<Pubkey>> {
134+
AddressLookupTable::deserialize(*alt.try_borrow_data().unwrap())
135+
.map(|alt| alt.addresses.to_vec())
136+
.map_err(|_e| error!(crate::ErrorCode::InvalidAddressLookupTable))
137+
}
138+
139+
/// Helper function to build a map of addresses to their ALT indices
140+
pub fn build_address_to_alt_index_map(
141+
alt_accounts: &[AccountInfo],
142+
) -> Result<HashMap<Pubkey, (u8, u8)>> {
143+
let mut address_to_alt_index_map: HashMap<Pubkey, (u8, u8)> = HashMap::new();
144+
for (alt_index, alt) in alt_accounts.iter().enumerate() {
145+
let addresses_in_alt = deserialize_alt(alt)?;
146+
for (address_index_in_alt, address_in_alt) in addresses_in_alt.iter().enumerate() {
147+
address_to_alt_index_map
148+
.insert(*address_in_alt, (alt_index as u8, address_index_in_alt as u8));
149+
}
150+
}
151+
Ok(address_to_alt_index_map)
152+
}

packages/layerzero-v2/solana/anchor-latest/libs/oapp/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
use anchor_lang::prelude::*;
22

3+
pub mod common;
34
pub mod endpoint_cpi;
5+
pub mod lz_compose_types_v2;
6+
pub mod lz_receive_types_v2;
47
pub mod options;
58

69
pub use endpoint_interface as endpoint;
710

811
pub const LZ_RECEIVE_TYPES_SEED: &[u8] = b"LzReceiveTypes";
12+
pub const LZ_COMPOSE_TYPES_SEED: &[u8] = b"LzComposeTypes";
913

1014
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
1115
pub struct LzReceiveParams {
@@ -26,3 +30,9 @@ pub struct LzComposeParams {
2630
pub message: Vec<u8>,
2731
pub extra_data: Vec<u8>,
2832
}
33+
34+
#[error_code]
35+
pub enum ErrorCode {
36+
#[msg("Invalid address lookup table data")]
37+
InvalidAddressLookupTable,
38+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use crate::{common::AccountMetaRef, endpoint_cpi::EVENT_SEED};
2+
use anchor_lang::{prelude::*, solana_program::keccak::hash};
3+
use endpoint_interface::COMPOSED_MESSAGE_HASH_SEED;
4+
5+
pub const LZ_COMPOSE_TYPES_VERSION: u8 = 2;
6+
7+
/// The payload returned from `lz_compose_types_info` when version == 2.
8+
/// Provides information needed to construct the call to `lz_compose_types_v2`.
9+
///
10+
/// This structure is stored at a deterministic PDA and serves as the bridge between
11+
/// the version discovery phase and the actual execution planning phase of V2.
12+
///
13+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
14+
pub struct LzComposeTypesV2Accounts {
15+
pub accounts: Vec<Pubkey>,
16+
}
17+
18+
/// Output of the lz_compose_types_v2 instruction.
19+
///
20+
/// This structure enables the multi-instruction execution model where OApps can
21+
/// define multiple instructions to be executed atomically by the Executor.
22+
/// The Executor constructs a single transaction containing all returned instructions.
23+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
24+
pub struct LzComposeTypesV2Result {
25+
/// The version of context account
26+
pub context_version: u8,
27+
/// ALTs required for this execution context
28+
/// Used by the Executor to resolve AltIndex references in AccountMetaRef
29+
/// Enables efficient account list compression for complex transactions
30+
pub alts: Vec<Pubkey>,
31+
/// The complete list of instructions required for LzCompose execution
32+
/// MUST include exactly one LzCompose instruction
33+
/// MAY include additional Standard instructions for preprocessing/postprocessing
34+
/// Instructions are executed in the order returned
35+
pub instructions: Vec<Instruction>,
36+
}
37+
38+
/// The list of instructions that can be executed in the LzCompose transaction.
39+
///
40+
/// V2's multi-instruction model enables complex patterns such as:
41+
/// - Preprocessing steps before lz_receive (e.g., account initialization)
42+
/// - Postprocessing steps after lz_receive (e.g., verification, cleanup)
43+
/// - ABA messaging patterns with additional LayerZero sends
44+
/// - Conditional execution flows based on message content
45+
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
46+
pub enum Instruction {
47+
/// The main LzCompose instruction (exactly one required per transaction)
48+
/// This instruction composes the outgoing cross-chain message
49+
LzCompose {
50+
/// Account list for the lz_compose instruction
51+
/// Uses AddressLocator for flexible address resolution
52+
accounts: Vec<AccountMetaRef>,
53+
},
54+
/// Arbitrary custom instruction for preprocessing/postprocessing
55+
/// Enables OApps to implement complex execution flows
56+
Standard {
57+
/// Target program ID for the custom instruction
58+
program_id: Pubkey,
59+
/// Account list for the custom instruction
60+
/// Uses same AddressLocator system as LzCompose
61+
accounts: Vec<AccountMetaRef>,
62+
/// Instruction data payload
63+
/// Raw bytes containing the instruction's parameters
64+
data: Vec<u8>,
65+
},
66+
}
67+
68+
/// V2 version of get_accounts_for_clear_compose that returns AccountMetaRef
69+
pub fn get_accounts_for_clear_compose(
70+
endpoint_program: Pubkey,
71+
from: &Pubkey,
72+
to: &Pubkey,
73+
guid: &[u8; 32],
74+
index: u16,
75+
composed_message: &[u8],
76+
) -> Vec<AccountMetaRef> {
77+
let (composed_message_account, _) = Pubkey::find_program_address(
78+
&[
79+
COMPOSED_MESSAGE_HASH_SEED,
80+
&from.to_bytes(),
81+
&to.to_bytes(),
82+
&guid[..],
83+
&index.to_be_bytes(),
84+
&hash(composed_message).to_bytes(),
85+
],
86+
&endpoint_program,
87+
);
88+
89+
let (event_authority_account, _) =
90+
Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program);
91+
92+
vec![
93+
AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
94+
AccountMetaRef { pubkey: (*to).into(), is_writable: false },
95+
AccountMetaRef { pubkey: composed_message_account.into(), is_writable: true },
96+
AccountMetaRef { pubkey: event_authority_account.into(), is_writable: false },
97+
AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
98+
]
99+
}

0 commit comments

Comments
 (0)