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
9 changes: 7 additions & 2 deletions packages/rs-platform-wallet-ffi/src/identity_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use platform_wallet::{IdentityTokenSyncInfo, IdentityTokenSyncState};

use crate::error::*;
use crate::handle::*;
use crate::runtime::runtime;
use crate::runtime::{block_on_worker, runtime};
use crate::{check_ptr, unwrap_option_or_return};

/// Flattened per-(identity, token) row mirroring
Expand Down Expand Up @@ -170,7 +170,12 @@ pub unsafe extern "C" fn platform_wallet_manager_identity_sync_sync_now(
handle: Handle,
) -> PlatformWalletFFIResult {
let option = PLATFORM_WALLET_MANAGER_STORAGE.with_item(handle, |manager| {
runtime().block_on(manager.identity_sync().sync_now());
// `block_on_worker`, NOT `runtime().block_on`: the pass
// verifies GroveDB proofs whose recursion blows the ~512 KB
// stack of the host's dispatch/concurrency calling thread
// (same SIGBUS as the shielded/dashpay Sync Now buttons).
let mgr = manager.identity_sync_arc();
block_on_worker(async move { mgr.sync_now().await });
});
unwrap_option_or_return!(option);
PlatformWalletFFIResult::ok()
Expand Down
22 changes: 19 additions & 3 deletions packages/rs-platform-wallet-ffi/src/platform_address_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use dash_sdk::platform::address_sync::AddressSyncMetrics;
use crate::error::*;
use crate::handle::*;
use crate::platform_address_types::AddressSyncConfigFFI;
use crate::runtime::runtime;
use crate::runtime::{run_on_big_stack_thread, runtime};
use crate::{check_ptr, unwrap_option_or_return};

/// Flattened sync metrics for one wallet result in a platform-address sync pass.
Expand Down Expand Up @@ -190,9 +190,25 @@ pub unsafe extern "C" fn platform_wallet_manager_platform_address_sync_sync_now(
handle: Handle,
) -> PlatformWalletFFIResult {
let option = PLATFORM_WALLET_MANAGER_STORAGE.with_item(handle, |manager| {
runtime().block_on(manager.platform_address_sync().sync_now());
// Big-stack polling, NOT bare `runtime().block_on`: the pass
// verifies GroveDB proofs whose recursion blows the ~512 KB
// stack of the host's dispatch/concurrency calling thread
// (same SIGBUS as the shielded/dashpay Sync Now buttons).
// `run_on_big_stack_thread` rather than `block_on_worker`
// because this future trips rustc's implied-lifetime-bound
// limitation (rust-lang/rust issue #100013) against
// `block_on_worker`'s `Send + 'static` bounds.
run_on_big_stack_thread(|| {
runtime().block_on(manager.platform_address_sync().sync_now());
})
});
unwrap_option_or_return!(option);
let spawn_result = unwrap_option_or_return!(option);
if let Err(e) = spawn_result {
return PlatformWalletFFIResult::err(
PlatformWalletFFIResultCode::ErrorWalletOperation,
format!("failed to spawn big-stack thread for address sync: {e}"),
);
}
PlatformWalletFFIResult::ok()
}

Expand Down
64 changes: 64 additions & 0 deletions packages/rs-platform-wallet-ffi/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,70 @@ where
rt.block_on(async move { rt.spawn(future).await.expect("tokio worker panicked") })
}

