@layerzerolabs/protocol-stellar-v2 0.2.33 → 0.2.35

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.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +351 -367
  2. package/.turbo/turbo-lint.log +220 -223
  3. package/.turbo/turbo-test.log +1993 -1796
  4. package/Cargo.lock +10 -10
  5. package/Cargo.toml +1 -1
  6. package/contracts/common-macros/src/storage.rs +7 -5
  7. package/contracts/common-macros/src/tests/storage/snapshots/common_macros__tests__storage__generate_storage__snapshot_generated_storage_code.snap +3 -3
  8. package/contracts/endpoint-v2/src/endpoint_v2.rs +5 -4
  9. package/contracts/endpoint-v2/src/interfaces/messaging_channel.rs +7 -8
  10. package/contracts/endpoint-v2/src/messaging_channel.rs +78 -45
  11. package/contracts/endpoint-v2/src/storage.rs +8 -3
  12. package/contracts/endpoint-v2/src/tests/endpoint_setup.rs +2 -2
  13. package/contracts/endpoint-v2/src/tests/endpoint_v2/clear.rs +12 -15
  14. package/contracts/endpoint-v2/src/tests/endpoint_v2/verifiable.rs +46 -9
  15. package/contracts/endpoint-v2/src/tests/messaging_channel/burn.rs +7 -23
  16. package/contracts/endpoint-v2/src/tests/messaging_channel/clear_payload.rs +23 -20
  17. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound.rs +94 -1
  18. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound_nonce.rs +17 -15
  19. package/contracts/endpoint-v2/src/tests/messaging_channel/mod.rs +1 -1
  20. package/contracts/endpoint-v2/src/tests/messaging_channel/nilify.rs +48 -13
  21. package/contracts/endpoint-v2/src/tests/messaging_channel/pending_inbound_nonces.rs +111 -0
  22. package/contracts/endpoint-v2/src/tests/messaging_channel/skip.rs +15 -25
  23. package/contracts/layerzero-views/src/layerzero_view.rs +2 -2
  24. package/contracts/layerzero-views/src/tests/layerzero_view_tests.rs +3 -4
  25. package/contracts/layerzero-views/src/tests/setup.rs +0 -21
  26. package/contracts/message-libs/blocked-message-lib/src/lib.rs +4 -4
  27. package/contracts/message-libs/uln-302/src/send_uln.rs +5 -5
  28. package/contracts/oapps/counter/src/counter.rs +6 -0
  29. package/contracts/oapps/oapp/src/oapp_sender.rs +3 -2
  30. package/contracts/oapps/oft/integration-tests/extensions/test_oft_fee.rs +5 -11
  31. package/contracts/oapps/oft/integration-tests/extensions/test_pausable.rs +7 -14
  32. package/contracts/oapps/oft/integration-tests/extensions/test_rate_limiter.rs +11 -22
  33. package/contracts/oapps/oft/integration-tests/setup.rs +59 -7
  34. package/contracts/oapps/oft/integration-tests/utils.rs +28 -2
  35. package/contracts/oapps/oft/src/extensions/oft_fee.rs +5 -0
  36. package/contracts/oapps/oft/src/interfaces/mintable.rs +14 -0
  37. package/contracts/oapps/oft/src/interfaces/mod.rs +2 -2
  38. package/contracts/oapps/oft/src/oft.rs +8 -7
  39. package/contracts/oapps/oft/src/oft_types/mint_burn.rs +8 -8
  40. package/contracts/oapps/oft/src/oft_types/mod.rs +3 -4
  41. package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +7 -5
  42. package/contracts/oapps/sac-manager/src/errors.rs +14 -0
  43. package/contracts/{sac-manager → oapps/sac-manager}/src/lib.rs +0 -4
  44. package/contracts/oapps/sac-manager/src/sac_manager.rs +115 -0
  45. package/contracts/oapps/sac-manager/src/storage.rs +20 -0
  46. package/contracts/{sac-manager → oapps/sac-manager}/src/tests/mod.rs +0 -4
  47. package/contracts/oapps/sac-manager/src/tests/sac_manager/clawback.rs +86 -0
  48. package/contracts/oapps/sac-manager/src/tests/sac_manager/mint.rs +58 -0
  49. package/contracts/{sac-manager → oapps/sac-manager}/src/tests/sac_manager/mod.rs +1 -3
  50. package/contracts/oapps/sac-manager/src/tests/sac_manager/set_minter.rs +69 -0
  51. package/contracts/oapps/sac-manager/src/tests/sac_manager/test_helper.rs +18 -0
  52. package/contracts/oapps/sac-manager/src/tests/sac_manager/view_functions.rs +28 -0
  53. package/contracts/{sac-manager → oapps/sac-manager}/src/tests/test_helper.rs +16 -59
  54. package/docs/layerzero-v2-on-stellar.md +46 -2
  55. package/package.json +8 -3
  56. package/sdk/.turbo/turbo-test.log +424 -429
  57. package/sdk/dist/generated/bml.d.ts +3 -3
  58. package/sdk/dist/generated/bml.js +3 -3
  59. package/sdk/dist/generated/counter.d.ts +32 -3
  60. package/sdk/dist/generated/counter.js +6 -3
  61. package/sdk/dist/generated/dvn.d.ts +3 -3
  62. package/sdk/dist/generated/dvn.js +3 -3
  63. package/sdk/dist/generated/dvn_fee_lib.d.ts +2 -2
  64. package/sdk/dist/generated/dvn_fee_lib.js +2 -2
  65. package/sdk/dist/generated/endpoint.d.ts +12 -13
  66. package/sdk/dist/generated/endpoint.js +7 -7
  67. package/sdk/dist/generated/executor.d.ts +3 -3
  68. package/sdk/dist/generated/executor.js +3 -3
  69. package/sdk/dist/generated/executor_fee_lib.d.ts +2 -2
  70. package/sdk/dist/generated/executor_fee_lib.js +2 -2
  71. package/sdk/dist/generated/executor_helper.d.ts +2 -2
  72. package/sdk/dist/generated/executor_helper.js +2 -2
  73. package/sdk/dist/generated/layerzero_view.d.ts +3 -3
  74. package/sdk/dist/generated/layerzero_view.js +3 -3
  75. package/sdk/dist/generated/oft.d.ts +32 -3
  76. package/sdk/dist/generated/oft.js +7 -4
  77. package/sdk/dist/generated/price_feed.d.ts +3 -3
  78. package/sdk/dist/generated/price_feed.js +3 -3
  79. package/sdk/dist/generated/sac_manager.d.ts +47 -318
  80. package/sdk/dist/generated/sac_manager.js +24 -129
  81. package/sdk/dist/generated/sml.d.ts +2 -2
  82. package/sdk/dist/generated/sml.js +2 -2
  83. package/sdk/dist/generated/treasury.d.ts +2 -2
  84. package/sdk/dist/generated/treasury.js +2 -2
  85. package/sdk/dist/generated/uln302.d.ts +3 -3
  86. package/sdk/dist/generated/uln302.js +3 -3
  87. package/sdk/dist/generated/upgrader.d.ts +2 -2
  88. package/sdk/dist/generated/upgrader.js +2 -2
  89. package/sdk/package.json +6 -1
  90. package/sdk/test/oft-sml.test.ts +72 -36
  91. package/sdk/test/sac-manager-redistribution.test.ts +38 -182
  92. package/contracts/endpoint-v2/src/tests/messaging_channel/lazy_inbound_nonce.rs +0 -39
  93. package/contracts/oapps/oft/src/interfaces/mint_burnable.rs +0 -18
  94. package/contracts/sac-manager/src/errors.rs +0 -18
  95. package/contracts/sac-manager/src/extensions/mod.rs +0 -6
  96. package/contracts/sac-manager/src/extensions/redistribution.rs +0 -109
  97. package/contracts/sac-manager/src/extensions/supply_control/mod.rs +0 -488
  98. package/contracts/sac-manager/src/extensions/supply_control/rate_limit.rs +0 -126
  99. package/contracts/sac-manager/src/interfaces/mod.rs +0 -3
  100. package/contracts/sac-manager/src/interfaces/sac_manager.rs +0 -52
  101. package/contracts/sac-manager/src/sac_manager.rs +0 -193
  102. package/contracts/sac-manager/src/storage.rs +0 -20
  103. package/contracts/sac-manager/src/tests/redistribution/mod.rs +0 -1
  104. package/contracts/sac-manager/src/tests/redistribution/redistribute_funds.rs +0 -82
  105. package/contracts/sac-manager/src/tests/sac_manager/admin_mint.rs +0 -206
  106. package/contracts/sac-manager/src/tests/sac_manager/burn.rs +0 -215
  107. package/contracts/sac-manager/src/tests/sac_manager/clawback.rs +0 -209
  108. package/contracts/sac-manager/src/tests/sac_manager/mint.rs +0 -252
  109. package/contracts/sac-manager/src/tests/sac_manager/set_oft_address.rs +0 -47
  110. package/contracts/sac-manager/src/tests/sac_manager/test_helper.rs +0 -75
  111. package/contracts/sac-manager/src/tests/sac_manager/view_functions.rs +0 -60
  112. package/contracts/sac-manager/src/tests/supply_control/enumerable_set.rs +0 -256
  113. package/contracts/sac-manager/src/tests/supply_control/mod.rs +0 -8
  114. package/contracts/sac-manager/src/tests/supply_control/refill.rs +0 -90
  115. package/contracts/sac-manager/src/tests/supply_control/set_mint_whitelist.rs +0 -245
  116. package/contracts/sac-manager/src/tests/supply_control/set_supply_controller.rs +0 -267
  117. package/contracts/sac-manager/src/tests/supply_control/set_supply_controller_manager.rs +0 -122
  118. package/contracts/sac-manager/src/tests/supply_control/test_helper.rs +0 -38
  119. package/contracts/sac-manager/src/tests/supply_control/update_allow_any_mint_burn.rs +0 -114
  120. package/contracts/sac-manager/src/tests/supply_control/update_limit_config.rs +0 -257
  121. /package/contracts/{sac-manager → oapps/sac-manager}/Cargo.toml +0 -0
  122. /package/contracts/{sac-manager → oapps/sac-manager}/src/tests/sac_manager/set_admin.rs +0 -0
  123. /package/contracts/{sac-manager → oapps/sac-manager}/src/tests/sac_manager/set_authorized.rs +0 -0
