Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f4703d6
feat(swift-sdk): use SPV-synced quorums for Platform proof verification
QuantumExplorer Mar 31, 2026
ad6904a
refactor(rs-sdk-ffi): move SPV context provider from Swift to Rust
QuantumExplorer Mar 31, 2026
d9e478e
feat(platform-wallet): add pure-Rust SpvContextProvider
QuantumExplorer Mar 31, 2026
8d2bb27
fix: address CodeRabbit review feedback on SPV quorums PR
QuantumExplorer Apr 2, 2026
fa7e531
feat: use pure-Rust SpvContextProvider, bump rust-dashcore
QuantumExplorer Apr 2, 2026
54b6ed0
fix: eliminate race between didSet and handleNetworkSwitch
QuantumExplorer Apr 2, 2026
cfde224
fix: use try_read() instead of blocking_read(), add dep: prefix
QuantumExplorer Apr 2, 2026
6f67870
Merge v4.1-dev into feat/ios-spv-quorums; re-integrate SPV context pr…
shumkov Jul 11, 2026
05fd41d
style(platform-wallet-ffi): apply rustfmt; drop internal re-integrati…
shumkov Jul 11, 2026
3fad6f8
feat(swift-sdk): verify proofs against SPV quorums via attach-after-b…
shumkov Jul 13, 2026
600b440
feat(sdk): share context provider across SDK clones; live Auto/SPV/Tr…
shumkov Jul 13, 2026
9a2289a
test(sdk,rs-sdk-ffi): cover context-provider clone-sharing + install/…
shumkov Jul 13, 2026
584e68c
fix(rs-platform-wallet): reverse quorum hash to internal order for SP…
shumkov Jul 14, 2026
1deb36b
fix(rs-platform-wallet): guard byte-order via production helper + pin…
shumkov Jul 14, 2026
8263be0
fix(rs-sdk-ffi): install SPV quorum provider as a composite over trusted
shumkov Jul 14, 2026
a53b983
fix(swift-sdk): force SPV install in SPV mode instead of gating on ru…
shumkov Jul 14, 2026
a0fa683
fix(rs-platform-wallet): avoid block_in_place panic on a current-thre…
shumkov Jul 15, 2026
6fc782f
fix(rs-platform-wallet): fail closed on current-thread runtime, not a…
shumkov Jul 15, 2026
3a5e96e
refactor(sdk): add adaptive SPV context provider
shumkov Jul 15, 2026
ba2fbef
docs: remove adaptive SPV provider spec
shumkov Jul 15, 2026
d4046ca
refactor(sdk): separate adaptive and SPV providers
shumkov Jul 16, 2026
4961a0e
chore(sdk): remove unused HTTP dependency
shumkov Jul 16, 2026
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
23 changes: 12 additions & 11 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion packages/rs-platform-wallet-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description = "C FFI bindings for platform-wallet"
crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
platform-wallet = { path = "../rs-platform-wallet" }
platform-wallet = { path = "../rs-platform-wallet", features = ["spv-context"] }
dpp = { path = "../rs-dpp" }
dash-sdk = { path = "../rs-sdk", features = ["wallet"] }
# Needed for `SignerHandle` + `VTableSigner` so the `*_with_signer`
Expand Down
46 changes: 46 additions & 0 deletions packages/rs-platform-wallet-ffi/src/spv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,49 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_clear_storage(
unwrap_result_or_return!(result);
PlatformWalletFFIResult::ok()
}

