@layerzerolabs/oapp-solana-contracts 1.2.63 → 1.2.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/common.rs CHANGED
@@ -1,9 +1,4 @@
1
- use std::collections::HashMap;
2
-
3
1
  use anchor_lang::{prelude::*, Discriminator};
4
- use solana_address_lookup_table_interface::state::AddressLookupTable;
5
-
6
- use crate::errors::OAppError;
7
2
 
8
3
  pub const EXECUTION_CONTEXT_SEED: &[u8] = b"ExecutionContext";
9
4
  pub const EXECUTION_CONTEXT_VERSION_1: u8 = 1;
@@ -47,110 +42,33 @@ impl anchor_lang::AccountDeserialize for ExecutionContextV1 {
47
42
  }
48
43
  }
49
44
 
50
- /// A generic account locator used in LZ execution planning for V2.
51
- /// Can reference the address directly, via ALT, or as a placeholder.
45
+ /// How a planned instruction names an account; the Executor resolves it when building delivery.
52
46
  ///
53
- /// This enum enables the compact account referencing design of V2, supporting:
54
- /// - OApps to request multiple signer accounts, not just a single Executor EOA
55
- /// - Dynamic creation of writable EOA-based data accounts
56
- /// - Efficient encoding of addresses via ALTs, reducing account list size
57
- ///
58
- /// The legacy is_signer flag is removed. Instead, signer roles are explicitly
59
- /// declared through Payer and indexed Signer(u8) variants.
60
- #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
47
+ /// Only `Payer` and `Signer` resolve to signers, so a literal `Address` can never request a
48
+ /// signature.
49
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
61
50
  pub enum AddressLocator {
62
- /// Directly supplied public key - standard address reference
51
+ /// A literal account address.
63
52
  Address(Pubkey),
64
- /// Indexed address from a specific Address Lookup Table (ALT)
65
- /// Format: (ALT list index, address index within ALT)
66
- /// Enables efficient account list compression via Solana's ALT mechanism
67
- AltIndex(u8, u8),
68
- /// Executor's fee payer - substituted by the Executor's EOA
69
- /// This is the primary signer and fee payer for the transaction
53
+ /// The Executor's fee payer.
70
54
  Payer,
71
- /// Additional signer accounts - substituted by EOAs provided by the Executor
72
- /// The u8 index identifies each signer's position in the signer list,
73
- /// allowing the OApp to reference multiple distinct signers for dynamic account creation
55
+ /// An additional Executor-provided signer, e.g. the keypair of an account the plan creates.
56
+ /// Equal indices refer to the same key throughout the plan.
74
57
  Signer(u8),
75
- /// A context account provided by the Executor containing execution
76
- /// metadata, such as SOL spend limits.
58
+ /// The Executor's execution-context PDA (`[EXECUTION_CONTEXT_SEED, payer, context_version]`
59
+ /// under the Executor program), e.g. an [`ExecutionContextV1`].
77
60
  Context,
78
- // Append more address placeholders in the future.
79
61
  }
80
62
 
