Skip to content

bug(rs-sdk): Send is not general enough after fetch_request inlining in d820fe244 #3361

@lklimek

Description

@lklimek

Summary

Commit 037c387c8c763accb9d1a9d225c7a9ee9062ca7c (feat(rs-sdk): add shielded pool SDK support (#3230)) introduced a Send lifetime regression that breaks downstream consumers who tokio::spawn futures that call Fetch::fetch() or FetchMany::fetch_many().

Binary-search proof (tested against dash-evo-tool):

Commit Result
0d10ecd4 (parent of 037c387c8) ✅ Compiles
037c387c8 (feat(rs-sdk): add shielded pool SDK support) ❌ Send error

Error

error: implementation of `std::marker::Send` is not general enough
    = note: `std::marker::Send` would have to be implemented for the type `&dash_sdk::platform::DataContract`
    = note: ...but `std::marker::Send` is actually implemented for the type `&'0 dash_sdk::platform::DataContract`, for some specific lifetime `'0`

error: implementation of `std::marker::Send` is not general enough
    = note: `std::marker::Send` would have to be implemented for the type `&Sdk`
    = note: ...but `std::marker::Send` is actually implemented for the type `&'0 Sdk`, for some specific lifetime `'0`

These errors occur on any tokio::spawn(async move { sdk_call().await }) that transitively calls Fetch::fetch_with_metadata_and_proof or FetchMany::fetch_many_with_metadata_and_proof.

Root Cause

The shielded pool SDK commit added new Fetch/FetchMany impls and new FromProof implementations. The additional type complexity in the trait impls appears to have tipped the Rust compiler's async Send inference over the edge.

The underlying fragility was introduced by d820fe244 (revert(sdk): deserialization error due to outdated contract cache (#3114)), which removed the standalone fetch_request() / fetch_many_request() helper functions and inlined their retry logic into the #[async_trait] default trait methods. This worked for the simpler type set at the time, but became unprovable once the shielded pool types were added.

Before d820fe2 (robust):

// Standalone function with explicit bounds — compiler analyzes Send independently
async fn fetch_request<O, R>(
    sdk: &Sdk,
    request: &R,
    settings: Option<RequestSettings>,
) -> Result<(Option<O>, ResponseMetadata, Proof), Error>
where
    O: Sized + Send + Debug + MockResponse + FromProof<R, ...>,
    R: TransportRequest + Clone + Debug,
{ /* closure + retry */ }

After d820fe2 (fragile, broke at 037c387):

// Inlined into #[async_trait] default method — compiler must prove Send
// for the entire future state machine at once
async fn fetch_with_metadata_and_proof<Q: Query<...>>(...) {
    let request = &query.query(sdk.prove())?;
    let fut = |settings| async move {
        // captures &sdk and &request
        sdk.parse_proof_with_metadata_and_proof(request.clone(), response).await
    };
    retry(sdk.address_list(), settings, fut).await.into_inner()
}

The standalone function acted as an opaque boundary for the compiler's Send analysis. When inlined, the compiler must reason about the entire #[async_trait] desugared future, including the where Self: Sized + 'a bound from FromProof::maybe_from_proof_with_metadata. This creates a higher-ranked lifetime relationship that the compiler cannot prove is Send for all lifetimes — producing the "not general enough" error.

Suggested Fix

Restore the standalone helper functions that separate the Send-provable unit:

async fn fetch_request<O, R>(sdk: &Sdk, request: &R, settings: Option<RequestSettings>) -> ...
where
    O: Sized + Send + Debug + MockResponse + FromProof<R, ...>,
    R: TransportRequest + Clone + Debug,
{ /* retry logic here */ }

Alternative: box the inner future to create an opaque boundary (Box::pin(async { ... }).await).

Reproduction

Any downstream crate that does:

tokio::spawn(async move {
    let result = SomeType::fetch(&sdk, query, None).await;
});

Will fail to compile with the Send not general enough error.

Affected Commit Range

  • Fragility introduced: d820fe244 (inlined fetch_request)
  • Actually broke: 037c387c8 (added shielded pool types)
  • Last known working: 0d10ecd4fa35a72cb8a69c3e6c6c395db370c183 (parent of 037c387)

🤖 Co-authored by Claudius the Magnificent AI Agent

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions