Repository navigation
fix(dash-spv)!: verify MnListDiff against its block's coinbase before applying #1200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
0499656
2359fca
55e4015
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ use std::sync::Arc; | |
|
|
||
| use crate::error::{StorageError, StorageResult}; | ||
| use crate::storage::BlockHeaderStorage; | ||
| use crate::validation::MasternodeListValidator; | ||
| use dashcore::bls_sig_utils::BLSSignature; | ||
| use dashcore::hashes::Hash; | ||
| use dashcore::network::constants::NetworkExt; | ||
|
|
@@ -95,6 +96,7 @@ pub struct MasternodeListEngine<H: BlockHeaderStorage> { | |
| pub masternode_lists: BTreeMap<CoreBlockHeight, MasternodeList>, | ||
| rotated_quorums_per_cycle: BTreeMap<BlockHash, BTreeMap<u16, QualifiedQuorumEntry>>, | ||
| network: Network, | ||
| validator: Option<MasternodeListValidator<H>>, | ||
| } | ||
|
|
||
| /// The most quorums of `llmq_type` a run of `blocks` blocks can add: one per | ||
|
|
@@ -236,11 +238,26 @@ impl<H: BlockHeaderStorage> Drop for QRInfoRollback<'_, H> { | |
| impl<H: BlockHeaderStorage> MasternodeListEngine<H> { | ||
| /// An empty engine for `network` that looks block heights up in `headers`. | ||
| pub fn new(network: Network, headers: Arc<RwLock<H>>) -> Self { | ||
| let validator = MasternodeListValidator::new(Arc::clone(&headers)); | ||
| Self::with_validator(network, headers, Some(validator)) | ||
| } | ||
|
|
||
| #[cfg(any(test, feature = "test-utils"))] | ||
| pub fn new_without_validation(network: Network, headers: Arc<RwLock<H>>) -> Self { | ||
| Self::with_validator(network, headers, None) | ||
| } | ||
|
|
||
| fn with_validator( | ||
| network: Network, | ||
| headers: Arc<RwLock<H>>, | ||
| validator: Option<MasternodeListValidator<H>>, | ||
| ) -> Self { | ||
| Self { | ||
| headers, | ||
| masternode_lists: BTreeMap::new(), | ||
| rotated_quorums_per_cycle: BTreeMap::new(), | ||
| network, | ||
| validator, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1005,9 +1022,23 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> { | |
| let base_block_hash = masternode_list_diff.base_block_hash; | ||
| let base_height = self.required_height_of(&base_block_hash).await?; | ||
| let diff_end_height = self.required_height_of(&block_hash).await?; | ||
|
|
||
| let coinbase_tx = match &self.validator { | ||
| Some(validator) => { | ||
| validator.validate_coinbase_proof(&masternode_list_diff, diff_end_height).await?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve header-storage errors during proof validation. If
|
||
| Some(masternode_list_diff.coinbase_tx.clone()) | ||
| } | ||
| None => None, | ||
| }; | ||
|
|
||
| if base_height == 0 { | ||
| let masternode_list = | ||
| MasternodeList::from_diff(masternode_list_diff, diff_end_height, self.network)?; | ||
|
|
||
| if let (Some(validator), Some(coinbase_tx)) = (&self.validator, &coinbase_tx) { | ||
| validator.validate_coinbase_roots(&masternode_list, coinbase_tx)?; | ||
| } | ||
|
|
||
| self.masternode_lists.insert(diff_end_height, masternode_list); | ||
| return Ok(()); | ||
| } | ||
|
|
@@ -1017,6 +1048,11 @@ impl<H: BlockHeaderStorage> MasternodeListEngine<H> { | |
| }; | ||
| let masternode_list = | ||
| base_masternode_list.apply_diff(masternode_list_diff, diff_end_height, self.network)?; | ||
|
|
||
| if let (Some(validator), Some(coinbase_tx)) = (&self.validator, &coinbase_tx) { | ||
| validator.validate_coinbase_roots(&masternode_list, coinbase_tx)?; | ||
| } | ||
|
|
||
| // A quorum the diff brings may already have a status in another list. | ||
| let mut changes = Vec::new(); | ||
| for (quorum_type, quorums) in masternode_list.quorums.iter() { | ||
|
|
@@ -1146,28 +1182,43 @@ pub(crate) mod test_support { | |
| network: Network, | ||
| heights: HashMap<BlockHash, CoreBlockHeight>, | ||
| ) -> Self { | ||
| Self::new(network, Arc::new(RwLock::new(MockHeaderStorage(heights)))) | ||
| Self::new_without_validation( | ||
| network, | ||
| Arc::new(RwLock::new(MockHeaderStorage::new(heights))), | ||
| ) | ||
| } | ||
|
|
||
| pub(crate) fn headers(&self) -> Arc<RwLock<MockHeaderStorage>> { | ||
| self.headers.clone() | ||
| } | ||
|
|
||
| /// Stands in for the headers a fixture lacks. | ||
| pub(crate) fn feed_merkle_roots<'a>( | ||
| &mut self, | ||
| diffs: impl IntoIterator<Item = &'a MnListDiff>, | ||
| ) { | ||
| let mut headers = self.headers.try_write().expect("unlocked"); | ||
| for diff in diffs { | ||
| let merkle_root = diff.coinbase_merkle_root().expect("proves its coinbase"); | ||
| headers.merkle_roots.insert(diff.block_hash, merkle_root); | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn feed_block_height(&mut self, height: CoreBlockHeight, block_hash: BlockHash) { | ||
| self.headers.try_write().expect("unlocked").0.insert(block_hash, height); | ||
| self.headers.try_write().expect("unlocked").heights.insert(block_hash, height); | ||
| } | ||
|
|
||
| pub(crate) fn forget_block(&mut self, block_hash: &BlockHash) { | ||
| self.headers.try_write().expect("unlocked").0.remove(block_hash); | ||
| self.headers.try_write().expect("unlocked").heights.remove(block_hash); | ||
| } | ||
|
|
||
| pub(crate) fn block_height(&self, block_hash: &BlockHash) -> Option<CoreBlockHeight> { | ||
| self.headers.try_read().expect("unlocked").0.get(block_hash).copied() | ||
| self.headers.try_read().expect("unlocked").heights.get(block_hash).copied() | ||
| } | ||
|
|
||
| pub(crate) fn block_hash_at(&self, height: CoreBlockHeight) -> Option<BlockHash> { | ||
| let headers = self.headers.try_read().expect("unlocked"); | ||
| headers.0.iter().find(|(_, h)| **h == height).map(|(block_hash, _)| *block_hash) | ||
| headers.heights.iter().find(|(_, h)| **h == height).map(|(block_hash, _)| *block_hash) | ||
| } | ||
|
|
||
| /// Verifies `chain_lock` as the ChainLock manager does. | ||
|
|
@@ -1214,9 +1265,8 @@ pub(crate) mod test_support { | |
| #[cfg(test)] | ||
| mod tests { | ||
| use super::test_support::{fixture_heights, quorum_entry, TestEngine}; | ||
| use super::{ | ||
| build_cycle_quorum_map, FeedError, MasternodeListEngine, QRInfoFeedResult, WORK_DIFF_DEPTH, | ||
| }; | ||
| use super::{build_cycle_quorum_map, qr_info_diffs}; | ||
| use super::{FeedError, MasternodeListEngine, QRInfoFeedResult, WORK_DIFF_DEPTH}; | ||
| use crate::error::{StorageError, StorageResult}; | ||
| use crate::storage::{BlockHeaderStorage, BlockHeaderTip}; | ||
| use crate::test_utils::MockHeaderStorage; | ||
|
|
@@ -1236,6 +1286,7 @@ mod tests { | |
| use dashcore::sml::llmq_type::network::NetworkLLMQExt; | ||
| use dashcore::sml::llmq_type::{LLMQType, QUORUM_MEMBER_LIST_OFFSET}; | ||
| use dashcore::sml::masternode_list::MasternodeList; | ||
| use dashcore::sml::masternode_list_entry::MasternodeListEntry; | ||
| use dashcore::sml::quorum_entry::qualified_quorum_entry::{ | ||
| QualifiedQuorumEntry, VerifyingChainLockSignaturesType, | ||
| }; | ||
|
|
@@ -1246,6 +1297,7 @@ mod tests { | |
| use std::sync::atomic::{AtomicUsize, Ordering}; | ||
| use std::sync::Arc; | ||
| use std::time::Duration; | ||
| use test_case::test_case; | ||
| use tokio::sync::RwLock; | ||
|
|
||
| fn make_qualified_quorum_entry( | ||
|
|
@@ -1579,15 +1631,17 @@ mod tests { | |
| /// engine state mirrors production cold start: empty masternode lists | ||
| /// over the captured header heights. | ||
| fn load_qrinfo_2518986_fixture() -> (TestEngine, QRInfo) { | ||
| let engine = TestEngine::knowing( | ||
| let heights = fixture_heights(include_bytes!( | ||
| "../../dash/tests/data/test_DML_diffs/block_container_2518986.dat" | ||
| )); | ||
| let mut engine = TestEngine::new( | ||
| Network::Mainnet, | ||
| fixture_heights(include_bytes!( | ||
| "../../dash/tests/data/test_DML_diffs/block_container_2518986.dat" | ||
| )), | ||
| Arc::new(RwLock::new(MockHeaderStorage::new(heights))), | ||
| ); | ||
| let qr_info: QRInfo = decode_fixture(include_bytes!( | ||
| "../../dash/tests/data/test_DML_diffs/qrinfo_2518986.dat" | ||
| )); | ||
| engine.feed_merkle_roots(qr_info_diffs(&qr_info)); | ||
| (engine, qr_info) | ||
| } | ||
|
|
||
|
|
@@ -1647,9 +1701,27 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test_case(true; "matching its commitments")] | ||
| #[test_case(false; "off its masternode list root")] | ||
| #[tokio::test] | ||
| async fn validate_first_qr_info_on_fresh_engine_with_mixed_cycle_quorums() { | ||
| let (mut engine, qr_info) = load_qrinfo_2518986_fixture(); | ||
| async fn validate_first_qr_info_on_fresh_engine_with_mixed_cycle_quorums(committed: bool) { | ||
| let (mut engine, mut qr_info) = load_qrinfo_2518986_fixture(); | ||
| if !committed { | ||
| let tip = &mut qr_info.mn_list_diff_tip; | ||
| tip.new_masternodes.push(MasternodeListEntry::dummy(0x11)); | ||
| let tip_hash = tip.block_hash; | ||
| let result = engine.feed_qr_info(qr_info).await; | ||
| assert!( | ||
| matches!( | ||
| result, | ||
| Err(FeedError::Refused(QuorumValidationError::SMLError( | ||
| SmlError::CoinbaseCommitmentMismatch(hash) | ||
| ))) if hash == tip_hash | ||
| ), | ||
| "got {result:?}" | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // The fixture's active set mixes cycles: quorum index 0 carries the | ||
| // previous cycle's commitment (its DKG failed in the current cycle), | ||
|
|
@@ -2084,14 +2156,22 @@ mod tests { | |
| unreadable: BlockHash, | ||
| readable: usize, | ||
| ) -> MasternodeListEngine<FailingFrom> { | ||
| let inner = { | ||
| let headers = engine.headers(); | ||
| let headers = headers.try_read().expect("unlocked"); | ||
| MockHeaderStorage { | ||
| heights: headers.heights.clone(), | ||
| merkle_roots: headers.merkle_roots.clone(), | ||
| } | ||
| }; | ||
| let headers = Arc::new(RwLock::new(FailingFrom { | ||
| inner: MockHeaderStorage(engine.headers().try_read().expect("unlocked").0.clone()), | ||
| inner, | ||
| unreadable, | ||
| readable, | ||
| reads: AtomicUsize::new(0), | ||
| hangs: false, | ||
| })); | ||
| let mut failing = MasternodeListEngine::new(engine.network, headers); | ||
| let mut failing = MasternodeListEngine::new_without_validation(engine.network, headers); | ||
| failing.masternode_lists = engine.masternode_lists.clone(); | ||
| failing.rotated_quorums_per_cycle = engine.rotated_quorums_per_cycle.clone(); | ||
| failing | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.