diff --git a/chain/src/chain.rs b/chain/src/chain.rs index 5d359b5133..ca5311595b 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -254,15 +254,18 @@ impl Chain { let is_more_work = head.is_some(); let mut is_next_block = false; + let mut reorg_depth = None; if let Some(head) = head { if head.prev_block_h == prev_head.last_block_h { is_next_block = true; + } else { + reorg_depth = Some(prev_head.height.saturating_sub(head.height) + 1); } } match (is_more_work, is_next_block) { (true, true) => BlockStatus::Next, - (true, false) => BlockStatus::Reorg, + (true, false) => BlockStatus::Reorg(reorg_depth.unwrap_or(0)), (false, _) => BlockStatus::Fork, } } diff --git a/chain/src/types.rs b/chain/src/types.rs index 27895e7c46..b3f7b782bb 100644 --- a/chain/src/types.rs +++ b/chain/src/types.rs @@ -169,5 +169,5 @@ pub enum BlockStatus { Fork, /// Block updates the chain head via a (potentially disruptive) "reorg". /// Previous block was not our previous chain head. - Reorg, + Reorg(u64), } diff --git a/chain/tests/mine_simple_chain.rs b/chain/tests/mine_simple_chain.rs index 80da3b0ef3..a0d31f5220 100644 --- a/chain/tests/mine_simple_chain.rs +++ b/chain/tests/mine_simple_chain.rs @@ -26,9 +26,11 @@ use self::keychain::{ExtKeychain, ExtKeychainPath, Keychain}; use self::util::{RwLock, StopState}; use chrono::Duration; use grin_chain as chain; +use grin_chain::{BlockStatus, ChainAdapter, Options}; use grin_core as core; use grin_keychain as keychain; use grin_util as util; +use std::cell::RefCell; use std::fs; use std::sync::Arc; @@ -51,6 +53,41 @@ fn setup(dir_name: &str, genesis: Block) -> Chain { .unwrap() } +/// Adapter to retrieve last status +pub struct StatusAdapter { + pub last_status: RwLock>, +} + +impl StatusAdapter { + pub fn new(last_status: RwLock>) -> Self { + StatusAdapter { last_status } + } +} + +impl ChainAdapter for StatusAdapter { + fn block_accepted(&self, _b: &Block, status: BlockStatus, _opts: Options) { + *self.last_status.write() = Some(status); + } +} + +/// Creates a `Chain` instance with `StatusAdapter` attached to it. +fn setup_with_status_adapter(dir_name: &str, genesis: Block, adapter: Arc) -> Chain { + util::init_test_logger(); + clean_output_dir(dir_name); + let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new())); + let chain = chain::Chain::init( + dir_name.to_string(), + adapter, + genesis, + pow::verify_size, + verifier_cache, + false, + ) + .unwrap(); + + chain +} + #[test] fn mine_empty_chain() { global::set_mining_mode(ChainTypes::AutomatedTesting); @@ -158,6 +195,78 @@ where } } +#[test] +// This test creates a reorg at REORG_DEPTH by mining a block with difficulty that +// exceeds original chain total difficulty. +// +// Illustration of reorg with NUM_BLOCKS_MAIN = 6 and REORG_DEPTH = 5: +// +// difficulty: 1 2 3 4 5 6 +// +// / [ 2 ] - [ 3 ] - [ 4 ] - [ 5 ] - [ 6 ] <- original chain +// [ Genesis ] -[ 1 ]- * +// ^ \ [ 2' ] - ................................ <- reorg chain with depth 5 +// | +// difficulty: 1 | 24 +// | +// \----< Fork point and chain reorg +fn mine_reorg() { + // Test configuration + const NUM_BLOCKS_MAIN: u64 = 6; // Number of blocks to mine in main chain + const REORG_DEPTH: u64 = 5; // Number of blocks to be discarded from main chain after reorg + + const DIR_NAME: &str = ".grin_reorg"; + clean_output_dir(DIR_NAME); + + global::set_mining_mode(ChainTypes::AutomatedTesting); + let kc = ExtKeychain::from_random_seed(false).unwrap(); + + let genesis = pow::mine_genesis_block().unwrap(); + { + // Create chain that reports last block status + let mut last_status = RwLock::new(None); + let adapter = Arc::new(StatusAdapter::new(last_status)); + let chain = setup_with_status_adapter(DIR_NAME, genesis.clone(), adapter.clone()); + + // Add blocks to main chain with gradually increasing difficulty + let mut prev = chain.head_header().unwrap(); + for n in 1..=NUM_BLOCKS_MAIN { + let b = prepare_block(&kc, &prev, &chain, n); + prev = b.header.clone(); + chain.process_block(b, chain::Options::SKIP_POW).unwrap(); + } + + let head = chain.head_header().unwrap(); + assert_eq!(head.height, NUM_BLOCKS_MAIN); + assert_eq!(head.hash(), prev.hash()); + + // Reorg chain should exceed main chain's total difficulty to be considered + let reorg_difficulty = head.total_difficulty().to_num(); + + // Create one block for reorg chain forking off NUM_BLOCKS_MAIN - REORG_DEPTH height + let fork_head = chain + .get_header_by_height(NUM_BLOCKS_MAIN - REORG_DEPTH) + .unwrap(); + let b = prepare_fork_block(&kc, &fork_head, &chain, reorg_difficulty); + let reorg_head = b.header.clone(); + chain.process_block(b, chain::Options::SKIP_POW).unwrap(); + + // Check that reorg is correctly reported in block status + assert_eq!( + *adapter.last_status.read(), + Some(BlockStatus::Reorg(REORG_DEPTH)) + ); + + // Chain should be switched to the reorganized chain + let head = chain.head_header().unwrap(); + assert_eq!(head.height, NUM_BLOCKS_MAIN - REORG_DEPTH + 1); + assert_eq!(head.hash(), reorg_head.hash()); + } + + // Cleanup chain directory + clean_output_dir(DIR_NAME); +} + #[test] fn mine_forks() { global::set_mining_mode(ChainTypes::AutomatedTesting); diff --git a/servers/src/common/adapters.rs b/servers/src/common/adapters.rs index f2af4178ff..91f20975a7 100644 --- a/servers/src/common/adapters.rs +++ b/servers/src/common/adapters.rs @@ -722,7 +722,12 @@ impl ChainAdapter for ChainToPoolAndNetAdapter { // Reconcile the txpool against the new block *after* we have broadcast it too our peers. // This may be slow and we do not want to delay block propagation. // We only want to reconcile the txpool against the new block *if* total work has increased. - if status == BlockStatus::Next || status == BlockStatus::Reorg { + let is_reorg = if let BlockStatus::Reorg(_) = status { + true + } else { + false + }; + if status == BlockStatus::Next || is_reorg { let mut tx_pool = self.tx_pool.write(); let _ = tx_pool.reconcile_block(b); @@ -732,7 +737,7 @@ impl ChainAdapter for ChainToPoolAndNetAdapter { tx_pool.truncate_reorg_cache(cutoff); } - if status == BlockStatus::Reorg { + if is_reorg { let _ = self.tx_pool.write().reconcile_reorg_cache(&b.header); } } diff --git a/servers/src/common/hooks.rs b/servers/src/common/hooks.rs index 00cb7c4935..140f31db50 100644 --- a/servers/src/common/hooks.rs +++ b/servers/src/common/hooks.rs @@ -117,11 +117,12 @@ impl NetEvents for EventLogger { impl ChainEvents for EventLogger { fn on_block_accepted(&self, block: &core::Block, status: &BlockStatus) { match status { - BlockStatus::Reorg => { + BlockStatus::Reorg(depth) => { warn!( - "block_accepted (REORG!): {:?} at {} (diff: {})", + "block_accepted (REORG!): {:?} at {} (depth: {}, diff: {})", block.hash(), block.header.height, + depth, block.header.total_difficulty(), ); } @@ -261,16 +262,29 @@ impl WebHook { impl ChainEvents for WebHook { fn on_block_accepted(&self, block: &core::Block, status: &BlockStatus) { - let status = match status { - BlockStatus::Reorg => "reorg", + let status_str = match status { + BlockStatus::Reorg(_) => "reorg", BlockStatus::Fork => "fork", BlockStatus::Next => "head", }; - let payload = json!({ - "hash": block.header.hash().to_hex(), - "status": status, - "data": block - }); + + // Add additional `depth` field to the JSON in case of reorg + let payload = if let BlockStatus::Reorg(depth) = status { + json!({ + "hash": block.header.hash().to_hex(), + "status": status_str, + "data": block, + + "depth": depth + }) + } else { + json!({ + "hash": block.header.hash().to_hex(), + "status": status_str, + "data": block + }) + }; + if !self.make_request(&payload, &self.block_accepted_url) { error!( "Failed to serialize block {} at height {}",