Skip to content
Draft
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
8 changes: 8 additions & 0 deletions dash-spv/src/client/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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::transaction::special_transaction::coinbase::CoinbasePayload;
use dashcore::{BlockHash, QuorumHash};
use key_wallet_manager::WalletInterface;
use std::sync::Arc;
Expand Down Expand Up @@ -67,6 +68,13 @@ impl<W: WalletInterface, N: NetworkManager> DashSpvClient<W, N> {
engine.blocking_read().latest_masternode_list().cloned()
}

/// The coinbase payload of the newest block a masternode list diff proved it
/// in, `None` while masternode sync is off or has validated none yet.
pub async fn latest_coinbase(&self) -> Option<CoinbasePayload> {
let engine = self.masternode_engine.as_ref()?;
engine.read().await.latest_coinbase().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.
/// Returns `SpvError::QuorumLookupError` if the quorum is not found.
Expand Down
30 changes: 29 additions & 1 deletion dash-spv/src/sml_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ use dashcore::sml::masternode_list::MasternodeList;
use dashcore::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry;
use dashcore::sml::quorum_entry::qualified_quorum_entry::VerifyingChainLockSignaturesType;
use dashcore::sml::quorum_validation_error::QuorumValidationError;
use dashcore::transaction::special_transaction::coinbase::CoinbasePayload;
use dashcore::transaction::special_transaction::quorum_commitment::QuorumEntry;
use dashcore::Network;
use dashcore::{BlockHash, QuorumHash};
use dashcore::{BlockHash, QuorumHash, Transaction};
use tokio::sync::RwLock;

/// Blocks between a rotation cycle's base and its work block (Dash Core's `WORK_DIFF_DEPTH`).
Expand Down Expand Up @@ -59,6 +60,7 @@ pub struct MasternodeListEngine<H: BlockHeaderStorage> {
pub masternode_lists: BTreeMap<CoreBlockHeight, MasternodeList>,
rotated_quorums_per_cycle: BTreeMap<BlockHash, BTreeMap<u16, QualifiedQuorumEntry>>,
network: Network,
latest_coinbase: Option<CoinbasePayload>,
}

/// The most quorums of `llmq_type` a run of `blocks` blocks can add: one per
Expand Down Expand Up @@ -166,9 +168,15 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> {
masternode_lists: BTreeMap::new(),
rotated_quorums_per_cycle: BTreeMap::new(),
network,
latest_coinbase: None,
}
}

/// The coinbase payload of the newest block a validated diff proved it in.
pub fn latest_coinbase(&self) -> Option<&CoinbasePayload> {
self.latest_coinbase.as_ref()
}

/// The height of `block_hash` in the header chain. The genesis block, and
/// the all-zero hash standing for it, is at 0 even when the chain starts
/// at a checkpoint.
Expand Down Expand Up @@ -818,6 +826,7 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> {
let masternode_list =
MasternodeList::from_diff(masternode_list_diff, diff_end_height, self.network)?;
self.masternode_lists.insert(diff_end_height, masternode_list);
self.observe_coinbase_if_higher(coinbase_tx);
return Ok(());
}

Expand All @@ -843,12 +852,28 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> {
}
}
self.masternode_lists.insert(diff_end_height, masternode_list);
self.observe_coinbase_if_higher(coinbase_tx);
for (quorum_type, quorum_hash, status) in changes {
self.set_quorum_status(quorum_type, quorum_hash, &status);
}
Ok(())
}

fn observe_coinbase_if_higher(&mut self, coinbase_tx: Option<Transaction>) {
let Some(payload) = coinbase_tx
.and_then(|tx| tx.special_transaction_payload)
.and_then(|payload| payload.to_coinbase_payload().ok())
else {
return;
};

if self.latest_coinbase.as_ref().is_some_and(|latest| latest.height > payload.height) {
return;
}

self.latest_coinbase = Some(payload);
}

