@layerzerolabs/protocol-stellar-v2 0.2.39 → 0.2.41

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 (114) hide show
  1. package/.turbo/turbo-build.log +226 -313
  2. package/.turbo/turbo-lint.log +98 -227
  3. package/.turbo/turbo-test.log +1803 -1954
  4. package/contracts/common-macros/src/lib.rs +38 -15
  5. package/contracts/common-macros/src/lz_contract.rs +12 -21
  6. package/contracts/common-macros/src/tests/lz_contract.rs +17 -8
  7. package/contracts/common-macros/src/tests/snapshots/common_macros__tests__lz_contract__snapshot_generated_lz_contract_code.snap +20 -0
  8. package/contracts/common-macros/src/upgradeable.rs +37 -30
  9. package/contracts/endpoint-v2/src/endpoint_v2.rs +4 -3
  10. package/contracts/endpoint-v2/src/errors.rs +2 -2
  11. package/contracts/endpoint-v2/src/messaging_channel.rs +11 -0
  12. package/contracts/endpoint-v2/src/messaging_composer.rs +1 -0
  13. package/contracts/endpoint-v2/src/tests/endpoint_v2/clear.rs +12 -25
  14. package/contracts/endpoint-v2/src/tests/endpoint_v2/initializable.rs +4 -4
  15. package/contracts/endpoint-v2/src/tests/endpoint_v2/verifiable.rs +50 -10
  16. package/contracts/endpoint-v2/src/tests/endpoint_v2/verify.rs +6 -35
  17. package/contracts/endpoint-v2/src/tests/messaging_channel/burn.rs +2 -2
  18. package/contracts/endpoint-v2/src/tests/messaging_channel/clear_payload.rs +50 -1
  19. package/contracts/endpoint-v2/src/tests/messaging_channel/inbound.rs +78 -0
  20. package/contracts/endpoint-v2/src/tests/messaging_channel/insert_and_drain_pending_nonces.rs +272 -0
  21. package/contracts/endpoint-v2/src/tests/messaging_channel/mod.rs +1 -0
  22. package/contracts/endpoint-v2/src/tests/messaging_channel/nilify.rs +10 -5
  23. package/contracts/endpoint-v2/src/tests/messaging_channel/skip.rs +30 -0
  24. package/contracts/macro-integration-tests/tests/runtime/oapp/mod.rs +22 -1
  25. package/contracts/macro-integration-tests/tests/runtime/oapp/oapp_core.rs +13 -11
  26. package/contracts/macro-integration-tests/tests/runtime/oapp/options_type3.rs +13 -10
  27. package/contracts/macro-integration-tests/tests/runtime/oapp/receiver.rs +15 -11
  28. package/contracts/macro-integration-tests/tests/runtime/oapp/sender.rs +3 -2
  29. package/contracts/macro-integration-tests/tests/runtime/ownable/two_step_transfer.rs +14 -12
  30. package/contracts/macro-integration-tests/tests/runtime/upgradeable/migrate_guard_and_state.rs +3 -9
  31. package/contracts/macro-integration-tests/tests/ui/lz_contract/fail/upgradeable_invalid_inner_option.stderr +24 -1
  32. package/contracts/macro-integration-tests/tests/ui/lz_contract/fail/upgradeable_missing_internal.stderr +3 -3
  33. package/contracts/macro-integration-tests/tests/ui/lz_contract/pass/upgradeable_rbac.rs +44 -0
  34. package/contracts/macro-integration-tests/tests/ui/oapp/pass/custom_all.rs +3 -0
  35. package/contracts/macro-integration-tests/tests/ui/oapp/pass/custom_single_trait.rs +3 -0
  36. package/contracts/macro-integration-tests/tests/ui/ownable/pass/basic.rs +1 -1
  37. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/attr_args.stderr +1 -1
  38. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/missing_auth_trait.stderr +2 -2
  39. package/contracts/macro-integration-tests/tests/ui/upgradeable/fail/missing_upgradeable_internal.stderr +2 -2
  40. package/contracts/macro-integration-tests/tests/ui/upgradeable/pass/rbac.rs +44 -0
  41. package/contracts/oapps/counter/integration_tests/utils.rs +5 -3
  42. package/contracts/oapps/counter/src/tests/mod.rs +16 -1
  43. package/contracts/oapps/counter/src/tests/test_counter.rs +5 -2
  44. package/contracts/oapps/oapp/src/oapp_core.rs +21 -7
  45. package/contracts/oapps/oapp/src/oapp_options_type3.rs +7 -5
  46. package/contracts/oapps/oapp/src/tests/mod.rs +21 -0
  47. package/contracts/oapps/oapp/src/tests/oapp_core.rs +12 -10
  48. package/contracts/oapps/oapp/src/tests/oapp_options_type3.rs +11 -7
  49. package/contracts/oapps/oapp/src/tests/oapp_receiver.rs +4 -2
  50. package/contracts/oapps/oapp/src/tests/oapp_sender.rs +3 -2
  51. package/contracts/oapps/oapp/src/tests/test_macros.rs +15 -0
  52. package/contracts/oapps/oapp-macros/src/generators.rs +6 -0
  53. package/contracts/oapps/oapp-macros/src/tests/snapshots/oapp_macros__tests__oapp__snapshot_generate_oapp.snap +15 -0
  54. package/contracts/oapps/oft/integration-tests/setup.rs +22 -4
  55. package/contracts/oapps/oft/integration-tests/utils.rs +94 -13
  56. package/contracts/oapps/oft/src/extensions/oft_fee.rs +23 -10
  57. package/contracts/oapps/oft/src/extensions/pausable.rs +31 -10
  58. package/contracts/oapps/oft/src/extensions/rate_limiter.rs +9 -4
  59. package/contracts/oapps/oft/src/oft.rs +1 -2
  60. package/contracts/oapps/oft/src/tests/extensions/oft_fee.rs +39 -27
  61. package/contracts/oapps/oft/src/tests/extensions/pausable.rs +38 -24
  62. package/contracts/oapps/oft/src/tests/extensions/rate_limiter.rs +87 -69
  63. package/contracts/oapps/oft-core/integration-tests/setup.rs +27 -3
  64. package/contracts/oapps/oft-core/src/oft_core.rs +10 -5
  65. package/contracts/oapps/oft-core/src/tests/test_msg_inspector.rs +20 -20
  66. package/contracts/oapps/oft-core/src/tests/test_utils.rs +31 -3
  67. package/contracts/upgrader/src/lib.rs +67 -30
  68. package/contracts/upgrader/src/tests/test_data/test_upgradeable_contract3.wasm +0 -0
  69. package/contracts/upgrader/src/tests/test_data/test_upgradeable_contract4.wasm +0 -0
  70. package/contracts/upgrader/src/tests/test_upgrader.rs +50 -4
  71. package/contracts/utils/src/ownable.rs +16 -5
  72. package/contracts/utils/src/tests/ownable.rs +39 -39
  73. package/contracts/utils/src/upgradeable.rs +60 -17
  74. package/docs/oapp-guide.md +4 -4
  75. package/package.json +3 -4
  76. package/sdk/.turbo/turbo-test.log +381 -366
  77. package/sdk/dist/generated/bml.d.ts +4 -4
  78. package/sdk/dist/generated/bml.js +6 -6
  79. package/sdk/dist/generated/counter.d.ts +158 -12
  80. package/sdk/dist/generated/counter.js +32 -12
  81. package/sdk/dist/generated/dvn.d.ts +4 -6
  82. package/sdk/dist/generated/dvn.js +8 -8
  83. package/sdk/dist/generated/dvn_fee_lib.d.ts +8 -10
  84. package/sdk/dist/generated/dvn_fee_lib.js +8 -8
  85. package/sdk/dist/generated/endpoint.d.ts +9 -9
  86. package/sdk/dist/generated/endpoint.js +9 -9
  87. package/sdk/dist/generated/executor.d.ts +9 -11
  88. package/sdk/dist/generated/executor.js +11 -11
  89. package/sdk/dist/generated/executor_fee_lib.d.ts +9 -11
  90. package/sdk/dist/generated/executor_fee_lib.js +11 -11
  91. package/sdk/dist/generated/executor_helper.d.ts +4 -4
  92. package/sdk/dist/generated/executor_helper.js +6 -6
  93. package/sdk/dist/generated/layerzero_view.d.ts +9 -11
  94. package/sdk/dist/generated/layerzero_view.js +11 -11
  95. package/sdk/dist/generated/oft.d.ts +194 -27
  96. package/sdk/dist/generated/oft.js +44 -22
  97. package/sdk/dist/generated/price_feed.d.ts +8 -10
  98. package/sdk/dist/generated/price_feed.js +8 -8
  99. package/sdk/dist/generated/sac_manager.d.ts +8 -8
  100. package/sdk/dist/generated/sac_manager.js +6 -6
  101. package/sdk/dist/generated/sml.d.ts +9 -9
  102. package/sdk/dist/generated/sml.js +9 -9
  103. package/sdk/dist/generated/treasury.d.ts +9 -9
  104. package/sdk/dist/generated/treasury.js +9 -9
  105. package/sdk/dist/generated/uln302.d.ts +9 -9
  106. package/sdk/dist/generated/uln302.js +9 -9
  107. package/sdk/dist/generated/upgrader.d.ts +25 -16
  108. package/sdk/dist/generated/upgrader.js +5 -5
  109. package/sdk/package.json +1 -1
  110. package/sdk/test/counter-sml.test.ts +20 -0
  111. package/sdk/test/counter-uln.test.ts +20 -0
  112. package/sdk/test/oft-sml.test.ts +22 -0
  113. package/sdk/test/upgrader.test.ts +1 -0
  114. package/turbo.json +1 -8