81
63
  impl From<Pubkey> for AddressLocator {
82
- fn from(pubkey: Pubkey) -> Self {
83
- AddressLocator::Address(pubkey)
64
+ fn from(key: Pubkey) -> Self {
65
+ Self::Address(key)
84
66
  }
85
67
  }
86
68
 
87
- /// Account metadata for V2 execution planning.
88
- /// Used by the Executor to construct the final transaction.
89
- ///
90
- /// V2 removes the legacy is_signer flag from V1's AccountMeta.
91
- /// Instead, signer roles are explicitly declared through AddressLocator variants.
92
- /// This provides clearer semantics and enables multiple signer support.
93
- #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
69
+ /// One account of a planned instruction. Signer status comes from the locator, not a flag.
70
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
94
71
  pub struct AccountMetaRef {
95
- /// The account address locator - supports multiple resolution strategies
96
72
  pub pubkey: AddressLocator,
97
- /// Whether the account should be writable in the final transaction
98
73
  pub is_writable: bool,
99
74
  }
100
-
101
- pub fn compact_accounts_with_alts(
102
- alt_accounts: &[AccountInfo],
103
- instruction_accounts: Vec<AccountMetaRef>,
104
- ) -> Result<Vec<AccountMetaRef>> {
105
- // Build address lookup table mapping from remaining_accounts
106
- // This enables efficient account referencing via ALT indices
107
- let address_to_alt_index_map = build_address_to_alt_index_map(alt_accounts)?;
108
-
109
- // Convert accounts to use ALT indices where possible
110
- let compacted_accounts = instruction_accounts
111
- .into_iter()
112
- .map(|mut account_meta| {
113
- if let AddressLocator::Address(pubkey) = account_meta.pubkey {
114
- account_meta.pubkey = to_address_locator(&address_to_alt_index_map, pubkey);
115
- }
116
- account_meta
117
- })
118
- .collect();
119
-
120
- Ok(compacted_accounts)
121
- }
122
-
123
- pub fn to_address_locator(
124
- address_to_alt_index_map: &HashMap<Pubkey, (u8, u8)>,
125
- key: Pubkey,
126
- ) -> AddressLocator {
127
- address_to_alt_index_map
128
- .get(&key)
129
- .map(|alt_index| AddressLocator::AltIndex(alt_index.0, alt_index.1))
130
- .unwrap_or(AddressLocator::Address(key))
131
- }
132
-
133
- /// Helper function to deserialize AddressLookupTable data
134
- pub fn deserialize_alt(alt: &AccountInfo) -> Result<Vec<Pubkey>> {
135
- let data = alt
136
- .try_borrow_data()
137
- .map_err(|_| error!(OAppError::InvalidAddressLookupTable))?;
138
- AddressLookupTable::deserialize(*data)
139
- .map(|alt| alt.addresses.to_vec())
140
- .map_err(|_| error!(OAppError::InvalidAddressLookupTable))
141
- }
142
-
143
- /// Helper function to build a map of addresses to their ALT indices
144
- pub fn build_address_to_alt_index_map(
145
- alt_accounts: &[AccountInfo],
146
- ) -> Result<HashMap<Pubkey, (u8, u8)>> {
147
- let mut address_to_alt_index_map: HashMap<Pubkey, (u8, u8)> = HashMap::new();
148
- for (alt_index, alt) in alt_accounts.iter().enumerate() {
149
- let addresses_in_alt = deserialize_alt(alt)?;
150
- for (address_index_in_alt, address_in_alt) in addresses_in_alt.iter().enumerate() {
151
- address_to_alt_index_map
152
- .insert(*address_in_alt, (alt_index as u8, address_index_in_alt as u8));
153
- }
154
- }
155
- Ok(address_to_alt_index_map)
156
- }
@@ -22,10 +22,14 @@
22
22
 
23
23
  use crate::endpoint::{
24
24
  self,
25
- cpi::accounts::{Clear, ClearCompose, Quote, RegisterOapp, Send, SendCompose, SetDelegate},
25
+ cpi::accounts::{
26
+ Burn, Clear, ClearCompose, Nilify, Quote, RegisterOapp, Send, SendCompose, SetDelegate,
27
+ Skip,
28
+ },
26
29
  types::{
27
- ClearComposeParams, ClearParams, MessagingFee, MessagingReceipt, QuoteParams,
28
- RegisterOAppParams, SendComposeParams, SendParams, SetDelegateParams,
30
+ BurnParams, ClearComposeParams, ClearParams, MessagingFee, MessagingReceipt, NilifyParams,
31
+ QuoteParams, RegisterOAppParams, SendComposeParams, SendParams, SetDelegateParams,
32
+ SkipParams,
29
33
  },
30
34
  };
31
35
  use anchor_lang::{
@@ -41,6 +45,9 @@ pub const QUOTE_MIN_ACCOUNTS_LEN: usize = 7;
41
45
  pub const CLEAR_MIN_ACCOUNTS_LEN: usize = 8;
42
46
  pub const SEND_COMPOSE_MIN_ACCOUNTS_LEN: usize = 7;
43
47
  pub const CLEAR_COMPOSE_MIN_ACCOUNTS_LEN: usize = 5;
48
+ pub const SKIP_MIN_ACCOUNTS_LEN: usize = 9;
49
+ pub const BURN_MIN_ACCOUNTS_LEN: usize = 8;
50
+ pub const NILIFY_MIN_ACCOUNTS_LEN: usize = 8;
44
51
 
45
52
  fn validate(accounts: &[AccountInfo], endpoint_program: Pubkey, min_len: usize) -> Result<()> {
46
53
  require!(accounts.len() >= min_len, ErrorCode::AccountNotEnoughKeys);
@@ -249,9 +256,9 @@ pub fn quote(
249
256
 
250
257
  /// Marks an inbound message as received by clearing it from the Endpoint.
251
258
  ///
252
- /// Called during `lz_receive` processing to acknowledge that the OApp has consumed the
253
- /// message payload for a given (srcEid, sender, nonce) tuple. This advances the
254
- /// inbound nonce and allows subsequent messages to be delivered.
259
+ /// Called during `lz_receive` processing to validate and consume the verified payload for a
260
+ /// given (srcEid, sender, nonce) tuple. The inbound nonce must already cover this message;
261
+ /// clearing the payload does not advance it.
255
262
  ///
256
263
  /// # Accounts layout
257
264
  /// The `accounts` slice must follow the `Clear` struct field order:
@@ -268,9 +275,8 @@ pub fn quote(
268
275
  ///
269
276
  /// # Accounts validation
270
277
  /// - `accounts[0]` must equal the caller-supplied `endpoint_program` parameter.
271
- /// - `accounts[1]` is the `signer` — pinned to `receiver`. The OApp PDA path signs via `seeds`; the
272
- /// delegate path requires the delegate to already be a signer. The Endpoint accepts either the
273
- /// OApp PDA (`params.receiver`) or the registered delegate as `signer`.
278
+ /// - `accounts[1]` is the `signer` — pinned to `receiver`, as is `params.receiver`, so the
279
+ /// Endpoint's delegate-signer path is not available through this wrapper.
274
280
  ///
275
281
  /// # Returns
276
282
  /// The payload hash (`[u8; 32]`) of the cleared message.
@@ -283,6 +289,7 @@ pub fn clear(
283
289
  ) -> Result<[u8; 32]> {
284
290
  validate(accounts, endpoint_program, CLEAR_MIN_ACCOUNTS_LEN)?;
285
291
  require_keys_eq!(accounts[1].key(), receiver, ErrorCode::ConstraintAddress);
292
+ require_keys_eq!(params.receiver, receiver, ErrorCode::ConstraintAddress);
286
293
 
287
294
  let cpi_accounts = Clear {
288
295
  signer: accounts[1].clone(),
@@ -300,11 +307,146 @@ pub fn clear(
300
307
  Ok(result.get())
301
308
  }
302
309
 
303
- /// Sends a composed message to another program on the same chain.
310
+ /// Skips the next inbound nonce on the Endpoint messaging channel.
311
+ ///
312
+ /// # Accounts layout
313
+ /// The `accounts` slice must follow the `Skip` struct field order:
314
+ ///
315
+ /// 0. `endpoint_program`
316
+ /// 1. `signer`
317
+ /// 2. `oapp_registry`
318
+ /// 3. `nonce`
319
+ /// 4. `pending_inbound_nonce`
320
+ /// 5. `payload_hash`
321
+ /// 6. `endpoint`
322
+ /// 7. `event_authority`
323
+ /// 8. `program`
324
+ /// 9. remaining accounts (variable length)
325
+ ///
326
+ /// # Accounts validation
327
+ /// - `accounts[0]` must equal the caller-supplied `endpoint_program` parameter.
328
+ /// - `accounts[1]` is the `signer` — pinned to `receiver`, as is `params.receiver`.
329
+ pub fn skip(
330
+ endpoint_program: Pubkey,
331
+ receiver: Pubkey,
332
+ accounts: &[AccountInfo],
333
+ seeds: &[&[u8]],
334
+ params: SkipParams,
335
+ ) -> Result<()> {
336
+ validate(accounts, endpoint_program, SKIP_MIN_ACCOUNTS_LEN)?;
337
+ require_keys_eq!(accounts[1].key(), receiver, ErrorCode::ConstraintAddress);
338
+ require_keys_eq!(params.receiver, receiver, ErrorCode::ConstraintAddress);
339
+
340
+ let cpi_accounts = Skip {
341
+ signer: accounts[1].clone(),
342
+ oapp_registry: accounts[2].clone(),
343
+ nonce: accounts[3].clone(),
344
+ pending_inbound_nonce: accounts[4].clone(),
345
+ payload_hash: accounts[5].clone(),
346
+ endpoint: accounts[6].clone(),
347
+ event_authority: accounts[7].clone(),
348
+ program: accounts[8].clone(),
349
+ };
350
+ endpoint::cpi::skip(
351
+ make_cpi_ctx(accounts, cpi_accounts, SKIP_MIN_ACCOUNTS_LEN).with_signer(&[seeds]),
352
+ params,
353
+ )
354
+ }
355
+
356
+ /// Permanently burns a verified payload hash so it can never be executed or re-verified.
357
+ ///
358
+ /// # Accounts layout
359
+ /// The `accounts` slice must follow the `Burn` struct field order:
360
+ ///
361
+ /// 0. `endpoint_program`
362
+ /// 1. `signer`
363
+ /// 2. `oapp_registry`
364
+ /// 3. `nonce`
365
+ /// 4. `payload_hash`
366
+ /// 5. `endpoint`
367
+ /// 6. `event_authority`
368
+ /// 7. `program`
369
+ /// 8. remaining accounts (variable length)
370
+ ///
371
+ /// # Accounts validation
372
+ /// - `accounts[0]` must equal the caller-supplied `endpoint_program` parameter.
373
+ /// - `accounts[1]` is the `signer` — pinned to `receiver`, as is `params.receiver`.
374
+ pub fn burn(
375
+ endpoint_program: Pubkey,
376
+ receiver: Pubkey,
377
+ accounts: &[AccountInfo],
378
+ seeds: &[&[u8]],
379
+ params: BurnParams,
380
+ ) -> Result<()> {
381
+ validate(accounts, endpoint_program, BURN_MIN_ACCOUNTS_LEN)?;
382
+ require_keys_eq!(accounts[1].key(), receiver, ErrorCode::ConstraintAddress);
383
+ require_keys_eq!(params.receiver, receiver, ErrorCode::ConstraintAddress);
384
+
385
+ let cpi_accounts = Burn {
386
+ signer: accounts[1].clone(),
387
+ oapp_registry: accounts[2].clone(),
388
+ nonce: accounts[3].clone(),
389
+ payload_hash: accounts[4].clone(),
390
+ endpoint: accounts[5].clone(),
391
+ event_authority: accounts[6].clone(),
392
+ program: accounts[7].clone(),
393
+ };
394
+ endpoint::cpi::burn(
395
+ make_cpi_ctx(accounts, cpi_accounts, BURN_MIN_ACCOUNTS_LEN).with_signer(&[seeds]),
396
+ params,
397
+ )
398
+ }
399
+
400
+ /// Temporarily nilifies a payload hash (blocks execution until re-verification).
401
+ ///
402
+ /// Note: `nilify` has no Endpoint settings account.
403
+ ///
404
+ /// # Accounts layout
405
+ /// The `accounts` slice must follow the `Nilify` struct field order:
406
+ ///
407
+ /// 0. `endpoint_program`
408
+ /// 1. `signer`
409
+ /// 2. `oapp_registry`
410
+ /// 3. `nonce`
411
+ /// 4. `pending_inbound_nonce`
412
+ /// 5. `payload_hash`
413
+ /// 6. `event_authority`
414
+ /// 7. `program`
415
+ /// 8. remaining accounts (variable length)
416
+ ///
417
+ /// # Accounts validation
418
+ /// - `accounts[0]` must equal the caller-supplied `endpoint_program` parameter.
419
+ /// - `accounts[1]` is the `signer` — pinned to `receiver`, as is `params.receiver`.
420
+ pub fn nilify(
421
+ endpoint_program: Pubkey,
422
+ receiver: Pubkey,
423
+ accounts: &[AccountInfo],
424
+ seeds: &[&[u8]],
425
+ params: NilifyParams,
426
+ ) -> Result<()> {
427
+ validate(accounts, endpoint_program, NILIFY_MIN_ACCOUNTS_LEN)?;
428
+ require_keys_eq!(accounts[1].key(), receiver, ErrorCode::ConstraintAddress);
429
+ require_keys_eq!(params.receiver, receiver, ErrorCode::ConstraintAddress);
430
+
431
+ let cpi_accounts = Nilify {
432
+ signer: accounts[1].clone(),
433
+ oapp_registry: accounts[2].clone(),
434
+ nonce: accounts[3].clone(),
435
+ pending_inbound_nonce: accounts[4].clone(),
436
+ payload_hash: accounts[5].clone(),
437
+ event_authority: accounts[6].clone(),
438
+ program: accounts[7].clone(),
439
+ };
440
+ endpoint::cpi::nilify(
441
+ make_cpi_ctx(accounts, cpi_accounts, NILIFY_MIN_ACCOUNTS_LEN).with_signer(&[seeds]),
442
+ params,
443
+ )
444
+ }
445
+
446
+ /// Queues a composed message for another program on the same chain.
304
447
  ///
305
- /// Composition allows an OApp to trigger follow-up logic in a separate program after
306
- /// receiving a cross-chain message. The Endpoint stores the composed message hash,
307
- /// and the target program's `lz_compose` handler is invoked to process it.
448
+ /// The Endpoint stores the composed message hash and emits an event. The target program's
449
+ /// `lz_compose` handler is invoked separately; this CPI does not execute the composer.
308
450
  ///
309
451
  /// # Accounts layout
310
452
  /// The `accounts` slice must follow the `SendCompose` struct field order:
package/src/errors.rs CHANGED
@@ -1,10 +1,9 @@
1
1
  use anchor_lang::error_code;
2
2
 
3
3
  /// Custom error codes for OApp operations
4
- #[error_code]
4
+ // The offset keeps these codes distinct from RBAC's and the consuming program's own errors.
5
+ #[error_code(offset = 11000)]
5
6
  pub enum OAppError {
6
- #[msg("Invalid address lookup table")]
7
- InvalidAddressLookupTable,
8
7
  #[msg("Invalid options")]
9
8
  InvalidOptions,
10
9
  #[msg("Invalid options length")]
@@ -13,4 +12,6 @@ pub enum OAppError {
13
12
  NoPeer,
14
13
  #[msg("TLV update would change entry size")]
15
14
  TlvLengthMismatch,
15
+ #[msg("Planner output must be a writable, empty scratch account owned by the planner")]
16
+ InvalidPlanOutput,
16
17
  }
package/src/lib.rs CHANGED
@@ -12,13 +12,16 @@
12
12
  //! - **Account macros** – re-exported from [`oapp_macros`] to generate boilerplate Anchor account
13
13
  //! structs for standard OApp instructions.
14
14
  //! - **Types & traits** – shared parameter / return structs, the [`OAppState`] trait for PDA signer
15
- //! seeds, and messaging option builders.
15
+ //! seeds, [`MessagingChannelRoles`] for messaging-channel RBAC, and messaging option builders.
16
+ //! - **Receive / compose planning** – plan formats ([`lz_receive_types_v3`],
17
+ //! [`lz_compose_types_v3`]) and the [`lz_plan`] output writer.
16
18
 
17
19
  pub mod common;
18
20
  pub mod endpoint_cpi;
19
21
  pub mod events;
20
- pub mod lz_compose_types_v2;
21
- pub mod lz_receive_types_v2;
22
+ pub mod lz_compose_types_v3;
23
+ pub mod lz_plan;
24
+ pub mod lz_receive_types_v3;
22
25
  pub mod oapp_info;
23
26
  pub mod options;
24
27
  pub mod types;
@@ -59,15 +62,12 @@ pub const RECEIVE_LIBRARY_CONFIG_SEED: &[u8] = b"ReceiveLibraryConfig";
59
62
  // Event
60
63
  pub const EVENT_SEED: &[u8] = b"__event_authority";
61
64
 
62
- // PDA seed helpers below use `Box::leak` to produce the `&'static [&'static [u8]]`
63
- // shape required by Anchor's `#[account(seeds = ...)]` attribute. Each call leaks
64
- // ~64 bytes. **SVM-only**: harmless on-chain because the BPF VM allocator is torn
65
- // down at the end of every transaction, so leaks never accumulate. Do not call
66
- // these from long-running off-chain Rust (native test runners, indexers, CLIs) —
67
- // derive PDAs with stack-local arrays in those contexts instead.
68
-
69
- /// PDA seeds for an OApp's `lz_receive_types_accounts` PDA.
70
- /// Seed shape: [LZ_RECEIVE_TYPES_SEED, oapp_key]
65
+ /// PDA seeds for an OApp's `lz_receive_types_accounts` PDA: `[LZ_RECEIVE_TYPES_SEED, oapp_key]`.
66
+ ///
67
+ /// Uses `Box::leak` to produce the `'static` shape Anchor's `#[account(seeds = ...)]` requires,
68
+ /// leaking ~64 bytes per call. **SVM-only**: on-chain the leak lives only as long as the program's
69
+ /// VM heap. Do not call this from long-running off-chain Rust (native test runners, indexers,
70
+ /// CLIs); derive the PDA with stack-local seeds there.
71
71
  pub fn lz_receive_types_seeds(oapp_key: &Pubkey) -> &'static [&'static [u8]] {
72
72
  let key_bytes: &'static [u8; 32] = Box::leak(Box::new(oapp_key.to_bytes()));
73
73
  Box::leak(Box::new([LZ_RECEIVE_TYPES_SEED, key_bytes.as_slice()]))
@@ -80,3 +80,17 @@ pub fn lz_receive_types_seeds(oapp_key: &Pubkey) -> &'static [&'static [u8]] {
80
80
  pub trait OAppState<const N: usize> {
81
81
  fn signer_seeds(&self) -> [&[u8]; N];
82
82
  }
83
+
84
+ /// Roles gating the `#[oapp]`-generated messaging-channel instructions; the program's role enum
85
+ /// must implement this.
86
+ ///
87
+ /// The DefaultAdmin does not need them: it is the OApp's Endpoint delegate and can clear, skip,
88
+ /// burn and nilify on the Endpoint directly. These roles let other keys do the same through the
89
+ /// OApp. The two are meant to be distinct roles, which the framework does not check.
90
+ pub trait MessagingChannelRoles {
91
+ /// Role for `clear`, `skip`, and `burn`.
92
+ fn messaging_channel_manager_role() -> Self;
93
+
94
+ /// Role for `nilify`.
95
+ fn message_nilifier_role() -> Self;
96
+ }
@@ -0,0 +1,72 @@
1
+ //! Compose plan format (planner version 3) and the Endpoint `clear_compose` account helper.
2
+ //!
3
+ //! See `docs/lz-compose-types.md` for how compose planning differs from receive planning.
4
+ use crate::{common::AccountMetaRef, COMPOSED_MESSAGE_HASH_SEED, EVENT_SEED};
5
+ use anchor_lang::prelude::*;
6
+ use solana_keccak_hasher::hash;
7
+
8
+ /// Planner version returned by `lz_compose_types_info`.
9
+ pub const LZ_COMPOSE_TYPES_VERSION: u8 = 3;
10
+
11
+ /// Returned by `lz_compose_types_info`: the planner's read-only, app-specific inputs.
12
+ ///
13
+ /// The client prepends the payer and the plan scratch account. Account order is part of the
14
+ /// planner ABI, including Anchor's optional-account sentinels.
15
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
16
+ pub struct LzComposeTypesV3Accounts {
17
+ pub accounts: Vec<Pubkey>,
18
+ }
19
+
20
+ /// Compose plan written by `lz_compose_types_v3`.
21
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
22
+ pub struct LzComposeTypesV3Result {
23
+ /// Execution-context version; selects the account
24
+ /// [`AddressLocator::Context`](crate::common::AddressLocator::Context) resolves to.
25
+ pub context_version: u8,
26
+ /// Executed in order in one transaction. Must contain exactly one `LzCompose`; `Standard`
27
+ /// instructions may precede or follow it.
28
+ pub instructions: Vec<ComposeInstruction>,
29
+ }
30
+
31
+ /// One instruction of a compose plan.
32
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
33
+ pub enum ComposeInstruction {
34
+ /// The delivery: this program's `lz_compose`, called with the original params.
35
+ LzCompose { accounts: Vec<AccountMetaRef> },
36
+ /// Any other instruction, submitted as given.
37
+ Standard { program_id: Pubkey, accounts: Vec<AccountMetaRef>, data: Vec<u8> },
38
+ }
39
+
40
+ /// Accounts for the Endpoint `clear_compose` CPI, in the order
41
+ /// [`crate::endpoint_cpi::clear_compose`] expects.
42
+ pub fn get_accounts_for_clear_compose(
43
+ endpoint_program: Pubkey,
44
+ from: &Pubkey,
45
+ to: &Pubkey,
46
+ guid: &[u8; 32],
47
+ index: u16,
48
+ composed_message: &[u8],
49
+ ) -> Vec<AccountMetaRef> {
50
+ let (composed_message_account, _) = Pubkey::find_program_address(
51
+ &[
52
+ COMPOSED_MESSAGE_HASH_SEED,
53
+ &from.to_bytes(),
54
+ &to.to_bytes(),
55
+ &guid[..],
56
+ &index.to_be_bytes(),
57
+ &hash(composed_message).to_bytes(),
58
+ ],
59
+ &endpoint_program,
60
+ );
61
+
62
+ let (event_authority_account, _) =
63
+ Pubkey::find_program_address(&[EVENT_SEED], &endpoint_program);
64
+
65
+ vec![
66
+ AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
67
+ AccountMetaRef { pubkey: (*to).into(), is_writable: false },
68
+ AccountMetaRef { pubkey: composed_message_account.into(), is_writable: true },
69
+ AccountMetaRef { pubkey: event_authority_account.into(), is_writable: false },
70
+ AccountMetaRef { pubkey: endpoint_program.into(), is_writable: false },
71
+ ]
72
+ }
package/src/lz_plan.rs ADDED
@@ -0,0 +1,71 @@
1
+ //! Output transport for `lz_receive_types_v3` / `lz_compose_types_v3` plans.
2
+ //!
3
+ //! The caller supplies the output ("scratch") account: it creates it with its own keypair in a
4
+ //! top-level `SystemProgram::create_account`, owned by the planner program, ahead of the planner
5
+ //! instruction. The program never allocates or resizes the scratch and pays no rent. A top-level
6
+ //! create is not a CPI, so `MAX_PERMITTED_DATA_INCREASE` does not apply and the caller alone
7
+ //! decides how large the scratch is.
8
+ //!
9
+ //! The plan is written Borsh-encoded at offset 0, with no header. [`write()`] returns the encoded
10
+ //! length whether or not the plan fit, and planners return that as their instruction's return data:
11
+ //! a caller whose scratch was too small re-simulates with a larger one, and otherwise reads
12
+ //! `data[..length]` from the same simulation's post-state. The scratch's own length says nothing
13
+ //! about the plan.
14
+ //!
15
+ //! Planning is meant to be simulated, not submitted, but nothing here enforces that: a signed
16
+ //! transaction can run a planner and leave the scratch account allocated.
17
+
18
+ use crate::errors::OAppError;
19
+ use anchor_lang::prelude::*;
20
+
21
+ fn validate_output(program: &Pubkey, output: &AccountInfo) -> Result<()> {
22
+ // Checked here so a planner that forgets `mut` fails with this error, not the runtime's.
23
+ require!(output.is_writable, OAppError::InvalidPlanOutput);
24
+ require_keys_eq!(*output.owner, *program, OAppError::InvalidPlanOutput);
25
+ require!(output.try_borrow_data()?.iter().all(|byte| *byte == 0), OAppError::InvalidPlanOutput);
26
+ Ok(())
27
+ }
28
+
29
+ /// Writes `plan` into `output` if it fits, and returns the plan's encoded length either way.
30
+ ///
31
+ /// Fails with `InvalidPlanOutput` unless `output` is writable, owned by `program`, and all of its
32
+ /// data is zero. Taking a [`Signer`] means a caller can only hand over an account whose key it
33
+ /// holds, and the all-zero check keeps the planner from overwriting initialized program state or an
34
+ /// earlier plan.
35
+ pub fn write<T: AnchorSerialize>(program: &Pubkey, output: &Signer, plan: &T) -> Result<u32> {
36
+ let output = output.as_ref();
37
+ validate_output(program, output)?;
38
+ let payload = borsh::to_vec(plan)?;
39
+ if payload.len() <= output.data_len() {
40
+ output.try_borrow_mut_data()?[..payload.len()].copy_from_slice(&payload);
41
+ }
42
+ // A plan is bounded by the SBF heap, far below u32::MAX.
43
+ Ok(payload.len() as u32)
44
+ }
45
+
46
+ #[cfg(test)]
47
+ mod tests {
48
+ use super::*;
49
+
50
+ #[test]
51
+ fn output_must_be_writable_owned_and_zeroed() {
52
+ let (program, key) = (Pubkey::new_unique(), Pubkey::new_unique());
53
+ let run = |writable: bool, owner: Pubkey, data: Vec<u8>| {
54
+ let (mut lamports, mut data) = (1, data);
55
+ let info =
56
+ AccountInfo::new(&key, true, writable, &mut lamports, &mut data, &owner, false);
57
+ write(&program, &Signer::try_from(&info).unwrap(), &7u32)
58
+ };
59
+ assert_eq!(run(true, program, vec![0; 8]).unwrap(), 4);
60
+ for (writable, owner, data) in [
61
+ (false, program, vec![0; 8]),
62
+ (true, Pubkey::new_unique(), vec![0; 8]),
63
+ (true, program, vec![1; 8]),
64
+ ] {
65
+ assert_eq!(
66
+ run(writable, owner, data).unwrap_err(),
67
+ OAppError::InvalidPlanOutput.into()
68
+ );
69
+ }
70
+ }
71
+ }