/// Install this wallet manager's SPV-synced quorum data as the proof-verification
/// context provider on an already-built Platform SDK, replacing its trusted
/// provider. Subsequent proof-verified queries on that SDK handle resolve quorum
/// public keys live from the locally synced masternode list.
///
/// Call this only once the manager's SPV client is started and its masternode
/// list is synced (see `platform_wallet_manager_sync_progress`); before that,
/// lookups fail closed (no per-lookup fallback once installed).
///
/// `dash_sdk::Sdk` shares its context-provider slot across clones, so this
/// single install also switches the manager's own cloned SDK (used for wallet /
/// DashPay sync) to SPV — both the app's queries and the manager's background
/// sync verify against the same SPV-synced quorum data.
///
/// # Safety
/// - `manager_handle` must be a live `PlatformWalletManager` handle.
/// - `sdk_handle` must be a valid `SDKHandle` for the duration of the call.
#[no_mangle]
pub unsafe extern "C" fn platform_wallet_manager_attach_spv_context(
manager_handle: Handle,
sdk_handle: *mut rs_sdk_ffi::SDKHandle,
network: crate::types::FFINetwork,
) -> rs_sdk_ffi::DashSDKResult {
// A shared handle to the manager's SPV runtime — the same runtime the SPV
// client writes to during sync. Cloned out of the storage closure so it
// outlives the borrow and backs the provider for as long as it's installed.
let spv = match PLATFORM_WALLET_MANAGER_STORAGE.with_item(manager_handle, |m| m.spv_arc()) {
Some(spv) => spv,
None => {
return rs_sdk_ffi::DashSDKResult::error(rs_sdk_ffi::DashSDKError::new(
rs_sdk_ffi::DashSDKErrorCode::InvalidParameter,
"Invalid wallet manager handle".to_string(),
));
}
};

let network: crate::types::Network = network.into();
let provider = platform_wallet::spv_context_provider::SpvContextProvider::new(spv, network);
let handle = Box::into_raw(Box::new(rs_sdk_ffi::ContextProviderWrapper::new(provider)))
as *mut rs_sdk_ffi::ContextProviderHandle;

// `dash_sdk_install_context_provider` TAKES ownership of the wrapper box and
// reclaims it exactly once — this function must not reclaim it.
rs_sdk_ffi::dash_sdk_install_context_provider(sdk_handle, handle)
Comment thread
shumkov marked this conversation as resolved.
Outdated
Comment thread
shumkov marked this conversation as resolved.
Outdated
Comment thread
shumkov marked this conversation as resolved.
Outdated
}
10 changes: 10 additions & 0 deletions packages/rs-platform-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ dash-spv = { workspace = true }
# Core dependencies
dashcore = { workspace = true }

# SPV context provider dependency (optional). `dash-spv` and `tokio` are
# already non-optional dependencies above, so the `spv-context` feature only
# needs to pull in the context-provider trait crate.
dash-context-provider = { path = "../rs-context-provider", optional = true }

# Standard dependencies
thiserror = "1.0"
async-trait = "0.1"
Expand Down Expand Up @@ -112,6 +117,11 @@ rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider"

[features]
default = ["bls", "eddsa"]
# SPV-backed Platform context provider (delegates quorum lookups to the SPV
# runtime). `dash-spv`/`tokio` are already non-optional deps; this pulls in the
# context-provider trait crate plus `rt-multi-thread` (the sync->async bridge
# in `spv_context_provider` uses `tokio::task::block_in_place`).
spv-context = ["dep:dash-context-provider", "tokio/rt-multi-thread"]
bls = ["key-wallet/bls", "key-wallet-manager/bls"]
eddsa = ["key-wallet/eddsa", "key-wallet-manager/eddsa"]
shielded = ["dep:grovedb-commitment-tree", "dep:rusqlite", "dep:zip32", "dep:futures", "dash-sdk/shielded", "dpp/shielded-client"]
Expand Down
3 changes: 3 additions & 0 deletions packages/rs-platform-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub(crate) mod test_support;
mod util;
pub mod wallet;

#[cfg(feature = "spv-context")]
pub mod spv_context_provider;

pub use error::PlatformWalletError;
pub use events::{PlatformEventHandler, PlatformEventManager};
pub use key_wallet::wallet::managed_wallet_info::asset_lock_builder::AssetLockFundingType;
Expand Down
71 changes: 69 additions & 2 deletions packages/rs-platform-wallet/src/spv/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ use crate::wallet::platform_wallet::PlatformWalletInfo;
type SpvClient =
DashSpvClient<WalletManager<PlatformWalletInfo>, PeerNetworkManager, DiskStorageManager>;

