From 126fa41c4877bbc71aa4e21aee23140d3304107a Mon Sep 17 00:00:00 2001 From: Rob N Date: Tue, 25 Jun 2024 17:14:25 -1000 Subject: [PATCH] test(lib): sql handles regtest reorg --- src/db/sqlite/headers.rs | 6 +- tests/node.rs | 117 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/src/db/sqlite/headers.rs b/src/db/sqlite/headers.rs index 963d035..1e379ba 100644 --- a/src/db/sqlite/headers.rs +++ b/src/db/sqlite/headers.rs @@ -142,8 +142,8 @@ impl HeaderStore for SqliteHeaderDb { ) -> Result<(), DatabaseError> { let mut write_lock = self.conn.lock().await; let tx = write_lock.transaction().map_err(|_| DatabaseError::Write)?; - for (h, header) in header_chain { - if h.ge(&height) { + for (new_height, header) in header_chain { + if new_height.ge(&height) { let hash: String = header.block_hash().to_string(); let version: i32 = header.version.to_consensus(); let prev_hash: String = header.prev_blockhash.as_raw_hash().to_string(); @@ -155,7 +155,7 @@ impl HeaderStore for SqliteHeaderDb { tx.execute( stmt, params![ - height, + new_height, hash, version, prev_hash, diff --git a/tests/node.rs b/tests/node.rs index 72df354..265635f 100644 --- a/tests/node.rs +++ b/tests/node.rs @@ -44,6 +44,18 @@ async fn new_node(addrs: HashSet) -> (Node, Client) { (node, client) } +async fn new_node_sql(addrs: HashSet) -> (Node, Client) { + let host = (IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)), PORT); + let builder = kyoto::node::builder::NodeBuilder::new(bitcoin::Network::Regtest); + let (node, client) = builder + .add_peers(vec![host]) + .add_scripts(addrs) + .build_node() + .await; + (node, client) +} + +// This test may be run as much as required without altering Bitcoin Core's database. #[tokio::test] async fn test_reorg() { let rpc_result = initialize_client(); @@ -53,12 +65,13 @@ async fn test_reorg() { return; } let rpc = rpc_result.unwrap(); + // Mine some blocks let miner = rpc.get_new_address(None, None).unwrap().assume_checked(); rpc.generate_to_address(10, &miner).unwrap(); tokio::time::sleep(Duration::from_secs(1)).await; let best = rpc.get_best_block_hash().unwrap(); tokio::time::sleep(Duration::from_secs(1)).await; - // Make sure we sync up to the tip under usual conditions. + // Build and run a node let mut scripts = HashSet::new(); let other = rpc.get_new_address(None, None).unwrap().assume_checked(); scripts.insert(other.into()); @@ -76,6 +89,7 @@ async fn test_reorg() { _ => {} } } + // Reorganize the blocks let old_best = best; let old_height = rpc.get_block_count().unwrap(); rpc.invalidate_block(&best).unwrap(); @@ -83,6 +97,7 @@ async fn test_reorg() { rpc.generate_to_address(2, &miner).unwrap(); tokio::time::sleep(Duration::from_secs(2)).await; let best = rpc.get_best_block_hash().unwrap(); + // Make sure the reorg was caught while let Ok(message) = recv.recv().await { match message { kyoto::node::messages::NodeMessage::Dialog(d) => println!("{d}"), @@ -104,6 +119,7 @@ async fn test_reorg() { rpc.stop().unwrap(); } +// This test may be repeated as much as required without altering Bitcoin Core's database. #[tokio::test] async fn test_broadcast() { let rpc_result = initialize_client(); @@ -112,10 +128,12 @@ async fn test_broadcast() { println!("Bitcoin Core is not running. Skipping this test..."); return; } + // Mine some blocks let rpc = rpc_result.unwrap(); let miner = rpc.get_new_address(None, None).unwrap().assume_checked(); rpc.generate_to_address(105, &miner).unwrap(); tokio::time::sleep(Duration::from_secs(5)).await; + // Send to a random address let other = rpc.get_new_address(None, None).unwrap().assume_checked(); rpc.send_to_address( &other.clone(), @@ -128,7 +146,9 @@ async fn test_broadcast() { None, ) .unwrap(); + // Confirm the transaction rpc.generate_to_address(10, &miner).unwrap(); + // Get inputs and outputs for a new transaction let inputs = rpc .list_unspent(Some(1), None, Some(&[&other]), None, None) .unwrap(); @@ -143,6 +163,7 @@ async fn test_broadcast() { let burn = rpc.get_new_address(None, None).unwrap().assume_checked(); let mut outs = HashMap::new(); outs.insert(burn.to_string(), Amount::from_sat(1000)); + // Create and sign the transaction let tx = rpc .create_raw_transaction(&raw_tx_inputs, &outs, None, None) .unwrap(); @@ -155,6 +176,7 @@ async fn test_broadcast() { let (mut node, mut client) = new_node(scripts.clone()).await; tokio::task::spawn(async move { node.run().await }); let (mut sender, mut recv) = client.split(); + // Broadcast the transaction to the network sender .broadcast_tx(TxBroadcast::new( signed.transaction().unwrap(), @@ -189,11 +211,11 @@ async fn test_long_chain() { return; } let rpc = rpc_result.unwrap(); + // Mine a lot of blocks let miner = rpc.get_new_address(None, None).unwrap().assume_checked(); rpc.generate_to_address(500, &miner).unwrap(); tokio::time::sleep(Duration::from_secs(15)).await; let best = rpc.get_best_block_hash().unwrap(); - // Make sure we sync up to the tip under usual conditions. let mut scripts = HashSet::new(); let other = rpc.get_new_address(None, None).unwrap().assume_checked(); scripts.insert(other.into()); @@ -214,3 +236,94 @@ async fn test_long_chain() { sender.shutdown().await.unwrap(); rpc.stop().unwrap(); } + +// This test requires a clean Bitcoin Core regtest instance or unchange headers from Bitcoin Core since the last test. +#[tokio::test] +async fn test_sql() { + let rpc_result = initialize_client(); + // If we can't fetch the genesis block then bitcoind is not running. Just exit. + if let Err(_) = rpc_result { + println!("Bitcoin Core is not running. Skipping this test..."); + return; + } + let rpc = rpc_result.unwrap(); + // Mine some blocks. + let miner = rpc.get_new_address(None, None).unwrap().assume_checked(); + rpc.generate_to_address(10, &miner).unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + let best = rpc.get_best_block_hash().unwrap(); + tokio::time::sleep(Duration::from_secs(1)).await; + let mut scripts = HashSet::new(); + let other = rpc.get_new_address(None, None).unwrap().assume_checked(); + scripts.insert(other.into()); + let (mut node, mut client) = new_node_sql(scripts.clone()).await; + tokio::task::spawn(async move { node.run().await }); + let (_, mut recv) = client.split(); + while let Ok(message) = recv.recv().await { + match message { + kyoto::node::messages::NodeMessage::Dialog(d) => println!("{d}"), + kyoto::node::messages::NodeMessage::Warning(e) => println!("{e}"), + kyoto::node::messages::NodeMessage::Synced(update) => { + println!("Done"); + assert_eq!(update.tip().hash, best); + break; + } + _ => {} + } + } + client.shutdown().await.unwrap(); + // Reorganize the blocks + let old_best = best; + let old_height = rpc.get_block_count().unwrap(); + rpc.invalidate_block(&best).unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + rpc.generate_to_address(2, &miner).unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + let best = rpc.get_best_block_hash().unwrap(); + // Spin up the node on a cold start + let (mut node, mut client) = new_node_sql(scripts.clone()).await; + tokio::task::spawn(async move { node.run().await }); + let (_, mut recv) = client.split(); + // Make sure the reorganization is caught after a cold start + while let Ok(message) = recv.recv().await { + match message { + kyoto::node::messages::NodeMessage::Dialog(d) => println!("{d}"), + kyoto::node::messages::NodeMessage::Warning(e) => println!("{e}"), + kyoto::node::messages::NodeMessage::BlocksDisconnected(blocks) => { + assert_eq!(blocks.len(), 1); + assert_eq!(blocks.first().unwrap().header.block_hash(), old_best); + assert_eq!(old_height as u32, blocks.first().unwrap().height); + } + kyoto::node::messages::NodeMessage::Synced(update) => { + println!("Done"); + assert_eq!(update.tip().hash, best); + break; + } + _ => {} + } + } + client.shutdown().await.unwrap(); + // Mine more blocks + rpc.generate_to_address(2, &miner).unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + let best = rpc.get_best_block_hash().unwrap(); + // Make sure the node does not have any corrupted headers + let (mut node, mut client) = new_node_sql(scripts.clone()).await; + tokio::task::spawn(async move { node.run().await }); + let (_, mut recv) = client.split(); + // The node properly syncs after persisting a reorg + while let Ok(message) = recv.recv().await { + match message { + kyoto::node::messages::NodeMessage::Dialog(d) => println!("{d}"), + kyoto::node::messages::NodeMessage::Warning(e) => println!("{e}"), + kyoto::node::messages::NodeMessage::Synced(update) => { + println!("Done"); + assert_eq!(update.tip().hash, best); + break; + } + _ => {} + } + } + client.shutdown().await.unwrap(); + rpc.stop().unwrap(); +}