@@ -63,35 +63,6 @@ fn test_verify_success() {
63
63
  }
64
64
 
65
65
  // Storage & Nonce Behavior
66
- #[test]
67
- fn test_verify_stores_payload_hash() {
68
- let context = setup();
69
- let env = &context.env;
70
- let endpoint_client = &context.endpoint_client;
71
-
72
- let src_eid = 2u32;
73
- let sender = BytesN::from_array(env, &[1u8; 32]);
74
- let receiver = env.register(MockReceiver, ());
75
- let nonce = 1u64;
76
-
77
- // Setup receive library
78
- let receive_lib = context.setup_default_receive_lib(src_eid, 0);
79
-
80
- // Create payload hash
81
- let payload_hash = default_payload_hash(env);
82
-
83
- let origin = Origin { src_eid, sender: sender.clone(), nonce };
84
-
85
- // Mock auth for receive_lib
86
- context.mock_auth(&receive_lib, "verify", (&receive_lib, &origin, &receiver, &payload_hash));
87
-
88
- endpoint_client.verify(&receive_lib, &origin, &receiver, &payload_hash);
89
-
90
- // Verify inbound payload hash was stored
91
- let stored_hash = endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce);
92
- assert_eq!(stored_hash, Some(payload_hash));
93
- }
94
-
95
66
  #[test]
