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
2 changes: 1 addition & 1 deletion dash-spv/src/bin/generate_checkpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
headers.push(client.header_at(h).await?.ok_or(format!("header {h} not stored"))?);
}

BlockHeaderValidator::new()
BlockHeaderValidator::new(network, vec![], height - CHECKPOINT_HEADERS as u32)
.validate(&headers)
.map_err(|e| format!("checkpoint {height} is invalid: {e}"))?;

Expand Down
9 changes: 6 additions & 3 deletions dash-spv/src/chain/checkpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,12 @@ mod tests {
assert!(manager.checkpoints().next().is_some(), "{network} must have checkpoints");

for checkpoint in manager.checkpoints() {
BlockHeaderValidator::new().validate(checkpoint.headers()).unwrap_or_else(|e| {
panic!("checkpoint {} of {network} is invalid: {e}", checkpoint.height())
});
let before = checkpoint.height() - CHECKPOINT_HEADERS as u32;
BlockHeaderValidator::new(network, vec![], before)
.validate(checkpoint.headers())
.unwrap_or_else(|e| {
panic!("checkpoint {} of {network} is invalid: {e}", checkpoint.height())
});
}
}

Expand Down
1 change: 1 addition & 0 deletions dash-spv/src/client/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ impl<W: WalletInterface, N: NetworkManager> DashSpvClient<W, N> {
storage.block_headers(),
storage.metadata(),
checkpoint_manager,
config.network,
)
.await?,
);
Expand Down
9 changes: 9 additions & 0 deletions dash-spv/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ pub enum ValidationError {
#[error("Invalid proof of work")]
InvalidProofOfWork,

#[error(
"Invalid difficulty at height {height}: bits {actual:#010x}, required {required:#010x}"
)]
InvalidDifficulty {
height: u32,
actual: u32,
required: u32,
},

#[error("Invalid header chain: {0}")]
InvalidHeaderChain(String),

Expand Down
39 changes: 33 additions & 6 deletions dash-spv/src/sync/block_headers/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use crate::validation::{BlockHeaderValidator, Validator};
use dashcore::block::Header;
use dashcore::ephemerealdata::chain_lock::ChainLock;
use dashcore::network::message_blockdata::Inventory;
use dashcore::BlockHash;
use dashcore::pow::DGW_PAST_BLOCKS;
use dashcore::{BlockHash, Network};
use tokio::sync::RwLock;

/// Headers manager for downloading and validating block headers.
Expand All @@ -52,6 +53,8 @@ pub struct BlockHeadersManager<H: BlockHeaderStorage, M: MetadataStorage> {
/// Peers we've sent a GetHeaders to after sync, so Dash Core knows our tip
/// and can send us header announcements instead of inv.
pub(super) announced_peers: HashSet<SocketAddr>,
/// Network whose difficulty rules new headers are checked against.
network: Network,
}

impl<H: BlockHeaderStorage, M: MetadataStorage> std::fmt::Debug for BlockHeadersManager<H, M> {
Expand All @@ -69,6 +72,7 @@ impl<H: BlockHeaderStorage, M: MetadataStorage> BlockHeadersManager<H, M> {
header_storage: Arc<RwLock<H>>,
metadata_storage: Arc<RwLock<M>>,
checkpoint_manager: &'static CheckpointManager,
network: Network,
) -> SyncResult<Self> {
let tip = header_storage
.read()
Expand Down Expand Up @@ -96,6 +100,7 @@ impl<H: BlockHeaderStorage, M: MetadataStorage> BlockHeadersManager<H, M> {
checkpoint_manager,
pending_announcements: HashMap::new(),
announced_peers: HashSet::new(),
network,
})
}

