Skip to content
Closed
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
4 changes: 0 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions packages/rs-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ zeroize = "1.8"
# Concurrency
once_cell = "1.20"

# HTTP client for diagnostics
reqwest = { version = "0.12", features = ["json", "rustls-tls-native-roots"] }

[build-dependencies]
cbindgen = "0.27"
Expand Down
17 changes: 1 addition & 16 deletions packages/rs-sdk-ffi/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::sync::{Arc, OnceLock};
use tokio::runtime::Runtime;
use tracing::{debug, error, info, warn};
use tracing::{error, info, warn};

use dash_sdk::dpp::dashcore::Network;
use dash_sdk::dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructure;
Expand Down Expand Up @@ -506,21 +506,6 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) -

let runtime_clone = runtime.handle().clone();
runtime_clone.spawn(async move {
// First, try a simple HTTP test
debug!("dash_sdk_create_trusted: testing basic HTTP connectivity");
match reqwest::get("https://www.google.com").await {
Ok(_) => debug!("dash_sdk_create_trusted: basic HTTP test successful (Google)"),
Err(e) => warn!(error = %e, "dash_sdk_create_trusted: basic HTTP test failed"),
}

// Try the quorums endpoint directly
debug!("dash_sdk_create_trusted: testing quorums endpoint directly");
match reqwest::get("https://quorums.testnet.networks.dash.org/quorums").await {
Ok(resp) => debug!(status = %resp.status(), "dash_sdk_create_trusted: direct quorums endpoint test successful"),
Err(e) => warn!(error = %e, "dash_sdk_create_trusted: direct quorums endpoint test failed"),
}

// Now try through the provider
match provider_for_prefetch.update_quorum_caches().await {
Ok(_) => info!("dash_sdk_create_trusted: successfully prefetched quorums"),
Err(e) => warn!(error = %e, "dash_sdk_create_trusted: failed to prefetch quorums; continuing"),
Expand Down
4 changes: 2 additions & 2 deletions packages/rs-sdk-trusted-context-provider/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ tracing = "0.1.41"
lru = "0.16.3"
arc-swap = "1.7.1"
hex = "0.4.3"
futures = "0.3"
url = "2.5"
tokio = { version = "1.40", features = ["rt"] }

[features]
default = []
Expand All @@ -47,4 +47,4 @@ token-history-contract = ["dpp/token-history-contract"]
keywords-contract = ["dpp/keywords-contract"]

[dev-dependencies]
tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.40", features = ["macros", "rt-multi-thread", "net", "io-util"] }
199 changes: 186 additions & 13 deletions packages/rs-sdk-trusted-context-provider/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,21 +618,54 @@ impl ContextProvider for TrustedHttpContextProvider {
)));
}

// For non-WASM targets, we can use block_on to fetch
// For non-WASM targets, run the async fetch on a dedicated OS thread with its
// own tokio runtime. This avoids two classes of failure:
//
// 1. `futures::executor::block_on` deadlocks when called from inside a tokio
// runtime because reqwest's I/O needs the tokio reactor, which is blocked
// waiting for `block_on` to return.
//
// 2. `tokio::task::block_in_place` panics on current-thread runtimes.
//
// Spawning a scoped OS thread sidesteps both: the new thread has no tokio
// context, its own current-thread runtime, and borrows `self` safely via
// `std::thread::scope` (the scope is exited only after the thread joins).
//
// The overhead is acceptable because quorum cache misses are rare events.
#[cfg(not(target_arch = "wasm32"))]
{
// Use blocking to run async code in sync context
let quorum =
match futures::executor::block_on(self.find_quorum(quorum_type, quorum_hash)) {
Ok(q) => q,
Err(e) => {
debug!("Error finding quorum: {}", e);
return Err(ContextProviderError::Generic(format!(
"Failed to find quorum: {}",
e
)));
}
};
let find_future = self.find_quorum(quorum_type, quorum_hash);
let thread_result = std::thread::scope(|s| {
s.spawn(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
TrustedContextProviderError::NetworkError(format!(
"Failed to create tokio runtime for quorum fetch: {}",
e
))
})?
.block_on(find_future)
})
.join()
});

let quorum = match thread_result {
Ok(Ok(q)) => q,
Ok(Err(e)) => {
debug!("Error finding quorum: {}", e);
return Err(ContextProviderError::Generic(format!(
"Failed to find quorum: {}",
e
)));
}
Err(_) => {
return Err(ContextProviderError::Generic(
"Quorum fetch thread panicked".to_string(),
));
}
};

