-
Notifications
You must be signed in to change notification settings - Fork 50
Description
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(inlinedfetch_request) - Actually broke:
037c387c8(added shielded pool types) - Last known working:
0d10ecd4fa35a72cb8a69c3e6c6c395db370c183(parent of 037c387)
🤖 Co-authored by Claudius the Magnificent AI Agent