96
67
  fn test_verify_overwrites_payload_hash_for_same_nonce() {
97
68
  let context = setup();
@@ -262,7 +233,7 @@ fn test_verify_path_not_initializable() {
262
233
  context.mock_auth(&receive_lib, "verify", (&receive_lib, &origin, &receiver, &payload_hash));
263
234
 
264
235
  // Library validation passes (receive_lib matches default)
265
- // initializable checks: lazy_inbound_nonce == 0, and receiver.allow_initialize_path(origin) == false
236
+ // initializable checks: inbound_nonce == 0, and receiver.allow_initialize_path(origin) == false
266
237
  // So initializable returns false, should panic with PathNotInitializable error
267
238
  let result = endpoint_client.try_verify(&receive_lib, &origin, &receiver, &payload_hash);
268
239
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::PathNotInitializable.into());
@@ -281,7 +252,7 @@ fn test_verify_path_not_verifiable() {
281
252
  // Setup receive library and set as default (so library validation passes)
282
253
  let receive_lib = context.setup_default_receive_lib(src_eid, 0);
283
254
 
284
- // Skip nonce 1 to set lazy_inbound_nonce to 1 (so initializable passes: lazy_nonce > 0)
255
+ // Skip nonce 1 to advance inbound_nonce to 1 (so initializable passes: inbound_nonce > 0)
285
256
  context.mock_auth(&receiver, "skip", (&receiver, &receiver, &src_eid, &sender, &1u64));
286
257
  endpoint_client.skip(&receiver, &receiver, &src_eid, &sender, &1);
287
258
 
@@ -291,15 +262,15 @@ fn test_verify_path_not_verifiable() {
291
262
  let payload = build_payload(env, &guid, &message);
292
263
  let payload_hash = keccak256(env, &payload);
293
264
 
294
- // Try to verify nonce 1, but lazy_nonce is already 1, so nonce 1 is not verifiable
295
- // verifiable checks: nonce > lazy_inbound_nonce (1 > 1 is false) OR has payload hash (false)
265
+ // Try to verify nonce 1, but inbound_nonce is already 1, so nonce 1 is not verifiable
266
+ // verifiable checks: nonce > inbound_nonce (1 > 1 is false) OR has payload hash (false)
296
267
  let origin = Origin { src_eid, sender, nonce: 1 };
297
268
 
298
269
  // Mock auth for receive_lib
299
270
  context.mock_auth(&receive_lib, "verify", (&receive_lib, &origin, &receiver, &payload_hash));
300
271
 
301
- // Library validation passes, initializable passes (lazy_nonce > 0)
302
- // verifiable fails (nonce <= lazy_nonce and no payload hash), should panic with PathNotVerifiable error
272
+ // Library validation passes, initializable passes (inbound_nonce > 0)
273
+ // verifiable fails (nonce <= inbound_nonce and no payload hash), should panic with PathNotVerifiable error
303
274
  let result = endpoint_client.try_verify(&receive_lib, &origin, &receiver, &payload_hash);
304
275
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::PathNotVerifiable.into());
305
276
  }
@@ -258,9 +258,9 @@ fn test_burn_payload_hash_not_found_when_storage_none() {
258
258
  assert_eq!(result.err().unwrap().ok().unwrap(), EndpointError::PayloadHashNotFound.into());
259
259
  }
260
260
 
261
- // Failure when nonce is greater than lazy nonce
261
+ // Failure when nonce is greater than `inbound_nonce`
262
262
  #[test]
263
- fn test_burn_invalid_nonce_when_greater_than_lazy_nonce() {
263
+ fn test_burn_invalid_nonce_when_greater_than_inbound_nonce() {
264
264
  let context = setup();
265
265
  let env = &context.env;
266
266
 
@@ -52,6 +52,8 @@ fn test_clear_payload_success() {
52
52
  let payload_hash = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, nonce, &payload);
53
53
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(payload_hash.clone()));
54
54
 
55
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), nonce);
56
+
55
57
  clear_payload(&context, &receiver, src_eid, &sender, nonce, &payload);
