Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
4 changes: 4 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions packages/rs-platform-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ key-wallet-manager = { workspace = true, optional = true }
# Core dependencies
dashcore = { workspace = true }

# SPV context provider dependencies (optional)
dash-spv = { workspace = true, optional = true }
dash-context-provider = { path = "../rs-context-provider", optional = true }
tokio = { version = "1.41", optional = true }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Standard dependencies
thiserror = "1.0"
async-trait = "0.1"
Expand All @@ -35,3 +40,4 @@ default = ["bls", "eddsa", "manager"]
bls = ["key-wallet/bls"]
eddsa = ["key-wallet/eddsa"]
manager = ["key-wallet-manager"]
spv-context = ["dash-spv", "dash-context-provider", "tokio"]
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 @@ -12,6 +12,9 @@ pub mod identity_manager;
pub mod managed_identity;
pub mod platform_wallet_info;

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

// Re-export main types at crate root
pub use block_time::BlockTime;
pub use contact_request::ContactRequest;
Expand Down
142 changes: 142 additions & 0 deletions packages/rs-platform-wallet/src/spv_context_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! SPV-based Context Provider
//!
//! Pure Rust implementation that reads quorum data directly from a
//! [`MasternodeListEngine`], with no FFI calls.
//!
//! # Architecture
//!
//! The [`SpvContextProvider`] holds an `Arc<RwLock<MasternodeListEngine>>`
//! (shared with the SPV client) and reads quorum public keys by looking up
//! the masternode list closest to the requested core chain-locked height.
//!
//! This design eliminates the need for FFI round-trips: the same in-memory
//! masternode list engine that the SPV client populates during sync is read
//! directly by the Platform SDK's proof verifier.
//!
//! # Usage
//!
//! ```ignore
//! use std::sync::Arc;
//! use tokio::sync::RwLock;
//! use dash_spv::MasternodeListEngine;
//! use dashcore::Network;
//! use platform_wallet::spv_context_provider::SpvContextProvider;
//!
//! let engine: Arc<RwLock<MasternodeListEngine>> = /* from DashSpvClient */;
//! let provider = SpvContextProvider::new(engine, Network::Testnet);
//! ```

use std::sync::Arc;

use dash_context_provider::ContextProvider;
use dash_context_provider::ContextProviderError;
use dash_spv::LLMQType;
use dash_spv::MasternodeListEngine;
use dashcore::hashes::Hash;
use dashcore::Network;
use dashcore::QuorumHash;
use dpp::data_contract::TokenConfiguration;
use dpp::prelude::{CoreBlockHeight, DataContract, Identifier};
use dpp::version::PlatformVersion;
use tokio::sync::RwLock;

/// Context provider backed by an SPV client's synced masternode data.
///
/// Reads quorum public keys directly from the [`MasternodeListEngine`]
/// without any FFI calls. The engine is shared with the SPV client via
/// `Arc<RwLock<...>>`, so all data stays in-process.
pub struct SpvContextProvider {
masternode_engine: Arc<RwLock<MasternodeListEngine>>,
network: Network,
}

impl SpvContextProvider {
/// Create a new SPV context provider.
///
/// # Arguments
///
/// * `masternode_engine` - Shared reference to the masternode list engine,
/// typically obtained from [`DashSpvClient::masternode_list_engine()`].
/// * `network` - The Dash network (mainnet, testnet, devnet, etc.).
pub fn new(masternode_engine: Arc<RwLock<MasternodeListEngine>>, network: Network) -> Self {
Self {
masternode_engine,
network,
}
}
}

