Skip to content
Open
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
58 changes: 15 additions & 43 deletions dash-spv-ffi/src/platform_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,53 +101,25 @@ pub unsafe extern "C" fn ffi_dash_spv_get_quorum_public_key(
};

let engine_guard = engine.blocking_read();
let (before, _after) = engine_guard.masternode_lists_around_height(core_chain_locked_height);
let ml = match before {
Some(ml) => ml,
None => {
return FFIResult::error(
FFIErrorCode::ValidationError,
&format!(
"No masternode list found at or before height {}",
core_chain_locked_height
),
);
}
};

let list_height = ml.known_height;
match ml.quorums.get(&llmq_type) {
Some(quorums) => match quorums.get(&quorum_hash) {
Some(quorum) => {
let pubkey_bytes: &[u8; 48] = quorum.quorum_entry.quorum_public_key.as_ref();
std::ptr::copy_nonoverlapping(
pubkey_bytes.as_ptr(),
out_pubkey,
QUORUM_PUBKEY_SIZE,
);

FFIResult {
error_code: 0,
error_message: ptr::null(),
}
match engine_guard.quorum_entry_for_hash_at_or_before_height(
llmq_type,
quorum_hash,
core_chain_locked_height,
) {
Some((_list_height, quorum)) => {
let pubkey_bytes: &[u8; 48] = quorum.quorum_entry.quorum_public_key.as_ref();
std::ptr::copy_nonoverlapping(pubkey_bytes.as_ptr(), out_pubkey, QUORUM_PUBKEY_SIZE);

FFIResult {
error_code: 0,
error_message: ptr::null(),
}
None => FFIResult::error(
FFIErrorCode::ValidationError,
&format!(
"Quorum not found: type {} at list height {} (requested {}) with hash {:x} (masternode list exists with {} quorums of this type)",
quorum_type,
list_height,
core_chain_locked_height,
quorum_hash,
quorums.len()
),
),
},
}
None => FFIResult::error(
FFIErrorCode::ValidationError,
&format!(
"No quorums of type {} found at list height {} (requested {})",
quorum_type, list_height, core_chain_locked_height
"Quorum not found: type {} at or before height {} with hash {:x}",
quorum_type, core_chain_locked_height, quorum_hash
),
),
}
Expand Down
73 changes: 24 additions & 49 deletions dash-spv/src/client/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,56 +53,31 @@ impl<W: WalletInterface, N: NetworkManager, S: StorageManager> DashSpvClient<W,
) -> Result<QualifiedQuorumEntry> {
let masternode_engine = self.masternode_list_engine()?;
let masternode_engine_guard = masternode_engine.read().await;
let (before, _after) = masternode_engine_guard.masternode_lists_around_height(height);
if let Some(ml) = before {
let list_height = ml.known_height;
match ml.quorums.get(&quorum_type) {
Some(quorums) => match quorums.get(&quorum_hash) {
Some(quorum) => {
tracing::debug!(
"Found quorum type {} at list height {} (requested {}) with hash {}",
quorum_type,
list_height,
height,
hex::encode(quorum_hash)
);
return Ok(quorum.clone());
}
None => {
let message = format!(
"Quorum not found: type {} at list height {} (requested {}) with hash {} (masternode list exists with {} quorums of this type)",
quorum_type,
list_height,
height,
hex::encode(quorum_hash),
quorums.len()
);
tracing::warn!(message);
return Err(SpvError::QuorumLookupError(message));
}
},
None => {
tracing::warn!(
"No quorums of type {} found at list height {} (requested {}) (masternode list exists)",
quorum_type,
list_height,
height
);
return Err(SpvError::QuorumLookupError(format!(
"No quorums of type {} found at list height {} (requested {})",
quorum_type, list_height, height
)));
}
match masternode_engine_guard.quorum_entry_for_hash_at_or_before_height(
quorum_type,
quorum_hash,
height,
) {
Some((list_height, quorum)) => {
tracing::debug!(
"Found quorum type {} at list height {} (requested {}) with hash {}",
quorum_type,
list_height,
height,
hex::encode(quorum_hash)
);
Ok(quorum.clone())
}
None => {
let message = format!(
"Quorum not found: type {} at or before height {} with hash {}",
quorum_type,
height,
hex::encode(quorum_hash)
);
tracing::warn!("{}", message);
Err(SpvError::QuorumLookupError(message))
}
}

tracing::warn!(
"No masternode list found at or before height {} - cannot retrieve quorum",
height
);
Err(SpvError::QuorumLookupError(format!(
"No masternode list found at or before height {}",
height
)))
}
}
208 changes: 208 additions & 0 deletions dash/src/sml/masternode_list_engine/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
use crate::QuorumHash;
use crate::prelude::CoreBlockHeight;
use crate::sml::llmq_entry_verification::LLMQEntryVerificationStatus;
use crate::sml::llmq_type::LLMQType;
use crate::sml::masternode_list::MasternodeList;
use crate::sml::masternode_list_engine::MasternodeListEngine;
use crate::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry;

/// How many active windows below the lookup height [`MasternodeListEngine::quorum_entry_for_hash_at_or_before_height`]
/// searches before giving up. A signing quorum referenced by a proof was selected at a lagged
/// height that can exceed one active window (Platform selects roughly 4.5 DKG intervals back), so a
/// single window is too tight. Four windows covers that lag with wide margin while still bounding a
/// miss to a fixed span of lists rather than every list the engine has accumulated.
const QUORUM_WALK_BACK_ACTIVE_WINDOWS: u32 = 4;

impl MasternodeListEngine {
/// Retrieves the closest masternode lists before and after a given core block height.
Expand Down Expand Up @@ -38,4 +49,201 @@ impl MasternodeListEngine {

(lower, upper)
}

/// Resolves a quorum entry by type and hash, searching masternode lists at or below
/// `height` from the nearest downward and returning the first one that still holds it.
///
/// The nearest list at or below `height` may no longer contain the quorum: once a quorum
/// retires out of the active set, `apply_diff` drops it from every list built from that
/// point on. A signing quorum selected at a lagged height can therefore be absent from the
/// nearest list yet still present in an earlier, retained one. Walking backward returns that
/// earlier full entry rather than failing the lookup. Entries marked `Invalid` are skipped.
///
/// The returned `CoreBlockHeight` is the height of the list the entry was resolved from. The
/// first match is the highest list still holding the quorum, so a hit stops a few cycles back at
/// most. The walk is floored at `QUORUM_WALK_BACK_ACTIVE_WINDOWS` active windows below `height`
/// (derived from the type's DKG interval and active quorum count): a legitimately referenced
/// signing quorum cannot be older than that, so flooring it bounds a miss to a fixed span of
/// lists rather than scanning every list the engine has accumulated.
pub fn quorum_entry_for_hash_at_or_before_height(
&self,
llmq_type: LLMQType,
quorum_hash: QuorumHash,
height: CoreBlockHeight,
) -> Option<(CoreBlockHeight, &QualifiedQuorumEntry)> {
let params = llmq_type.params();
let active_window =
params.signing_active_quorum_count.saturating_mul(params.dkg_params.interval);
let floor =
height.saturating_sub(active_window.saturating_mul(QUORUM_WALK_BACK_ACTIVE_WINDOWS));

self.masternode_lists.range(floor..=height).rev().find_map(|(_, list)| {
list.quorum_entry_of_type_for_quorum_hash(llmq_type, quorum_hash)
.filter(|quorum| {
!matches!(quorum.verified, LLMQEntryVerificationStatus::Invalid(_))
})
.map(|quorum| (list.known_height, quorum))
})
}
}

#[cfg(test)]
mod tests {
use std::slice;

use hashes::Hash;

use super::*;
use crate::BlockHash;
use crate::bls_sig_utils::{BLSPublicKey, BLSSignature};
use crate::hash_types::QuorumVVecHash;
use crate::sml::quorum_validation_error::QuorumValidationError;
use crate::transaction::special_transaction::quorum_commitment::QuorumEntry;

const PLATFORM_TYPE: LLMQType = LLMQType::LlmqtypeDevnetPlatform;

fn quorum_entry(quorum_hash: QuorumHash, pubkey: u8) -> QualifiedQuorumEntry {
let mut entry: QualifiedQuorumEntry = QuorumEntry {
version: 2,
llmq_type: PLATFORM_TYPE,
quorum_hash,
quorum_index: Some(0),
signers: vec![true; 4],
valid_members: vec![true; 4],
quorum_public_key: BLSPublicKey::from([pubkey; 48]),
quorum_vvec_hash: QuorumVVecHash::all_zeros(),
threshold_sig: BLSSignature::from([1; 96]),
all_commitment_aggregated_signature: BLSSignature::from([1; 96]),
}
.into();
entry.verified = LLMQEntryVerificationStatus::Verified;
entry
}

fn list_with_quorums(height: u32, quorums: &[QualifiedQuorumEntry]) -> MasternodeList {
let mut list =
MasternodeList::empty(BlockHash::from_byte_array([height as u8; 32]), height);
let by_hash = list.quorums.entry(PLATFORM_TYPE).or_default();
for quorum in quorums {
by_hash.insert(quorum.quorum_entry.quorum_hash, quorum.clone());
}
list
}

/// A quorum retired out of the active set is dropped from the nearest list at or below the
/// lookup height, but the backward walk resolves it from the earlier list that still holds it.
#[test]
fn resolves_retired_quorum_from_earlier_list() {
let retired_hash = QuorumHash::from_byte_array([0xAB; 32]);
let active_hash = QuorumHash::from_byte_array([0xCD; 32]);
let retired = quorum_entry(retired_hash, 7);

let mut engine = MasternodeListEngine::default();
// Pre-retirement list still holds the retired quorum.
engine.masternode_lists.insert(148, list_with_quorums(148, slice::from_ref(&retired)));
// Post-retirement list holds only the then-active quorum, not the retired one.
engine
.masternode_lists
.insert(208, list_with_quorums(208, &[quorum_entry(active_hash, 9)]));

// The nearest list at or below the lookup height no longer holds the retired quorum.
let nearest = engine.masternode_lists_around_height(208).0.unwrap();
assert!(
nearest.quorum_entry_of_type_for_quorum_hash(PLATFORM_TYPE, retired_hash).is_none()
);

// The walk resolves it from the earlier retained list, returning that list's height.
let (resolved_height, resolved) = engine
.quorum_entry_for_hash_at_or_before_height(PLATFORM_TYPE, retired_hash, 208)
.expect("retired quorum resolves from earlier list");
assert_eq!(resolved_height, 148);
assert_eq!(resolved.quorum_entry.quorum_public_key, retired.quorum_entry.quorum_public_key);
}

/// While still in the active set the quorum resolves from the nearest list directly.
#[test]
fn resolves_active_quorum_from_nearest_list() {
let hash = QuorumHash::from_byte_array([0xAB; 32]);
let mut engine = MasternodeListEngine::default();
engine.masternode_lists.insert(148, list_with_quorums(148, &[quorum_entry(hash, 7)]));

let (resolved_height, _) = engine
.quorum_entry_for_hash_at_or_before_height(PLATFORM_TYPE, hash, 148)
.expect("active quorum resolves");
assert_eq!(resolved_height, 148);
}

/// A lookup below every retained list, or for an unknown hash, finds nothing.
#[test]
fn returns_none_when_not_present() {
let hash = QuorumHash::from_byte_array([0xAB; 32]);
let mut engine = MasternodeListEngine::default();
engine.masternode_lists.insert(148, list_with_quorums(148, &[quorum_entry(hash, 7)]));

assert!(
engine.quorum_entry_for_hash_at_or_before_height(PLATFORM_TYPE, hash, 100).is_none()
);
assert!(
engine
.quorum_entry_for_hash_at_or_before_height(
PLATFORM_TYPE,
QuorumHash::from_byte_array([0xEE; 32]),
208
)
.is_none()
);
}

/// An `Invalid` entry is skipped, even when it is the only list holding the hash.
#[test]
fn skips_invalid_entries() {
let hash = QuorumHash::from_byte_array([0xAB; 32]);
let mut invalid = quorum_entry(hash, 7);
invalid.verified =
LLMQEntryVerificationStatus::Invalid(QuorumValidationError::InvalidQuorumPublicKey);

let mut engine = MasternodeListEngine::default();
engine.masternode_lists.insert(148, list_with_quorums(148, &[invalid]));

assert!(
engine.quorum_entry_for_hash_at_or_before_height(PLATFORM_TYPE, hash, 208).is_none()
);
}

/// The walk is floored at a few active windows below the lookup height: a quorum that only
/// survives in a list older than the floor is treated as not found, while one within the window
/// still resolves. This bounds a miss instead of scanning every accumulated list.
#[test]
fn does_not_walk_below_active_window_floor() {
let params = PLATFORM_TYPE.params();
let span = params.signing_active_quorum_count
* params.dkg_params.interval
* QUORUM_WALK_BACK_ACTIVE_WINDOWS;
let height = span + 5_000;
let floor = height - span;

let within_hash = QuorumHash::from_byte_array([0x11; 32]);
let below_hash = QuorumHash::from_byte_array([0x22; 32]);

let mut engine = MasternodeListEngine::default();
// One list just above the floor and one well below it.
engine
.masternode_lists
.insert(floor + 100, list_with_quorums(floor + 100, &[quorum_entry(within_hash, 7)]));
engine
.masternode_lists
.insert(floor - 100, list_with_quorums(floor - 100, &[quorum_entry(below_hash, 9)]));

let (resolved_height, _) = engine
.quorum_entry_for_hash_at_or_before_height(PLATFORM_TYPE, within_hash, height)
.expect("quorum within the window resolves");
assert_eq!(resolved_height, floor + 100);

assert!(
engine
.quorum_entry_for_hash_at_or_before_height(PLATFORM_TYPE, below_hash, height)
.is_none(),
"quorum below the floor must not be walked to"
);
}
}
Loading