56
58
 
57
59
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), None);
@@ -88,6 +90,42 @@ fn test_clear_payload_keeps_other_payload_hashes_intact() {
88
90
  let _ = hash3; // hash3 is only used to ensure it was computed and stored for nonce 3.
89
91
  }
90
92
 
93
+ #[test]
94
+ fn test_clear_payload_does_not_change_pending_inbound_nonces() {
95
+ let context = setup();
96
+ let env = &context.env;
97
+ let endpoint_client = &context.endpoint_client;
98
+
99
+ let receiver = Address::generate(env);
100
+ let src_eid = 2;
101
+ let sender = BytesN::from_array(env, &[1u8; 32]);
102
+
103
+ // Verify 1 and 2 consecutively, then verify 4 out-of-order.
104
+ // This produces: inbound_nonce = 2, pending_inbound_nonces = [4].
105
+ let payload1 = Bytes::from_array(env, &[0x01]);
106
+ let payload2 = Bytes::from_array(env, &[0x02]);
107
+ let payload4 = Bytes::from_array(env, &[0x04]);
108
+
109
+ let hash1 = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 1, &payload1);
110
+ let hash2 = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 2, &payload2);
111
+ let hash4 = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 4, &payload4);
112
+
113
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
114
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
115
+
116
+ // Clear nonce 2. This must not affect pending nonces (which are > inbound_nonce).
117
+ clear_payload(&context, &receiver, src_eid, &sender, 2, &payload2);
118
+
119
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
120
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
121
+
122
+ // Only nonce 2 is cleared; others remain.
123
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), None);
124
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &1), Some(hash1));
125
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &4), Some(hash4));
126
+ let _ = hash2; // hash2 is only used to ensure it was computed and stored for nonce 2.
127
+ }
128
+
91
129
  // Clearing a nonce <= inbound nonce does not update inbound nonce
92
130
  #[test]
93
131
  fn test_clear_payload_does_not_update_inbound_nonce_when_nonce_is_not_greater() {
@@ -132,7 +170,7 @@ fn test_clear_payload_payload_hash_not_found_when_nonce_is_checkpointed_but_miss
132
170
  let src_eid = 2;
133
171
  let sender = BytesN::from_array(env, &[1u8; 32]);
134
172
 
135
- // nonce <= lazy_nonce, so clear_payload will NOT run the "has_payload for all intermediate nonces" check.
173
+ // nonce <= inbound_nonce, so clear_payload will NOT run the "has_payload for all intermediate nonces" check.
136
174
  // It should fail at the payload hash check instead.
137
175
  env.as_contract(&endpoint_client.address, || {
138
176
  storage::EndpointStorage::set_inbound_nonce(env, &receiver, src_eid, &sender, &5u64)
@@ -186,6 +224,7 @@ fn test_clear_payload_not_stored() {
186
224
  fn test_clear_payload_missing_intermediate_nonce() {
187
225
  let context = setup();
188
226
  let env = &context.env;
227
+ let endpoint_client = &context.endpoint_client;
189
228
 
190
229
  let receiver = Address::generate(env);
191
230
  let src_eid = 2;
@@ -198,9 +237,19 @@ fn test_clear_payload_missing_intermediate_nonce() {
198
237
  let _ = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 1, &payload1);
199
238
  let _ = inbound_as_verified_from_payload(&context, &receiver, src_eid, &sender, 3, &payload3);
200
239
 
240
+ // inbound_nonce should be 1 (nonce 2 is missing), and nonce 3 should be pending.
241
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
242
+ assert_eq!(
243
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
244
+ soroban_sdk::vec![env, 3u64]
245
+ );
246
+
201
247
  // Clearing nonce 1 succeeds.
202
248
  clear_payload(&context, &receiver, src_eid, &sender, 1, &payload1);
203
249
 
250
+ // clear_payload does not advance inbound_nonce; it remains 1.
251
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
252
+
204
253
  // Try to clear nonce 3 - should panic because inbound_nonce is still 1 (nonce 3 is out-of-order).
205
254
  clear_payload(&context, &receiver, src_eid, &sender, 3, &payload3);
206
255
  }
@@ -123,6 +123,36 @@ fn test_inbound_out_of_order_populates_pending_and_drains_when_gap_closed() {
123
123
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64), Some(hash_2));
124
124
  }
125
125
 
126
+ #[test]
127
+ fn test_inbound_when_nonce_already_pending_does_not_advance_inbound_nonce() {
128
+ let context = setup();
129
+ let env = &context.env;
130
+ let endpoint_client = &context.endpoint_client;
131
+
132
+ let receiver = Address::generate(env);
133
+ let src_eid = 2;
134
+ let sender = BytesN::from_array(env, &[1u8; 32]);
135
+
136
+ // First inbound at nonce 2 adds it to pending (gap at nonce 1).
137
+ let hash_2_a = BytesN::from_array(env, &[0x22u8; 32]);
138
+ env.as_contract(&endpoint_client.address, || {
139
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 2u64, &hash_2_a)
140
+ });
141
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
142
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 2u64]);
143
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64), Some(hash_2_a.clone()));
144
+
145
+ // Re-inbound at the same nonce should overwrite the payload hash, but should NOT duplicate pending
146
+ // entries nor advance inbound_nonce (gap at nonce 1 still exists).
147
+ let hash_2_b = BytesN::from_array(env, &[0x23u8; 32]);
148
+ env.as_contract(&endpoint_client.address, || {
149
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 2u64, &hash_2_b)
150
+ });
151
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
152
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 2u64]);
153
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2u64), Some(hash_2_b));
154
+ }
155
+
126
156
  #[test]