impl ContextProvider for SpvContextProvider {
fn get_quorum_public_key(
&self,
quorum_type: u32,
quorum_hash: [u8; 32],
core_chain_locked_height: u32,
) -> Result<[u8; 48], ContextProviderError> {
let llmq_type: LLMQType = (quorum_type as u8).into();
let quorum_hash = QuorumHash::from_byte_array(quorum_hash);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

let engine = self.masternode_engine.blocking_read();
let (before, _after) = engine.masternode_lists_around_height(core_chain_locked_height);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

let ml = before.ok_or_else(|| {
ContextProviderError::InvalidQuorum(format!(
"No masternode list found at or before height {}",
core_chain_locked_height
))
})?;

let list_height = ml.known_height;

let quorums = ml.quorums.get(&llmq_type).ok_or_else(|| {
ContextProviderError::InvalidQuorum(format!(
"No quorums of type {} found at list height {} (requested {})",
quorum_type, list_height, core_chain_locked_height
))
})?;

let quorum = quorums.get(&quorum_hash).ok_or_else(|| {
ContextProviderError::InvalidQuorum(format!(
"Quorum not found: type {} at list height {} (requested {}) \
with hash {:x} (masternode list has {} quorums of this type)",
quorum_type,
list_height,
core_chain_locked_height,
quorum_hash,
quorums.len()
))
})?;

let pubkey_bytes: &[u8; 48] = quorum.quorum_entry.quorum_public_key.as_ref();
Ok(*pubkey_bytes)
}

fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
let height = match self.network {
Network::Mainnet => 1_888_888,
Network::Testnet => 1_289_520,
Network::Devnet => 1,
_ => 0,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Ok(height)
}

fn get_data_contract(
&self,
_data_contract_id: &Identifier,
_platform_version: &PlatformVersion,
) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
// Data contract lookup is handled by the SDK's contract cache,
// not the SPV layer.
Ok(None)
}

fn get_token_configuration(
&self,
_token_id: &Identifier,
) -> Result<Option<TokenConfiguration>, ContextProviderError> {
// Token configuration lookup is handled by the SDK's contract cache,
// not the SPV layer.
Ok(None)
}
}
3 changes: 3 additions & 0 deletions packages/rs-sdk-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ rs-sdk-trusted-context-provider = { path = "../rs-sdk-trusted-context-provider",
] }
simple-signer = { path = "../simple-signer" }

# SPV client integration for quorum-based proof verification
dash-spv-ffi = { workspace = true }

# Platform Wallet integration for DashPay support
platform-wallet-ffi = { path = "../rs-platform-wallet-ffi" }

Expand Down
1 change: 1 addition & 0 deletions packages/rs-sdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod sdk;
mod shielded;
mod signer;
mod signer_simple;
pub mod spv_context_provider;
mod system;
mod token;
mod types;
Expand Down
54 changes: 54 additions & 0 deletions packages/rs-sdk-ffi/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,60 @@ pub unsafe extern "C" fn dash_sdk_create_with_callbacks(
result
}

/// Create a new SDK instance using SPV-synced quorum data for proof verification.
///
/// Instead of fetching quorum keys from a trusted HTTP endpoint, this uses
/// quorum data from the SPV client's locally synced masternode list.
///
/// # Safety
/// - `config` must be a valid pointer to a DashSDKConfig structure
/// - `spv_client` must be a valid pointer to an FFIDashSpvClient that outlives the SDK
#[no_mangle]
pub unsafe extern "C" fn dash_sdk_create_with_spv_context(
config: *const DashSDKConfig,
spv_client: *mut std::os::raw::c_void,
) -> DashSDKResult {
if config.is_null() {
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InvalidParameter,
"Config is null".to_string(),
));
}

if spv_client.is_null() {
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InvalidParameter,
"SPV client pointer is null".to_string(),
));
}

info!("dash_sdk_create_with_spv_context: creating SDK with SPV quorum provider");

let context_provider = crate::spv_context_provider::SpvContextProvider::new(spv_client);
let wrapper = Box::new(ContextProviderWrapper::new(context_provider));
let context_provider_handle = Box::into_raw(wrapper) as *mut ContextProviderHandle;

let config_ref = &*config;
let extended_config = DashSDKConfigExtended {
base_config: 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,
},
context_provider: context_provider_handle,
core_sdk_handle: std::ptr::null_mut(),
};

let result = dash_sdk_create_extended(&extended_config);

// Reclaim the wrapper -- the SDK has already cloned what it needs
let _ = Box::from_raw(context_provider_handle as *mut ContextProviderWrapper);

result
}

/// Get the current network the SDK is connected to
///
/// # Safety
Expand Down
Loading
Loading