Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
69 changes: 69 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,72 @@ pub unsafe extern "C" fn platform_wallet_manager_spv_clear_storage(
unwrap_result_or_return!(result);
PlatformWalletFFIResult::ok()
}

/// Create a Platform SDK that verifies proofs using this wallet manager's
/// SPV-synced quorum data instead of a trusted HTTP quorum service.
///
/// The returned SDK holds a shared reference to the manager's SPV runtime and
/// resolves quorum public keys live from the locally synced masternode list.
/// The manager's SPV client should be started (and ideally synced) for lookups
/// to succeed; otherwise proof verification fails closed until it catches up.
///
/// # Safety
/// - `config` must be a valid pointer to a `DashSDKConfig` for the call.
/// - `manager_handle` must be a live `PlatformWalletManager` handle.
#[no_mangle]
pub unsafe extern "C" fn platform_wallet_manager_create_sdk_with_spv_context(
manager_handle: Handle,
config: *const rs_sdk_ffi::DashSDKConfig,
) -> rs_sdk_ffi::DashSDKResult {
if config.is_null() {
return rs_sdk_ffi::DashSDKResult::error(rs_sdk_ffi::DashSDKError::new(
rs_sdk_ffi::DashSDKErrorCode::InvalidParameter,
"Config is null".to_string(),
));
}
let config_ref = &*config;

// 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 the SDK's lifetime.
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 = config_ref.network.into();

let provider = platform_wallet::spv_context_provider::SpvContextProvider::new(spv, network);
let wrapper = Box::new(rs_sdk_ffi::ContextProviderWrapper::new(provider));
let context_provider = Box::into_raw(wrapper) as *mut rs_sdk_ffi::ContextProviderHandle;

let extended = rs_sdk_ffi::DashSDKConfigExtended {
base_config: rs_sdk_ffi::DashSDKConfig {
network: config_ref.network,
dapi_addresses: config_ref.dapi_addresses,
skip_asset_lock_proof_verification: config_ref.skip_asset_lock_proof_verification,
request_retry_count: config_ref.request_retry_count,
request_timeout_ms: config_ref.request_timeout_ms,
quorum_url: config_ref.quorum_url,
platform_version: config_ref.platform_version,
},
context_provider,
core_sdk_handle: std::ptr::null_mut(),
};

let result = rs_sdk_ffi::dash_sdk_create_extended(&extended);

// `dash_sdk_create_extended` only borrows the wrapper and clones the inner
// provider `Arc`; it never takes ownership. Reclaim the box so the wrapper
// (and its `Arc<SpvRuntime>`) isn't leaked on every SDK creation.
drop(Box::from_raw(
context_provider as *mut rs_sdk_ffi::ContextProviderWrapper,
));

result
}
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
51 changes: 50 additions & 1 deletion packages/rs-platform-wallet/src/spv/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,12 @@ impl SpvRuntime {
))?;

let llmq_type = LLMQType::from(quorum_type as u8);
let qh = QuorumHash::from_byte_array(quorum_hash).reverse();
// `quorum_hash` arrives in internal byte order (drive-abci sends it via
// `to_byte_array()`, verified by the consensus equality check in
// finalize_block_proposal), and the masternode engine keys its quorum
// map in that same internal order. So the lookup key must NOT be
// reversed — reversing guarantees a miss on every real quorum.
let qh = QuorumHash::from_byte_array(quorum_hash);

let quorum = client
.get_quorum_at_height(height, llmq_type, qh)
Expand Down Expand Up @@ -460,4 +465,48 @@ mod tests {
);
}
}

/// Regression guard for the quorum-hash byte order in
/// [`SpvRuntime::get_quorum_public_key`].
///
/// A Platform proof carries `quorum_hash` in internal byte order
/// (drive-abci emits it via `to_byte_array()`), and the masternode engine
/// keys its quorum map — `quorum_entry_of_type_for_quorum_hash`, i.e. a
/// `BTreeMap<QuorumHash, _>::get` — in that same internal order. So the
/// lookup key must be `QuorumHash::from_byte_array(quorum_hash)` with **no**
/// reversal, which is what `get_quorum_public_key` now uses.
///
/// A previous version reversed it (`.reverse()`); this test pins why that
/// was wrong: the reversed key is absent from the map, so every real lookup
/// missed and fell through to fail-closed rejection (silently masked by the
/// trusted-quorum fallback). Re-introducing the reversal fails this test.
#[test]
fn quorum_hash_lookup_uses_internal_order_not_reversed() {
use dashcore::hashes::Hash;
use dashcore::QuorumHash;
use std::collections::BTreeMap;

// 32 distinct bytes so internal order differs from reversed order.
let wire_bytes: [u8; 32] = std::array::from_fn(|i| (i as u8) + 1);

// Model the engine's quorum map, keyed exactly as the engine keys it:
// by the `QuorumHash` in internal byte order.
let pubkey = [0xABu8; 48];
let mut quorums: BTreeMap<QuorumHash, [u8; 48]> = BTreeMap::new();
quorums.insert(QuorumHash::from_byte_array(wire_bytes), pubkey);

// What `get_quorum_public_key` does now (no reverse): the key matches.
assert_eq!(
quorums.get(&QuorumHash::from_byte_array(wire_bytes)),
Some(&pubkey),
"internal-order hash must find the quorum"
);

// The old buggy `.reverse()`: flipped key is absent → miss (the bug).
assert_eq!(
quorums.get(&QuorumHash::from_byte_array(wire_bytes).reverse()),
None,
"reversed hash must NOT find the quorum — this was the bug"
);
}
Comment thread
shumkov marked this conversation as resolved.
Outdated
Comment thread
shumkov marked this conversation as resolved.
Outdated
}
Loading
Loading