127
157
  #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
128
158
  fn test_inbound_rejects_nonce_beyond_pending_window() {
@@ -141,6 +171,52 @@ fn test_inbound_rejects_nonce_beyond_pending_window() {
141
171
  });
142
172
  }
143
173
 
174
+ #[test]
175
+ fn test_inbound_accepts_upper_bound_when_inbound_nonce_nonzero() {
176
+ let context = setup();
177
+ let env = &context.env;
178
+ let endpoint_client = &context.endpoint_client;
179
+
180
+ let receiver = Address::generate(env);
181
+ let src_eid = 2;
182
+ let sender = BytesN::from_array(env, &[1u8; 32]);
183
+
184
+ // Set inbound_nonce to 100 so the upper bound is 356 (= 100 + 256).
185
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
186
+
187
+ let hash = BytesN::from_array(env, &[0xabu8; 32]);
188
+ env.as_contract(&endpoint_client.address, || {
189
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 356u64, &hash)
190
+ });
191
+
192
+ // Nonce 356 is not consecutive to 100, so it stays pending and inbound_nonce does not advance.
193
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 100);
194
+ assert_eq!(
195
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
196
+ soroban_sdk::vec![env, 356u64]
197
+ );
198
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &356u64), Some(hash));
199
+ }
200
+
201
+ #[test]
202
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
203
+ fn test_inbound_rejects_beyond_upper_bound_when_inbound_nonce_nonzero() {
204
+ let context = setup();
205
+ let env = &context.env;
206
+ let endpoint_client = &context.endpoint_client;
207
+
208
+ let receiver = Address::generate(env);
209
+ let src_eid = 2;
210
+ let sender = BytesN::from_array(env, &[1u8; 32]);
211
+
212
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
213
+
214
+ let hash = BytesN::from_array(env, &[0xabu8; 32]);
215
+ env.as_contract(&endpoint_client.address, || {
216
+ EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 357u64, &hash)
217
+ });
218
+ }
219
+
144
220
  #[test]
145
221
  #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
146
222
  fn test_inbound_rejects_reverify_when_nonce_leq_inbound_and_payload_missing() {
@@ -184,4 +260,6 @@ fn test_inbound_allows_reverify_when_nonce_leq_inbound_and_payload_exists() {
184
260
  EndpointV2::inbound_for_test(env, &receiver, src_eid, &sender, 1u64, &new_hash)
185
261
  });
186
262
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &1u64), Some(new_hash));
263
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
264
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
187
265
  }
