Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 3 additions & 1 deletion dash-spv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ hex-conservative = { workspace = true }
# Parallelization
rayon = { workspace = true }

# Masternode engine snapshots
bincode = { workspace = true, features = ["serde"] }

# Test utilities (optional)
tempfile = { version = "3.0", optional = true }
dashcore-rpc = { path = "../rpc-client", optional = true }
Expand All @@ -56,7 +59,6 @@ serde = { workspace = true, features = ["std", "derive"] }
serde_json = "1.0"

[dev-dependencies]
bincode = { workspace = true }
dash-spv = { path = ".", features = ["test-utils"] }
dashcore = { path = "../dash", features = ["test-utils"] }
key-wallet = { path = "../key-wallet", features = ["test-utils"] }
Expand Down
5 changes: 0 additions & 5 deletions dash-spv/src/client/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,6 @@ impl<W: WalletInterface, N: NetworkManager> DashSpvClient<W, N> {
self.storage.lock().await.clear().await?;

self.sync_coordinator.lock().await.reset_progress();
if let Some(engine) = &self.masternode_engine {
let network = self.config.read().await.network;
let headers = self.storage.lock().await.block_headers();
*engine.write().await = MasternodeListEngine::new(network, headers);
}

Ok(())
}
Expand Down
3 changes: 1 addition & 2 deletions dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ impl<W: WalletInterface, N: NetworkManager> DashSpvClient<W, N> {

let masternode_engine = {
if config.enable_masternodes {
let engine = storage.masternodes().read().await.load_engine().await;
Some(Arc::new(RwLock::new(engine)))
Some(storage.masternodes().read().await.engine())
} else {
None
}
Expand Down
64 changes: 10 additions & 54 deletions dash-spv/src/client/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
use crate::error::{Result, SpvError};
use crate::network::NetworkManager;
use crate::sml_engine::MasternodeListEngine;
use crate::storage::{BlockHeaderStorage, PersistentBlockHeaderStorage, StorageManager};
use crate::storage::PersistentBlockHeaderStorage;
use dashcore::hashes::Hash;
use dashcore::sml::llmq_type::LLMQType;
use dashcore::sml::masternode_list::MasternodeList;
use dashcore::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry;
use dashcore::{BlockHash, QuorumHash};
use dashcore::QuorumHash;
use key_wallet_manager::WalletInterface;
use std::sync::Arc;
use tokio::sync::RwLock;
Expand Down Expand Up @@ -67,41 +67,21 @@ impl<W: WalletInterface, N: NetworkManager> DashSpvClient<W, N> {
engine.blocking_read().latest_masternode_list().cloned()
}

/// Get a quorum entry by type and hash at a specific block height. A height
/// whose lists the engine no longer holds is rebuilt from storage.
/// Get a quorum entry by type and hash at a specific block height. Only
/// heights within the lists the engine holds resolve.
/// Returns `SpvError::QuorumLookupError` if the quorum is not found.
pub async fn get_quorum_at_height(
&self,
height: u32,
quorum_type: LLMQType,
quorum_hash: QuorumHash,
) -> Result<QualifiedQuorumEntry> {
let (in_memory, oldest_list) = {
let engine = self.masternode_list_engine()?;
let engine = engine.read().await;
let quorum = engine
.quorum_entry_for_hash_at_or_before_height(quorum_type, quorum_hash, height)
.map(|(_, quorum)| quorum.clone());
(quorum, engine.masternode_lists.keys().next().copied())
};

let quorum = match in_memory {
Some(quorum) => Some(quorum),
None => {
let (headers, masternodes) = {
let storage = self.storage.lock().await;
(storage.block_headers(), storage.masternodes())
};
let quorum_block = BlockHash::from_byte_array(quorum_hash.to_byte_array());
let mined = headers.read().await.get_header_height_by_hash(&quorum_block).await?;
if storage_may_hold(mined, height, oldest_list) {
let log = masternodes.read().await.message_log();
log.quorum_entry_at_or_before(quorum_type, quorum_hash, height).await
} else {
None
}
}
};
let engine = self.masternode_list_engine()?;
let quorum = engine
.read()
.await
.quorum_entry_for_hash_at_or_before_height(quorum_type, quorum_hash, height)
.map(|(_, quorum)| quorum.clone());

quorum.ok_or_else(|| {
let message = format!(
Expand All @@ -115,27 +95,3 @@ impl<W: WalletInterface, N: NetworkManager> DashSpvClient<W, N> {
})
}
}

/// Whether the stored messages can resolve a quorum the engine missed. A
/// quorum's hash is the block it was mined in, so a hash outside the header
/// chain names no quorum, one mined above `height` was not active there, and
/// one mined at or above the engine's oldest list was held by every list the
/// engine kept since. Only a quorum mined below that list may sit in lists the
/// engine has pruned.
fn storage_may_hold(mined: Option<u32>, height: u32, oldest_list: Option<u32>) -> bool {
mined.is_some_and(|mined| mined <= height && oldest_list.is_none_or(|oldest| mined < oldest))
}

#[cfg(test)]
mod tests {
use super::storage_may_hold;

#[test]
fn only_a_quorum_mined_below_the_oldest_list_goes_to_storage() {
assert!(!storage_may_hold(None, 1_000, Some(500)), "not a block in the header chain");
assert!(!storage_may_hold(Some(1_001), 1_000, Some(500)), "mined above the lookup");
assert!(!storage_may_hold(Some(600), 1_000, Some(500)), "the engine held all its lists");
assert!(storage_may_hold(Some(400), 1_000, Some(500)), "its lists may be pruned");
assert!(storage_may_hold(Some(400), 1_000, None), "the engine holds no list yet");
}
}
29 changes: 29 additions & 0 deletions dash-spv/src/sml_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,35 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> {
}
}

/// The engine's lists and rotated cycles as bytes, see [`Self::from_snapshot`].
pub fn snapshot(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::serde::encode_to_vec(
(&self.masternode_lists, &self.rotated_quorums_per_cycle),
bincode::config::standard(),
)
}

/// An engine holding the lists and rotated cycles [`Self::snapshot`] encoded.
pub fn from_snapshot(
network: Network,
headers: Arc<RwLock<H>>,
bytes: &[u8],
) -> Result<Self, bincode::error::DecodeError> {
let ((masternode_lists, rotated_quorums_per_cycle), read) =
bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;

if read != bytes.len() {
return Err(bincode::error::DecodeError::Other("trailing bytes after the snapshot"));
}

Ok(Self {
headers,
masternode_lists,
rotated_quorums_per_cycle,
network,
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/// The height of `block_hash` in the header chain, `None` when the chain
/// does not hold it. The genesis block, and the all-zero hash standing for
/// it, is at 0 even when the chain starts at a checkpoint. Fails when the
Expand Down
Loading
Loading