/// Verifies the newest list's unverified non-rotating quorums against their
/// work-block lists (DIP-6). Rotated ones are verified per QRInfo.
async fn verify_newest_quorums(&mut self) {
Expand Down Expand Up @@ -1401,6 +1426,9 @@ mod tests {
.await
.expect("first QRInfo on a fresh engine must feed cleanly");

let coinbase = engine.latest_coinbase().expect("observes the tip's coinbase");
assert_eq!((coinbase.height, coinbase.version), (2518986, 3));

assert_eq!(feed_result.rotated_quorum_count, 32);
assert_eq!(
feed_result.fully_verified_count, 32,
Expand Down
17 changes: 16 additions & 1 deletion dash-spv/src/sml_engine/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> {
}
}
self.rotated_quorums_per_cycle.retain(|hash, _| !dropped.contains(hash));

if self.latest_coinbase.as_ref().is_some_and(|coinbase| coinbase.height > height) {
self.latest_coinbase = None;
}
}

/// Sets the verification status of a quorum in every list holding it.
Expand Down Expand Up @@ -426,7 +430,9 @@ mod prune_tests {
use super::*;
use crate::sml_engine::test_support::TestEngine;
use dashcore::network::message_sml::MnListDiff;
use dashcore::transaction::special_transaction::coinbase::CoinbasePayload;
use dashcore::{BlockHash, Network};
use test_case::test_case;

const TIP: CoreBlockHeight = 1_000_000;
/// Within the window at `TIP` and below the next QRInfo's h-4c reach.
Expand Down Expand Up @@ -463,8 +469,14 @@ mod prune_tests {
/// the dropped blocks may answer for them: the lists above the fork go, and
/// so does a rotated cycle whose base left the chain or whose commitments
/// could have been mined above the fork.
#[test_case(TIP - 1, true; "keeps a coinbase below the fork")]
#[test_case(TIP, true; "keeps a coinbase at the fork")]
#[test_case(TIP + 1, false; "drops a coinbase above the fork")]
#[tokio::test]
async fn truncating_above_a_fork_drops_what_the_dropped_blocks_built() {
async fn truncating_above_a_fork_drops_what_the_dropped_blocks_built(
coinbase_height: CoreBlockHeight,
coinbase_kept: bool,
) {
let fork = TIP;
let window_end = Network::Mainnet.isd_llmq_type().params().dkg_params.mining_window_end;
let mut engine = TestEngine::dummy_with_lists(&[fork - 100, fork, fork + 1, fork + 50]);
Expand All @@ -478,9 +490,12 @@ mod prune_tests {
for height in [fork + 1, left_the_chain, fork + 50] {
engine.forget_block(&BlockHash::dummy(height));
}
engine.latest_coinbase = Some(CoinbasePayload::dummy(coinbase_height));

engine.truncate_above(fork).await;

assert_eq!(engine.latest_coinbase.is_some(), coinbase_kept);

assert_eq!(heights(&engine), vec![fork - 100, fork]);
assert_eq!(
engine.rotated_quorums_per_cycle.keys().collect::<Vec<_>>(),
Expand Down
20 changes: 20 additions & 0 deletions dash/src/test_utils/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::ops::Range;

use hashes::Hash;

use crate::hash_types::{MerkleRootMasternodeList, MerkleRootQuorums};
use crate::transaction::special_transaction::coinbase::CoinbasePayload;
use crate::{Address, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};

/// A special transaction type no Dash Core version assigns
Expand Down Expand Up @@ -76,3 +80,19 @@ impl Transaction {
}
}
}

impl CoinbasePayload {
/// A current version payload at `height` committing to empty lists.
pub fn dummy(height: u32) -> Self {
CoinbasePayload {
version: CoinbasePayload::CURRENT_VERSION,
height,
merkle_root_masternode_list: MerkleRootMasternodeList::all_zeros(),
merkle_root_quorums: MerkleRootQuorums::all_zeros(),
best_cl_height: None,
best_cl_signature: None,
asset_locked_amount: None,
merkle_root_asset_unlocks: None,
}
}
}
Loading