@@ -0,0 +1,272 @@
1
+ use soroban_sdk::{testutils::Address as _, Address, BytesN};
2
+
3
+ use crate::{endpoint_v2::EndpointV2, tests::endpoint_setup::setup};
4
+
5
+ fn insert_and_drain(
6
+ context: &crate::tests::endpoint_setup::TestSetup,
7
+ receiver: &Address,
8
+ src_eid: u32,
9
+ sender: &BytesN<32>,
10
+ nonce: u64,
11
+ ) {
12
+ let env = &context.env;
13
+ let endpoint_client = &context.endpoint_client;
14
+ env.as_contract(&endpoint_client.address, || {
15
+ EndpointV2::insert_and_drain_pending_nonces_for_test(env, receiver, src_eid, sender, nonce)
16
+ });
17
+ }
18
+
19
+ #[test]
20
+ fn test_insert_and_drain_inserts_sorted_and_dedupes() {
21
+ let context = setup();
22
+ let env = &context.env;
23
+ let endpoint_client = &context.endpoint_client;
24
+
25
+ let receiver = Address::generate(env);
26
+ let src_eid = 2u32;
27
+ let sender = BytesN::from_array(env, &[1u8; 32]);
28
+
29
+ // Start with inbound_nonce = 0.
30
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
31
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
32
+
33
+ // Insert out of order: 3 then 2. Pending should remain sorted.
34
+ insert_and_drain(&context, &receiver, src_eid, &sender, 3);
35
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
36
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
37
+ assert_eq!(
38
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
39
+ soroban_sdk::vec![env, 2u64, 3u64]
40
+ );
41
+
42
+ // Re-inserting an already pending nonce should be a no-op (no duplicates, no drain).
43
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
44
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
45
+ assert_eq!(
46
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
47
+ soroban_sdk::vec![env, 2u64, 3u64]
48
+ );
49
+
50
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
51
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
52
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
53
+ }
54
+
55
+ #[test]
56
+ fn test_insert_and_drain_drains_consecutive_sequence() {
57
+ let context = setup();
58
+ let env = &context.env;
59
+ let endpoint_client = &context.endpoint_client;
60
+
61
+ let receiver = Address::generate(env);
62
+ let src_eid = 2u32;
63
+ let sender = BytesN::from_array(env, &[1u8; 32]);
64
+
65
+ // Insert 2 and 3, then insert 1. Inserting 1 should drain 1,2,3 and advance inbound_nonce to 3.
66
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
67
+ insert_and_drain(&context, &receiver, src_eid, &sender, 3);
68
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
69
+ assert_eq!(
70
+ endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender),
71
+ soroban_sdk::vec![env, 2u64, 3u64]
72
+ );
73
+
74
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
75
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 3);
76
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
77
+ }
78
+
79
+ #[test]
80
+ fn test_insert_and_drain_leaves_nonconsecutive_tail_pending() {
81
+ let context = setup();
82
+ let env = &context.env;
83
+ let endpoint_client = &context.endpoint_client;
84
+
85
+ let receiver = Address::generate(env);
86
+ let src_eid = 2u32;
87
+ let sender = BytesN::from_array(env, &[1u8; 32]);
88
+
89
+ // Insert 2 and 4, then insert 1. Drain should advance to 2 but leave 4 pending (gap at 3).
90
+ insert_and_drain(&context, &receiver, src_eid, &sender, 2);
91
+ insert_and_drain(&context, &receiver, src_eid, &sender, 4);
92
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
93
+
94
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
95
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
96
+ }
97
+
98
+ #[test]
99
+ fn test_insert_and_drain_accepts_upper_bound_when_inbound_nonce_zero() {
100
+ let context = setup();
101
+ let env = &context.env;
102
+ let endpoint_client = &context.endpoint_client;
103
+
104
+ let receiver = Address::generate(env);
105
+ let src_eid = 2u32;
106
+ let sender = BytesN::from_array(env, &[1u8; 32]);
107
+
108
+ // inbound_nonce == 0: 256 is allowed.
109
+ insert_and_drain(&context, &receiver, src_eid, &sender, 256);
110
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
111
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 256u64]);
112
+ }
113
+
114
+ #[test]
115
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
116
+ fn test_insert_and_drain_rejects_beyond_upper_bound_when_inbound_nonce_zero() {
117
+ let context = setup();
118
+ let env = &context.env;
119
+
120
+ let receiver = Address::generate(env);
121
+ let src_eid = 2u32;
122
+ let sender = BytesN::from_array(env, &[1u8; 32]);
123
+
124
+ // inbound_nonce == 0: 257 is out of range (max is 256).
125
+ insert_and_drain(&context, &receiver, src_eid, &sender, 257);
126
+ }
127
+
128
+ #[test]
129
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
130
+ fn test_insert_and_drain_rejects_nonce_leq_inbound_nonce() {
131
+ let context = setup();
132
+ let env = &context.env;
133
+ let endpoint_client = &context.endpoint_client;
134
+
135
+ let receiver = Address::generate(env);
136
+ let src_eid = 2u32;
137
+ let sender = BytesN::from_array(env, &[1u8; 32]);
138
+
139
+ // Advance inbound_nonce to 5 via storage utility.
140
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 5);
141
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 5);
142
+
143
+ // Calling with new_nonce == inbound_nonce should panic InvalidNonce.
144
+ insert_and_drain(&context, &receiver, src_eid, &sender, 5);
145
+ }
146
+
147
+ #[test]
148
+ fn test_insert_and_drain_accepts_upper_bound_when_inbound_nonce_nonzero() {
149
+ let context = setup();
150
+ let env = &context.env;
151
+ let endpoint_client = &context.endpoint_client;
152
+
153
+ let receiver = Address::generate(env);
154
+ let src_eid = 2u32;
155
+ let sender = BytesN::from_array(env, &[1u8; 32]);
156
+
157
+ // inbound_nonce == 100: upper bound is 356 (= 100 + 256).
158
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
159
+ insert_and_drain(&context, &receiver, src_eid, &sender, 356);
160
+
161
+ // Not consecutive to 100, so it remains pending and inbound_nonce does not advance.
162
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 100);
163
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 356u64]);
164
+ }
165
+
166
+ #[test]
167
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
168
+ fn test_insert_and_drain_rejects_beyond_upper_bound_when_inbound_nonce_nonzero() {
169
+ let context = setup();
170
+ let env = &context.env;
171
+
172
+ let receiver = Address::generate(env);
173
+ let src_eid = 2u32;
174
+ let sender = BytesN::from_array(env, &[1u8; 32]);
175
+
176
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 100);
177
+
178
+ // inbound_nonce == 100: 357 is out of range (max is 356).
179
+ insert_and_drain(&context, &receiver, src_eid, &sender, 357);
180
+ }
181
+
182
+ #[test]
183
+ fn test_insert_and_drain_drains_across_existing_pending_tail_when_inbound_nonce_nonzero() {
184
+ let context = setup();
185
+ let env = &context.env;
186
+ let endpoint_client = &context.endpoint_client;
187
+
188
+ let receiver = Address::generate(env);
189
+ let src_eid = 2u32;
190
+ let sender = BytesN::from_array(env, &[1u8; 32]);
191
+
192
+ // Set inbound_nonce to 2.
193
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 2);
194
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
195
+
196
+ // Insert 4 first -> pending [4], inbound_nonce remains 2 (missing 3).
197
+ insert_and_drain(&context, &receiver, src_eid, &sender, 4);
198
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
199
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
200
+
201
+ // Insert 3 -> should drain 3 then 4 and advance inbound_nonce to 4.
202
+ insert_and_drain(&context, &receiver, src_eid, &sender, 3);
203
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 4);
204
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
205
+ }
206
+
207
+ #[test]
208
+ fn test_insert_and_drain_drains_single_next_nonce_when_no_pending() {
209
+ let context = setup();
210
+ let env = &context.env;
211
+ let endpoint_client = &context.endpoint_client;
212
+
213
+ let receiver = Address::generate(env);
214
+ let src_eid = 2u32;
215
+ let sender = BytesN::from_array(env, &[1u8; 32]);
216
+
217
+ context.set_inbound_nonce(&receiver, src_eid, &sender, 5);
218
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 5);
219
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
220
+
221
+ // Insert exactly inbound_nonce + 1. This should drain immediately and leave pending empty.
222
+ insert_and_drain(&context, &receiver, src_eid, &sender, 6);
223
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 6);
224
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
225
+ }
226
+
227
+ #[test]
228
+ fn test_insert_and_drain_window_holds_255_when_inbound_plus_one_missing_then_drains() {
229
+ let context = setup();
230
+ let env = &context.env;
231
+ let endpoint_client = &context.endpoint_client;
232
+
233
+ let receiver = Address::generate(env);
234
+ let src_eid = 2u32;
235
+ let sender = BytesN::from_array(env, &[1u8; 32]);
236
+
237
+ // Fill the pending window with 2..=256 while nonce 1 is still missing.
238
+ // This creates 255 pending nonces and inbound_nonce remains 0.
239
+ for nonce in 2u64..=256u64 {
240
+ insert_and_drain(&context, &receiver, src_eid, &sender, nonce);
241
+ }
242
+
243
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
244
+ let pending = endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender);
245
+ assert_eq!(pending.len(), 255);
246
+ assert_eq!(pending.get(0).unwrap(), 2u64);
247
+ assert_eq!(pending.get(254).unwrap(), 256u64);
248
+
249
+ // Inserting nonce 1 closes the gap and drains the entire window.
250
+ insert_and_drain(&context, &receiver, src_eid, &sender, 1);
251
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 256);
252
+ assert!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender).is_empty());
253
+ }
254
+
255
+ #[test]
256
+ #[should_panic(expected = "Error(Contract, #11)")] // EndpointError::InvalidNonce
257
+ fn test_insert_and_drain_rejects_257_when_missing_inbound_plus_one() {
258
+ let context = setup();
259
+ let env = &context.env;
260
+
261
+ let receiver = Address::generate(env);
262
+ let src_eid = 2u32;
263
+ let sender = BytesN::from_array(env, &[1u8; 32]);
264
+
265
+ // Keep inbound_nonce at 0 and fill 2..=256 into pending.
266
+ for nonce in 2u64..=256u64 {
267
+ insert_and_drain(&context, &receiver, src_eid, &sender, nonce);
268
+ }
269
+
270
+ // 257 is outside the allowed range (0, 256] while inbound_nonce is still 0.
271
+ insert_and_drain(&context, &receiver, src_eid, &sender, 257);
272
+ }
@@ -3,6 +3,7 @@ mod clear_payload;
3
3
  mod inbound;
