@layerzerolabs/protocol-stellar-v2 0.2.34 → 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.
- package/.turbo/turbo-build.log +250 -251
- package/.turbo/turbo-lint.log +226 -231
- package/.turbo/turbo-test.log +1994 -1731
- package/Cargo.lock +10 -10
- package/Cargo.toml +1 -1
- package/contracts/common-macros/src/storage.rs +7 -5
- package/contracts/common-macros/src/tests/storage/snapshots/common_macros__tests__storage__generate_storage__snapshot_generated_storage_code.snap +3 -3
- package/contracts/endpoint-v2/src/endpoint_v2.rs +5 -4
- package/contracts/endpoint-v2/src/interfaces/messaging_channel.rs +7 -8
- package/contracts/endpoint-v2/src/messaging_channel.rs +78 -45
- package/contracts/endpoint-v2/src/storage.rs +8 -3
- package/contracts/endpoint-v2/src/tests/endpoint_setup.rs +2 -2
- package/contracts/endpoint-v2/src/tests/endpoint_v2/clear.rs +12 -15
- package/contracts/endpoint-v2/src/tests/endpoint_v2/verifiable.rs +46 -9
- package/contracts/endpoint-v2/src/tests/messaging_channel/burn.rs +7 -23
- package/contracts/endpoint-v2/src/tests/messaging_channel/clear_payload.rs +23 -20
- package/contracts/endpoint-v2/src/tests/messaging_channel/inbound.rs +94 -1
- package/contracts/endpoint-v2/src/tests/messaging_channel/inbound_nonce.rs +17 -15
- package/contracts/endpoint-v2/src/tests/messaging_channel/mod.rs +1 -1
- package/contracts/endpoint-v2/src/tests/messaging_channel/nilify.rs +48 -13
- package/contracts/endpoint-v2/src/tests/messaging_channel/pending_inbound_nonces.rs +111 -0
- package/contracts/endpoint-v2/src/tests/messaging_channel/skip.rs +15 -25
- package/contracts/layerzero-views/src/layerzero_view.rs +2 -2
- package/contracts/layerzero-views/src/tests/layerzero_view_tests.rs +3 -4
- package/contracts/layerzero-views/src/tests/setup.rs +0 -21
- package/contracts/message-libs/blocked-message-lib/src/lib.rs +4 -4
- package/contracts/message-libs/uln-302/src/send_uln.rs +5 -5
- package/contracts/oapps/counter/src/counter.rs +6 -0
- package/contracts/oapps/oapp/src/oapp_sender.rs +3 -2
- package/contracts/oapps/oft/src/extensions/oft_fee.rs +5 -0
- package/contracts/oapps/oft/src/oft.rs +5 -4
- package/docs/layerzero-v2-on-stellar.md +46 -2
- package/package.json +3 -3
- package/sdk/.turbo/turbo-test.log +312 -316
- package/sdk/dist/generated/bml.d.ts +3 -3
- package/sdk/dist/generated/bml.js +3 -3
- package/sdk/dist/generated/counter.d.ts +32 -3
- package/sdk/dist/generated/counter.js +6 -3
- package/sdk/dist/generated/dvn.d.ts +3 -3
- package/sdk/dist/generated/dvn.js +3 -3
- package/sdk/dist/generated/dvn_fee_lib.d.ts +2 -2
- package/sdk/dist/generated/dvn_fee_lib.js +2 -2
- package/sdk/dist/generated/endpoint.d.ts +12 -13
- package/sdk/dist/generated/endpoint.js +7 -7
- package/sdk/dist/generated/executor.d.ts +3 -3
- package/sdk/dist/generated/executor.js +3 -3
- package/sdk/dist/generated/executor_fee_lib.d.ts +2 -2
- package/sdk/dist/generated/executor_fee_lib.js +2 -2
- package/sdk/dist/generated/executor_helper.d.ts +2 -2
- package/sdk/dist/generated/executor_helper.js +2 -2
- package/sdk/dist/generated/layerzero_view.d.ts +3 -3
- package/sdk/dist/generated/layerzero_view.js +3 -3
- package/sdk/dist/generated/oft.d.ts +32 -3
- package/sdk/dist/generated/oft.js +6 -3
- package/sdk/dist/generated/price_feed.d.ts +3 -3
- package/sdk/dist/generated/price_feed.js +3 -3
- package/sdk/dist/generated/sac_manager.d.ts +24 -3
- package/sdk/dist/generated/sac_manager.js +4 -3
- package/sdk/dist/generated/sml.d.ts +2 -2
- package/sdk/dist/generated/sml.js +2 -2
- package/sdk/dist/generated/treasury.d.ts +2 -2
- package/sdk/dist/generated/treasury.js +2 -2
- package/sdk/dist/generated/uln302.d.ts +3 -3
- package/sdk/dist/generated/uln302.js +3 -3
- package/sdk/dist/generated/upgrader.d.ts +2 -2
- package/sdk/dist/generated/upgrader.js +2 -2
- package/sdk/package.json +1 -1
- package/contracts/endpoint-v2/src/tests/messaging_channel/lazy_inbound_nonce.rs +0 -39
package/Cargo.lock
CHANGED
|
@@ -1755,9 +1755,9 @@ dependencies = [
|
|
|
1755
1755
|
|
|
1756
1756
|
[[package]]
|
|
1757
1757
|
name = "soroban-ledger-snapshot"
|
|
1758
|
-
version = "25.1.
|
|
1758
|
+
version = "25.1.1"
|
|
1759
1759
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1760
|
-
checksum = "
|
|
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.
|
|
1772
|
+
version = "25.1.1"
|
|
1773
1773
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1774
|
-
checksum = "
|
|
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.
|
|
1796
|
+
version = "25.1.1"
|
|
1797
1797
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1798
|
-
checksum = "
|
|
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.
|
|
1816
|
+
version = "25.1.1"
|
|
1817
1817
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1818
|
-
checksum = "
|
|
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.
|
|
1828
|
+
version = "25.1.1"
|
|
1829
1829
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1830
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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(||
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.
|
|
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.
|
|
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
|
|
169
|
-
|
|
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
|
|
175
|
-
origin.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
|
|
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
|
|
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
|
|
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
|
|
102
|
-
fn
|
|
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
|
-
|
|
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
|
|
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 >
|
|
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
|
|
81
|
-
let
|
|
82
|
-
assert_with_error!(env, nonce <=
|
|
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
|
-
|
|
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
|
|
132
|
-
fn
|
|
133
|
-
EndpointStorage::
|
|
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
|
-
///
|
|
166
|
-
///
|
|
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
|
|
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
|
-
///
|
|
193
|
-
///
|
|
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
|
|
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
|
-
///
|
|
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
|
-
|
|
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
|
|
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::
|
|
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
|
-
//
|
|
122
|
+
// Inbound nonce is advanced during verify, not during clear
|
|
123
123
|
#[test]
|
|
124
|
-
fn
|
|
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
|
|
140
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
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
|
|
183
|
-
|
|
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
|
|
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.
|
|
412
|
+
assert_eq!(endpoint_client.inbound_nonce(&receiver, &src_eid, &sender), 2);
|
|
416
413
|
|
|
417
|
-
// Clearing an older nonce should not change
|
|
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.
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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:
|
|
83
|
-
assert_eq!(endpoint_client.
|
|
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
|
-
//
|
|
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.
|
|
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
|
+
}
|