diff --git a/dash-spv/src/bin/generate_checkpoints.rs b/dash-spv/src/bin/generate_checkpoints.rs index d19d4b7fd..b4dfb8cfb 100644 --- a/dash-spv/src/bin/generate_checkpoints.rs +++ b/dash-spv/src/bin/generate_checkpoints.rs @@ -120,7 +120,7 @@ async fn main() -> Result<(), Box> { 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}"))?; diff --git a/dash-spv/src/chain/checkpoints.rs b/dash-spv/src/chain/checkpoints.rs index e3871e4c9..d06a6bfaf 100644 --- a/dash-spv/src/chain/checkpoints.rs +++ b/dash-spv/src/chain/checkpoints.rs @@ -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()) + }); } } diff --git a/dash-spv/src/client/lifecycle.rs b/dash-spv/src/client/lifecycle.rs index 587d9088e..8399659d9 100644 --- a/dash-spv/src/client/lifecycle.rs +++ b/dash-spv/src/client/lifecycle.rs @@ -125,6 +125,7 @@ impl DashSpvClient { storage.block_headers(), storage.metadata(), checkpoint_manager, + config.network, ) .await?, ); diff --git a/dash-spv/src/error.rs b/dash-spv/src/error.rs index 12eaab0c5..14068c5d0 100644 --- a/dash-spv/src/error.rs +++ b/dash-spv/src/error.rs @@ -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), diff --git a/dash-spv/src/sync/block_headers/manager.rs b/dash-spv/src/sync/block_headers/manager.rs index 397c5e69c..426046a2c 100644 --- a/dash-spv/src/sync/block_headers/manager.rs +++ b/dash-spv/src/sync/block_headers/manager.rs @@ -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. @@ -52,6 +53,8 @@ pub struct BlockHeadersManager { /// 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, + /// Network whose difficulty rules new headers are checked against. + network: Network, } impl std::fmt::Debug for BlockHeadersManager { @@ -69,6 +72,7 @@ impl BlockHeadersManager { header_storage: Arc>, metadata_storage: Arc>, checkpoint_manager: &'static CheckpointManager, + network: Network, ) -> SyncResult { let tip = header_storage .read() @@ -96,6 +100,7 @@ impl BlockHeadersManager { checkpoint_manager, pending_announcements: HashMap::new(), announced_peers: HashSet::new(), + network, }) } @@ -112,8 +117,23 @@ impl BlockHeadersManager { async fn store_headers(&mut self, headers: &[HashedBlockHeader]) -> SyncResult { 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?; @@ -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")] @@ -424,6 +449,7 @@ mod tests { storage.block_headers(), storage.metadata(), create_test_checkpoint_manager(), + Network::Regtest, ) .await .unwrap(); @@ -459,6 +485,7 @@ mod tests { storage.block_headers(), storage.metadata(), create_test_checkpoint_manager(), + Network::Regtest, ) .await .unwrap(); diff --git a/dash-spv/src/validation/header.rs b/dash-spv/src/validation/header.rs index ed6bfc015..02e66738a 100644 --- a/dash-spv/src/validation/header.rs +++ b/dash-spv/src/validation/header.rs @@ -1,16 +1,33 @@ 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
, + /// 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
, tip_height: u32) -> Self { + Self { + params: Params::new(network), + ancestors, + tip_height, + } } } @@ -18,7 +35,15 @@ 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
= 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() { @@ -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(()) })?; @@ -58,14 +99,14 @@ 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()); @@ -73,7 +114,7 @@ mod tests { #[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()); @@ -81,7 +122,7 @@ mod tests { #[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 @@ -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)])); @@ -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); @@ -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()); @@ -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, + .. + }) + )); + } } diff --git a/dash/src/consensus/params.rs b/dash/src/consensus/params.rs index a05499491..9ed599b22 100644 --- a/dash/src/consensus/params.rs +++ b/dash/src/consensus/params.rs @@ -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, } impl Params { @@ -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. @@ -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. @@ -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. @@ -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`. diff --git a/dash/src/pow.rs b/dash/src/pow.rs index 652bfe2bf..d4fc9f674 100644 --- a/dash/src/pow.rs +++ b/dash/src/pow.rs @@ -13,6 +13,9 @@ use core::ops::{Add, Sub}; use dash_num::Arith256; use dash_types::Numeric; +use crate::Network; +use crate::block::Header; +use crate::consensus::Params; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::hash_types::BlockHash; use crate::io::{self, Read, Write}; @@ -416,6 +419,97 @@ impl Decodable for CompactTarget { } } +pub const DGW_PAST_BLOCKS: usize = 24; + +impl Params { + pub fn next_work_required( + &self, + window: &[Header], + last_height: u32, + time: u32, + ) -> Option { + if self.network == Network::Devnet || last_height.saturating_add(1) < self.pow_dgw_height { + return None; + } + + if self.no_pow_retargeting { + return Some(self.pow_limit.to_compact_lossy()); + } + + let last = window.last()?; + let elapsed = i64::from(time) - i64::from(last.time); + let spacing = self.pow_target_spacing as i64; + + if self.allow_min_difficulty_blocks && elapsed > 2 * 60 * 60 { + return Some(self.pow_limit.to_compact_lossy()); + } + + if self.allow_min_difficulty_blocks && elapsed > spacing * 4 { + let target = Target(Target::from_compact(last.bits).0 * 10); + return Some(target.min(self.pow_limit).to_compact_lossy()); + } + + let window = &window[window.len().checked_sub(DGW_PAST_BLOCKS)?..]; + + Some(self.dark_gravity_wave(window).to_compact_lossy()) + } + + /// Whether `bits` meets the `required` difficulty at `height`, as Core's + /// `ContextualCheckBlockHeader` judges it. + pub fn bits_meet_required( + &self, + height: u32, + bits: CompactTarget, + required: CompactTarget, + ) -> bool { + if self.pow_dgw_tolerant_height.is_some_and(|tolerant| height <= tolerant) { + let actual = bits_to_difficulty(bits); + let required = bits_to_difficulty(required); + return (actual - required).abs() <= actual * 0.5; + } + bits == required + } + + /// Dash Core's `DarkGravityWave` over exactly [`DGW_PAST_BLOCKS`] headers, oldest first. + fn dark_gravity_wave(&self, window: &[Header]) -> Target { + // Not quite an average, but the one Core computes, newest first. + let mut average = Arith256::ZERO; + for (count, header) in (1u32..).zip(window.iter().rev()) { + let target = Target::from_compact(header.bits).0; + average = if count == 1 { + target + } else { + (average * count + target) / u64::from(count + 1) + }; + } + + // Spans one interval fewer than the target timespan, as in Core. + let target_timespan = DGW_PAST_BLOCKS as i64 * self.pow_target_spacing as i64; + let actual_timespan = (i64::from(window[DGW_PAST_BLOCKS - 1].time) + - i64::from(window[0].time)) + .clamp(target_timespan / 3, target_timespan * 3); + + let target = Target(average * actual_timespan as u32 / target_timespan as u64); + target.min(self.pow_limit) + } +} + +/// Dash Core's `ConvertBitsToDouble`. +fn bits_to_difficulty(bits: CompactTarget) -> f64 { + let bits = bits.to_consensus(); + let mut shift = (bits >> 24) & 0xff; + let mut difficulty = f64::from(0x0000ffff) / f64::from(bits & 0x00ffffff); + while shift < 29 { + difficulty *= 256.0; + shift += 1; + } + while shift > 29 { + difficulty /= 256.0; + shift -= 1; + } + difficulty +} + // Target::MAX as a float value. Calculated with `Arith256::to_f64`. // This is validated in the unit tests as well. const TARGET_MAX_F64: f64 = 2.695953529101131e67; @@ -462,6 +556,49 @@ mod tests { } } + #[test_case(Network::Mainnet, 1_999_999, 182, Some(0x194c14f3); "mainnet dgw")] + #[test_case(Network::Devnet, 2_000_000, 0, None; "devnet")] + #[test_case(Network::Mainnet, 34_138, 0, None; "before dgw")] + #[test_case(Network::Regtest, 34_139, 0, Some(0x207fffff); "regtest")] + #[test_case(Network::Testnet, 2_000_000, 2 * 60 * 60 + 1, Some(0x1e0fffff); "testnet after two hours")] + #[test_case(Network::Testnet, 2_000_000, 4 * 150 + 1, Some(0x1a03023d); "testnet after four spacings")] + fn next_work_required_rules( + network: Network, + last_height: u32, + elapsed: u32, + required: Option, + ) { + let headers = Header::mainnet_fixture(); + let window = &headers[..DGW_PAST_BLOCKS]; + let time = window[DGW_PAST_BLOCKS - 1].time + elapsed; + + assert_eq!( + Params::new(network).next_work_required(window, last_height, time), + required.map(CompactTarget::from_consensus) + ); + } + + // The first DGW header on mainnet, 34140, whose bits differ from DGW's. + #[test_case(Network::Mainnet, 34_140, 0x1c00f8a9, true; "mainnet first dgw header")] + #[test_case(Network::Mainnet, 68_589, 0x1c00f8a9, true; "mainnet last tolerant height")] + #[test_case(Network::Mainnet, 68_590, 0x1c00f8a9, false; "mainnet first exact height")] + #[test_case(Network::Mainnet, 68_590, 0x1c00f987, true; "mainnet exact bits")] + #[test_case(Network::Mainnet, 68_589, 0x1c01f154, false; "mainnet under half the difficulty")] + #[test_case(Network::Mainnet, 68_589, 0x1c007cc0, false; "mainnet over twice the difficulty")] + #[test_case(Network::Testnet, 34_140, 0x1c00f8a9, false; "testnet")] + fn bits_meet_required_rules(network: Network, height: u32, bits: u32, meets: bool) { + let required = CompactTarget::from_consensus(0x1c00f987); + + assert_eq!( + Params::new(network).bits_meet_required( + height, + CompactTarget::from_consensus(bits), + required + ), + meets + ); + } + #[test] fn compact_target_from_hex_str_happy_path() { let actual = CompactTarget::from_hex_str("0x01003456").unwrap(); diff --git a/dash/src/test_utils/block.rs b/dash/src/test_utils/block.rs index 93a2ae497..a1fe2195b 100644 --- a/dash/src/test_utils/block.rs +++ b/dash/src/test_utils/block.rs @@ -37,6 +37,25 @@ impl Header { } } + /// Mainnet headers 1999976 to 2000005: 24 headers of history for Dark Gravity Wave, + /// then six whose `bits` it reproduces. + pub fn mainnet_fixture() -> Vec { + include_str!("../../tests/data/mainnet_headers_1999976.txt") + .lines() + .map(|line| { + let [_, hash, header] = line.split(' ').collect::>()[..] else { + panic!("height, hash and header"); + }; + let header: Header = crate::consensus::deserialize( + &hex_conservative::decode_to_vec(header).unwrap(), + ) + .unwrap(); + assert_eq!(header.block_hash().to_string(), hash); + header + }) + .collect() + } + pub fn dummy_batch(height_range: Range) -> Vec { height_range.map(Self::dummy).collect() } diff --git a/dash/tests/data/mainnet_headers_1999976.txt b/dash/tests/data/mainnet_headers_1999976.txt new file mode 100644 index 000000000..7d63c2c7b --- /dev/null +++ b/dash/tests/data/mainnet_headers_1999976.txt @@ -0,0 +1,30 @@ +1999976 00000000000000138d8b2dd784e4b8267e721e883ae565c8b67b1a37322be0b3 0000002007ea0913a26961269d12213460d2244a6df44f367fe7f6a01c00000000000000d041a240576e772e2356c5fd8db14d9d021d729154ef2fa06c481b37d5f5bc9642a29865518a3a19481037fe +1999977 00000000000000054eb25a634864e4da659d530380466fe34c34304c70c426ac 00000020b3e02b32371a7bb6c865e53a881e727e26b8e484d72d8b8d13000000000000009ae06edf064838c038ea8ec7b37b5243472bb206b0c4f42b31efd4e45866d96164a2986580314019635010fe +1999978 00000000000000033d5e70755e96eb0261a27eba7388f3d0bf7517ff2c79ae19 00000020ac26c4704c30344ce36f468003539d65dae46448635ab24e050000000000000015342d949823b06b2bcd094c67166bd8cff4efd4b3970f160c7c44eb9af8ba80cca39865c7003f194e4dd864 +1999979 0000000000000027fdecaa95e75340e429d74c6198926d3306e401a63e3113f9 0000002019ae792cff1775bfd0f38873ba7ea26102eb965e75705e3d0300000000000000e4e5adc2e448afbb37495a6e36225849a2fa1d171f3a54c35565d64baa47b8c3f8a39865283b44199bcef4ec +1999980 0000000000000037250fbeb58228dc6b60a1ec8d8dfcd4c7c691382f3a73035d 00000020f913313ea601e406336d9298614cd729e44053e795aaecfd2700000000000000c06d819cc44afab953d8a7d6a6e7e23cbb4625bef4bde5e7f67a723097c2a3f250a49865ef214419ae1e7839 +1999981 0000000000000016ca183790d8c679506bd6b64f95338e1be3e25ef098a45fcb 000000205d03733a2f3891c6c7d4fc8d8deca1606bdc2882b5be0f253700000000000000b549b1c3f2ba8b52028d3cc5f74e58aa2fd90bbe514e84641afa503db2f042f95ba49865eff9441999348293 +1999982 000000000000003a41e9f22fe82dc2a4efa2d592a498495ae94659c93869a7e1 00000020cb5fa498f05ee2e31b8e33954fb6d66b5079c6d8903718ca1600000000000000f659ee3c0c744c1bc6db5e8f8f87589d3044a2ed3d6e4f8623ae5328c73e0b3a3ea698654755411924920d12 +1999983 0000000000000025a8d395bc71439a45450eff029b80f3b53575098f0e206fef 00000020e1a76938c95946e95a4998a492d5a2efa4c22de82ff2e9413a000000000000005810346616027bb93fb1a2762f9d55524f83e96f9e50d622dee9b38a984a59ab54a69865196c4919ab2981db +1999984 0000000000000005b6cf960f846f492be6d215f719459a03dac9e1e802a06952 00000020ef6f200e8f097535b5f3809b02ff0e45459a4371bc95d3a8250000000000000089555f304ec6b1d5d87c33e710b0ef8bc965aa8497eb617ce67421fb421222209da79865cce646197a2ec7fd +1999985 00000000000000245a174be3b93880967b1ac687edccff64fa8714f399e87613 000000205269a002e8e1c9da039a4519f715d2e62b496f840f96cfb60500000000000000301a722c884f6033d0db713b196f93c600326d7af728da140a2e44c0a8bf3ada6da8986594f34b19f222ca55 +1999986 0000000000000037ee083383b48e4b1513a66bd730fb8377a68bdadc3c76ac3e 000000201376e899f31487fa64ffcced87c61a7b968038b9e34b175a2400000000000000145688f308c6e4a931c960f92a44b3e24b58150879a14e76655806ffc4e96d29a3a898656ed24d1945809da0 +1999987 000000000000002f764a41233db5984db6def204efc5b47d8db3575908d3726b 000000203eac763cdcda8ba67783fb30d76ba613154b8eb4833308ee3700000000000000f937c27d3effb2934727198cbca29de9e1ae61f864f495e68ded7776580b942450a9986540aa4e193c00d38e +1999988 000000000000002b73ccd2075eceac84f566a311c96295649e21599ffabcca72 000000206b72d3085957b38d7db4c5ef04f2deb64d98b53d23414a762f0000000000000027b557e6283954f4d5cf2c176c255cc6ecd44fa8e9e74877ac5d27b0a1250ab782a9986502a24e1987092536 +1999989 000000000000003b9ed4f08b75bc493b3415f680c9d6dfe67a1b4d98fdea38f2 0000002072cabcfa9f59219e649562c911a366f584acce5e07d2cc732b0000000000000095182fa56e866b66370d745f6ab25b5d506213accda1c92d4c241cbd69f9d9eb33aa986553e34b190337debb +1999990 000000000000002ba2e26d77120241fb3b7a8cf197177f72dee6e175bcbea9bc 00000020f238eafd984d1b7ae6dfd6c980f615343b49bc758bf0d49e3b000000000000001a853793704336957adc1ff68efe338bc086bc68b7221733b1e0f113bab845f6d3aa986525494c19c3619e26 +1999991 000000000000003e2e567d09d3aa48f496441b1396688e64d31d1c892ead5dfc 00000020bca9bebc75e1e6de727f1797f18c7a3bfb410212776de2a22b000000000000007af6a6907eca5bab8d3146c4b7490ca1b55905c2514c709d362d2397bed4d7a87dab9865e87f4b19d49a14ee +1999992 000000000000001dfb58c78cf03be91dd14474a6010cd67b0980592f42ce14d5 00000020fc5dad2e891c1dd3648e6896131b4496f448aad3097d562e3e00000000000000d023f15dfe1e18a10c12b74d1b7f62f6b740504c745c0ca3f2cef02b6f954ff486ab98658cc948194a236398 +1999993 0000000000000037e81132933b4868edd8b172367aa3005cea5409a23cd1cd13 00000020d514ce422f5980097bd60c01a67444d11de93bf08cc758fb1d000000000000008080291fe782dda78680fcc480cef9edd0efcd73bcb2d9e2ca31f6957de2410e20ac9865549747196a78c12b +1999994 000000000000002c0761c9185e5cbf585f303c101acf48bd071ca1eb73d0e5e4 0000002013cdd13ca20954ea5c00a37a3672b1d8ed68483b933211e83700000000000000ecb082ebdef242f4fdf1e7d3f7ab36849fcace759dfc9e4a965430e6e15004dcb0ac986522ae48195a3d8ed6 +1999995 000000000000003abff7975670c723479dc01f7abeb420c6ec78a9c11c42aa8b 00000020e4e5d073eba11c07bd48cf1a103c305f58bf5c5e18c961072c00000000000000e3903aba0df1a7a57e946ff228841daa0a6bfaee6f043cd47fa348a8797e51b9e6ac9865502445192a4e2aa7 +1999996 0000000000000044d56e2919fc1685f14d8fbaef2566f8f9e0cbf8338379965d 000000208baa421cc1a978ecc620b4be7a1fc09d4723c7705697f7bf3a00000000000000ec040a3bc34f6aea40f2e6e369262d13d2bbe251b06ef93074ba8ad00e7ca83d23ad98655b5e45190261fe92 +1999997 0000000000000023490733fe790c4cc82b843c32c63385f6040481b013b26977 000000205d96798333f8cbe0f9f86625efba8f4df18516fc19296ed54400000000000000209d4509686447a7132c5c09f1c0bdbf8494c1ecd430735b09316df98be3bae3cbaf98658c5241198a223937 +1999998 000000000000003b6e2c2847499023dde8c95d06a1779c3b69b54a16c2891564 000000207769b213b0810404f68533c6323c842bc84c0c79fe33074923000000000000009ee8dde6aa5acf7a01432c1f0d1a5fe0a0cd154bb9be609b82827cd364ec1587e5af98650a364c196f84de8e +1999999 000000000000001dc4e83bba91118506a4db6b9cad0dc2cbb45db216280b0632 00000020641589c2164ab5693b9c77a1065dc9e8dd23904947282c6e3b000000000000009fe7b797f2311707b416d43992ec761c8d5a51f75f1fda940e21312cc93558e332b198652b064d194b037712 +2000000 0000000000000009bd68b5e00976c3f7482d4cc12b6596614fbba5678ef13a59 0000002032060b2816b25db4cbc20dad9c6bdba406851191ba3be8c41d0000000000000029b59c356e22194a12227b79e867a56bc730b3b445b285634a1c763696cb3865e8b19865f3144c19c67780e8 +2000001 00000000000000034a513b4b5098496009d110b9409cd3a3803485e2ade35168 00000020593af18e67a5bb4f6196652bc14c2d48f7c37609e0b568bd0900000000000000242844e799a0fe4f1d458c57861557b473ad507620af2bbbec063064fbf1bc3d82b398654ec24f199425c4fc +2000002 000000000000002b19831eb172a23d3d92a9b866ae790dd45fe1f8c106a2cc9a 000000206851e3ade2853480a3d39c40b910d109604998504b3b514a0300000000000000c6a20e62e2f97579f0fce19f15c11d811a3a14e63aa5b89cca35adfe950f3950ebb798657a9f5119a82f37bb +2000003 0000000000000027650f61f8fd387f9d0152e8bde04af831a668b93b596dd945 000000209acca206c1f8e15fd40d79ae66b8a9923d3da272b11e83192b00000000000000cd44f260e4c0898fceaccc50e0450bc87dc4297a4c34899aa751163a8d524cc214b89865efcd6819c3301910 +2000004 0000000000000003d7efa32f843c30a95368c1af29021f393e0f2edaa515ac53 0000002045d96d593bb968a631f84ae0bde852019d7f38fdf8610f65270000000000000008357ffa407f9f314e0d6f1068adeba05810d397e8ebfb83bca3393effde06b039b898650f336b196610cfb1 +2000005 0000000000000034f98dab191bf077c0e0f036a918cab535c7b50d492a06d321 0000002053ac15a5da2e0f3e391f0229afc16853a9303c842fa3efd70300000000000000875565ddf2a1143108542d1fe3e15336966186146b6a1776d38a22afb2b7cc347bb89865e5176e19664e5a21