@layerzerolabs/oapp-solana-contracts 1.2.62 → 1.2.64
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/Anchor.toml +1 -1
- package/Cargo.toml +2 -6
- package/README.md +15 -9
- package/macros/Cargo.toml +1 -1
- package/macros/src/instructions/composer/lz_compose_types_info.rs +2 -2
- package/macros/src/instructions/composer/lz_compose_types_v3.rs +18 -0
- package/macros/src/instructions/composer/mod.rs +2 -2
- package/macros/src/instructions/oapp/burn.rs +69 -0
- package/macros/src/instructions/oapp/clear.rs +70 -0
- package/macros/src/instructions/oapp/lz_receive_types_info.rs +1 -3
- package/macros/src/instructions/oapp/lz_receive_types_v3.rs +17 -0
- package/macros/src/instructions/oapp/mod.rs +10 -2
- package/macros/src/instructions/oapp/nilify.rs +69 -0
- package/macros/src/instructions/oapp/skip.rs +68 -0
- package/macros/src/lib.rs +2 -2
- package/macros/src/oapp.rs +1 -0
- package/package.json +9 -10
- package/src/common.rs +14 -96
- package/src/endpoint_cpi.rs +155 -13
- package/src/errors.rs +4 -3
- package/src/lib.rs +26 -12
- package/src/lz_compose_types_v3.rs +72 -0
- package/src/lz_plan.rs +71 -0
- package/src/{lz_receive_types_v2.rs → lz_receive_types_v3.rs} +27 -54
- package/src/oapp_info.rs +1 -1
- package/src/options.rs +5 -15
- package/src/types.rs +44 -0
- package/macros/src/instructions/composer/lz_compose_types_v2.rs +0 -17
- package/macros/src/instructions/oapp/lz_receive_types_v2.rs +0 -17
- package/src/lz_compose_types_v2.rs +0 -88
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
|
-
///
|
|
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
|
-
///
|
|
54
|
-
///
|
|
55
|
-
|
|
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
|
-
///
|
|
51
|
+
/// A literal account address.
|
|
63
52
|
Address(Pubkey),
|
|
64
|
-
///
|
|
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
|
-
///
|
|
72
|
-
///
|
|
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
|
-
///
|
|
76
|
-
///
|
|
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(
|
|
83
|
-
|
|
64
|
+
fn from(key: Pubkey) -> Self {
|
|
65
|
+
Self::Address(key)
|
|
84
66
|
}
|
|
85
67
|
}
|
|
86
68
|
|
|
87
|
-
///
|
|
88
|
-
|
|
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
|
-
}
|
package/src/endpoint_cpi.rs
CHANGED
|
@@ -22,10 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
use crate::endpoint::{
|
|
24
24
|
self,
|
|
25
|
-
cpi::accounts::{
|
|
25
|
+
cpi::accounts::{
|
|
26
|
+
Burn, Clear, ClearCompose, Nilify, Quote, RegisterOapp, Send, SendCompose, SetDelegate,
|
|
27
|
+
Skip,
|
|
28
|
+
},
|
|
26
29
|
types::{
|
|
27
|
-
ClearComposeParams, ClearParams, MessagingFee, MessagingReceipt,
|
|
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
|
|
253
|
-
///
|
|
254
|
-
///
|
|
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
|
|
272
|
-
/// delegate path
|
|
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
|
-
///
|
|
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
|
-
///
|
|
306
|
-
///
|
|
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
|
-
|
|
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
|
|
21
|
-
pub mod
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|