Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ pub unsafe extern "C" fn asset_lock_manager_resume(
check_ptr!(out_derivation_path);

let out_point = parse_outpoint(txid, vout);
let timeout = Duration::from_secs(timeout_secs);
// `timeout_secs == 0` requests an unbounded wait (a ChainLock is
// guaranteed finality; a broadcast lock is pending, never failed).
let timeout = (timeout_secs != 0).then(|| Duration::from_secs(timeout_secs));

let option = ASSET_LOCK_MANAGER_STORAGE.with_item(handle, |manager| {
runtime().block_on(manager.resume_asset_lock(&out_point, timeout))
Expand Down Expand Up @@ -92,7 +94,8 @@ pub unsafe extern "C" fn asset_lock_manager_resume(
/// Returns `ok` on a successful proof resolution, an error on
/// timeout / wait failure. The Swift caller is expected to schedule
/// this on a background queue — `runtime().block_on(...)` parks the
/// calling thread for up to `timeout_secs`.
/// calling thread for up to `timeout_secs` (or **indefinitely** when
/// `timeout_secs == 0`, since a ChainLock is guaranteed finality).
#[no_mangle]
pub unsafe extern "C" fn asset_lock_manager_catch_up_blocking(
handle: Handle,
Expand All @@ -103,7 +106,9 @@ pub unsafe extern "C" fn asset_lock_manager_catch_up_blocking(
check_ptr!(txid);

let out_point = parse_outpoint(txid, vout);
let timeout = Duration::from_secs(timeout_secs);
// `timeout_secs == 0` requests an unbounded wait (a ChainLock is
// guaranteed finality; a broadcast lock is pending, never failed).
let timeout = (timeout_secs != 0).then(|| Duration::from_secs(timeout_secs));

tracing::info!(
outpoint = %out_point,
Expand Down
6 changes: 6 additions & 0 deletions packages/rs-platform-wallet-ffi/src/shielded_send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,9 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_fund_from_asset_lock(
// pool-seeding path uses its own dedicated FFI entry point).
0,
None,
// User-facing funding: wait for the ChainLock indefinitely —
// a broadcast asset lock is pending finality, never failed.
None,
)
.await
});
Expand Down Expand Up @@ -1061,6 +1064,9 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_resume_fund_from_asset
// Resuming a single-note fund (not a seeding batch).
0,
None,
// User-facing funding: wait for the ChainLock indefinitely —
// a broadcast asset lock is pending finality, never failed.
None,
)
.await
});
Expand Down
8 changes: 6 additions & 2 deletions packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,13 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
.await?;
self.queue_asset_lock_changeset(cs_broadcast);

// 5. Wait for proof via SPV events.
// 5. Wait for proof via SPV events. The 300s bound is an
// InstantSend-preference window, NOT a finality timeout: on
// expiry the resolver falls back to an unbounded ChainLock wait
// (`upgrade_to_chain_lock_proof(None)`), so a broadcast lock is
// never surfaced as "failed" just because IS was slow.
let proof = self
.wait_for_proof(&out_point, Duration::from_secs(300))
.wait_for_proof(&out_point, Some(Duration::from_secs(300)))
.await?;

// 5b. If we got an IS-lock proof, check whether the transaction is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,23 @@ use crate::wallet::asset_lock::manager::AssetLockManager;
// Timeout policy
// ---------------------------------------------------------------------------

/// Time we will wait for a ChainLock to materialise after an IS-lock
/// fallback is triggered. 180s mirrors the existing fallback shape and
/// is roughly the worst-case ChainLock latency we've observed in
/// testnet operation. Promoted to a constant so the registration,
/// top-up, and address-funding flows can't drift apart on this number.
/// Bounded ChainLock wait used *only* by the shielded seed pool, where a
/// `FinalityTimeout` is a deliberate pacing signal — rapid back-to-back
/// batches chain unconfirmed L1 change outputs, and around core's
/// unconfirmed-ancestor depth limit IS/CL proofs stop arriving until a
/// block lands; the seed pool catches the timeout, pauses, and resumes
/// the tracked lock (see `shielded/seed_pool.rs`).
///
/// The user-facing funding flows (identity registration / top-up,
/// platform-address top-up, and user-initiated shielded funding) do NOT
/// use this: they wait for a ChainLock **indefinitely**
/// (`upgrade_to_chain_lock_proof(None)`), because a ChainLock is
/// deterministic finality that will eventually cover any broadcast
/// asset-lock tx — so a broadcast lock is *pending*, never *failed*.
///
/// Only the shielded seed pool consumes this, so it is `shielded`-gated
/// to avoid a dead-code warning in builds without that feature.
#[cfg(feature = "shielded")]
pub(crate) const CL_FALLBACK_TIMEOUT: Duration = Duration::from_secs(180);

/// Delay between retries when Platform rejected with CL-height-too-low.
Expand Down Expand Up @@ -408,8 +420,12 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
}
}
AssetLockFunding::FromExistingAssetLock { out_point } => {
// 300s is an InstantSend-preference window, not a finality
// timeout: on expiry the caller falls back to an unbounded
// ChainLock wait, so a resumed broadcast lock never fails
// just because IS was slow.
match self
.resume_asset_lock(&out_point, Duration::from_secs(300))
.resume_asset_lock(&out_point, Some(Duration::from_secs(300)))
.await
{
Ok((proof, path)) => Ok(FundingResolution::Resolved(ResolvedFunding {
Expand Down
97 changes: 65 additions & 32 deletions packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,21 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
/// Called from the recovery layer when `put_to_platform` fails with
/// `InvalidInstantAssetLockProofSignature`. If the TX is already
/// chain-locked, constructs the proof immediately. Otherwise, **waits**
/// for a ChainLock via SPV events (up to 10 minutes) so the caller
/// doesn't see a failure — just a longer wait.
/// for a ChainLock via SPV events so the caller doesn't see a failure —
/// just a longer wait.
///
/// `timeout` is `Option<Duration>`: `None` waits **indefinitely**. A
/// ChainLock is deterministic finality that will eventually cover any
/// broadcast asset-lock tx, so the user-facing funding flows
/// (identity registration / top-up, platform-address top-up, shielded
/// funding) pass `None` — a broadcast lock is pending, never failed.
/// The only bounded caller is the shielded seed pool, where a
/// `FinalityTimeout` is a deliberate pacing signal for the
/// unconfirmed-ancestor stall (see `CL_FALLBACK_TIMEOUT`).
pub(crate) async fn upgrade_to_chain_lock_proof(
&self,
out_point: &OutPoint,
timeout: Duration,
timeout: Option<Duration>,
) -> Result<dpp::prelude::AssetLockProof, PlatformWalletError> {
use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
use key_wallet::transaction_checking::TransactionContext;
Expand Down Expand Up @@ -253,16 +262,18 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
/// Wait for a ChainLock that covers the given transaction.
///
/// Subscribes to SPV events and waits until the transaction's block
/// is chain-locked.
/// is chain-locked. `timeout` is `Option<Duration>`: `None` waits
/// **indefinitely** (a ChainLock is guaranteed finality that will
/// eventually arrive, so a broadcast lock is pending, not failed).
async fn wait_for_chain_lock(
&self,
account_index: u32,
out_point: &OutPoint,
timeout: Duration,
timeout: Option<Duration>,
) -> Result<u32, PlatformWalletError> {
use key_wallet::transaction_checking::TransactionContext;

let deadline = tokio::time::Instant::now() + timeout;
let deadline = timeout.map(|t| tokio::time::Instant::now() + t);

loop {
// Arm the `Notify` future BEFORE the state check, closing
Expand Down Expand Up @@ -304,18 +315,26 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
}
}

let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
}

// Wait for a lock event notification or timeout. The
// `notified` future is the one we armed above, so any
// CL/IS event since then is already buffered into it.
tokio::select! {
_ = &mut notified => continue,
_ = tokio::time::sleep(remaining) => {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
// Wait for a lock event notification (or timeout, when one is
// configured). The `notified` future is the one we armed above,
// so any CL/IS event since then is already buffered into it.
match deadline {
Some(dl) => {
let remaining = dl.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
}
tokio::select! {
_ = &mut notified => continue,
_ = tokio::time::sleep(remaining) => {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
}
}
}
// No deadline: wait indefinitely for the next lock event.
None => {
notified.as_mut().await;
continue;
}
}
}
Expand All @@ -330,17 +349,23 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
///
/// Returns a properly-constructed `AssetLockProof` on success, or
/// `FinalityTimeout` if the timeout elapses first.
///
/// `timeout` is `Option<Duration>`: `None` waits **indefinitely** for
/// either an InstantSend or a ChainLock proof. Bounded callers use the
/// deadline as an InstantSend-preference window — on expiry they get a
/// `FinalityTimeout` and fall back to an (unbounded) ChainLock wait via
/// [`Self::upgrade_to_chain_lock_proof`].
pub(in crate::wallet::asset_lock) async fn wait_for_proof(
&self,
out_point: &OutPoint,
timeout: Duration,
timeout: Option<Duration>,
) -> Result<dpp::prelude::AssetLockProof, PlatformWalletError> {
use dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
use dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof;
use key_wallet::transaction_checking::TransactionContext;

tracing::info!(outpoint = %out_point, ?timeout, "wait_for_proof: entered");
let deadline = tokio::time::Instant::now() + timeout;
let deadline = timeout.map(|t| tokio::time::Instant::now() + t);
let mut iter: u32 = 0;

// Read account_index and transaction from the tracked lock.
Expand Down Expand Up @@ -520,18 +545,26 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
}
}

let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
}

// Wait for a lock event notification or timeout. The
// `notified` future is the one we armed above, so any
// IS/CL event since then is already buffered into it.
tokio::select! {
_ = &mut notified => continue,
_ = tokio::time::sleep(remaining) => {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
// Wait for a lock event notification (or timeout, when one is
// configured). The `notified` future is the one we armed above,
// so any IS/CL event since then is already buffered into it.
match deadline {
Some(dl) => {
let remaining = dl.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
}
tokio::select! {
_ = &mut notified => continue,
_ = tokio::time::sleep(remaining) => {
return Err(PlatformWalletError::FinalityTimeout(*out_point));
}
}
}
// No deadline: wait indefinitely for the next lock event.
None => {
notified.as_mut().await;
continue;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,15 @@ impl<B: TransactionBroadcaster + ?Sized> AssetLockManager<B> {
/// registration or top-up via the `_with_signer` SDK methods. The
/// caller passes `derivation_path` to the same signer used for the
/// build phase when the credit output is later consumed on Platform.
///
/// `timeout` is `Option<Duration>` and is only consulted when the lock
/// still needs a proof (`Built` / `Broadcast`): `None` waits
/// **indefinitely** for finality. For `InstantSendLocked` / `ChainLocked`
/// the proof already exists and no wait happens, so the value is moot.
pub async fn resume_asset_lock(
&self,
out_point: &OutPoint,
timeout: Duration,
timeout: Option<Duration>,
) -> Result<(dpp::prelude::AssetLockProof, DerivationPath), PlatformWalletError> {
tracing::info!(outpoint = %out_point, ?timeout, "resume_asset_lock: entered");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ use dash_sdk::platform::transition::top_up_identity::TopUpIdentity;
use crate::error::{is_instant_lock_proof_invalid, PlatformWalletError};
use crate::wallet::asset_lock::orchestration::{
out_point_from_proof, submit_with_cl_height_retry, FundingResolution, ResolvedFunding,
CL_FALLBACK_TIMEOUT,
};
use crate::wallet::asset_lock::AssetLockFunding;

Expand Down Expand Up @@ -184,7 +183,7 @@ impl IdentityWallet {
);
let chain_proof = self
.asset_locks
.upgrade_to_chain_lock_proof(&out_point, CL_FALLBACK_TIMEOUT)
.upgrade_to_chain_lock_proof(&out_point, None)
.await?;
// Recover the credit-output derivation path. The
// asset lock is now CL-attached (status advanced by
Expand All @@ -193,10 +192,7 @@ impl IdentityWallet {
// proof branch and just re-derives the path. This is
// cheap (no SPV wait) and avoids duplicating the
// path-derivation logic here.
let (_, path) = self
.asset_locks
.resume_asset_lock(&out_point, CL_FALLBACK_TIMEOUT)
.await?;
let (_, path) = self.asset_locks.resume_asset_lock(&out_point, None).await?;
ResolvedFunding {
proof: chain_proof,
path,
Expand Down Expand Up @@ -248,7 +244,7 @@ impl IdentityWallet {
);
let chain_proof = self
.asset_locks
.upgrade_to_chain_lock_proof(&out_point, CL_FALLBACK_TIMEOUT)
.upgrade_to_chain_lock_proof(&out_point, None)
.await?;
submit_with_cl_height_retry(settings, |s| {
placeholder.put_to_platform_and_wait_for_response_with_signer(
Expand Down Expand Up @@ -404,12 +400,9 @@ impl IdentityWallet {
);
let chain_proof = self
.asset_locks
.upgrade_to_chain_lock_proof(&out_point, CL_FALLBACK_TIMEOUT)
.await?;
let (_, path) = self
.asset_locks
.resume_asset_lock(&out_point, CL_FALLBACK_TIMEOUT)
.upgrade_to_chain_lock_proof(&out_point, None)
.await?;
let (_, path) = self.asset_locks.resume_asset_lock(&out_point, None).await?;
ResolvedFunding {
proof: chain_proof,
path,
Expand Down Expand Up @@ -445,7 +438,7 @@ impl IdentityWallet {
);
let chain_proof = self
.asset_locks
.upgrade_to_chain_lock_proof(&out_point, CL_FALLBACK_TIMEOUT)
.upgrade_to_chain_lock_proof(&out_point, None)
.await?;
submit_with_cl_height_retry(settings, |s| {
identity.top_up_identity_with_signer(
Expand Down
Loading
Loading