// Parse the public key from the 'key' field
let pubkey_hex = quorum.key.trim_start_matches("0x");
Expand Down Expand Up @@ -816,6 +849,146 @@ impl ContextProvider for TrustedHttpContextProvider {
mod tests {
use super::*;

/// Regression test for https://github.com/dashpay/platform/issues/3432.
///
/// Verifies that `get_quorum_public_key` does **not** deadlock when called
/// from inside a tokio runtime on a cache miss.
///
/// ## Why current-thread runtime?
///
/// The deadlock only manifests on a **current-thread** (single-threaded)
/// runtime. On a multi-thread runtime, reqwest can complete I/O on a
/// different worker thread even when the calling thread is blocked inside
/// `futures::executor::block_on`, masking the bug.
///
/// On a current-thread runtime the single thread is both the executor and
/// the I/O driver. Calling `futures::executor::block_on` from within an
/// active tokio task parks that thread indefinitely, starving the I/O
/// driver → deadlock.
///
/// ## Test structure
///
/// The mock HTTP server is spun up on its own OS thread (outside the
/// current-thread runtime) so it is unaffected by what the runtime thread
/// does. `get_quorum_public_key` is called directly from an async context
/// running on the current-thread runtime. A channel with a 5-second
/// timeout detects whether the call completes or deadlocks.
#[test]
fn test_get_quorum_public_key_no_deadlock_inside_tokio_runtime() {
use std::time::Duration;

let pubkey_bytes = [0xABu8; 48];
let pubkey_hex = hex::encode(pubkey_bytes);
let quorum_hash: [u8; 32] = [0x01u8; 32];
let quorum_hash_hex = hex::encode(quorum_hash);

let current_json = serde_json::json!({
"success": true,
"data": [{
"quorum_hash": quorum_hash_hex,
"key": pubkey_hex,
"height": 1000u64,
"valid_members_count": 50u32,
}]
})
.to_string();

let previous_json = serde_json::json!({
"success": true,
"data": { "height": 999u64, "quorums": [] }
})
.to_string();

// Spin up the mock HTTP server on a dedicated OS thread (independent of
// the current-thread runtime below so it keeps responding while the
// runtime thread is blocked).
let server_rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();

let listener = server_rt
.block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
.unwrap();
let server_port = listener.local_addr().unwrap().port();

// Shutdown signal so the server thread exits after the test completes.
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();

let server_handle = std::thread::spawn(move || {
server_rt.block_on(async move {
// Handle connections sequentially — sufficient for a test mock.
loop {
tokio::select! {
accepted = listener.accept() => {
let Ok((mut stream, _)) = accepted else { break };
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
let mut reader = BufReader::new(&mut stream);
let mut request_line = String::new();
let _ = reader.read_line(&mut request_line).await;
let body = if request_line.contains("/quorums")
&& !request_line.contains("/previous")
{
current_json.clone()
} else {
previous_json.clone()
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(response.as_bytes()).await;
}
_ = &mut shutdown_rx => break,
}
}
});
});

let base_url = format!("http://127.0.0.1:{}", server_port);
let provider = TrustedHttpContextProvider::new_with_url(
Network::Testnet,
base_url,
NonZeroUsize::new(100).unwrap(),
)
.expect("provider creation should succeed");

// Channel used to receive the result with a timeout so a deadlock is
// caught as a test failure rather than a hang.
let (tx, rx) = std::sync::mpsc::channel::<Result<[u8; 48], ContextProviderError>>();

// Run get_quorum_public_key inside a current-thread tokio runtime.
// This is the exact scenario that deadlocked before the fix.
let client_handle = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
// We call the synchronous function directly from async context —
// this is what proof-verification code does in the FFI layer.
let result = rt.block_on(async move {
// Calling get_quorum_public_key here (sync fn, inside async).
// Before fix: futures::executor::block_on → deadlock.
// After fix: std::thread::scope + new runtime → no deadlock.
provider.get_quorum_public_key(1, quorum_hash, 0)
});
let _ = tx.send(result);
});

let result = rx
.recv_timeout(Duration::from_secs(5))
.expect("timed out after 5s — get_quorum_public_key likely deadlocked");

let key = result.expect("get_quorum_public_key should return Ok");
assert_eq!(key, pubkey_bytes);

// Clean up: signal server shutdown and join both threads.
let _ = shutdown_tx.send(());
server_handle.join().expect("server thread panicked");
client_handle.join().expect("client thread panicked");
}

#[test]
fn test_get_quorum_base_url() {
assert_eq!(
Expand Down
Loading
Loading