4
4
  mod inbound_nonce;
5
5
  mod inbound_payload_hash;
6
+ mod insert_and_drain_pending_nonces;
6
7
  mod pending_inbound_nonces;
7
8
  mod next_guid;
8
9
  mod nilify;
@@ -92,6 +92,7 @@ fn test_nilify_success_with_stored_payload() {
92
92
 
93
93
  // Verify payload hash is stored.
94
94
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(payload_hash.clone()));
95
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
95
96
 
96
97
  // Nilify the payload.
97
98
  let payload_hash_opt = Some(payload_hash.clone());
@@ -114,9 +115,10 @@ fn test_nilify_success_with_stored_payload() {
114
115
  let nilified_hash = endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce);
115
116
  let expected_nil_hash = nil_hash(&context);
116
117
  assert_eq!(nilified_hash, Some(expected_nil_hash));
118
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
117
119
  }
118
120
 
119
- // Successful nilify with None (non-verified nonce) when nonce > lazy nonce
121
+ // Successful nilify with None (no existing payload hash) when nonce > inbound_nonce
120
122
  #[test]
121
123
  fn test_nilify_success_with_empty_payload() {
122
124
  let context = setup();
@@ -150,9 +152,9 @@ fn test_nilify_success_with_empty_payload() {
150
152
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
151
153
  }
152
154
 
153
- // Nilify with None counts as "verified" for inbound_nonce (but does not change lazy nonce)
155
+ // Nilify with None counts toward `inbound_nonce` advancement (via pending-nonce draining).
154
156
  #[test]
155
- fn test_nilify_with_none_advances_inbound_nonce_without_changing_lazy_nonce() {
157
+ fn test_nilify_with_none_advances_inbound_nonce_without_changing_payload_hashes() {
156
158
  let context = setup();
157
159
  let env = &context.env;
158
160
  let endpoint_client = &context.endpoint_client;
@@ -211,7 +213,7 @@ fn test_nilify_closes_gap_and_drains_pending_nonces() {
211
213
  );
212
214
  }
213
215
 
214
- // Nilify is allowed when nonce <= lazy nonce if a payload hash exists
216
+ // Nilify is allowed when nonce <= inbound_nonce if an `inbound_payload_hash` exists
215
217
  #[test]
216
218
  fn test_nilify_allows_when_nonce_is_checkpointed_if_payload_exists() {
217
219
  let context = setup();
@@ -232,7 +234,7 @@ fn test_nilify_allows_when_nonce_is_checkpointed_if_payload_exists() {
232
234
  assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 10);
233
235
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(payload_hash.clone()));
234
236
 
235
- // Even though nonce <= lazy_nonce, this should succeed because a payload hash exists.
237
+ // Even though nonce <= inbound_nonce, this should succeed because a payload hash exists.
236
238
  let payload_hash_opt = Some(payload_hash.clone());
237
239
  nilify_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce, &payload_hash_opt);
238
240
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(nil_hash(&context)));
@@ -253,16 +255,19 @@ fn test_nilify_allows_repeated_call_when_already_nilified() {
253
255
 
254
256
  // First nilify: verified payload hash -> nil hash.
255
257
  context.inbound_as_verified(&receiver, src_eid, &sender, nonce, &payload_hash);
258
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
256
259
  let payload_hash_opt = Some(payload_hash);
257
260
  nilify_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce, &payload_hash_opt);