/// Build the masternode-engine lookup key from a proof-supplied quorum hash.
///
/// The SDK's proof verifier supplies the quorum hash in display (big-endian)
/// byte order — the same order the trusted HTTP provider matches against — but
/// the masternode engine keys its quorum map in internal (little-endian) order.
/// The bytes must therefore be reversed before building the [`QuorumHash`] key;
/// without this every real quorum misses. Verified against a synced testnet
/// node: the engine stores the reversed form of each requested hash (e.g.
/// requested `0000…7f`, stored `…000000`).
///
/// [`SpvRuntime::get_quorum_public_key`] calls this so the byte-order regression
/// test exercises the exact transform the production lookup uses.
fn quorum_lookup_key(display_order: [u8; 32]) -> QuorumHash {
let mut internal_order = display_order;
internal_order.reverse();
QuorumHash::from_byte_array(internal_order)
}

/// SPV client runtime — owns the `DashSpvClient` and drives sync.
///
/// Events are dispatched through [`PlatformEventManager`] to all registered
Expand Down Expand Up @@ -165,7 +183,7 @@ impl SpvRuntime {
))?;

let llmq_type = LLMQType::from(quorum_type as u8);
let qh = QuorumHash::from_byte_array(quorum_hash).reverse();
let qh = quorum_lookup_key(quorum_hash);

let quorum = client
.get_quorum_at_height(height, llmq_type, qh)
Expand Down Expand Up @@ -393,7 +411,7 @@ mod tests {
use key_wallet_manager::WalletManager;
use tokio::sync::RwLock;

use super::{classify_spv_broadcast_error, SpvRuntime};
use super::{classify_spv_broadcast_error, quorum_lookup_key, SpvRuntime};
use crate::broadcaster::BroadcastError;
use crate::events::PlatformEventManager;
use crate::wallet::platform_wallet::PlatformWalletInfo;
Expand Down Expand Up @@ -460,4 +478,53 @@ mod tests {
);
}
}

/// Regression guard for the quorum-hash byte order used by
/// [`SpvRuntime::get_quorum_public_key`].
///
/// This exercises the production transform directly — [`quorum_lookup_key`]
/// is the exact function `get_quorum_public_key` calls to build its engine
/// lookup key — so dropping (or re-introducing a spurious) reversal there
/// fails this test, rather than the test asserting standalone `BTreeMap`
/// semantics that can't detect a change in the real code.
///
/// The masternode engine keys its quorum map — `quorum_entry_of_type_for_quorum_hash`,
/// a `BTreeMap<QuorumHash, _>::get` — in internal byte order, but the SDK
/// proof verifier supplies the hash in display (reversed) order. Verified
/// end-to-end against a synced testnet node: the engine stores the reversed
/// form of each requested hash (requested `0000…`, stored `…0000`), so a
/// non-reversed lookup misses every real quorum and falls through to
/// fail-closed rejection (previously masked by the trusted-quorum fallback).
#[test]
fn quorum_hash_reversed_to_internal_order_before_lookup() {
use dashcore::hashes::Hash;
use dashcore::QuorumHash;
use std::collections::BTreeMap;

// The hash as the proof verifier supplies it (display order).
let display_bytes: [u8; 32] = std::array::from_fn(|i| (i as u8) + 1);
// The engine keys the quorum under the internal (reversed) order.
let mut internal_bytes = display_bytes;
internal_bytes.reverse();

let pubkey = [0xABu8; 48];
let mut quorums: BTreeMap<QuorumHash, [u8; 48]> = BTreeMap::new();
quorums.insert(QuorumHash::from_byte_array(internal_bytes), pubkey);

// The production key builder must land on the internally-keyed quorum.
assert_eq!(
quorums.get(&quorum_lookup_key(display_bytes)),
Some(&pubkey),
"quorum_lookup_key must reverse display order to the engine's internal key"
);

// Sanity: the un-reversed display-order key is absent — this is exactly
// the miss that `quorum_lookup_key`'s reversal exists to prevent, so if
// the reversal is removed the assertion above fails.
assert_eq!(
quorums.get(&QuorumHash::from_byte_array(display_bytes)),
None,
"using the hash without reversal must miss — this was the regression"
);
}
}
Loading
Loading