/// Run `f` to completion on a freshly spawned scoped OS thread with the
/// same 8 MB stack the runtime workers get, blocking the caller until it
/// returns. Errors (instead of panicking) if the OS refuses to spawn
/// the thread, so `extern "C"` callers can surface the failure through
/// their `PlatformWalletFFIResult` rather than aborting the host.
///
/// Escape hatch for call sites that need big-stack polling but whose
/// future cannot satisfy [`block_on_worker`]'s `Send + 'static` bounds
/// (e.g. rustc's implied-lifetime-bound limitation, rust-lang/rust
/// issue #100013). The closure typically wraps
/// `runtime().block_on(...)` — the future is then created *and* polled
/// entirely on the big-stack thread, so no `Send`/`'static` proof is
/// needed for the future itself. Prefer [`block_on_worker`] where it
/// compiles: it reuses pooled runtime workers instead of paying a
/// thread spawn per call.
///
/// A panic inside `f` is propagated as a panic here, matching
/// [`block_on_worker`]'s "tokio worker panicked" convention — a panic
/// in the pass is a bug, not a recoverable condition.
pub(crate) fn run_on_big_stack_thread<T: Send>(f: impl FnOnce() -> T + Send) -> std::io::Result<T> {
std::thread::scope(|scope| {
let handle = std::thread::Builder::new()
.name("pw-ffi-bigstack".into())
.stack_size(WORKER_STACK_BYTES)
.spawn_scoped(scope, f)?;
Ok(handle.join().expect("big-stack FFI thread panicked"))
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn run_on_big_stack_thread_round_trips_return_value() {
let out = run_on_big_stack_thread(|| 41 + 1).expect("spawn should succeed");
assert_eq!(out, 42);
}

/// The whole point of the helper: recursion far past the ~512 KB
/// host-thread stacks (and the 2 MB default test-thread stack)
/// must complete on the 8 MB thread.
#[test]
fn run_on_big_stack_thread_survives_deep_recursion() {
#[inline(never)]
fn recurse(depth: u32) -> u64 {
// ~1 KB frame the optimizer can't elide.
let frame = std::hint::black_box([depth as u64; 128]);
if depth == 0 {
frame[0]
} else {
recurse(depth - 1) + std::hint::black_box(frame[127])
}
}

// ~1000 frames * >1 KB each (debug frames run several KB with
// the black_box copies) lands well past the ~512 KB iOS host
// stacks this helper exists for, while staying comfortably
// under WORKER_STACK_BYTES.
let out = run_on_big_stack_thread(|| recurse(1_000)).expect("spawn should succeed");
assert!(out > 0);
}
}

#[cfg(feature = "tokio-metrics")]
mod metrics {
use std::time::Duration;
Expand Down
18 changes: 14 additions & 4 deletions packages/rs-platform-wallet-ffi/src/shielded_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use zeroize::Zeroizing;
use crate::error::*;
use crate::handle::*;
use crate::identity_keys_from_mnemonic::parse_mnemonic_any_language;
use crate::runtime::runtime;
use crate::runtime::{block_on_worker, runtime};
use crate::shielded_types::ShieldedSyncWalletResultFFI;
use crate::{check_ptr, unwrap_option_or_return};
use rs_sdk_ffi::{
Expand Down Expand Up @@ -170,7 +170,15 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_sync_sync_now(
handle: Handle,
) -> PlatformWalletFFIResult {
let option = PLATFORM_WALLET_MANAGER_STORAGE.with_item(handle, |manager| {
runtime().block_on(manager.shielded_sync().sync_now(true));
let mgr = manager.shielded_sync_arc();
// `block_on_worker`, NOT `runtime().block_on`: the host calls
// this from a dispatch/concurrency thread with ~512 KB of
// stack, and polling the whole notes-sync future there blows
// it (SIGBUS "Thread stack size exceeded" observed on-device
// and on-sim 2026-07-07 from the Sync Now button). The worker
// dispatch moves the compute onto the runtime's 8 MB-stack
// threads (see runtime.rs) — same fix as dashpay_sync.
block_on_worker(async move { mgr.sync_now(true).await });
});
unwrap_option_or_return!(option);
PlatformWalletFFIResult::ok()
Expand Down Expand Up @@ -548,9 +556,11 @@ pub unsafe extern "C" fn platform_wallet_manager_shielded_sync_wallet(

let option = PLATFORM_WALLET_MANAGER_STORAGE.with_item(handle, |manager| {
// Per-wallet sync_wallet is exclusively a user-initiated
// entry point — same `force=true` reasoning as
// entry point — same `force=true` reasoning and same
// `block_on_worker` stack-size requirement as
// `platform_wallet_manager_shielded_sync_sync_now`.
runtime().block_on(manager.shielded_sync().sync_wallet(&wallet_id, true))
let mgr = manager.shielded_sync_arc();
block_on_worker(async move { mgr.sync_wallet(&wallet_id, true).await })
});
let result = unwrap_option_or_return!(option);
match result {
Expand Down
Loading