258
261
 
259
262
  let nil = nil_hash(&context);
260
263
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(nil.clone()));
264
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
261
265
 
262
266
  // Second nilify: passing the current stored nil hash should succeed.
263
267
  let nil_opt = Some(nil.clone());
264
268
  nilify_with_auth(&context, &receiver, &receiver, src_eid, &sender, nonce, &nil_opt);
265
269
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &nonce), Some(nil));
270
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 1);
266
271
  }
267
272
 
268
273
  // Delegate authorization (delegate(receiver) is allowed)
@@ -248,6 +248,36 @@ fn test_skip_closes_gap_and_advances_inbound_nonce() {
248
248
  assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), Some(payload_hash_2));
249
249
  }
250
250
 
251
+ // Skipping a missing nonce can close *part* of the gap while leaving later gaps pending.
252
+ #[test]
253
+ fn test_skip_closes_gap_but_pending_inbound_nonces_not_empty() {
254
+ let context = setup();
255
+ let env = &context.env;
256
+ let endpoint_client = &context.endpoint_client;
257
+
258
+ let receiver = Address::generate(env);
259
+ let src_eid = 2;
260
+ let sender = BytesN::from_array(env, &[1u8; 32]);
261
+
262
+ // Verify nonce 2 and 4 out-of-order. inbound_nonce stays 0 because nonce 1 is missing, and
263
+ // pending list contains [2, 4] (gap at 1 and 3).
264
+ let payload_hash_2 = BytesN::from_array(env, &[0x11u8; 32]);
265
+ let payload_hash_4 = BytesN::from_array(env, &[0x22u8; 32]);
266
+ context.inbound_as_verified(&receiver, src_eid, &sender, 2, &payload_hash_2);
267
+ context.inbound_as_verified(&receiver, src_eid, &sender, 4, &payload_hash_4);
268
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 0);
269
+
270
+ // Skip nonce 1 closes the first gap and drains consecutive pending nonces up to 2,
271
+ // but nonce 4 remains pending because nonce 3 is still missing.
272
+ skip_with_auth(&context, &receiver, &receiver, src_eid, &sender, 1);
273
+ assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
274
+ assert_eq!(endpoint_client.pending_inbound_nonces(&receiver, &src_eid, &sender), soroban_sdk::vec![env, 4u64]);
275
+
276
+ // Payload hashes remain intact.
277
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &2), Some(payload_hash_2));
278
+ assert_eq!(endpoint_client.inbound_payload_hash(&receiver, &src_eid, &sender, &4), Some(payload_hash_4));
279
+ }
280
+
251
281
  // Repeated skip of the same nonce is rejected
252
282
  #[test]
253
283
  fn test_skip_rejects_repeated_same_nonce() {