diff --git a/src/cli/args.rs b/src/cli/args.rs index 04b0326..54f58ad 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -113,6 +113,9 @@ pub struct StartArgs { #[arg(long)] pub peer_publish_interval: Option, + + #[arg(long)] + pub debug_print_chain: bool, } #[derive(Clone, Parser, Debug)] diff --git a/src/cli/commands/util.rs b/src/cli/commands/util.rs index 97a4941..edf3871 100644 --- a/src/cli/commands/util.rs +++ b/src/cli/commands/util.rs @@ -96,6 +96,7 @@ pub async fn server( .unwrap_or_else(|| "tari-p2pool".to_string()), ); config_builder.with_peer_publish_interval(args.peer_publish_interval); + config_builder.with_debug_print_chain(args.debug_print_chain); if let Some(stats_server_port) = args.stats_server_port { config_builder.with_stats_server_port(stats_server_port); } diff --git a/src/server/config.rs b/src/server/config.rs index 7bafb36..7d0775c 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -155,6 +155,11 @@ impl ConfigBuilder { self } + pub fn with_debug_print_chain(&mut self, config: bool) -> &mut Self { + self.config.p2p_service.debug_print_chain = config; + self + } + pub fn build(&self) -> Config { self.config.clone() } diff --git a/src/server/p2p/network.rs b/src/server/p2p/network.rs index 6e1d53c..4beb01d 100644 --- a/src/server/p2p/network.rs +++ b/src/server/p2p/network.rs @@ -4,12 +4,14 @@ use std::{ collections::HashMap, fmt::Display, + fs, hash::Hash, + io::Write, num::NonZeroU32, ops::ControlFlow, path::PathBuf, sync::Arc, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use convert_case::{Case, Casing}; @@ -152,6 +154,7 @@ pub struct Config { pub user_agent: String, pub grey_list_clear_interval: Duration, pub is_seed_peer: bool, + pub debug_print_chain: bool, } impl Default for Config { @@ -169,6 +172,7 @@ impl Default for Config { user_agent: "tari-p2pool".to_string(), grey_list_clear_interval: Duration::from_secs(2 * 60), is_seed_peer: false, + debug_print_chain: false, } } } @@ -1233,6 +1237,14 @@ where S: ShareChain publish_peer_info_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); let mut grey_list_clear_interval = tokio::time::interval(self.config.grey_list_clear_interval); + + let mut debug_chain_graph = if self.config.debug_print_chain { + tokio::time::interval(Duration::from_secs(30)) + } else { + // only once a day, but even then will be skipped + tokio::time::interval(Duration::from_secs(60 * 60 * 24)) + }; + debug_chain_graph.set_missed_tick_behavior(MissedTickBehavior::Skip); // TODO: Not sure why this is done on a loop instead of just once.... // let mut kademlia_bootstrap_interval = tokio::time::interval(Duration::from_secs(12 * 60 * 60)); // kademlia_bootstrap_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); @@ -1324,10 +1336,76 @@ where S: ShareChain _ = grey_list_clear_interval.tick() => { self.network_peer_store.clear_grey_list(); }, + _ = debug_chain_graph.tick() => { + if self.config.debug_print_chain { + self.print_debug_chain_graph().await; + } + }, } } } + async fn print_debug_chain_graph(&self) { + self.print_debug_chain_graph_inner(&self.share_chain_random_x, "randomx") + .await; + self.print_debug_chain_graph_inner(&self.share_chain_sha3x, "sha3x") + .await; + } + + async fn print_debug_chain_graph_inner(&self, chain: &S, prefix: &str) { + let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .append(true) + .open(format!("{}_blocks_{}.txt", prefix, time)) + .unwrap(); + + file.write(b"@startuml\n").unwrap(); + file.write(b"digraph B {\n").unwrap(); + let blocks = chain.all_blocks().await.expect("errored"); + for b in blocks { + file.write( + format!( + "B{} [label=\"{} - {}\"]\n", + &b.hash.to_hex()[0..8], + &b.height, + &b.hash.to_hex()[0..8] + ) + .as_bytes(), + ) + .unwrap(); + file.write(format!("B{} -> B{}\n", &b.hash.to_hex()[0..8], &b.prev_hash.to_hex()[0..8]).as_bytes()) + .unwrap(); + for u in b.uncles.iter().take(3) { + file.write( + format!( + "B{} -> B{} [style=dotted]\n", + &b.hash.to_hex()[0..8], + &u.1.to_hex()[0..8] + ) + .as_bytes(), + ) + .unwrap(); + } + if b.uncles.len() > 3 { + file.write( + format!( + "B{} -> B{}others [style=dotted, label=\"{} others\"]\n", + &b.hash.to_hex()[0..8], + &b.hash.to_hex()[0..8], + b.uncles.len() - 3 + ) + .as_bytes(), + ) + .unwrap(); + } + } + + file.write(b"}\n").unwrap(); + } + async fn parse_seed_peers(&mut self) -> Result, Error> { let mut seed_peers_result = HashMap::new(); diff --git a/src/sharechain/error.rs b/src/sharechain/error.rs index de16152..d91609c 100644 --- a/src/sharechain/error.rs +++ b/src/sharechain/error.rs @@ -41,6 +41,8 @@ pub enum Error { BlockParentDoesNotExist { missing_parents: Vec<(u64, FixedHash)> }, #[error("Missing block validation params!")] MissingBlockValidationParams, + #[error("Block is not an uncle of the main chain. Height: {height}, Hash: {hash}")] + UncleInMainChain { height: u64, hash: FixedHash }, } #[derive(Error, Debug)] diff --git a/src/sharechain/in_memory.rs b/src/sharechain/in_memory.rs index ff13f82..0a3da57 100644 --- a/src/sharechain/in_memory.rs +++ b/src/sharechain/in_memory.rs @@ -154,7 +154,7 @@ impl InMemoryShareChain { } // Check if already added. - if let Some(level) = p2_chain.get_at_height(new_block_p2pool_height) { + if let Some(level) = p2_chain.level_at_height(new_block_p2pool_height) { if level.blocks.contains_key(&block.hash) { info!(target: LOG_TARGET, "[{:?}] ✅ Block already added: {:?}", self.pow_algo, block.height); return Ok(()); @@ -226,7 +226,7 @@ impl InMemoryShareChain { ); for uncle in cur_block.uncles.iter() { let uncle_block = p2_chain - .get_at_height(uncle.0) + .level_at_height(uncle.0) .ok_or_else(|| Error::UncleBlockNotFound)? .blocks .get(&uncle.1) @@ -249,7 +249,7 @@ impl InMemoryShareChain { ); for uncle in cur_block.uncles.iter() { let uncle_block = p2_chain - .get_at_height(uncle.0) + .level_at_height(uncle.0) .ok_or_else(|| Error::UncleBlockNotFound)? .blocks .get(&uncle.1) @@ -368,7 +368,7 @@ impl ShareChain for InMemoryShareChain { ); for uncle in new_tip_block.uncles.iter() { let uncle_block = chain_read_lock - .get_at_height(uncle.0) + .level_at_height(uncle.0) .ok_or_else(|| Error::UncleBlockNotFound)? .blocks .get(&uncle.1) @@ -430,19 +430,25 @@ impl ShareChain for InMemoryShareChain { let mut excluded_uncles = vec![]; let mut uncles = vec![]; for height in new_height.saturating_sub(3)..new_height { - let older_level = chain_read_lock.get_at_height(height).ok_or(Error::BlockLevelNotFound)?; - excluded_uncles.push(older_level.chain_block.clone()); + let older_level = chain_read_lock + .level_at_height(height) + .ok_or(Error::BlockLevelNotFound)?; let chain_block = older_level.block_in_main_chain().ok_or(Error::BlockNotFound)?; + // Blocks in the main chain can't be uncles + excluded_uncles.push(chain_block.hash); for uncle in chain_block.uncles.iter() { excluded_uncles.push(uncle.1); } for block in older_level.blocks.iter() { - if !excluded_uncles.contains(&block.0) { - uncles.push((height, block.0.clone())); - } + uncles.push((height, block.0.clone())); } } + // Remove excluded. + for excluded in excluded_uncles.iter() { + uncles.retain(|uncle| &uncle.1 != excluded); + } + Ok(P2Block::builder() .with_timestamp(EpochTime::now()) .with_prev_hash(last_block_hash) @@ -458,7 +464,7 @@ impl ShareChain for InMemoryShareChain { let mut blocks = Vec::new(); for block in requested_blocks { - if let Some(level) = p2_chain_read_lock.get_at_height(block.0) { + if let Some(level) = p2_chain_read_lock.level_at_height(block.0) { if let Some(block) = level.blocks.get(&block.1) { blocks.push(block.clone()); } else { @@ -562,6 +568,18 @@ impl ShareChain for InMemoryShareChain { let difficulty = chain_read_lock.lwma.get_difficulty().unwrap_or(Difficulty::min()); cmp::max(min, cmp::min(max, difficulty)) } + + // For debugging only + async fn all_blocks(&self) -> Result>, Error> { + let chain_read_lock = self.p2_chain.read().await; + let mut res = Vec::new(); + for level in &chain_read_lock.levels { + for block in level.blocks.values() { + res.push(block.clone()); + } + } + Ok(res) + } } #[cfg(test)] @@ -703,7 +721,7 @@ pub mod test { .p2_chain .read() .await - .get_at_height(i as u64 - 2) + .level_at_height(i as u64 - 2) .unwrap() .chain_block; // lets create an uncle block diff --git a/src/sharechain/mod.rs b/src/sharechain/mod.rs index 1356a9e..cee4adb 100644 --- a/src/sharechain/mod.rs +++ b/src/sharechain/mod.rs @@ -131,4 +131,6 @@ pub(crate) trait ShareChain: Send + Sync + 'static { async fn miners_with_shares(&self, squad: Squad) -> Result)>, Error>; async fn get_target_difficulty(&self, height: u64) -> Difficulty; + + async fn all_blocks(&self) -> Result>, Error>; } diff --git a/src/sharechain/p2chain.rs b/src/sharechain/p2chain.rs index 233e012..982f822 100644 --- a/src/sharechain/p2chain.rs +++ b/src/sharechain/p2chain.rs @@ -45,7 +45,7 @@ pub const MAX_EXTRA_SYNC: usize = 2000; pub struct P2Chain { pub cached_shares: Option)>>, - levels: VecDeque, + pub(crate) levels: VecDeque, total_size: usize, share_window: usize, total_accumulated_tip_difficulty: AccumulatedDifficulty, @@ -54,7 +54,7 @@ pub struct P2Chain { } impl P2Chain { - pub fn get_at_height(&self, height: u64) -> Option<&P2ChainLevel> { + pub fn level_at_height(&self, height: u64) -> Option<&P2ChainLevel> { let tip = self.levels.front()?.height; if height > tip { return None; @@ -65,7 +65,7 @@ impl P2Chain { } fn get_block_at_height(&self, height: u64, hash: &FixedHash) -> Option<&Arc> { - let level = self.get_at_height(height)?; + let level = self.level_at_height(height)?; level.blocks.get(hash) } @@ -148,13 +148,13 @@ impl P2Chain { if self.current_tip >= self.share_window as u64 { // our tip is more than the share window so its possible that we need to drop a block out of the pow window if let Some(level) = self - .get_at_height(self.current_tip.saturating_sub(self.share_window as u64)) + .level_at_height(self.current_tip.saturating_sub(self.share_window as u64)) .cloned() { let block = level.block_in_main_chain().ok_or(Error::BlockNotFound)?; self.decrease_total_chain_difficulty(block.target_difficulty)?; for (height, block_hash) in &block.uncles { - if let Some(link_level) = self.get_at_height(*height) { + if let Some(link_level) = self.level_at_height(*height) { let uncle_block = link_level.blocks.get(&block_hash).ok_or(Error::BlockNotFound)?; self.decrease_total_chain_difficulty(uncle_block.target_difficulty)?; } @@ -185,7 +185,17 @@ impl P2Chain { } // now lets check the uncles for uncle in block.uncles.iter() { - if self.get_block_at_height(uncle.0, &uncle.1).is_none() { + if self.get_block_at_height(uncle.0, &uncle.1).is_some() { + // Uncle cannot be in the main chain + if let Some(level) = self.level_at_height(uncle.0) { + if level.chain_block == uncle.1 { + return Err(Error::UncleInMainChain { + height: uncle.0, + hash: uncle.1.clone(), + }); + } + } + } else { missing_parents.push((uncle.0, uncle.1.clone())); } } @@ -291,8 +301,8 @@ impl P2Chain { self.cached_shares = None; // lets fix the chain let mut current_block = block; - while self.get_at_height(current_block.height.saturating_sub(1)).is_some() { - let parent_level = (self.get_at_height(current_block.height.saturating_sub(1)).unwrap()).clone(); + while self.level_at_height(current_block.height.saturating_sub(1)).is_some() { + let parent_level = (self.level_at_height(current_block.height.saturating_sub(1)).unwrap()).clone(); if current_block.prev_hash != parent_level.chain_block { // safety check let nextblock = parent_level.blocks.get(¤t_block.prev_hash); @@ -333,7 +343,7 @@ impl P2Chain { } // let see if we already have a block that builds on top of this - if let Some(next_level) = self.get_at_height(new_block_height + 1).cloned() { + if let Some(next_level) = self.level_at_height(new_block_height + 1).cloned() { // we have a height here, lets check the blocks for block in next_level.blocks.iter() { if block.1.prev_hash == hash { @@ -349,6 +359,14 @@ impl P2Chain { pub fn add_block_to_chain(&mut self, block: Arc) -> Result<(), Error> { let new_block_height = block.height; let block_hash = block.hash.clone(); + + // Uncle cannot be the same as prev_hash + if block.uncles.iter().any(|(_, hash)| hash == &block.prev_hash) { + return Err(Error::InvalidBlock { + reason: "Uncle cannot be the same as prev_hash".to_string(), + }); + } + // edge case no current chain, lets just add if self.get_tip().is_none() { let new_level = P2ChainLevel::new(block); @@ -417,7 +435,7 @@ impl P2Chain { Some(height) => height, None => return None, }; - let parent_level = match self.get_at_height(parent_height) { + let parent_level = match self.level_at_height(parent_height) { Some(level) => level, None => return None, }; @@ -425,7 +443,7 @@ impl P2Chain { } pub fn get_tip(&self) -> Option<&P2ChainLevel> { - self.get_at_height(self.current_tip) + self.level_at_height(self.current_tip) } pub fn get_height(&self) -> u64 { @@ -474,14 +492,14 @@ mod test { // 0..9 blocks should have been trimmed out for i in 10..41 { - let level = chain.get_at_height(i).unwrap(); + let level = chain.level_at_height(i).unwrap(); assert_eq!(level.block_in_main_chain().unwrap().original_block.header.nonce, i); } - let level = chain.get_at_height(10).unwrap(); + let level = chain.level_at_height(10).unwrap(); assert_eq!(level.block_in_main_chain().unwrap().original_block.header.nonce, 10); - assert!(chain.get_at_height(0).is_none()); + assert!(chain.level_at_height(0).is_none()); } #[test] @@ -530,13 +548,13 @@ mod test { } for i in 11..41 { - let level = chain.get_at_height(i).unwrap(); + let level = chain.level_at_height(i).unwrap(); let block = level.block_in_main_chain().unwrap(); let parent = chain.get_parent_block(&block).unwrap(); assert_eq!(parent.original_block.header.nonce, i - 1); } - let level = chain.get_at_height(10).unwrap(); + let level = chain.level_at_height(10).unwrap(); let block = level.block_in_main_chain().unwrap(); assert!(chain.get_parent_block(&block).is_none()); } @@ -608,7 +626,7 @@ mod test { AccumulatedDifficulty::from_u128(145).unwrap() // 31+30+29+28+27 ); - let block_29 = chain.get_at_height(29).unwrap().block_in_main_chain().unwrap(); + let block_29 = chain.level_at_height(29).unwrap().block_in_main_chain().unwrap(); prev_hash = block_29.generate_hash(); timestamp = block_29.timestamp; @@ -701,7 +719,7 @@ mod test { timestamp = timestamp.checked_add(EpochTime::from(10)).unwrap(); let mut uncles = Vec::new(); if i > 1 { - let prev_hash_uncle = chain.get_at_height(i - 2).unwrap().chain_block; + let prev_hash_uncle = chain.level_at_height(i - 2).unwrap().chain_block; // lets create an uncle block let block = P2Block::builder() .with_timestamp(timestamp) @@ -750,7 +768,7 @@ mod test { timestamp = timestamp.checked_add(EpochTime::from(10)).unwrap(); let mut uncles = Vec::new(); if i > 1 { - let prev_hash_uncle = chain.get_at_height(i - 2).unwrap().chain_block; + let prev_hash_uncle = chain.level_at_height(i - 2).unwrap().chain_block; // lets create an uncle block let block = P2Block::builder() .with_timestamp(timestamp) @@ -779,7 +797,7 @@ mod test { let address = new_random_address(); timestamp = timestamp.checked_add(EpochTime::from(10)).unwrap(); let mut uncles = Vec::new(); - let prev_hash_uncle = chain.get_at_height(6).unwrap().chain_block; + let prev_hash_uncle = chain.level_at_height(6).unwrap().chain_block; // lets create an uncle block let block = P2Block::builder() .with_timestamp(timestamp) @@ -790,7 +808,7 @@ mod test { .build(); uncles.push((7, block.hash)); chain.add_block_to_chain(block).unwrap(); - prev_hash = chain.get_at_height(7).unwrap().chain_block; + prev_hash = chain.level_at_height(7).unwrap().chain_block; let block = P2Block::builder() .with_timestamp(timestamp) .with_height(8)