Expand All @@ -112,8 +117,23 @@ impl<H: BlockHeaderStorage, M: MetadataStorage> BlockHeadersManager<H, M> {
async fn store_headers(&mut self, headers: &[HashedBlockHeader]) -> SyncResult<BlockHeaderTip> {
debug_assert!(!headers.is_empty());

// Validate batch for internal continuity and PoW
BlockHeaderValidator::new().validate(headers)?;
// Validate batch for internal continuity, PoW and difficulty
let tip = self.tip().await?;
let storage = self.header_storage.read().await;

// A storage started at a checkpoint holds less history than a full window.
let first = tip
.height()
.saturating_sub(DGW_PAST_BLOCKS as u32 - 1)
.max(storage.get_start_height().await.unwrap_or(0));

let ancestors = storage.load_headers(first..tip.height() + 1).await?;

drop(storage);

let ancestors = ancestors.iter().map(|header| *header.header()).collect();

BlockHeaderValidator::new(self.network, ancestors, tip.height()).validate(headers)?;

// Store headers
self.header_storage.write().await.store_headers(headers).await?;
Expand Down Expand Up @@ -395,9 +415,14 @@ mod tests {
.await
.unwrap();
let checkpoint_manager = create_test_checkpoint_manager();
BlockHeadersManager::new(storage.block_headers(), storage.metadata(), checkpoint_manager)
.await
.expect("Failed to create BlockHeadersManager")
BlockHeadersManager::new(
storage.block_headers(),
storage.metadata(),
checkpoint_manager,
Network::Regtest,
)
.await
.expect("Failed to create BlockHeadersManager")
}

#[test_case(None, 5, Some(5); "no ChainLock")]
Expand All @@ -424,6 +449,7 @@ mod tests {
storage.block_headers(),
storage.metadata(),
create_test_checkpoint_manager(),
Network::Regtest,
)
.await
.unwrap();
Expand Down Expand Up @@ -459,6 +485,7 @@ mod tests {
storage.block_headers(),
storage.metadata(),
create_test_checkpoint_manager(),
Network::Regtest,
)
.await
.unwrap();
Expand Down
84 changes: 72 additions & 12 deletions dash-spv/src/validation/header.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
use rayon::prelude::*;
use std::time::Instant;

use dashcore::block::Header;
use dashcore::consensus::Params;
use dashcore::Network;

use crate::error::{ValidationError, ValidationResult};
use crate::types::HashedBlockHeader;
use crate::validation::Validator;

#[derive(Default)]
pub struct BlockHeaderValidator {}
/// Checks a batch of headers for continuity, proof of work and the difficulty Dash Core
/// requires. Headers without enough history before them are not checked for difficulty.
pub struct BlockHeaderValidator {
params: &'static Params,
/// The stored headers the batch extends, oldest first.
ancestors: Vec<Header>,
/// Height of the last of `ancestors`.
tip_height: u32,
}

impl BlockHeaderValidator {
pub fn new() -> Self {
Self {}
/// A validator for a batch on `network` extending `ancestors`, the last of which is at
/// `tip_height`.
pub fn new(network: Network, ancestors: Vec<Header>, tip_height: u32) -> Self {
Self {
params: Params::new(network),
ancestors,
tip_height,
}
}
}

impl Validator<&[HashedBlockHeader]> for BlockHeaderValidator {
fn validate(&self, hashed_headers: &[HashedBlockHeader]) -> ValidationResult<()> {
let start = Instant::now();

// Check PoW of i and continuity of i-1 to i in parallel
// The headers each one's difficulty is computed from: the ancestors, then the batch
let chain: Vec<Header> = self
.ancestors
.iter()
.copied()
.chain(hashed_headers.iter().map(|h| *h.header()))
.collect();

// Check continuity of i-1 to i, PoW and difficulty of i in parallel
hashed_headers.par_iter().enumerate().try_for_each(|(i, header)| {
// For the first header, skip chain continuity check since we don't have i-1 here
if i > 0 && header.header().prev_blockhash != *hashed_headers[i - 1].hash() {
Expand All @@ -32,6 +57,22 @@ impl Validator<&[HashedBlockHeader]> for BlockHeaderValidator {
if !header.header().target().is_met_by(*header.hash()) {
return Err(ValidationError::InvalidProofOfWork);
}

// Check the bits against the difficulty required at its height
let height = self.tip_height + 1 + i as u32;
let window = &chain[..self.ancestors.len() + i];
let bits = header.header().bits;
let required = self.params.next_work_required(window, height - 1, header.header().time);

if let Some(required) =
required.filter(|required| !self.params.bits_meet_required(height, bits, *required))
{
return Err(ValidationError::InvalidDifficulty {
height,
actual: bits.to_consensus(),
required: required.to_consensus(),
});
}
Ok(())
})?;

Expand All @@ -58,30 +99,30 @@ mod tests {

#[test]
fn test_empty_headers() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

assert!(validator.validate(&[]).is_ok());
}

#[test]
fn test_single_header() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

let headers = hashed(Header::dummy_chain(1, BlockHash::all_zeros()));
assert!(validator.validate(&headers).is_ok());
}

#[test]
fn test_valid_chain() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

let headers = hashed(Header::dummy_chain(10, BlockHash::all_zeros()));
assert!(validator.validate(&headers).is_ok());
}

#[test]
fn test_broken_chain() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

let mut headers = Header::dummy_chain(3, BlockHash::all_zeros());
// The third header doesn't connect to the second
Expand All @@ -93,7 +134,7 @@ mod tests {

#[test]
fn test_invalid_pow() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

// `Header::dummy` has a hard target its hash doesn't meet
let result = validator.validate(&hashed(vec![Header::dummy(0)]));
Expand All @@ -102,7 +143,7 @@ mod tests {

#[test]
fn test_genesis_blocks() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

for network in [Network::Mainnet, Network::Testnet, Network::Regtest] {
let genesis = HashedBlockHeader::from(genesis_block(network).header);
Expand All @@ -116,7 +157,7 @@ mod tests {

#[test]
fn test_invalid_pow_mid_chain() {
let validator = BlockHeaderValidator::new();
let validator = BlockHeaderValidator::new(Network::Regtest, vec![], 0);

// The third header has valid continuity but a hard target its hash doesn't meet
let mut headers = Header::dummy_chain(2, BlockHash::all_zeros());
Expand All @@ -130,4 +171,23 @@ mod tests {
let result = validator.validate(&hashed(headers));
assert!(matches!(result, Err(ValidationError::InvalidProofOfWork)));
}

#[test]
fn test_easier_difficulty() {
let headers = Header::mainnet_fixture();
let mut batch = headers[24..].to_vec();
// The easy target of the dummy chain, which the header's hash still meets
batch.last_mut().unwrap().bits = Header::dummy_chain(1, BlockHash::all_zeros())[0].bits;

let validator =
BlockHeaderValidator::new(Network::Mainnet, headers[..24].to_vec(), 1999999);
let result = validator.validate(&hashed(batch));
assert!(matches!(
result,
Err(ValidationError::InvalidDifficulty {
height: 2000005,
..
})
));
}
}
7 changes: 7 additions & 0 deletions dash/src/consensus/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub struct Params {
pub no_pow_retargeting: bool,
/// Block height from which the difficulty follows Dark Gravity Wave.
pub pow_dgw_height: u32,
/// Last block height whose bits only have to be within 50% of Dark Gravity Wave's, as early
/// mainnet ran its first versions.
pub pow_dgw_tolerant_height: Option<u32>,
}

impl Params {
Expand All @@ -73,6 +76,7 @@ impl Params {
allow_min_difficulty_blocks: false,
no_pow_retargeting: false,
pow_dgw_height: 34140,
pow_dgw_tolerant_height: Some(68589),
};

/// Testnet parameters.
Expand All @@ -90,6 +94,7 @@ impl Params {
allow_min_difficulty_blocks: true,
no_pow_retargeting: false,
pow_dgw_height: 4002,
pow_dgw_tolerant_height: None,
};

/// Devnet parameters, with Dash Core's defaults: each devnet can override its difficulty rules.
Expand All @@ -107,6 +112,7 @@ impl Params {
allow_min_difficulty_blocks: true,
no_pow_retargeting: false,
pow_dgw_height: 4001,
pow_dgw_tolerant_height: None,
};

/// Regtest parameters.
Expand All @@ -124,6 +130,7 @@ impl Params {
allow_min_difficulty_blocks: true,
no_pow_retargeting: true,
pow_dgw_height: 34140,
pow_dgw_tolerant_height: None,
};

/// The parameters of `network`.
Expand Down
Loading
Loading