package/Cargo.lock CHANGED
@@ -1755,9 +1755,9 @@ dependencies = [
1755
1755
 
1756
1756
  [[package]]
1757
1757
  name = "soroban-ledger-snapshot"
1758
- version = "25.1.0"
1758
+ version = "25.1.1"
1759
1759
  source = "registry+https://github.com/rust-lang/crates.io-index"
1760
- checksum = "99c5285c83e7a5581879b7a65033eae53b24ac9689975aa6887f1d8ee3e941c9"
1760
+ checksum = "66d569a1315f05216d024653ad87541aa15d3ff26dad9f8a98719cb53ccf2bf3"
1761
1761
  dependencies = [
1762
1762
  "serde",
1763
1763
  "serde_json",
@@ -1769,9 +1769,9 @@ dependencies = [
1769
1769
 
1770
1770
  [[package]]
1771
1771
  name = "soroban-sdk"
1772
- version = "25.1.0"
1772
+ version = "25.1.1"
1773
1773
  source = "registry+https://github.com/rust-lang/crates.io-index"
1774
- checksum = "b1262aa83e99a0fb3e8cd56d6e5ca4c28ac4f9871ac7173f65301a8b9a12c20f"
1774
+ checksum = "add8d19cfd2c9941bbdc7c8223c3cf9d7ff9af4554ba3bd4ae93e16b19b08aea"
1775
1775
  dependencies = [
1776
1776
  "arbitrary",
1777
1777
  "bytes-lit",
@@ -1793,9 +1793,9 @@ dependencies = [
1793
1793
 
1794
1794
  [[package]]
1795
1795
  name = "soroban-sdk-macros"
1796
- version = "25.1.0"
1796
+ version = "25.1.1"
1797
1797
  source = "registry+https://github.com/rust-lang/crates.io-index"
1798
- checksum = "93b62c526917a1e77b6dce3cd841b6c271f0fff344ea93ad92a8c45afe8051b6"
1798
+ checksum = "2a0107e34575ec704ce29407695462e79e6b0e13ce7af6431b2f15c313e34464"
1799
1799
  dependencies = [
1800
1800
  "darling 0.20.11",
1801
1801
  "heck 0.5.0",
@@ -1813,9 +1813,9 @@ dependencies = [
1813
1813
 
1814
1814
  [[package]]
1815
1815
  name = "soroban-spec"
1816
- version = "25.1.0"
1816
+ version = "25.1.1"
1817
1817
  source = "registry+https://github.com/rust-lang/crates.io-index"
1818
- checksum = "0186c943a78de7038ce7eee478f521f7a7665440101ae0d24b4a59833fb6d833"
1818
+ checksum = "53a1c9f6ccc6aa78518545e3cf542bd26f11d9085328a2e1c06c90514733fe15"
1819
1819
  dependencies = [
1820
1820
  "base64 0.22.1",
1821
1821
  "stellar-xdr",
@@ -1825,9 +1825,9 @@ dependencies = [
1825
1825
 
1826
1826
  [[package]]
1827
1827
  name = "soroban-spec-rust"
1828
- version = "25.1.0"
1828
+ version = "25.1.1"
1829
1829
  source = "registry+https://github.com/rust-lang/crates.io-index"
1830
- checksum = "7a948196ed0633be3a4125e0c7a4fc0bb6337942e538813b1f171331738f9058"
1830
+ checksum = "e8247d3c6256b544b2461606c6892351bb22978d751e07c1aea744377053d852"
1831
1831
  dependencies = [
1832
1832
  "prettyplease",
1833
1833
  "proc-macro2",
package/Cargo.toml CHANGED
@@ -14,7 +14,7 @@ license = "MIT"
14
14
  version = "0.0.1"
15
15
 
16
16
  [workspace.dependencies]
17
- soroban-sdk = { version = "25.1.0", features = ["hazmat-address", "hazmat-crypto"] }
17
+ soroban-sdk = { version = "25.1.1", features = ["hazmat-address", "hazmat-crypto"] }
18
18
  soroban-spec-typescript = "25.1.0" # used in tools/ts-bindings-gen
19
19
 
20
20
  # Third-party dependencies (production)
@@ -119,7 +119,7 @@ fn gen_accessor_methods(enum_name: &Ident, variant: &Variant) -> TokenStream {
119
119
 
120
120
  // Getter: returns the value directly (with default) or wrapped in Option.
121
121
  let (ret_type, ret_expr) = match &config.default_value {
122
- Some(default) => (quote! { #value_type }, quote! { value.unwrap_or(#default) }),
122
+ Some(default) => (quote! { #value_type }, quote! { value.unwrap_or_else(|| #default) }),
123
123
  None => (quote! { Option<#value_type> }, quote! { value }),
124
124
  };
125
125
  let ttl_on_get = extend_ttl.as_ref().map(|call| quote! { if value.is_some() { #call } });
@@ -131,10 +131,12 @@ fn gen_accessor_methods(enum_name: &Ident, variant: &Variant) -> TokenStream {
131
131
  };
132
132
 
133
133
  // TTL extender method — only for persistent/temporary storage (instance has no per-key TTL).
134
- let ttl_extender_method = (config.kind != StorageKind::Instance).then(|| quote! {
135
- pub fn #ttl_extender(#params, threshold: u32, extend_to: u32) {
136
- let key = #key;
137
- #accessor.extend_ttl(&key, threshold, extend_to);
134
+ let ttl_extender_method = (config.kind != StorageKind::Instance).then(|| {
135
+ quote! {
136
+ pub fn #ttl_extender(#params, threshold: u32, extend_to: u32) {
137
+ let key = #key;
138
+ #accessor.extend_ttl(&key, threshold, extend_to);
139
+ }
138
140
  }
139
141
  });
140
142
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- source: contracts/common-macros/src/tests/storage.rs
2
+ source: contracts/common-macros/src/tests/storage/generate_storage.rs
3
3
  assertion_line: 409
4
4
  expression: formatted
5
5
  ---
@@ -20,7 +20,7 @@ impl StorageKeys {
20
20
  pub fn counter(env: &soroban_sdk::Env) -> u32 {
21
21
  let key = StorageKeys::Counter;
22
22
  let value = env.storage().instance().get::<_, u32>(&key);
23
- value.unwrap_or(0)
23
+ value.unwrap_or_else(|| 0)
24
24
  }
25
25
  pub fn set_counter(env: &soroban_sdk::Env, value: &u32) {
26
26
  let key = StorageKeys::Counter;
@@ -46,7 +46,7 @@ impl StorageKeys {
46
46
  if value.is_some() {
47
47
  utils::ttl_configurable::extend_persistent_ttl(env, &key);
48
48
  }
49
- value.unwrap_or(String::from_str(env, "hello"))
49
+ value.unwrap_or_else(|| String::from_str(env, "hello"))
50
50
  }
51
51
  pub fn set_message(env: &soroban_sdk::Env, sender: &Address, value: &String) {
52
52
  let key = StorageKeys::Message(sender.clone());
@@ -1,4 +1,5 @@
1
1
  use crate::{
2
+ endpoint_v2::messaging_channel::PENDING_INBOUND_NONCE_MAX_LEN,
2
3
  errors::EndpointError,
3
4
  events::{DelegateSet, LzReceiveAlert, PacketDelivered, PacketSent, PacketVerified, ZroSet},
4
5
  interfaces::{ILayerZeroEndpointV2, IMessageLibManager, IMessagingChannel, MessagingFee, MessagingReceipt, Origin},
@@ -165,14 +166,14 @@ impl ILayerZeroEndpointV2 for EndpointV2 {
165
166
 
166
167
  /// Checks if a messaging path can be/has been initialized for the given origin and receiver.
167
168
  fn initializable(env: &Env, origin: &Origin, receiver: &Address) -> bool {
168
- let lazy_inbound_nonce = Self::lazy_inbound_nonce(env, receiver, origin.src_eid, &origin.sender);
169
- lazy_inbound_nonce > 0 || LayerZeroReceiverClient::new(env, receiver).allow_initialize_path(origin)
169
+ let inbound_nonce = Self::inbound_nonce(env, receiver, origin.src_eid, &origin.sender);
170
+ inbound_nonce > 0 || LayerZeroReceiverClient::new(env, receiver).allow_initialize_path(origin)
170
171
  }
171
172
 
172
173
  /// Checks if a message can be verified for the given origin and receiver.
173
174
  fn verifiable(env: &Env, origin: &Origin, receiver: &Address) -> bool {
174
- let lazy_inbound_nonce = Self::lazy_inbound_nonce(env, receiver, origin.src_eid, &origin.sender);
175
- origin.nonce > lazy_inbound_nonce
175
+ let inbound_nonce = Self::inbound_nonce(env, receiver, origin.src_eid, &origin.sender);
176
+ (origin.nonce > inbound_nonce && origin.nonce <= inbound_nonce + PENDING_INBOUND_NONCE_MAX_LEN)
176
177
  || EndpointStorage::has_inbound_payload_hash(env, receiver, origin.src_eid, &origin.sender, origin.nonce)
177
178
  }
178
179
 
@@ -1,4 +1,4 @@
1
- use soroban_sdk::{contractclient, Address, BytesN, Env};
1
+ use soroban_sdk::{contractclient, Address, BytesN, Env, Vec};
2
2
 
3
3
  /// EndpointV2's Interface for managing messaging channels, nonces, and payload hashes.
4
4
  #[contractclient(name = "MessagingChannelClient")]
@@ -78,19 +78,18 @@ pub trait IMessagingChannel {
78
78
  /// The current outbound nonce (0 if no messages sent yet)
79
79
  fn outbound_nonce(env: &Env, sender: &Address, dst_eid: u32, receiver: &BytesN<32>) -> u64;
80
80
 
81
- /// Returns the max index of the longest gapless sequence of verified nonces.
82
- /// Example: `[1,2,3,4,6,7] => 4`, `[1,2,6,8,10] => 2`
81
+ /// Returns the current inbound nonce for a specific path.
83
82
  ///
84
83
  /// # Arguments
85
84
  /// * `receiver` - The receiver OApp address
86
85
  /// * `src_eid` - The source endpoint ID
87
- /// * `sender` - The sender address on the source chain
86
+ /// * `sender` - The sender OApp address on the source chain
88
87
  ///
89
88
  /// # Returns
90
- /// The highest nonce in the gapless sequence starting from lazy_inbound_nonce
89
+ /// The current inbound nonce (0 if no messages received yet)
91
90
  fn inbound_nonce(env: &Env, receiver: &Address, src_eid: u32, sender: &BytesN<32>) -> u64;
92
91
 
93
- /// Returns the lazy inbound nonce (last checkpoint) for a specific path.
92
+ /// Returns the pending inbound nonces for a specific path.
94
93
  ///
95
94
  /// # Arguments
96
95
  /// * `receiver` - The receiver OApp address
@@ -98,8 +97,8 @@ pub trait IMessagingChannel {
98
97
  /// * `sender` - The sender OApp address on the source chain
99
98
  ///
100
99
  /// # Returns
101
- /// The lazy inbound nonce, updated when messages are cleared/executed
102
- fn lazy_inbound_nonce(env: &Env, receiver: &Address, src_eid: u32, sender: &BytesN<32>) -> u64;
100
+ /// The pending inbound nonces
101
+ fn pending_inbound_nonces(env: &Env, receiver: &Address, src_eid: u32, sender: &BytesN<32>) -> Vec<u64>;
103
102
 
104
103
  /// Returns the payload hash for a specific inbound nonce.
105
104
  ///
@@ -7,7 +7,7 @@ use crate::{
7
7
  util::{compute_guid, keccak256},
8
8
  };
9
9
  use common_macros::contract_impl;
10
- use soroban_sdk::{assert_with_error, Address, Bytes, BytesN, Env};
10
+ use soroban_sdk::{assert_with_error, Address, Bytes, BytesN, Env, Vec};
11
11
 
12
12
  /// Represents an empty payload hash (equivalent to bytes32(uint256(0)) in Solidity)
13
13
  const EMPTY_PAYLOAD_HASH_BYTES: [u8; 32] = [0u8; 32];
@@ -15,6 +15,9 @@ const EMPTY_PAYLOAD_HASH_BYTES: [u8; 32] = [0u8; 32];
15
15
  /// Represents a nilified payload hash (equivalent to bytes32(type(uint256).max) in Solidity)
16
16
  const NIL_PAYLOAD_HASH_BYTES: [u8; 32] = [0xffu8; 32];
17
17
 
18
+ /// Max number of out-of-order nonces in the pending list.
19
+ pub(super) const PENDING_INBOUND_NONCE_MAX_LEN: u64 = 256;
20
+
18
21
  #[contract_impl]
19
22
  impl IMessagingChannel for EndpointV2 {
20
23
  /// Skips the next expected inbound nonce without verifying.
@@ -25,7 +28,7 @@ impl IMessagingChannel for EndpointV2 {
25
28
 
26
29
  let next_nonce = Self::inbound_nonce(env, receiver, src_eid, sender) + 1;
27
30
  assert_with_error!(env, nonce == next_nonce, EndpointError::InvalidNonce);
28
- EndpointStorage::set_lazy_inbound_nonce(env, receiver, src_eid, sender, &nonce);
31
+ Self::insert_and_drain_pending_nonces(env, receiver, src_eid, sender, nonce);
29
32
 
30
33
  InboundNonceSkipped { src_eid, sender: sender.clone(), receiver: receiver.clone(), nonce }.publish(env);
31
34
  }
@@ -45,10 +48,14 @@ impl IMessagingChannel for EndpointV2 {
45
48
  Self::require_oapp_auth(env, caller, receiver);
46
49
 
47
50
  let cur_payload_hash = Self::inbound_payload_hash(env, receiver, src_eid, sender, nonce);
48
- let lazy_nonce = Self::lazy_inbound_nonce(env, receiver, src_eid, sender);
51
+ let inbound_nonce = Self::inbound_nonce(env, receiver, src_eid, sender);
49
52
 
50
53
  assert_with_error!(env, payload_hash == &cur_payload_hash, EndpointError::PayloadHashNotFound);
51
- assert_with_error!(env, nonce > lazy_nonce || cur_payload_hash.is_some(), EndpointError::InvalidNonce);
54
+ assert_with_error!(env, nonce > inbound_nonce || cur_payload_hash.is_some(), EndpointError::InvalidNonce);
55
+
56
+ if nonce > inbound_nonce {
57
+ Self::insert_and_drain_pending_nonces(env, receiver, src_eid, sender, nonce);
58
+ }
52
59
  EndpointStorage::set_inbound_payload_hash(env, receiver, src_eid, sender, nonce, &Self::nil_payload_hash(env));
53
60
 
54
61
  PacketNilified {
@@ -77,9 +84,9 @@ impl IMessagingChannel for EndpointV2 {
77
84
  let cur_payload_hash = Self::inbound_payload_hash(env, receiver, src_eid, sender, nonce);
78
85
  assert_with_error!(env, cur_payload_hash.as_ref() == Some(payload_hash), EndpointError::PayloadHashNotFound);
79
86
 
80
- // Check if nonce is at or below the lazy nonce
81
- let lazy_nonce = Self::lazy_inbound_nonce(env, receiver, src_eid, sender);
82
- assert_with_error!(env, nonce <= lazy_nonce, EndpointError::InvalidNonce);
87
+ // Check if nonce is at or below the inbound nonce
88
+ let inbound_nonce = Self::inbound_nonce(env, receiver, src_eid, sender);
89
+ assert_with_error!(env, nonce <= inbound_nonce, EndpointError::InvalidNonce);
83
90
 
84
91
  // Remove the payload hash from storage
85
92
  EndpointStorage::remove_inbound_payload_hash(env, receiver, src_eid, sender, nonce);
@@ -112,25 +119,15 @@ impl IMessagingChannel for EndpointV2 {
112
119
  /// Returns the max index of the longest gapless sequence of verified message nonces.
113
120
  ///
114
121
  /// The uninitialized value is 0. The first nonce is always 1.
115
- /// It starts from the `lazy_inbound_nonce` (last checkpoint) and iteratively checks
116
- /// if the next nonce has been verified.
117
122
  ///
118
123
  /// Note: OApp explicitly skipped nonces count as "verified" for these purposes.
119
- ///
120
- /// Examples: `[1,2,3,4,6,7] => 4`, `[1,2,6,8,10] => 2`, `[1,3,4,5,6] => 1`
121
124
  fn inbound_nonce(env: &Env, receiver: &Address, src_eid: u32, sender: &BytesN<32>) -> u64 {
122
- let mut nonce_cursor = Self::lazy_inbound_nonce(env, receiver, src_eid, sender);
123
-
124
- // Find the effective inbound current nonce
125
- while EndpointStorage::has_inbound_payload_hash(env, receiver, src_eid, sender, nonce_cursor + 1) {
126
- nonce_cursor += 1;
127
- }
128
- nonce_cursor
125
+ EndpointStorage::inbound_nonce(env, receiver, src_eid, sender)
129
126
  }
130
127
 
131
- /// Returns the lazy inbound nonce (last checkpoint) for a specific path.
132
- fn lazy_inbound_nonce(env: &Env, receiver: &Address, src_eid: u32, sender: &BytesN<32>) -> u64 {
133
- EndpointStorage::lazy_inbound_nonce(env, receiver, src_eid, sender)
128
+ /// Returns the pending inbound nonces for a specific path.
129
+ fn pending_inbound_nonces(env: &Env, receiver: &Address, src_eid: u32, sender: &BytesN<32>) -> Vec<u64> {
130
+ EndpointStorage::pending_inbound_nonces(env, receiver, src_eid, sender)
134
131
  }
135
132
 
136
133
  /// Returns the payload hash for a specific inbound nonce.
@@ -162,9 +159,8 @@ impl EndpointV2 {
162
159
 
163
160
  /// Records an inbound message payload hash for a specific nonce on a specific path.
164
161
  ///
165
- /// Inbound won't update the nonce eagerly to allow unordered verification.
166
- /// Instead, it will update the nonce lazily when the message is received.
167
- /// Messages can only be cleared in order to preserve censorship-resistance.
162
+ /// When nonce > inbound_nonce, inserts into the pending list and drains consecutive
163
+ /// nonces to update the effective inbound nonce.
168
164
  ///
169
165
  /// # Arguments
170
166
  /// * `receiver` - The receiver OApp address
@@ -181,22 +177,27 @@ impl EndpointV2 {
181
177
  payload_hash: &BytesN<32>,
182
178
  ) {
183
179
  assert_with_error!(env, payload_hash != &Self::empty_payload_hash(env), EndpointError::InvalidPayloadHash);
180
+
181
+ let inbound_nonce = Self::inbound_nonce(env, receiver, src_eid, sender);
182
+
183
+ // Only allow to verify new nonces or re-verify unexecuted nonces.
184
+ assert_with_error!(
185
+ env,
186
+ nonce > inbound_nonce || EndpointStorage::has_inbound_payload_hash(env, receiver, src_eid, sender, nonce),
187
+ EndpointError::InvalidNonce
188
+ );
189
+
190
+ if nonce > inbound_nonce {
191
+ Self::insert_and_drain_pending_nonces(env, receiver, src_eid, sender, nonce);
192
+ }
193
+
184
194
  EndpointStorage::set_inbound_payload_hash(env, receiver, src_eid, sender, nonce, payload_hash);
185
195
  }
186
196
 
187
- /// Clears a stored message payload and increments the lazy inbound nonce.
188
- ///
189
- /// Calling this function will clear the stored message and increment the
190
- /// `lazy_inbound_nonce` to the provided nonce.
197
+ /// Clears a stored message payload.
191
198
  ///
192
- /// Note: This function does not change `inbound_nonce`, it only changes
193
- /// the `lazy_inbound_nonce` up to the provided nonce.
194
- ///
195
- /// # EVM Alignment
196
- /// This implementation aligns with the EVM endpoint behavior. Executors should call
197
- /// `clear` from lower nonce to higher nonce sequentially. This ensures the range check
198
- /// `(current_nonce + 1..=nonce)` remains small, avoiding long iteration when verifying
199
- /// that all intermediate nonces have been verified.
199
+ /// Requires nonce <= inbound_nonce (no iteration, O(1) check). The inbound_nonce
200
+ /// is updated during verify when consecutive nonces are drained from the pending list.
200
201
  ///
201
202
  /// # Arguments
202
203
  /// * `receiver` - The receiver OApp address
@@ -212,15 +213,8 @@ impl EndpointV2 {
212
213
  nonce: u64,
213
214
  payload: &Bytes,
214
215
  ) {
215
- let current_nonce = Self::lazy_inbound_nonce(env, receiver, src_eid, sender);
216
-
217
- // Try to update the lazy inbound nonce until the target nonce
218
- if nonce > current_nonce {
219
- let has_payload = (current_nonce + 1..=nonce)
220
- .all(|n| EndpointStorage::has_inbound_payload_hash(env, receiver, src_eid, sender, n));
221
- assert_with_error!(env, has_payload, EndpointError::InvalidNonce);
222
- EndpointStorage::set_lazy_inbound_nonce(env, receiver, src_eid, sender, &nonce);
223
- }
216
+ let inbound_nonce = Self::inbound_nonce(env, receiver, src_eid, sender);
217
+ assert_with_error!(env, nonce <= inbound_nonce, EndpointError::InvalidNonce);
224
218
 
225
219
  // Check the hash of the payload to verify the executor has given the proper payload that has been verified
226
220
  let actual_hash = keccak256(env, payload);
@@ -231,6 +225,45 @@ impl EndpointV2 {
231
225
  EndpointStorage::remove_inbound_payload_hash(env, receiver, src_eid, sender, nonce);
232
226
  }
233
227
 
228
+ /// Inserts a nonce into a sorted pending list, then drains consecutive nonces from the front
229
+ /// to advance `inbound_nonce`.
230
+ ///
231
+ /// Bounded by `PENDING_INBOUND_NONCE_MAX_LEN` to prevent DDoS via unbounded list growth.
232
+ fn insert_and_drain_pending_nonces(
233
+ env: &Env,
234
+ receiver: &Address,
235
+ src_eid: u32,
236
+ sender: &BytesN<32>,
237
+ new_nonce: u64,
238
+ ) {
239
+ let inbound_nonce = Self::inbound_nonce(env, receiver, src_eid, sender);
240
+ assert_with_error!(
241
+ env,
242
+ new_nonce > inbound_nonce && new_nonce <= inbound_nonce + PENDING_INBOUND_NONCE_MAX_LEN,
243
+ EndpointError::InvalidNonce
244
+ );
245
+
246
+ let mut pending_nonces = Self::pending_inbound_nonces(env, receiver, src_eid, sender);
247
+
248
+ // Allow to re-verify at the same nonce and insert the new nonce if it doesn't already exist.
249
+ // When the binary_search returns an error, the nonce is not in the list and should be inserted.
250
+ if let Err(i) = pending_nonces.binary_search(new_nonce) {
251
+ pending_nonces.insert(i, new_nonce);
252
+
253
+ // Drain consecutive nonces from the front to advance the inbound nonce
254
+ let mut new_inbound_nonce = inbound_nonce;
255
+ while !pending_nonces.is_empty() && pending_nonces.first_unchecked() == new_inbound_nonce + 1 {
256
+ new_inbound_nonce = pending_nonces.pop_front_unchecked();
257
+ }
258
+
259
+ // Update the pending nonces and inbound nonce if needed
260
+ EndpointStorage::set_pending_inbound_nonces(env, receiver, src_eid, sender, &pending_nonces);
261
+ if new_inbound_nonce > inbound_nonce {
262
+ EndpointStorage::set_inbound_nonce(env, receiver, src_eid, sender, &new_inbound_nonce);
263
+ }
264
+ }
265
+ }
266
+
234
267
  /// Represents an empty payload hash
235
268
  fn empty_payload_hash(env: &Env) -> BytesN<32> {
236
269
  BytesN::from_array(env, &EMPTY_PAYLOAD_HASH_BYTES)
@@ -1,6 +1,6 @@
1
1
  use crate::Timeout;
2
2
  use common_macros::storage;
3
- use soroban_sdk::{Address, BytesN};
3
+ use soroban_sdk::{Address, BytesN, Vec};
4
4
 
5
5
  #[storage]
6
6
  pub enum EndpointStorage {
@@ -24,10 +24,15 @@ pub enum EndpointStorage {
24
24
  /// Messaging Channel
25
25
  /// ============================================================================================
26
26
 
27
- /// The lazy inbound nonce for a receiver
27
+ /// Sorted list of out-of-order verified nonces
28
+ #[persistent(Vec<u64>)]
29
+ #[default(Vec::new(env))]
30
+ PendingInboundNonces { receiver: Address, src_eid: u32, sender: BytesN<32> },
31
+
32
+ /// The current inbound nonce for a receiver
28
33
  #[persistent(u64)]
29
34
  #[default(0)]
30
- LazyInboundNonce { receiver: Address, src_eid: u32, sender: BytesN<32> },
35
+ InboundNonce { receiver: Address, src_eid: u32, sender: BytesN<32> },
31
36
 
32
37
  /// The inbound payload hash for a receiver
33
38
  #[persistent(BytesN<32>)]
@@ -270,11 +270,11 @@ impl<'a> TestSetup<'a> {
270
270
  self.endpoint_client.send_compose(from, to, guid, &index, message);
271
271
  }
272
272
 
273
- pub fn set_lazy_inbound_nonce(&self, receiver: &Address, src_eid: u32, sender: &BytesN<32>, lazy_nonce: u64) {
273
+ pub fn set_inbound_nonce(&self, receiver: &Address, src_eid: u32, sender: &BytesN<32>, inbound_nonce: u64) {
274
274
  let env = &self.env;
275
275
  let endpoint_client = &self.endpoint_client;
276
276
  env.as_contract(&endpoint_client.address, || {
277
- storage::EndpointStorage::set_lazy_inbound_nonce(env, receiver, src_eid, sender, &lazy_nonce)
277
+ storage::EndpointStorage::set_inbound_nonce(env, receiver, src_eid, sender, &inbound_nonce)
278
278
  });
279
279
  }
280
280
 
@@ -119,9 +119,9 @@ fn test_clear_emits_packet_delivered_event() {
119
119
  assert_eq_event(env, &endpoint_client.address, PacketDelivered { origin: origin.clone(), receiver: receiver.clone() });
120
120
  }
121
121
 
122
- // Lazy inbound nonce updates
122
+ // Inbound nonce is advanced during verify, not during clear
123
123
  #[test]
124
- fn test_clear_updates_lazy_inbound_nonce() {
124
+ fn test_clear_does_not_change_inbound_nonce() {
125
125
  let context = setup();
126
126
  let env = &context.env;
127
127
  let endpoint_client = &context.endpoint_client;
@@ -136,15 +136,13 @@ fn test_clear_updates_lazy_inbound_nonce() {
136
136
  let (_receive_lib, origin, _payload_hash) =
137
137
  arrange_verified_packet_with_auth(&context, src_eid, &sender, &receiver, nonce, &guid, &message);
138
138
 
139
- // Verify initial lazy inbound nonce
140
- let initial_lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
141
- assert_eq!(initial_lazy_nonce, 0, "Initial lazy inbound nonce should be 0");
139
+ // Verify advanced inbound nonce (happens during verify).
140
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), nonce);
142
141
 
143
142
  clear_packet_with_auth(&context, &receiver, &origin, &receiver, &guid, &message);
144
143
 
145
- // Verify lazy inbound nonce was updated via public interface
146
- let lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
147
- assert_eq!(lazy_nonce, nonce);
144
+ // Clear does not advance inbound nonce.
145
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), nonce);
148
146
  }
149
147
 
150
148
  // Sequential nonce behavior
@@ -179,9 +177,8 @@ fn test_clear_success_sequential_nonces_update_lazy_nonce_to_latest() {
179
177
  verify_packet_with_auth(&context, &receive_lib, &origin2, &receiver, &payload_hash2);
180
178
  clear_packet_with_auth(&context, &receiver, &origin2, &receiver, &guid2, &message2);
181
179
 
182
- // Verify lazy inbound nonce was updated to 2
183
- let lazy_nonce = endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender);
184
- assert_eq!(lazy_nonce, 2);
180
+ // Verify advanced inbound nonce.
181
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
185
182
  }
186
183
 
187
184
  // Authorization
@@ -410,11 +407,11 @@ fn test_clear_does_not_advance_lazy_nonce_when_clearing_older_nonce() {
410
407
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2 };
411
408
  verify_packet_with_auth(&context, &receive_lib, &origin2, &receiver, &payload_hash2);
412
409
 
413
- // Clear nonce 2 first (this advances lazy nonce to 2 because nonce 1..=2 are present).
410
+ // Clear nonce 2 first. This does not advance inbound nonce (it was advanced during verify).
414
411
  clear_packet_with_auth(&context, &receiver, &origin2, &receiver, &guid2, &message2);
415
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 2);
412
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
416
413
 
417
- // Clearing an older nonce should not change lazy nonce.
414
+ // Clearing an older nonce should not change inbound nonce.
418
415
  clear_packet_with_auth(&context, &receiver, &origin1, &receiver, &guid1, &message1);
419
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 2);
416
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
420
417
  }
@@ -1,6 +1,6 @@
1
1
  use soroban_sdk::{testutils::Address as _, BytesN};
2
2
 
3
- use crate::{tests::endpoint_setup::setup, tests::endpoint_setup::TestSetup, Origin};
3
+ use crate::{storage, tests::endpoint_setup::setup, tests::endpoint_setup::TestSetup, Origin};
4
4
 
5
5
  fn skip_with_auth(context: &TestSetup, receiver: &soroban_sdk::Address, src_eid: u32, sender: &BytesN<32>, nonce: u64) {
6
6
  // `skip` requires authorization from `caller` (the receiver or its delegate).
@@ -20,7 +20,7 @@ fn verify_with_auth(
20
20
  context.endpoint_client.verify(receive_lib, origin, receiver, payload_hash);
21
21
  }
22
22
 
23
- // New path (lazy nonce == 0) => verifiable when origin.nonce > lazy nonce
23
+ // New path (inbound nonce == 0) => verifiable when origin.nonce is within (0, 256]
24
24
  #[test]
25
25
  fn test_verifiable_new_path_nonce_1_true() {
26
26
  let context = setup();
@@ -30,12 +30,12 @@ fn test_verifiable_new_path_nonce_1_true() {
30
30
  let sender = BytesN::from_array(&context.env, &[1u8; 32]);
31
31
  let origin = Origin { src_eid, sender, nonce: 1 };
32
32
 
33
- // For a new path (lazy nonce is 0), nonce 1 > 0 should be verifiable.
33
+ // For a new path (inbound nonce is 0), nonce 1 should be verifiable.
34
34
  let result = endpoint_client.verifiable(&origin, &receiver);
35
35
  assert!(result);
36
36
  }
37
37
 
38
- // Established path (lazy nonce > 0) => verifiable when origin.nonce > lazy nonce
38
+ // Established path => verifiable when origin.nonce is in (inbound_nonce, inbound_nonce + 256]
39
39
  #[test]
40
40
  fn test_verifiable_after_skip_nonce_gt_lazy_true() {
41
41
  let context = setup();
@@ -48,7 +48,7 @@ fn test_verifiable_after_skip_nonce_gt_lazy_true() {
48
48
  // Establish the path by skipping nonce 1.
49
49
  skip_with_auth(&context, &receiver, src_eid, &sender, 1);
50
50
 
51
- // Now lazy_nonce = 1, nonce 2 > 1 should be verifiable.
51
+ // Now inbound_nonce = 1, nonce 2 should be verifiable.
52
52
  let origin = Origin { src_eid, sender, nonce: 2 };
53
53
  let result = endpoint_client.verifiable(&origin, &receiver);
54
54
  assert!(result);
@@ -79,8 +79,8 @@ fn test_verifiable_true_when_nonce_leq_lazy_but_payload_hash_exists() {
79
79
  // Advance lazy nonce to 3 while keeping payload hash for 2 (skip sets lazy nonce only).
80
80
  skip_with_auth(&context, &receiver, src_eid, &sender, 3);
81
81
 
82
- // Sanity: lazy nonce was advanced, and the payload hash for nonce 2 still exists.
83
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 3);
82
+ // Sanity: inbound nonce was advanced, and the payload hash for nonce 2 still exists.
83
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
84
84
  assert!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64).is_some());
85
85
 
86
86
  // Now nonce 2 <= lazy 3, but payload hash exists -> verifiable should be true.
@@ -98,13 +98,50 @@ fn test_verifiable_nonce_eq_lazy_false_without_payload_hash() {
98
98
  let sender = BytesN::from_array(env, &[1u8; 32]);
99
99
  let receiver = soroban_sdk::Address::generate(env);
100
100
 
101
- // Set lazy nonce to 2 without storing any payload hashes.
101
+ // Advance inbound nonce to 2 without storing any payload hashes at nonce 2.
102
102
  skip_with_auth(&context, &receiver, src_eid, &sender, 1);
103
103
  skip_with_auth(&context, &receiver, src_eid, &sender, 2);
104
- assert_eq!(endpoint_client.lazy_inbound_nonce(&receiver, &src_eid, &sender), 2);
104
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
105
105
 
106
106
  // nonce == lazy and payload hash missing -> verifiable should be false.
107
107
  let origin2 = Origin { src_eid, sender: sender.clone(), nonce: 2u64 };
108
108
  assert!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64).is_none());
109
109
  assert!(!endpoint_client.verifiable(&origin2, &receiver));
110
110
  }
111
+
112
+ #[test]
113
+ fn test_verifiable_upper_bound_is_enforced() {
114
+ let context = setup();
115
+ let env = &context.env;
116
+ let endpoint_client = &context.endpoint_client;
117
+
118
+ let src_eid = 2u32;
119
+ let sender = BytesN::from_array(env, &[1u8; 32]);
120
+ let receiver = soroban_sdk::Address::generate(env);
121
+
122
+ // For a new path inbound_nonce=0: 256 is allowed, 257 is not.
123
+ let origin_256 = Origin { src_eid, sender: sender.clone(), nonce: 256u64 };
124
+ let origin_257 = Origin { src_eid, sender, nonce: 257u64 };
125
+ assert!(endpoint_client.verifiable(&origin_256, &receiver));
126
+ assert!(!endpoint_client.verifiable(&origin_257, &receiver));
127
+ }
128
+
129
+ #[test]
130
+ fn test_verifiable_upper_bound_is_enforced_when_inbound_nonce_nonzero() {
131
+ let context = setup();
132
+ let env = &context.env;
133
+ let endpoint_client = &context.endpoint_client;
134
+
135
+ let src_eid = 2u32;
136
+ let sender = BytesN::from_array(env, &[1u8; 32]);
137
+ let receiver = soroban_sdk::Address::generate(env);
138
+
139
+ env.as_contract(&endpoint_client.address, || {
140
+ storage::EndpointStorage::set_inbound_nonce(env, &receiver, src_eid, &sender, &100u64)
141
+ });
142
+
143
+ let ok = Origin { src_eid, sender: sender.clone(), nonce: 356u64 }; // 100 + 256
144
+ let too_far = Origin { src_eid, sender, nonce: 357u64 };
145
+ assert!(endpoint_client.verifiable(&ok, &receiver));
146
+ assert!(!endpoint_client.verifiable(&too_far, &receiver));
147
+ }