Skip to content

fix: replace deadlocking futures::executor::block_on with runtime-aware dash-async crate (#3432)#3497

Merged
QuantumExplorer merged 8 commits into
v3.1-devfrom
feat/rs-dash-async
Apr 23, 2026
Merged

fix: replace deadlocking futures::executor::block_on with runtime-aware dash-async crate (#3432)#3497
QuantumExplorer merged 8 commits into
v3.1-devfrom
feat/rs-dash-async

Conversation

@lklimek

@lklimek lklimek commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Fixes #3432futures::executor::block_on inside a tokio runtime in TrustedHttpContextProvider causes a deadlock when quorum cache misses trigger async HTTP fetches.

Root cause (from #3432)

ContextProvider::get_quorum_public_key() is synchronous, but on cache miss it calls futures::executor::block_on(self.find_quorum(...)) which makes HTTP requests via reqwest. Nesting futures::executor::block_on inside a tokio runtime deadlocks on single-threaded runtimes or panics on multi-threaded runtimes. This affects the iOS SDK and any FFI consumer using the trusted context provider.

What was done?

New crate: packages/rs-dash-async (dash-async)

Extracted a runtime-aware block_on function that correctly handles all three scenarios:

  • No active runtime → creates a temporary current-thread runtime
  • Current-thread runtime → spawns a dedicated OS thread with its own runtime (avoids block_in_place panic)
  • Multi-thread runtime → uses block_in_place + spawn for efficient bridging

Also includes:

rs-sdk-trusted-context-provider — the fix

dash-sdk::sync — backward compatible

  • Re-exports block_on and AsyncError from dash-async
  • All existing use dash_sdk::sync::block_on imports continue to work unchanged
  • retry function and its tests remain in dash-sdk

rs-context-provider

  • Moved From<AsyncError> for ContextProviderError impl here (orphan rule — AsyncError is no longer local to dash-sdk)

How Has This Been Tested?

  • cargo test -p dash-async — 2 tests pass:
  • cargo test -p dash-sdk --lib -- sync::test — 40 retry tests pass
  • cargo test -p rs-sdk-trusted-context-provider — 8 tests pass
  • cargo check succeeds for all affected crates

Breaking Changes

None. All public APIs are backward compatible via re-exports.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Added async-to-sync bridging functionality supporting multiple Tokio runtime configurations (no runtime, current-thread, multi-thread).
  • Improvements

    • Consolidated async-to-sync logic into a shared component to reduce code duplication across packages.
    • Enhanced error handling for async operations across the workspace.

lklimek and others added 2 commits April 15, 2026 14:58
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The initial extraction only kept the multi-thread block_in_place path.
Restore the 3-way handling: no runtime (create temporary), current-thread
(spawn OS thread), multi-thread (block_in_place). Also restore the
current-thread runtime regression test and add missing tokio sync feature
for dev-dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@lklimek has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 32 minutes and 35 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 32 minutes and 35 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75f3d53d-482a-40e6-9f28-d6ada0c09582

📥 Commits

Reviewing files that changed from the base of the PR and between 29e475b and 5787e41.

📒 Files selected for processing (2)
  • packages/rs-dash-async/Cargo.toml
  • packages/rs-sdk-trusted-context-provider/src/provider.rs
📝 Walkthrough

Walkthrough

A new dash-async crate is created to provide an async-to-sync bridge (block_on) that safely handles multiple Tokio runtime configurations. The functionality is integrated across the workspace, replacing futures::executor::block_on usage and fixing related deadlock issues while adding error conversion traits and making relevant types cloneable.

Changes

Cohort / File(s) Summary
New dash-async Crate
packages/rs-dash-async/Cargo.toml, packages/rs-dash-async/src/lib.rs, packages/rs-dash-async/src/block_on.rs
Introduces AsyncError enum and platform-aware block_on<F>() function. Non-WASM: detects runtime flavor and branches between no-runtime, current-thread (spawns dedicated thread), and multi-thread (uses block_in_place). WASM: stub returning error. Includes tests for nested async→sync→async and current-thread scenarios.
Workspace & Dependency Integration
Cargo.toml, packages/rs-context-provider/Cargo.toml, packages/rs-sdk/Cargo.toml, packages/rs-sdk-trusted-context-provider/Cargo.toml
Adds dash-async to workspace members and integrates as dependency in three crates. Removes futures dependency from rs-sdk-trusted-context-provider.
Error Handling
packages/rs-context-provider/src/error.rs
Adds From<dash_async::AsyncError> trait implementation converting async errors to ContextProviderError::AsyncError.
SDK Sync Module Refactoring
packages/rs-sdk/src/sync.rs
Removes local block_on/AsyncError definitions (~180 lines) and replaces with re-export: pub use dash_async::{block_on, AsyncError};. Removes nested runtime test; retains existing retry tests.
TrustedHttpContextProvider Refactoring
packages/rs-sdk-trusted-context-provider/src/provider.rs
Derives Clone on provider struct; changes fallback_provider from Box<dyn ContextProvider> to Arc<dyn ContextProvider>; extracts quorum key parsing into helper; replaces futures::executor::block_on (platform-specific) with dash_async::block_on; updates error handling to log and map failures from async refetch.
Test Workflow
.github/workflows/tests-rs-workspace.yml
Adds --package dash-async to macOS nextest coverage step.

Sequence Diagram

sequenceDiagram
    participant Caller as Sync Caller
    participant BlockOn as block_on()
    participant TokioRT as Tokio Runtime<br/>(Detector)
    participant NewRT as New RT<br/>(No-Runtime Path)
    participant Worker as OS Thread<br/>(CurrentThread Path)
    participant PlaceSpawn as block_in_place<br/>(MultiThread Path)
    
    Caller->>BlockOn: block_on(future)
    BlockOn->>TokioRT: Check current handle
    
    alt No Tokio Runtime
        TokioRT-->>BlockOn: try_current() → None
        BlockOn->>NewRT: Create new_current_thread
        NewRT->>NewRT: Run future to completion
        NewRT-->>BlockOn: Output
    else Tokio CurrentThread
        TokioRT-->>BlockOn: CurrentThread detected
        BlockOn->>Worker: Spawn dedicated OS thread
        Worker->>Worker: Create isolated<br/>current-thread RT
        Worker->>Worker: Await future
        Worker->>BlockOn: Send Output via MPSC
        BlockOn-->>BlockOn: Join thread
    else Tokio MultiThread
        TokioRT-->>BlockOn: MultiThread detected
        BlockOn->>PlaceSpawn: block_in_place + spawn
        PlaceSpawn->>PlaceSpawn: Await spawned future
        PlaceSpawn->>BlockOn: Receive Output via MPSC
        BlockOn-->>BlockOn: Check task completion
    end
    
    BlockOn-->>Caller: Result<Output, AsyncError>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A new async bridge spans the gap,
No more futures caught in tokio's trap!
With threads and channels dancing in place,
Deadlocks dissolve—let sync embrace.
The dash-async way saves the day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: replacing futures::executor::block_on with a new runtime-aware dash-async crate to fix deadlock issues.
Linked Issues check ✅ Passed The PR fully addresses issue #3432 by introducing a runtime-aware block_on that handles three scenarios (no runtime, current-thread, multi-thread), preventing deadlocks without changing the public sync API.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the deadlock: new dash-async crate, integration in affected crates, migration from futures::executor, tests, and CI workflow updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rs-dash-async

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 15, 2026
@lklimek lklimek changed the title feat: extract rs-dash-async crate from dash-sdk sync module fix: replace deadlocking futures::executor::block_on with runtime-aware dash-async crate (#3432) Apr 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/rs-dash-async/src/block_on.rs (2)

87-97: Minor inconsistency in channel type selection.

The current-thread path uses sync_channel::<...>(1) while the multi-thread path uses unbounded channel(). Both work correctly since only one message is sent, but using sync_channel(1) consistently would be slightly more explicit about the expected single-message behavior.

This is a very minor style observation and doesn't affect correctness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dash-async/src/block_on.rs` around lines 87 - 97, In block_on's
multi-thread branch (the match arm using handle.spawn(worker(fut, tx))), replace
the unbounded std::sync::mpsc::channel() with a bounded
std::sync::mpsc::sync_channel(1) to mirror the current-thread path and make the
single-message intent explicit; ensure you create tx/rx with the same generic
type as the other branch (e.g., sync_channel::<...>(1)) so the spawned
worker(fut, tx), rx.recv(), and the hdl handling remain type-compatible.

209-216: Test name is misleading - it tests success, not failure.

The test test_block_on_fails_on_current_thread_runtime actually verifies that block_on succeeds on a current-thread runtime (the fix for the bug). Consider renaming to something like test_block_on_succeeds_on_current_thread_runtime to match the assertion on line 259-263.

✏️ Suggested rename
-    fn test_block_on_fails_on_current_thread_runtime() {
+    fn test_block_on_succeeds_on_current_thread_runtime() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dash-async/src/block_on.rs` around lines 209 - 216, The test
function name test_block_on_fails_on_current_thread_runtime is misleading
because the body asserts success; rename the test function to
test_block_on_succeeds_on_current_thread_runtime and update any references (doc
comment or CI/test filters) accordingly so the name matches the assertion and
comment describing the regression fix in the surrounding block_on test code.
packages/rs-sdk-trusted-context-provider/src/provider.rs (2)

631-646: Consider extracting public key parsing into a helper method.

The public key parsing logic (hex decode, length validation, array conversion) is duplicated three times in get_quorum_public_key: once for the current cache hit (lines 568-584), once for the previous cache hit (lines 594-610), and here for the refetch path.

♻️ Proposed helper extraction
impl TrustedHttpContextProvider {
    /// Parse a BLS public key from hex string
    fn parse_quorum_public_key(key: &str) -> Result<[u8; 48], ContextProviderError> {
        let pubkey_hex = key.trim_start_matches("0x");
        let pubkey_bytes = hex::decode(pubkey_hex).map_err(|e| {
            ContextProviderError::Generic(format!("Invalid hex in public key: {}", e))
        })?;

        if pubkey_bytes.len() != 48 {
            return Err(ContextProviderError::Generic(format!(
                "Invalid public key length: {} bytes, expected 48",
                pubkey_bytes.len()
            )));
        }

        pubkey_bytes.try_into().map_err(|_| {
            ContextProviderError::Generic("Failed to convert public key to array".to_string())
        })
    }
}

Then each call site becomes:

return Self::parse_quorum_public_key(&quorum.key);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk-trusted-context-provider/src/provider.rs` around lines 631 -
646, Extract the duplicated public-key parsing logic into a new helper method on
TrustedHttpContextProvider (e.g., fn parse_quorum_public_key(key: &str) ->
Result<[u8;48], ContextProviderError>) that performs trim_start_matches("0x"),
hex::decode with ContextProviderError::Generic on decode errors, checks
pubkey_bytes.len() == 48 and converts to [u8;48] returning the same
ContextProviderError::Generic on conversion failure; then replace each
duplicated block in get_quorum_public_key with a call to
Self::parse_quorum_public_key(&quorum.key) to DRY up the code and preserve
existing error behavior.

622-629: Consider using the existing From<AsyncError> conversion.

The error from block_on is manually mapped to ContextProviderError::Generic, but there's already a From<dash_async::AsyncError> for ContextProviderError impl in rs-context-provider/src/error.rs that maps to the semantic AsyncError variant. Using the ? operator with the From impl would preserve the error categorization.

♻️ Proposed refactor to use From impl
         let this = self.clone();
         let quorum =
             dash_async::block_on(async move { this.find_quorum(quorum_type, quorum_hash).await })
-                .map_err(|e| ContextProviderError::Generic(format!("block_on failed: {}", e)))?
+                ?  // Uses From<AsyncError> for ContextProviderError
                 .map_err(|e| {
                     debug!("Error finding quorum: {}", e);
                     ContextProviderError::Generic(format!("Failed to find quorum: {}", e))
                 })?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk-trusted-context-provider/src/provider.rs` around lines 622 -
629, The block_on error is being manually mapped to
ContextProviderError::Generic, but there is a From<dash_async::AsyncError> ->
ContextProviderError impl; replace the outer map_err mapping with the ? operator
so the dash_async::block_on(...) error converts via From automatically.
Specifically, change the block that calls dash_async::block_on(async move {
this.find_quorum(quorum_type, quorum_hash).await }) .map_err(|e|
ContextProviderError::Generic(...))? to simply use ? on the block_on result
(i.e., dash_async::block_on(...)?), then keep the existing inner map_err that
handles the find_quorum application error and debugging log; reference symbols:
block_on, find_quorum, ContextProviderError, and dash_async::AsyncError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/rs-dash-async/src/block_on.rs`:
- Around line 87-97: In block_on's multi-thread branch (the match arm using
handle.spawn(worker(fut, tx))), replace the unbounded std::sync::mpsc::channel()
with a bounded std::sync::mpsc::sync_channel(1) to mirror the current-thread
path and make the single-message intent explicit; ensure you create tx/rx with
the same generic type as the other branch (e.g., sync_channel::<...>(1)) so the
spawned worker(fut, tx), rx.recv(), and the hdl handling remain type-compatible.
- Around line 209-216: The test function name
test_block_on_fails_on_current_thread_runtime is misleading because the body
asserts success; rename the test function to
test_block_on_succeeds_on_current_thread_runtime and update any references (doc
comment or CI/test filters) accordingly so the name matches the assertion and
comment describing the regression fix in the surrounding block_on test code.

In `@packages/rs-sdk-trusted-context-provider/src/provider.rs`:
- Around line 631-646: Extract the duplicated public-key parsing logic into a
new helper method on TrustedHttpContextProvider (e.g., fn
parse_quorum_public_key(key: &str) -> Result<[u8;48], ContextProviderError>)
that performs trim_start_matches("0x"), hex::decode with
ContextProviderError::Generic on decode errors, checks pubkey_bytes.len() == 48
and converts to [u8;48] returning the same ContextProviderError::Generic on
conversion failure; then replace each duplicated block in get_quorum_public_key
with a call to Self::parse_quorum_public_key(&quorum.key) to DRY up the code and
preserve existing error behavior.
- Around line 622-629: The block_on error is being manually mapped to
ContextProviderError::Generic, but there is a From<dash_async::AsyncError> ->
ContextProviderError impl; replace the outer map_err mapping with the ? operator
so the dash_async::block_on(...) error converts via From automatically.
Specifically, change the block that calls dash_async::block_on(async move {
this.find_quorum(quorum_type, quorum_hash).await }) .map_err(|e|
ContextProviderError::Generic(...))? to simply use ? on the block_on result
(i.e., dash_async::block_on(...)?), then keep the existing inner map_err that
handles the find_quorum application error and debugging log; reference symbols:
block_on, find_quorum, ContextProviderError, and dash_async::AsyncError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56379006-6e11-4e56-a33c-7609591618e8

📥 Commits

Reviewing files that changed from the base of the PR and between 85a7117 and eec32ff.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • packages/rs-context-provider/Cargo.toml
  • packages/rs-context-provider/src/error.rs
  • packages/rs-dash-async/Cargo.toml
  • packages/rs-dash-async/src/block_on.rs
  • packages/rs-dash-async/src/lib.rs
  • packages/rs-sdk-trusted-context-provider/Cargo.toml
  • packages/rs-sdk-trusted-context-provider/src/provider.rs
  • packages/rs-sdk/Cargo.toml
  • packages/rs-sdk/src/sync.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the deadlock caused by using futures::executor::block_on inside a Tokio runtime by introducing a runtime-aware async→sync bridging implementation (dash-async) and migrating the trusted context provider to use it.

Changes:

  • Added new dash-async crate providing a runtime-flavor-aware block_on + AsyncError (including a WASM stub and regression tests for #3432).
  • Updated TrustedHttpContextProvider to use dash_async::block_on (and made it Clone by switching fallback_provider to Arc).
  • Updated dash-sdk::sync to re-export block_on/AsyncError, and moved From<AsyncError> conversion to rs-context-provider.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/rs-sdk/src/sync.rs Re-exports dash-async bridge types; removes local implementation/tests.
packages/rs-sdk/Cargo.toml Adds dash-async dependency.
packages/rs-sdk-trusted-context-provider/src/provider.rs Replaces futures::executor::block_on with dash_async::block_on; makes provider clonable via Arc fallback.
packages/rs-sdk-trusted-context-provider/Cargo.toml Adds dash-async, removes futures.
packages/rs-dash-async/src/lib.rs New crate entry point exporting block_on + AsyncError.
packages/rs-dash-async/src/block_on.rs Runtime-aware block_on implementation + tests (incl. #3432 regression).
packages/rs-dash-async/Cargo.toml New crate manifest and dependencies.
packages/rs-context-provider/src/error.rs Adds From<dash_async::AsyncError> to map to ContextProviderError::AsyncError.
packages/rs-context-provider/Cargo.toml Adds dash-async dependency.
Cargo.toml Adds rs-dash-async to workspace members.
Cargo.lock Records new crate and dependency graph updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/rs-dash-async/src/block_on.rs Outdated
Comment thread packages/rs-dash-async/src/block_on.rs Outdated
Comment thread packages/rs-sdk-trusted-context-provider/src/provider.rs Outdated
@lklimek
lklimek marked this pull request as ready for review April 15, 2026 14:41
@thepastaclaw

thepastaclaw commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 5787e41)

@codecov

codecov Bot commented Apr 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.61151% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.83%. Comparing base (bc21822) to head (5787e41).
⚠️ Report is 13 commits behind head on v3.1-dev.

Files with missing lines Patch % Lines
packages/rs-dash-async/src/block_on.rs 85.61% 20 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3497      +/-   ##
============================================
- Coverage     84.84%   84.83%   -0.01%     
============================================
  Files          2476     2477       +1     
  Lines        267915   267872      -43     
============================================
- Hits         227303   227242      -61     
- Misses        40612    40630      +18     
Components Coverage Δ
dpp 81.99% <ø> (-0.01%) ⬇️
drive 84.21% <ø> (ø)
drive-abci 87.46% <ø> (-0.03%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.10% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 55.66% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "c5852c74f5dd9cc0e525ee97e185b055e059fb14b31eb9c5982464a5925a1968"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

lklimek and others added 4 commits April 20, 2026 14:12
- dash-async: always join worker thread even when rx.recv() fails, so
  panics aren't swallowed and we don't leak detached threads
- dash-async: rename test_block_on_fails_on_current_thread_runtime to
  test_block_on_succeeds_on_current_thread_runtime to match its assertion
- dash-async: use bounded sync_channel(1) in the multi-thread branch for
  consistency with the current-thread branch (only one message flows)
- trusted-context-provider: rely on From<AsyncError> for ContextProviderError
  via ? rather than wrapping in Generic, so async-bridging failures keep
  their semantic AsyncError variant
- trusted-context-provider: extract parse_quorum_public_key helper to
  DRY up three duplicate "hex-decode BLS key" blocks in get_quorum_public_key

Addresses copilot-pull-request-reviewer and coderabbit nits on PR #3497.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Rust workspace coverage job enumerates packages explicitly for
`cargo llvm-cov nextest`, and the new `dash-async` crate was missing.
As a result the multi-thread `block_on` branch and its `worker` fn were
only reached transitively (without running the crate's own tests),
dropping patch coverage on packages/rs-dash-async/src/block_on.rs to
46.77% and failing codecov/patch on PR #3497.

Add `--package dash-async` so its tests run under coverage and the
existing `test_block_on_nested_async_sync` / current-thread tests
cover the full matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WASM stub never drives the future (always returns
AsyncError::Generic), so the native `F: Future + Send + 'static` /
`F::Output: Send` bounds were purely a compile-time obstacle: WASM is
single-threaded, and WASM-only async types — notably `JsFuture` (which
wraps `Rc<RefCell<..>>`) and reqwest's `wasm::Response` — are never
`Send`. Dropping the bounds lets legitimate callers like
`rs-sdk-trusted-context-provider::provider::get_quorum_public_key`
compile on wasm32. The native `block_on` keeps its full bound set.

Fixes "Build JS packages / Build JS" on PR #3497 where rs-sdk-trusted-
context-provider failed with E0277 "Rc<RefCell<..Inner>> cannot be sent
between threads safely" at provider.rs:610.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
.github/workflows/tests-rs-workspace.yml (1)

152-152: Consider mirroring --package dash-async in the Ubuntu backup job for parity.

The macOS nextest run now includes dash-async, but the Ubuntu fallback test-ubuntu job (lines 312–335) still uses an explicit package list that omits it. When the Mac runner is unavailable and the Ubuntu backup kicks in, dash-async tests — including the new current-thread deadlock regression for issue #3432 — will silently not run. Adding --package dash-async to the Ubuntu list would keep both paths in lockstep.

🔧 Suggested change to test-ubuntu
             --package dash-sdk \
+            --package dash-async \
             --package platform-value \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests-rs-workspace.yml at line 152, The Ubuntu backup job
test-ubuntu is missing the dash-async package that the macOS nextest run
includes; update the test command in the test-ubuntu job to include the same
--package dash-async flag so the Ubuntu fallback runs the same suite (including
the current-thread deadlock regression for issue `#3432`) and keep both jobs in
parity with the macOS nextest invocation.
packages/rs-dash-async/src/block_on.rs (1)

64-105: LGTM on the three-branch runtime-aware bridge.

The flavor detection and the separate strategy per scenario (temp runtime / dedicated OS thread / block_in_place+spawn) is sound, and the regression test for the current-thread case exercises the right path. One small observability nit in the current-thread branch: when the worker thread panics, the join() error payload is dropped and the caller only sees the generic "block_on worker thread panicked" string. If the root cause is, for example, a panic inside the user future, that message is lost and makes debugging harder. Consider extracting the panic message before discarding, e.g.:

🔧 Preserve panic message from the worker thread
-            match (join_result, recv_result) {
-                (Err(_), _) => Err(AsyncError::Generic(
-                    "block_on worker thread panicked".to_string(),
-                )),
+            match (join_result, recv_result) {
+                (Err(payload), _) => {
+                    let msg = payload
+                        .downcast_ref::<&'static str>()
+                        .map(|s| (*s).to_string())
+                        .or_else(|| payload.downcast_ref::<String>().cloned())
+                        .unwrap_or_else(|| "unknown panic payload".to_string());
+                    Err(AsyncError::Generic(format!(
+                        "block_on worker thread panicked: {msg}"
+                    )))
+                }
                 (Ok(()), Err(_)) => Err(AsyncError::Generic(
                     "block_on worker exited without sending a result".to_string(),
                 )),
                 (Ok(()), Ok(result)) => result,
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-dash-async/src/block_on.rs` around lines 64 - 105, The
current-thread branch of block_on drops the panic payload from
join_handle.join() and returns a generic AsyncError; modify the join_result
handling in block_on to extract the panic payload from the Err(Box<dyn Any +
Send>) returned by join_handle.join(), convert it to a readable string (try
downcasting to &str and String, falling back to a debug format), and include
that extracted message in the AsyncError::Generic returned when (join_result, _)
is (Err(_), _); keep the rest of the match logic unchanged and reference
join_handle.join(), join_result, and AsyncError::Generic to locate the changes.
packages/rs-sdk-trusted-context-provider/src/provider.rs (1)

608-616: block_on bridge wiring is correct; minor error-context nit.

The double-? layering (outer converts AsyncError via From, inner map_err wraps TrustedContextProviderError into Generic) is correct and the self.clone() gives you the required 'static future cheaply via the Arc fields. One small thing: debug!("Error finding quorum: {}", e) followed by wrapping into Generic(format!("Failed to find quorum: {}", e)) loses the structured error type at the boundary — if TrustedContextProviderError has more specific variants (e.g. QuorumNotFound, NetworkError) that callers might want to distinguish, consider either mapping them to the corresponding ContextProviderError variants or adding a #[from]-based conversion in TrustedContextProviderError → ContextProviderError. Not blocking; current behavior matches the PR scope.

🔧 Optional: preserve source chain via error-kind mapping
-        let this = self.clone();
-        let quorum =
-            dash_async::block_on(async move { this.find_quorum(quorum_type, quorum_hash).await })?
-                .map_err(|e| {
-                    debug!("Error finding quorum: {}", e);
-                    ContextProviderError::Generic(format!("Failed to find quorum: {}", e))
-                })?;
+        let this = self.clone();
+        let quorum =
+            dash_async::block_on(async move { this.find_quorum(quorum_type, quorum_hash).await })?
+                .map_err(|e| match e {
+                    TrustedContextProviderError::QuorumNotFound { quorum_hash, .. } => {
+                        ContextProviderError::InvalidQuorum(format!(
+                            "Quorum not found for hash: {quorum_hash}"
+                        ))
+                    }
+                    other => {
+                        debug!("Error finding quorum: {other}");
+                        ContextProviderError::Generic(format!("Failed to find quorum: {other}"))
+                    }
+                })?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk-trusted-context-provider/src/provider.rs` around lines 608 -
616, The debug log and Generic wrapping around the inner
TrustedContextProviderError loses the original error variants; update the
handling in the dash_async::block_on(...) call around
TrustedContextProvider::find_quorum to preserve the error kind by either (A)
matching/map_err on the TrustedContextProviderError returned by find_quorum and
convert each variant to a corresponding ContextProviderError variant (e.g., map
QuorumNotFound → ContextProviderError::QuorumNotFound, NetworkError →
ContextProviderError::NetworkError), or (B) implement
From<TrustedContextProviderError> for ContextProviderError (or add #[from] on
ContextProviderError) and then remove the manual Generic wrapping so you can use
the ? operator directly; locate the logic around self.clone(), find_quorum, and
Self::parse_quorum_public_key(&quorum.key) and apply one of these two fixes so
the specific error type is preserved instead of being hidden in Generic while
keeping the existing debug logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/rs-dash-async/src/block_on.rs`:
- Around line 121-133: The block_on stub on WASM (pub fn block_on) always
returns Err and needs clearer public documentation to prevent misuse; update the
block_on function doc comment to state explicitly that it is unsupported on
wasm32 (always returns AsyncError::Generic) and callers must use async APIs
(e.g., #[wasm_bindgen] async functions) instead, and add a prominent note in the
rs-dash-async crate-level docs (lib.rs or README) explaining the JSPI limitation
and the intended safety pattern (prefetchMainnet()/other prefetch factories +
refetch_if_not_found(false) leading to InvalidQuorum on cache misses) so
consumers understand why the stub exists and when they must avoid calling
block_on.

---

Nitpick comments:
In @.github/workflows/tests-rs-workspace.yml:
- Line 152: The Ubuntu backup job test-ubuntu is missing the dash-async package
that the macOS nextest run includes; update the test command in the test-ubuntu
job to include the same --package dash-async flag so the Ubuntu fallback runs
the same suite (including the current-thread deadlock regression for issue
`#3432`) and keep both jobs in parity with the macOS nextest invocation.

In `@packages/rs-dash-async/src/block_on.rs`:
- Around line 64-105: The current-thread branch of block_on drops the panic
payload from join_handle.join() and returns a generic AsyncError; modify the
join_result handling in block_on to extract the panic payload from the
Err(Box<dyn Any + Send>) returned by join_handle.join(), convert it to a
readable string (try downcasting to &str and String, falling back to a debug
format), and include that extracted message in the AsyncError::Generic returned
when (join_result, _) is (Err(_), _); keep the rest of the match logic unchanged
and reference join_handle.join(), join_result, and AsyncError::Generic to locate
the changes.

In `@packages/rs-sdk-trusted-context-provider/src/provider.rs`:
- Around line 608-616: The debug log and Generic wrapping around the inner
TrustedContextProviderError loses the original error variants; update the
handling in the dash_async::block_on(...) call around
TrustedContextProvider::find_quorum to preserve the error kind by either (A)
matching/map_err on the TrustedContextProviderError returned by find_quorum and
convert each variant to a corresponding ContextProviderError variant (e.g., map
QuorumNotFound → ContextProviderError::QuorumNotFound, NetworkError →
ContextProviderError::NetworkError), or (B) implement
From<TrustedContextProviderError> for ContextProviderError (or add #[from] on
ContextProviderError) and then remove the manual Generic wrapping so you can use
the ? operator directly; locate the logic around self.clone(), find_quorum, and
Self::parse_quorum_public_key(&quorum.key) and apply one of these two fixes so
the specific error type is preserved instead of being hidden in Generic while
keeping the existing debug logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d86f0442-99a9-4b3f-afec-3631279cba61

📥 Commits

Reviewing files that changed from the base of the PR and between eec32ff and 29e475b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .github/workflows/tests-rs-workspace.yml
  • packages/rs-dash-async/src/block_on.rs
  • packages/rs-sdk-trusted-context-provider/src/provider.rs

Comment thread packages/rs-dash-async/src/block_on.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/rs-sdk-trusted-context-provider/src/provider.rs Outdated
Comment thread packages/rs-dash-async/Cargo.toml Outdated
lklimek and others added 2 commits April 20, 2026 15:40
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@lklimek lklimek added the ready for final review Ready for the final review. If AI was involved in producing this PR, it has already had a reviewer. label Apr 20, 2026
@QuantumExplorer
QuantumExplorer merged commit e0867b5 into v3.1-dev Apr 23, 2026
36 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/rs-dash-async branch April 23, 2026 05:24

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The assigned detached HEAD 5787e41fad1f5ee835b138171ca06c27423d6507 changes only parse_quorum_public_key() in packages/rs-sdk-trusted-context-provider/src/provider.rs, replacing lowercase-only prefix stripping with explicit 0x/0X handling. That code change is correct, but there is no focused test covering the new uppercase 0X path, so this regression fix can be lost silently in a later refactor.

Reviewed commit: 5787e41

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-sdk-trusted-context-provider/src/provider.rs`:
- [SUGGESTION] lines 552-556: Add a regression test for uppercase `0X` quorum key prefixes
  This commit broadens `parse_quorum_public_key()` to accept uppercase `0X` as well as lowercase `0x`, but the module has no unit test that exercises the new branch. Because the helper is private and reached indirectly through cache and network code, a future cleanup could revert to lowercase-only handling without any test failure. Add a focused test that asserts both prefix casings decode to the same `[u8;48]` value.

Comment on lines +552 to +556
fn parse_quorum_public_key(key: &str) -> Result<[u8; 48], ContextProviderError> {
let pubkey_hex = key
.strip_prefix("0x")
.or_else(|| key.strip_prefix("0X"))
.unwrap_or(key);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Add a regression test for uppercase 0X quorum key prefixes

This commit broadens parse_quorum_public_key() to accept uppercase 0X as well as lowercase 0x, but the module has no unit test that exercises the new branch. Because the helper is private and reached indirectly through cache and network code, a future cleanup could revert to lowercase-only handling without any test failure. Add a focused test that asserts both prefix casings decode to the same [u8;48] value.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-sdk-trusted-context-provider/src/provider.rs`:
- [SUGGESTION] lines 552-556: Add a regression test for uppercase `0X` quorum key prefixes
  This commit broadens `parse_quorum_public_key()` to accept uppercase `0X` as well as lowercase `0x`, but the module has no unit test that exercises the new branch. Because the helper is private and reached indirectly through cache and network code, a future cleanup could revert to lowercase-only handling without any test failure. Add a focused test that asserts both prefix casings decode to the same `[u8;48]` value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for final review Ready for the final review. If AI was involved in producing this PR, it has already had a reviewer.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deadlock: futures::executor::block_on inside tokio runtime in TrustedHttpContextProvider

4 participants