diff --git a/Cargo.toml b/Cargo.toml index 2d9ed1b..82c5071 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ rust-bitvmx-storage-backend = { git = "https://github.com/FairgateLabs/rust-bitv bitvmx-settings = { git = "https://github.com/FairgateLabs/rust-bitvmx-settings.git", tag = "v0.5.1" } bitvmx-bitcoin-rpc = { git = "https://github.com/FairgateLabs/rust-bitvmx-bitcoin-rpc.git", tag = "v0.5.1" } bitcoind = { git = "https://github.com/FairgateLabs/rust-bitcoind.git", tag = "v0.5.1" } +bitcoincore-rpc = "0.19" bitcoincore-rpc-json = "0.19.0" [dev-dependencies] diff --git a/README.md b/README.md index fd05883..084917e 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,13 @@ The `IndexerApi` trait provides several methods to interact with the Bitcoin Ind - **get_blockchain_best_height**: Retrieves the current best block height from the Bitcoin blockchain. -- **get_height_to_sync**: Determines the next block height that needs to be synchronized. +- **get_block_by_height**: Retrieves a block by its height. -- **get_tx**: Retrieves transaction information for a given transaction ID. +- **get_block_by_hash**: Retrieves a block by its hash. + +- **get_transaction**: Retrieves transaction information for a given transaction ID. Returns a `TransactionInfo` with the transaction status. If the transaction is not found, returns `TransactionInfo` with `TransactionBlockchainStatus::NotFound`. + +- **get_estimated_fee_rate**: Retrieves the estimated fee rate from the most recently indexed block. ## Usage ```rust @@ -72,21 +76,37 @@ The `IndexerApi` trait provides several methods to interact with the Bitcoin Ind let blockchain_best_height = indexer.get_blockchain_best_height()?; println!("Blockchain best height: {}", blockchain_best_height); - // Determine the next block height to sync - let height_to_sync = indexer.get_height_to_sync()?; - println!("Next block height to sync: {}", height_to_sync); - // Example transaction ID (replace with a real one for actual use) let tx_id = "some_tx_id"; - if let Some(tx_info) = indexer.get_tx(&tx_id.parse()?)? { - println!("Transaction info: {:?}", tx_info); + let tx_info = indexer.get_transaction(&tx_id.parse()?, false)?; + println!("Transaction status: {:?}", tx_info.status); + match tx_info.status { + TransactionBlockchainStatus::InMempool => { + println!("Transaction is in mempool"); + } + TransactionBlockchainStatus::NotFound => { + println!("Transaction not found"); + } + TransactionBlockchainStatus::Orphan => { + println!("Transaction is orphaned"); + } + TransactionBlockchainStatus::Confirmed => { + println!("Transaction is confirmed with {} confirmations", + tx_info.confirmations); + } + TransactionBlockchainStatus::Finalized => { + println!("Transaction is finalized with {} confirmations", + tx_info.confirmations); + } } ``` -## Checkpoint Height Configuration +## Configuration + +### Checkpoint Height -The `checkpoint_height` is an optional setting in the indexer configuration that specifies a specific block height from which the indexing process should start. This can be useful for syncing from a specific height. Here’s how it works: +The `checkpoint_height` is an optional setting in the indexer configuration that specifies a specific block height from which the indexing process should start. This can be useful for syncing from a specific height. Here's how it works: - **With Checkpoint Height**: - If `checkpoint_height` is set, the indexer will begin syncing from the specified block height. @@ -94,21 +114,22 @@ The `checkpoint_height` is an optional setting in the indexer configuration that - Once a checkpoint is set and indexed, you cannot change it unless the database is cleared. - **Without Checkpoint Height**: - - If not set, indexing will start from the genesis block or from the last indexed height if there’s an existing index. + - If not set, indexing will start from the genesis block or from the last indexed height if there's an existing index. -To configure this, set the `checkpoint_height` in your settings: +To configure these settings: ```yaml # Example configuration in config.yaml settings: checkpoint_height: 2000 + confirmation_threshold: 6 # Transactions with 6+ confirmations are considered finalized ``` ## Development Setup 1. Clone the repository 2. Install dependencies: `cargo build` -3. Run tests: `cargo test -- --ignored --test-threads=1` +3. Run tests: `cargo test -- --test-threads=1` ## Examples - **get_estimated_fee_rate:** diff --git a/config/development.yaml b/config/development.yaml index 304c1eb..c82589a 100644 --- a/config/development.yaml +++ b/config/development.yaml @@ -12,4 +12,4 @@ settings: checkpoint_height: 1 # possible values: error | warn | info | debug | trace -log_level: info \ No newline at end of file +log_level: info diff --git a/src/errors.rs b/src/errors.rs index 1b67813..0d6ef58 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -44,4 +44,10 @@ pub enum IndexerError { #[error("Checkpoint height is behind indexed height")] CheckpointHeightBehindIndexedHeight, + + #[error("Missing transaction data in tx_status")] + MissingTransactionData, + + #[error("Missing block info in tx_status")] + MissingBlockInfo, } diff --git a/src/indexer.rs b/src/indexer.rs index bba9b3e..3d757a9 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -2,7 +2,7 @@ use crate::{ config::IndexerSettings, errors::IndexerError, store::{IndexerStore, StoreClient}, - types::{FullBlock, TransactionInfo}, + types::{FullBlock, TransactionBlockchainStatus, TransactionStatus}, }; use bitcoin::Txid; use bitvmx_bitcoin_rpc::{bitcoin_client::BitcoinClientApi, types::*}; @@ -14,6 +14,7 @@ where { pub bitcoin_client: B, pub store: Rc, + pub settings: IndexerSettings, } pub trait IndexerApi { @@ -45,8 +46,15 @@ pub trait IndexerApi { fn get_block_by_hash(&self, hash: &BlockHash) -> Result, IndexerError>; /// Retrieves transaction information for a given transaction ID. - /// Returns `Ok(Some(TransactionInfo))` if the transaction is found, `Ok(None)` if not found, or an `IndexerError` if an error occurs. - fn get_tx(&self, tx_id: &Txid) -> Result, IndexerError>; + /// Returns `Ok(TransactionInfo)` with the transaction status, or an `IndexerError` if an error occurs. + /// If the transaction is not found, returns `TransactionInfo` with `TransactionBlockchainStatus::NotFound`. + /// If `search_in_mempool` is true, the transaction will be searched in the mempool if it is not found in the indexed chain. + /// If `search_in_mempool` is false, the transaction will only be searched in the indexed chain. + fn get_transaction( + &self, + tx_id: &Txid, + search_in_mempool: bool, + ) -> Result; /// Retrieves the estimated fee rate from the most recently indexed block. /// Returns `Ok(u64)` with the fee rate in satoshis per virtual byte (sat/vB), or an `IndexerError` if an error occurs or the indexer is not synced. @@ -173,6 +181,7 @@ where Ok(Self { bitcoin_client, store, + settings, }) } } @@ -205,8 +214,54 @@ where Ok(self.store.get_best_block()?) } - fn get_tx(&self, tx_id: &Txid) -> Result, IndexerError> { - Ok(self.store.get_tx_info(tx_id)?) + fn get_transaction( + &self, + tx_id: &Txid, + search_in_mempool: bool, + ) -> Result { + // First, check if transaction is in storage + let tx_status = self.store.get_tx_info(tx_id)?; + + if let Some(mut tx_info) = tx_status { + // Update status based on confirmations and threshold + if tx_info.is_orphan() && search_in_mempool { + // If transaction is orphan, check if it's in mempool + let tx_mempool_status = self.bitcoin_client.get_mempool_entry(tx_id); + + if tx_mempool_status.is_err() { + // Transaction is not in mempool, mark as not found + tx_info.status = TransactionBlockchainStatus::NotFound; + tx_info.confirmations = 0; + tx_info.block_info = None; + } + } + + Ok(tx_info) + } else { + if !search_in_mempool { + return Ok(TransactionStatus { + tx: None, + block_info: None, + confirmations: 0, + status: TransactionBlockchainStatus::NotFound, + }); + } + + // Transaction not found in storage, check mempool + let tx_mempool_status = self.bitcoin_client.get_mempool_entry(tx_id); + let status = if tx_mempool_status.is_ok() { + TransactionBlockchainStatus::InMempool + } else { + TransactionBlockchainStatus::NotFound + }; + + Ok(TransactionStatus { + tx: None, + block_info: None, + confirmations: 0, + status, + }) + } } fn get_estimated_fee_rate(&self) -> Result { @@ -287,11 +342,15 @@ where return Ok(()); } - if best_indexer_height >= best_blockchain_height { + if best_indexer_height > best_blockchain_height { // This branch handles the scenario where the indexer has advanced further than the current blockchain tip. // This situation can occur if blocks have been invalidated or a reorg has caused the blockchain to roll back. - // To resolve this, update the indexer's synced and best heights to match the blockchain's current best height. - warn!("Indexer is ahead of the blockchain. Updating synced and best heights to match the blockchain's current best height."); + // Mark all blocks above the blockchain's best height as orphan before updating the height. + warn!("Indexer is ahead of the blockchain. Marking blocks as orphan and updating synced and best heights to match the blockchain's current best height."); + + self.store + .mark_following_blocks_as_orphan(best_blockchain_height + 1)?; + self.store.save_best_height(best_blockchain_height)?; return Ok(()); } @@ -448,37 +507,34 @@ fn estimate_fee_rate( #[cfg(test)] mod tests { use super::*; - use bitvmx_bitcoin_rpc::bitcoin_client::BitcoinClient; - use bitvmx_settings::settings; use crate::config::IndexerConfig; + use bitcoin::PublicKey; use bitcoind::bitcoind::Bitcoind; use bitcoind::config::BitcoindConfig; - use bitcoin::PublicKey; + use bitvmx_bitcoin_rpc::bitcoin_client::BitcoinClient; + use bitvmx_settings::settings; // Helper to setup bitcoind and BitcoinClient for tests - fn setup_bitcoind() -> Result<(BitcoinClient, Bitcoind, bitcoin::Address), Box> { + fn setup_bitcoind( + ) -> Result<(BitcoinClient, Bitcoind, bitcoin::Address), Box> { let config = settings::load::()?; let bitcoind_config = BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + // Wait for bitcoind to be ready std::thread::sleep(std::time::Duration::from_millis(500)); - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; - + Ok((bitcoin_client, bitcoind, wallet)) } fn get_random_pubkey() -> PublicKey { use bitcoin::key::rand::rngs::OsRng; use bitcoin::secp256k1::{Secp256k1, SecretKey}; - + let secp = Secp256k1::new(); let secret_key = SecretKey::new(&mut OsRng); let public_key = secret_key.public_key(&secp); @@ -488,10 +544,10 @@ mod tests { #[test] fn test_estimate_fee_rate_with_real_transactions() -> Result<(), Box> { let (bitcoin_client, bitcoind, wallet) = setup_bitcoind()?; - + // Mine 101 blocks to have mature coins (coinbase needs 100 confirmations) bitcoin_client.mine_blocks_to_address(101, &wallet)?; - + // Create 10 transactions by generating new addresses and mining to them // This creates actual transactions with inputs and outputs for _ in 0..10 { @@ -499,22 +555,32 @@ mod tests { let new_address = bitcoin_client.get_new_address(pubkey, bitcoin::Network::Regtest)?; bitcoin_client.mine_blocks_to_address(1, &new_address)?; } - + // Get the last mined block which should have coinbase transaction let best_block_height = bitcoin_client.get_best_block()?; - let block = bitcoin_client.get_block_by_height(&best_block_height)? + let block = bitcoin_client + .get_block_by_height(&best_block_height)? .ok_or("Block not found")?; - + // Verify estimate_fee_rate is called let fee_rate = estimate_fee_rate(&bitcoin_client, &block)?; - + // Block only has coinbase (1 tx), so should return 0 - assert_eq!(fee_rate, 0, "Fee rate should be 0 for blocks with only coinbase"); - + assert_eq!( + fee_rate, 0, + "Fee rate should be 0 for blocks with only coinbase" + ); + // Verify the block was processed from real blockchain data (not mocks) - assert!(block.txs.len() > 0, "Block should have transactions from real blockchain"); - assert!(block.txs[0].is_coinbase(), "First transaction should be coinbase"); - + assert!( + block.txs.len() > 0, + "Block should have transactions from real blockchain" + ); + assert!( + block.txs[0].is_coinbase(), + "First transaction should be coinbase" + ); + bitcoind.stop()?; Ok(()) } @@ -522,66 +588,78 @@ mod tests { #[test] fn test_estimate_fee_rate_computation_from_indexer() -> Result<(), Box> { use crate::store::IndexerStore; - use storage_backend::{storage::Storage, storage_config::StorageConfig}; use std::rc::Rc; - + use storage_backend::{storage::Storage, storage_config::StorageConfig}; + let (bitcoin_client, bitcoind, wallet) = setup_bitcoind()?; - + // Mine blocks with mature coins bitcoin_client.mine_blocks_to_address(105, &wallet)?; - + // Create indexer and let it process blocks - let db_path = format!("./test_output/estimate_fee_test_{}/db", std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_nanos()); + let db_path = format!( + "./test_output/estimate_fee_test_{}/db", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + ); let storage = Rc::new(Storage::new(&StorageConfig::new(db_path.clone(), None))?); let store = Rc::new(IndexerStore::new(storage)?); - + let indexer = crate::indexer::Indexer::new(bitcoin_client, store.clone(), None)?; - + // Process blocks through the indexer for _ in 0..5 { indexer.tick()?; } - + // Get the best block from indexer store let best_block = store.get_best_block()?; assert!(best_block.is_some(), "Indexer should have processed blocks"); - + let block = best_block.unwrap(); - + // Verify estimate_fee_rate was computed and stored by the indexer // The fee rate is stored in the FullBlock by the indexer when processing blocks // For blocks with only coinbase or few transactions, it will be 0 - assert!(block.estimated_fee_rate == 0, - "Fee rate should be 0 for blocks with insufficient transactions (computed by indexer)"); - + assert!( + block.estimated_fee_rate == 0, + "Fee rate should be 0 for blocks with insufficient transactions (computed by indexer)" + ); + // Clean up bitcoind.stop()?; let _ = std::fs::remove_dir_all(&db_path); - + Ok(()) } #[test] fn test_estimate_fee_rate_with_few_transactions() -> Result<(), Box> { let (bitcoin_client, bitcoind, wallet) = setup_bitcoind()?; - + // Mine a single block with only coinbase bitcoin_client.mine_blocks_to_address(1, &wallet)?; - + let best_block_height = bitcoin_client.get_best_block()?; - let block = bitcoin_client.get_block_by_height(&best_block_height)? + let block = bitcoin_client + .get_block_by_height(&best_block_height)? .ok_or("Block not found")?; - + // Verify this is real blockchain data (not mock) - assert!(block.txs.len() > 0, "Block should have at least coinbase from real blockchain"); + assert!( + block.txs.len() > 0, + "Block should have at least coinbase from real blockchain" + ); assert!(block.txs.len() <= 5, "Block should have few transactions"); - + // Call estimate_fee_rate on real block data let fee_rate = estimate_fee_rate(&bitcoin_client, &block)?; - assert_eq!(fee_rate, 0, "Fee rate should be 0 for blocks with <= 5 transactions"); - + assert_eq!( + fee_rate, 0, + "Fee rate should be 0 for blocks with <= 5 transactions" + ); + bitcoind.stop()?; Ok(()) } @@ -589,26 +667,36 @@ mod tests { #[test] fn test_estimate_fee_rate_returns_valid_result() -> Result<(), Box> { let (bitcoin_client, bitcoind, wallet) = setup_bitcoind()?; - + // Mine blocks to get real blockchain data bitcoin_client.mine_blocks_to_address(5, &wallet)?; - + let best_block_height = bitcoin_client.get_best_block()?; - let block = bitcoin_client.get_block_by_height(&best_block_height)? + let block = bitcoin_client + .get_block_by_height(&best_block_height)? .ok_or("Block not found")?; - + // Verify we're using real blockchain data assert!(block.height > 0, "Block should be from real blockchain"); - assert!(block.txs.len() > 0, "Block should contain transactions from real blockchain"); - + assert!( + block.txs.len() > 0, + "Block should contain transactions from real blockchain" + ); + // Call estimate_fee_rate and verify it returns Ok let result = estimate_fee_rate(&bitcoin_client, &block); - assert!(result.is_ok(), "estimate_fee_rate should return Ok with real blockchain data"); - + assert!( + result.is_ok(), + "estimate_fee_rate should return Ok with real blockchain data" + ); + let fee_rate = result?; // For blocks with only coinbase, fee_rate will be 0 - assert_eq!(fee_rate, 0, "Fee rate is 0 because block has insufficient transactions"); - + assert_eq!( + fee_rate, 0, + "Fee rate is 0 because block has insufficient transactions" + ); + bitcoind.stop()?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 2363675..b580bc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,11 +26,7 @@ fn main() -> Result<(), anyhow::Error> { let bitcoind_config = BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; diff --git a/src/store.rs b/src/store.rs index 2234a29..e1e9ca1 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::errors::IndexerStoreError; -use crate::types::{FullBlock, TransactionInfo}; +use crate::types::{FullBlock, TransactionBlockchainStatus, TransactionStatus}; use bitcoin::hash_types::BlockHash; use bitcoin::Transaction; use bitcoin::Txid; @@ -58,7 +58,7 @@ pub trait StoreClient { block: &BlockInfo, estimated_fee_rate: u64, ) -> Result<(), IndexerStoreError>; - fn get_tx_info(&self, tx_id: &Txid) -> Result, IndexerStoreError>; + fn get_tx_info(&self, tx_id: &Txid) -> Result, IndexerStoreError>; fn get_best_height(&self) -> Result, IndexerStoreError>; fn save_best_height(&self, height: BlockHeight) -> Result<(), IndexerStoreError>; @@ -154,23 +154,23 @@ impl StoreClient for IndexerStore { height: BlockHeight, ) -> Result, IndexerStoreError> { let key = self.get_key(StoreKey::BlockByHeight(height)); - let block_hash: Option = self.store.get(key)?; + let block_hash: Option = self.store.get(key, None)?; Ok(block_hash) } // Retrieve the block by its hash. fn get_block_by_hash(&self, hash: &BlockHash) -> Result, IndexerStoreError> { let key = self.get_key(StoreKey::BlockByHash(*hash)); - let block: Option = self.store.get(key)?; + let block: Option = self.store.get(key, None)?; Ok(block) } - fn get_tx_info(&self, tx_id: &Txid) -> Result, IndexerStoreError> { + fn get_tx_info(&self, tx_id: &Txid) -> Result, IndexerStoreError> { let key = self.get_key(StoreKey::TransactionById(*tx_id)); - let tx_data = self.store.get::<&str, (Transaction, BlockHash)>(&key)?; + let tx_data = self.store.get::<&str, (Transaction, BlockHash)>(&key, None)?; if let Some((tx, block_hash)) = tx_data { - let mut block_info = match self.get_block_by_hash(&block_hash)? { + let block_info = match self.get_block_by_hash(&block_hash)? { Some(block) => block, None => return Err(IndexerStoreError::BlockNotFound), }; @@ -183,15 +183,18 @@ impl StoreClient for IndexerStore { // If the block is orphaned or its height is greater than the best block height, // this indicates a reorg or block invalidation where the blockchain has reverted. - if block_info.orphan || block_info.height > best_block_height { + let status = if block_info.orphan || block_info.height > best_block_height { confirmations = 0; - block_info.orphan = true; - } + TransactionBlockchainStatus::Orphan + } else { + TransactionBlockchainStatus::Confirmed // Will be updated in indexer based on confirmations and threshold + }; - Ok(Some(TransactionInfo { - tx, - block_info, + Ok(Some(TransactionStatus { + tx: Some(tx), + block_info: Some(block_info), confirmations, + status, })) } else { Ok(None) @@ -214,7 +217,7 @@ impl StoreClient for IndexerStore { fn get_best_height(&self) -> Result, IndexerStoreError> { let key = self.get_key(StoreKey::BestBlock); - let height: Option = self.store.get(key)?; + let height: Option = self.store.get(key, None)?; Ok(height) } @@ -247,7 +250,7 @@ impl StoreClient for IndexerStore { fn get_checkpoint_height(&self) -> Result, IndexerStoreError> { let key = self.get_key(StoreKey::CheckpointHeight); - let height = self.store.get(key)?; + let height = self.store.get(key, None)?; Ok(height) } diff --git a/src/types.rs b/src/types.rs index ab3652e..18f8873 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,9 @@ -use bitcoin::{BlockHash, Transaction}; +use bitcoin::{BlockHash, Transaction, Txid}; use bitvmx_bitcoin_rpc::types::BlockHeight; use serde::{Deserialize, Serialize}; +use crate::errors::IndexerError; + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct FullBlock { pub height: BlockHeight, @@ -13,8 +15,94 @@ pub struct FullBlock { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct TransactionInfo { - pub tx: Transaction, - pub block_info: FullBlock, +pub struct TransactionStatus { + pub tx: Option, + pub block_info: Option, pub confirmations: u32, + pub status: TransactionBlockchainStatus, +} + +impl TransactionStatus { + pub fn new( + tx: Transaction, + block_info: FullBlock, + status: TransactionBlockchainStatus, + confirmations: u32, + ) -> Self { + Self { + tx: Some(tx), + block_info: Some(block_info), + confirmations, + status, + } + } + + pub fn is_finalized(&self, max_monitoring_confirmations: u32) -> bool { + // A transaction is considered finalized if: + // - The status is Finalized + // - The number of confirmations meets or exceeds the confirmation threshold + self.confirmations >= max_monitoring_confirmations + && self.status == TransactionBlockchainStatus::Finalized + } + + pub fn is_confirmed(&self) -> bool { + // A transaction is considered confirmed if it has been included in a block + // and has at least one confirmation (confirmations > 0), regardless of the exact number of confirmations. + // This means the transaction is in the main chain and not orphaned. + self.confirmations > 0 && self.status == TransactionBlockchainStatus::Confirmed + } + + pub fn is_orphan(&self) -> bool { + // An orphan transaction should have: + // block_info because it was mined at some point before. + // confirmations == 0, this is just a validation - orphan transactions should be moved to confirmation 0. + // is_orphan = true + // status = Orphan + if let Some(block_info) = &self.block_info { + self.confirmations == 0 + && block_info.orphan + && self.status == TransactionBlockchainStatus::Orphan + } else { + false + } + } + + pub fn is_in_mempool(&self) -> bool { + self.status == TransactionBlockchainStatus::InMempool + } + + pub fn is_not_found(&self) -> bool { + self.status == TransactionBlockchainStatus::NotFound + } + + pub fn tx_id_or_error(&self) -> Result { + let tx = self.tx_or_err()?; + Ok(tx.compute_txid()) + } + + pub fn tx_or_err(&self) -> Result<&Transaction, IndexerError> { + self.tx.as_ref().ok_or(IndexerError::MissingTransactionData) + } + + pub fn block_info_or_err(&self) -> Result<&FullBlock, IndexerError> { + self.block_info + .as_ref() + .ok_or(IndexerError::MissingBlockInfo) + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub enum TransactionBlockchainStatus { + // Represents a transaction that has been successfully confirmed by the network but a reorganization moved it out of the chain. + Orphan, + // Represents a transaction that has been successfully confirmed by the network + Confirmed, + // Represents when the transaction was confirmed by a certain number of blocks + Finalized, + + // Represents when the transaction is in mempool and not yet confirmed + InMempool, + + // Indicates that the transaction is not present in the blockchain, not in the mempool, and has not been confirmed. + NotFound, } diff --git a/tests/feerate_test.rs b/tests/feerate_test.rs index d4b40bb..7af2808 100644 --- a/tests/feerate_test.rs +++ b/tests/feerate_test.rs @@ -3,52 +3,48 @@ use bitcoin_indexer::{ config::{IndexerConfig, IndexerSettings}, indexer::{Indexer, IndexerApi}, }; -use bitvmx_bitcoin_rpc::bitcoin_client::{BitcoinClient, BitcoinClientApi}; use bitcoind::{bitcoind::Bitcoind, config::BitcoindConfig}; +use bitvmx_bitcoin_rpc::bitcoin_client::{BitcoinClient, BitcoinClientApi}; use bitvmx_settings::settings; mod utils; -use crate::utils::{clear_output, wait_for_port_available, get_indexer_store}; +use crate::utils::{clear_output, get_indexer_store, wait_for_port_available}; /// Tests fee rate estimation with 7 transactions having different fee rates. /// Verifies that the median fee rate is correctly calculated. #[test] fn test_get_estimated_fee_rate_with_seven_transactions() -> Result<(), anyhow::Error> { + use bitcoin::absolute::LockTime; use bitcoin::key::rand::rngs::OsRng; use bitcoin::secp256k1::{Secp256k1, SecretKey}; - use bitcoin::Amount; - use bitcoin::transaction::{Transaction, TxIn, TxOut}; - use bitcoin::absolute::LockTime; + use bitcoin::sighash::{EcdsaSighashType, SighashCache}; use bitcoin::transaction::Version; - use bitcoin::Sequence; + use bitcoin::transaction::{Transaction, TxIn, TxOut}; + use bitcoin::Amount; + use bitcoin::Network; use bitcoin::OutPoint; - use bitcoin::Witness; use bitcoin::PrivateKey; - use bitcoin::sighash::{SighashCache, EcdsaSighashType}; - use bitcoin::Network; use bitcoin::PublicKey; - + use bitcoin::Sequence; + use bitcoin::Witness; + clear_output(); - + let config = settings::load::()?; let bitcoind_config = BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; - + // Mine blocks to have mature coins bitcoin_client.mine_blocks_to_address(101, &wallet)?; - + let secp = Secp256k1::new(); - + // Step 1: Create 7 UTXOs with known private keys let mut utxo_data = Vec::new(); - + for i in 0..7 { let secret_key = SecretKey::new(&mut OsRng); let private_key = PrivateKey::new(secret_key, Network::Regtest); @@ -57,33 +53,37 @@ fn test_get_estimated_fee_rate_with_seven_transactions() -> Result<(), anyhow::E compressed: true, inner: secp_pubkey, }; - + let address = bitcoin_client.get_new_address(public_key.clone(), Network::Regtest)?; - + // Fund with 100,000 sats - let (funding_tx, vout) = bitcoin_client.fund_address(&address, Amount::from_sat(100_000))?; + let (funding_tx, vout) = + bitcoin_client.fund_address(&address, Amount::from_sat(100_000))?; let txid = funding_tx.compute_txid(); - + utxo_data.push((txid, vout, private_key, address.script_pubkey(), public_key)); println!(" Created UTXO {}: {}:{}", i, txid, vout); } - + // Step 2: Create and sign 7 transactions with different fees // Fee rates: 10, 20, 30, 40, 50, 60, 70 sat/vB (median should be 40) let mut signed_txs = Vec::new(); let fee_rates = vec![10, 20, 30, 40, 50, 60, 70]; // sat/vB - - for (i, (prev_txid, prev_vout, private_key, prev_script_pubkey, public_key)) in utxo_data.iter().enumerate() { + + for (i, (prev_txid, prev_vout, private_key, prev_script_pubkey, public_key)) in + utxo_data.iter().enumerate() + { let output_secret = SecretKey::new(&mut OsRng); - let output_secp_pubkey = bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &output_secret); + let output_secp_pubkey = + bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &output_secret); let output_public_key = PublicKey { compressed: true, inner: output_secp_pubkey, }; let output_address = bitcoin_client.get_new_address(output_public_key, Network::Regtest)?; - + let target_fee_rate = fee_rates[i]; - + // First pass: Create and sign with a temporary fee to get actual vsize let mut tx = Transaction { version: Version::TWO, @@ -102,34 +102,34 @@ fn test_get_estimated_fee_rate_with_seven_transactions() -> Result<(), anyhow::E script_pubkey: output_address.script_pubkey(), }], }; - + // Sign to get actual size let sighash_type = EcdsaSighashType::All; let mut sighash_cache = SighashCache::new(&tx); - + let sighash = sighash_cache.p2wpkh_signature_hash( 0, prev_script_pubkey, Amount::from_sat(100_000), sighash_type, )?; - + let msg = bitcoin::secp256k1::Message::from_digest(*sighash.as_byte_array()); let signature = secp.sign_ecdsa(&msg, &private_key.inner); - + let mut sig_with_hashtype = signature.serialize_der().to_vec(); sig_with_hashtype.push(sighash_type.to_u32() as u8); - + let sig_push_bytes = bitcoin::script::PushBytesBuf::try_from(sig_with_hashtype.clone()) .expect("Signature should fit in PushBytes"); - + tx.input[0].witness.push(sig_push_bytes.as_bytes()); tx.input[0].witness.push(public_key.to_bytes()); - + // Get actual vsize and calculate exact fee let actual_vsize = tx.vsize() as u64; let exact_fee = target_fee_rate * actual_vsize; - + // Second pass: Recreate transaction with exact fee and re-sign let mut final_tx = Transaction { version: Version::TWO, @@ -148,42 +148,49 @@ fn test_get_estimated_fee_rate_with_seven_transactions() -> Result<(), anyhow::E script_pubkey: output_address.script_pubkey(), }], }; - + // Re-sign with new output value (outputs are part of SegWit sighash) let mut final_sighash_cache = SighashCache::new(&final_tx); - + let final_sighash = final_sighash_cache.p2wpkh_signature_hash( 0, prev_script_pubkey, Amount::from_sat(100_000), sighash_type, )?; - + let final_msg = bitcoin::secp256k1::Message::from_digest(*final_sighash.as_byte_array()); let final_signature = secp.sign_ecdsa(&final_msg, &private_key.inner); - + let mut final_sig_with_hashtype = final_signature.serialize_der().to_vec(); final_sig_with_hashtype.push(sighash_type.to_u32() as u8); - + let final_sig_push_bytes = bitcoin::script::PushBytesBuf::try_from(final_sig_with_hashtype) .expect("Signature should fit in PushBytes"); - - final_tx.input[0].witness.push(final_sig_push_bytes.as_bytes()); + + final_tx.input[0] + .witness + .push(final_sig_push_bytes.as_bytes()); final_tx.input[0].witness.push(public_key.to_bytes()); - + let final_vsize = final_tx.vsize() as u64; let final_fee_rate = exact_fee / final_vsize; - - println!(" Transaction {}: fee={} sat, vsize={}, rate={} sat/vB (target: {})", - i, exact_fee, final_vsize, final_fee_rate, target_fee_rate); - + + println!( + " Transaction {}: fee={} sat, vsize={}, rate={} sat/vB (target: {})", + i, exact_fee, final_vsize, final_fee_rate, target_fee_rate + ); + // Verify exact fee rate - assert_eq!(final_fee_rate, target_fee_rate, - "Transaction {} should have exact fee rate {}", i, target_fee_rate); - + assert_eq!( + final_fee_rate, target_fee_rate, + "Transaction {} should have exact fee rate {}", + i, target_fee_rate + ); + signed_txs.push(final_tx); } - + // Step 3: Broadcast all transactions for (i, tx) in signed_txs.iter().enumerate() { match bitcoin_client.send_transaction(tx) { @@ -196,45 +203,56 @@ fn test_get_estimated_fee_rate_with_seven_transactions() -> Result<(), anyhow::E } } } - + // Step 4: Mine one block to include all transactions bitcoin_client.mine_blocks_to_address(1, &wallet)?; - + // Create indexer and process the block let store = get_indexer_store(); - let indexer = Indexer::new( - bitcoin_client, - store, - Some(IndexerSettings::new(Some(100))), - )?; - + let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(Some(100))))?; + // Sync to the latest block for _ in 0..20 { indexer.tick()?; } - + let best_block = indexer.get_best_block()?; assert!(best_block.is_some(), "Indexer should have processed blocks"); - + let block = best_block.unwrap(); - + // Verify the block has 8 transactions (1 coinbase + 7 spending) - assert_eq!(block.txs.len(), 8, "Block should have 8 transactions (1 coinbase + 7 spending)"); - assert!(block.txs[0].is_coinbase(), "First transaction should be coinbase"); - + assert_eq!( + block.txs.len(), + 8, + "Block should have 8 transactions (1 coinbase + 7 spending)" + ); + assert!( + block.txs[0].is_coinbase(), + "First transaction should be coinbase" + ); + // The median of fee rates [10, 20, 30, 40, 50, 60, 70] should be exactly 40 - assert_eq!(block.estimated_fee_rate, 40, - "Median fee rate should be exactly 40 sat/vB, got {}", - block.estimated_fee_rate); - + assert_eq!( + block.estimated_fee_rate, 40, + "Median fee rate should be exactly 40 sat/vB, got {}", + block.estimated_fee_rate + ); + // Test the get_estimated_fee_rate API method let estimated_fee_rate = indexer.get_estimated_fee_rate()?; - assert_eq!(estimated_fee_rate, 40, "get_estimated_fee_rate should return exactly 40 sat/vB"); + assert_eq!( + estimated_fee_rate, 40, + "get_estimated_fee_rate should return exactly 40 sat/vB" + ); assert_eq!(estimated_fee_rate, block.estimated_fee_rate); - + bitcoind.stop()?; clear_output(); - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); Ok(()) } @@ -245,11 +263,7 @@ fn test_get_estimated_fee_rate_indexer_not_synced() -> Result<(), anyhow::Error> let config = settings::load::()?; let bitcoind_config = BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -258,15 +272,11 @@ fn test_get_estimated_fee_rate_indexer_not_synced() -> Result<(), anyhow::Error> // Mine blocks to height 101 bitcoin_client.mine_blocks_to_address(101, &wallet)?; - + // Create a second client to mine more blocks later let bitcoin_client_2 = BitcoinClient::new_from_config(&config.bitcoin)?; - let indexer = Indexer::new( - bitcoin_client, - store, - Some(IndexerSettings::new(Some(100))), - )?; + let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(Some(100))))?; // Process the block so the indexer is at height 100 indexer.tick()?; @@ -274,8 +284,15 @@ fn test_get_estimated_fee_rate_indexer_not_synced() -> Result<(), anyhow::Error> // Mine one more block so blockchain is ahead bitcoin_client_2.mine_blocks_to_address(1, &wallet)?; - // Try to get estimated fee rate when indexer is not synced (indexer at 100, blockchain at 102) - let result = indexer.get_estimated_fee_rate(); + // Create a new indexer instance with the updated bitcoin client + let new_indexer = Indexer { + bitcoin_client: bitcoin_client_2, + store: indexer.store.clone(), + settings: IndexerSettings::default(), + }; + + // Try to get estimated fee rate when indexer is not synced + let result = new_indexer.get_estimated_fee_rate(); // Should return IndexerError::IndexerNotSynced assert!(result.is_err()); @@ -293,7 +310,10 @@ fn test_get_estimated_fee_rate_indexer_not_synced() -> Result<(), anyhow::Error> bitcoind.stop()?; clear_output(); - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); Ok(()) } @@ -304,11 +324,7 @@ fn test_get_estimated_fee_rate_not_estimated() -> Result<(), anyhow::Error> { let config = settings::load::()?; let bitcoind_config = BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -319,11 +335,7 @@ fn test_get_estimated_fee_rate_not_estimated() -> Result<(), anyhow::Error> { // This will cause blocks to have estimated_fee_rate = 0 because there are too few transactions bitcoin_client.mine_blocks_to_address(101, &wallet)?; - let indexer = Indexer::new( - bitcoin_client, - store, - Some(IndexerSettings::new(Some(100))), - )?; + let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(Some(100))))?; // Process the block indexer.tick()?; @@ -353,7 +365,9 @@ fn test_get_estimated_fee_rate_not_estimated() -> Result<(), anyhow::Error> { bitcoind.stop()?; clear_output(); - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); Ok(()) } - diff --git a/tests/get_tx_test.rs b/tests/get_tx_test.rs new file mode 100644 index 0000000..4040004 --- /dev/null +++ b/tests/get_tx_test.rs @@ -0,0 +1,129 @@ +use anyhow::Result; +use bitcoin::{address::NetworkChecked, Address, Amount, Network}; +use bitcoin_indexer::{ + config::IndexerConfig, + indexer::{Indexer, IndexerApi}, + types::TransactionBlockchainStatus, +}; +use bitcoincore_rpc::RpcApi; +use bitcoind::{bitcoind::Bitcoind, config::BitcoindConfig}; +use bitvmx_bitcoin_rpc::bitcoin_client::{BitcoinClient, BitcoinClientApi}; +use bitvmx_settings::settings; +use tracing::info; +mod utils; +use crate::utils::{clear_output, get_indexer_store, get_random_pubkey}; + +#[test] +fn test_get_transaction_lifecycle() -> Result<(), anyhow::Error> { + clear_output(); + + let config = settings::load::()?; + + let log_level = match &config.log_level { + Some(level) => level.parse().unwrap_or(tracing::Level::ERROR), + None => tracing::Level::INFO, + }; + + let _ = tracing_subscriber::fmt() + .with_max_level(log_level) + .try_init(); + + let bitcoind_config = BitcoindConfig::default(); + + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); + + bitcoind.start()?; + + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; + let wallet = bitcoin_client.init_wallet("test_wallet")?; + let indexer_store = get_indexer_store(); + let bitcoin_client_for_indexer = BitcoinClient::new_from_config(&config.bitcoin)?; + let indexer = Indexer::new(bitcoin_client_for_indexer, indexer_store.clone(), None)?; + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; + + // Step 1: Mine some blocks to have funds + info!("Mining 110 blocks to wallet"); + bitcoin_client.mine_blocks_to_address(110, &wallet)?; + + // Step 2: Index the blocks + info!("Indexing 110 blocks"); + for _ in 0..110 { + indexer.tick()?; + } + + info!("Checking that the indexer is at height 110"); + assert_eq!(indexer.get_best_height()?, Some(110)); + assert_eq!(bitcoin_client.get_best_block()?, 110); + + // Step 3: Create a transaction and send it to mempool + info!("Creating a transaction to send to mempool"); + let recipient_pubkey = get_random_pubkey(); + let address = bitcoin_client.get_new_address(recipient_pubkey, Network::Regtest)?; + let recipient_address: &Address = &address; + + // Use RPC client to send a transaction + let txid = bitcoin_client.client.send_to_address( + recipient_address, + Amount::from_sat(10000), + None, + None, + None, + None, + None, + None, + )?; + + info!("Transaction {} sent to mempool", txid); + + // Step 4: Check that the transaction is in mempool + info!("Checking that get_transaction returns InMempool status"); + let tx_info = indexer.get_transaction(&txid, true)?; + assert_eq!(tx_info.status, TransactionBlockchainStatus::InMempool); + assert_eq!(tx_info.confirmations, 0); + assert!(tx_info.block_info.is_none()); + assert!(tx_info.tx.is_none()); // Transaction not in storage yet, only in mempool + + // Step 5: Mine a block to confirm the transaction + info!("Mining a block to confirm the transaction"); + bitcoin_client.mine_blocks_to_address(1, &wallet)?; + + // Step 6: Index the new block + info!("Indexing the new block"); + indexer.tick()?; + + // Step 7: Check that the transaction is confirmed + info!("Checking that get_transaction returns Confirmed status"); + let tx_info = indexer.get_transaction(&txid, false)?; + assert_eq!(tx_info.status, TransactionBlockchainStatus::Confirmed); + assert_eq!(tx_info.confirmations, 1); + assert!(tx_info.block_info.is_some()); + assert!(tx_info.tx.is_some()); + assert!(!tx_info.block_info.as_ref().unwrap().orphan); + + // Step 8: Revert the chain (reorg) by invalidating the block + info!("Invalidating the block to cause a reorg and mining a new block to confirm the reorg"); + let block_height = bitcoin_client.get_best_block()?; + let block_to_invalidate = bitcoin_client.get_block_by_height(&block_height)?.unwrap(); + bitcoin_client.invalidate_block(&block_to_invalidate.hash)?; + bitcoin_client.mine_blocks_to_address(1, &address)?; + + // Step 9: Index to detect the reorg + info!("Indexing to detect the reorg"); + indexer.tick()?; + indexer.tick()?; + + let block_height_indexer_after = indexer.get_best_height()?.unwrap(); + let block_height_after = bitcoin_client.get_best_block()?; + assert_eq!(block_height_indexer_after, block_height_after); + + // Step 10: Check that the transaction is now orphan + info!("Checking that get_transaction returns Confirmed status with 1 confirmation"); + let tx_info = indexer.get_transaction(&txid, false)?; + assert_eq!(tx_info.status, TransactionBlockchainStatus::Confirmed); + assert_eq!(tx_info.confirmations, 1); + assert!(!tx_info.is_orphan()); + + clear_output(); + + Ok(()) +} diff --git a/tests/indexer_test.rs b/tests/indexer_test.rs index d1f45da..2c21803 100644 --- a/tests/indexer_test.rs +++ b/tests/indexer_test.rs @@ -1,3 +1,4 @@ +use bitcoin::Network; use bitcoin_indexer::{ config::{IndexerConfig, IndexerSettings}, errors::IndexerError, @@ -5,10 +6,10 @@ use bitcoin_indexer::{ store::{IndexerStore, StoreClient}, types::FullBlock, }; -use bitcoin::Network; use bitcoind::bitcoind::Bitcoind; use bitvmx_bitcoin_rpc::bitcoin_client::{BitcoinClient, BitcoinClientApi}; use bitvmx_settings::settings; +use storage_backend::storage::KeyValueStore; mod utils; use crate::utils::{clear_output, wait_for_port_available}; use utils::get_indexer_store; @@ -16,14 +17,10 @@ use utils::get_indexer_store; #[test] fn test_get_best_block() -> Result<(), anyhow::Error> { clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -38,7 +35,7 @@ fn test_get_best_block() -> Result<(), anyhow::Error> { store.clone(), Some(IndexerSettings::new(Some(100))), )?; - + // Initially the indexer should be at checkpoint height let best_block = indexer.get_best_block()?; assert!(best_block.is_some()); @@ -58,92 +55,85 @@ fn test_get_best_block() -> Result<(), anyhow::Error> { #[test] fn indexer_constructor_checkpoint_variants() -> Result<(), anyhow::Error> { clear_output(); - + let config = settings::load::()?; // 1. No indexed block, no checkpoint (should start from genesis) { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(12, &wallet)?; let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(None)))?; // Should have saved height_to_sync = 0 assert_eq!(indexer.get_best_height()?, Some(0)); bitcoind.stop()?; - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); } // 2. No indexed block, checkpoint = 11 (should start from 11) { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(12, &wallet)?; let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(Some(11))))?; assert_eq!(indexer.get_best_height()?, Some(11)); bitcoind.stop()?; - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); } // 3. No indexed block, checkpoint > blockchain height (should error) { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(10, &wallet)?; let result = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(Some(20)))); assert!(result.is_err()); bitcoind.stop()?; - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); } // 4. Indexed block exists, checkpoint is None (should start from indexed height) { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(12, &wallet)?; - + // Get block at height 10 and save it to store let block_10 = bitcoin_client.get_block_by_height(&10)?.unwrap(); store.save_new_best_block(&block_10, 0)?; @@ -152,25 +142,24 @@ fn indexer_constructor_checkpoint_variants() -> Result<(), anyhow::Error> { let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(None)))?; assert_eq!(indexer.get_best_height()?, Some(10)); bitcoind.stop()?; - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); } // 5. Indexed block exists, checkpoint does not exist in the database and passing a checkpoint height (should use indexed height) and warn user { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(12, &wallet)?; - + // Save block 11 as indexed let block_11 = bitcoin_client.get_block_by_height(&11)?.unwrap(); store.save_new_best_block(&block_11, 0)?; @@ -179,28 +168,27 @@ fn indexer_constructor_checkpoint_variants() -> Result<(), anyhow::Error> { let indexer = Indexer::new(bitcoin_client, store, Some(IndexerSettings::new(Some(10))))?; assert_eq!(indexer.get_best_height()?, Some(11)); bitcoind.stop()?; - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); } // 6. Indexed block exists, checkpoint exist and is different from the previous checkpoint height (should error) { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(12, &wallet)?; - + // Save checkpoint at 10 store.save_checkpoint_height(10)?; - + // Save block 10 as indexed let block_10 = bitcoin_client.get_block_by_height(&10)?.unwrap(); store.save_new_best_block(&block_10, 0)?; @@ -213,25 +201,24 @@ fn indexer_constructor_checkpoint_variants() -> Result<(), anyhow::Error> { Err(IndexerError::AlreadyIndexedWithDifferentCheckpointHeight) )); bitcoind.stop()?; - assert!(wait_for_port_available(5), "Port 18443 should be available after container stop"); + assert!( + wait_for_port_available(5), + "Port 18443 should be available after container stop" + ); } // 7. Indexed block exists, checkpoint == indexed height (should use indexed height) { let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; - + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; let store = get_indexer_store(); - + bitcoin_client.mine_blocks_to_address(12, &wallet)?; - + // Save block 12 as indexed let block_12 = bitcoin_client.get_block_by_height(&12)?.unwrap(); store.save_new_best_block(&block_12, 0)?; @@ -257,11 +244,7 @@ fn test_orphan_block_not_marked_during_reorg() -> Result<(), anyhow::Error> { let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -374,17 +357,17 @@ fn test_configuration_missing_optional_fields() -> Result<(), anyhow::Error> { * Load configuration from test YAML. * Construct Indexer with loaded config. * Verify default checkpoint height (0) is used. - * Expected Result: - * Configuration loads successfully. settings defaults to None. + * Expected Result: + * Configuration loads successfully. settings defaults to None. * Indexer initializes with default checkpoint height of 0. */ - + // Test that IndexerConfig can be constructed with optional fields set to None // This simulates what happens when YAML doesn't contain settings or log_level - + // First, load the default config to get valid storage and bitcoin config let default_config = settings::load::()?; - + // Create a config with optional fields as None let config_with_none_settings = IndexerConfig { storage: default_config.storage.clone(), @@ -392,22 +375,32 @@ fn test_configuration_missing_optional_fields() -> Result<(), anyhow::Error> { settings: None, log_level: None, }; - + // Verify optional fields are None - assert!(config_with_none_settings.settings.is_none(), "settings should be None when not specified"); - assert!(config_with_none_settings.log_level.is_none(), "log_level should be None when not specified"); - + assert!( + config_with_none_settings.settings.is_none(), + "settings should be None when not specified" + ); + assert!( + config_with_none_settings.log_level.is_none(), + "log_level should be None when not specified" + ); + // Verify that when settings is None, the indexer should use default behavior // The IndexerSettings::default() has checkpoint_height set to Some(DEFAULT_CHECKPOINT_HEIGHT) let default_settings = IndexerSettings::default(); - assert_eq!(default_settings.checkpoint_height, Some(0), "default checkpoint height should be 0"); - + assert_eq!( + default_settings.checkpoint_height, + Some(0), + "default checkpoint height should be 0" + ); + Ok(()) } #[test] fn test_indexersettings_defaults() { - /* + /* * Objective: Verify IndexerSettings::default() provides expected values. * Preconditions: None. * Input: None. @@ -415,27 +408,32 @@ fn test_indexersettings_defaults() { * Call IndexerSettings::default(). * Inspect checkpoint_height field. * Expected Result: checkpoint_height equals DEFAULT_CHECKPOINT_HEIGHT (0) - */ - + */ + // Create IndexerSettings with default values let default_settings = IndexerSettings::default(); - + // Verify checkpoint_height is set to DEFAULT_CHECKPOINT_HEIGHT (0) - assert!(default_settings.checkpoint_height.is_some(), "checkpoint_height should have a value"); + assert!( + default_settings.checkpoint_height.is_some(), + "checkpoint_height should have a value" + ); assert_eq!( - default_settings.checkpoint_height.unwrap(), - 0, + default_settings.checkpoint_height.unwrap(), + 0, "default checkpoint_height should be 0 (DEFAULT_CHECKPOINT_HEIGHT)" ); - + // Test that creating settings with None still uses default when Default trait is invoked let settings_with_none = IndexerSettings::new(None); - assert!(settings_with_none.checkpoint_height.is_none(), "IndexerSettings::new(None) should have None checkpoint"); - + assert!( + settings_with_none.checkpoint_height.is_none(), + "IndexerSettings::new(None) should have None checkpoint" + ); + // But Default::default() should have Some(0) assert_ne!( - settings_with_none.checkpoint_height, - default_settings.checkpoint_height, + settings_with_none.checkpoint_height, default_settings.checkpoint_height, "new(None) and default() should produce different results" ); } @@ -453,16 +451,12 @@ fn test_initialize_from_genesis_no_checkpoint() -> Result<(), anyhow::Error> { * Verify block 0 is stored. * Expected Result: Indexer initializes successfully. get_best_height() returns Some(0). Block 0 saved to storage with correct genesis block hash. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -508,16 +502,12 @@ fn test_initialize_from_valid_checkpoint() -> Result<(), anyhow::Error> { * Query storage for block at height 100. * Expected Result: Indexer saves checkpoint height 100 to storage. get_best_height() returns Some(100). Block at height 100 saved with correct hash from regtest chain. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -533,7 +523,6 @@ fn test_initialize_from_valid_checkpoint() -> Result<(), anyhow::Error> { bitcoin_client_for_indexer, store.clone(), Some(IndexerSettings::new(Some(100))), - )?; // Verify indexer starts at checkpoint height 100 @@ -566,16 +555,12 @@ fn test_checkpoint_ahead_of_blockchain_height_fails() -> Result<(), anyhow::Erro * Catch error result. * Expected Result: Construction fails with IndexerError::CheckpointHeightAheadOfBlockchainHeight. No storage mutations. Error message clearly indicates the issue. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -622,16 +607,12 @@ fn test_resume_from_existing_indexed_height() -> Result<(), anyhow::Error> { * Verify block hash at height 75 matches regtest chain. * Expected Result: Indexer initializes with best height = 75. No errors. Ready to sync block 76 on next tick. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -649,12 +630,12 @@ fn test_resume_from_existing_indexed_height() -> Result<(), anyhow::Error> { store.clone(), Some(IndexerSettings::new(None)), )?; - + // Sync up to height 75 for _ in 0..75 { indexer.tick()?; } - + // Verify we're at height 75 assert_eq!(indexer.get_best_height()?, Some(75)); } @@ -669,7 +650,7 @@ fn test_resume_from_existing_indexed_height() -> Result<(), anyhow::Error> { let indexer_new = Indexer::new( bitcoin_client, store.clone(), - Some(IndexerSettings::new(None)), // Changed from Some(100) to None + Some(IndexerSettings::new(None)), // Changed from Some(100) to None )?; // Verify best height is 75 (resume from where we left off) @@ -702,16 +683,12 @@ fn test_indexed_height_exceeds_blockchain_height() -> Result<(), anyhow::Error> * Attempt to construct indexer pointing to new node. * Expected Result: Construction fails with IndexerError::InconsistentBlockchain. Logs error message indicating indexer is ahead of blockchain. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -729,12 +706,12 @@ fn test_indexed_height_exceeds_blockchain_height() -> Result<(), anyhow::Error> store.clone(), Some(IndexerSettings::new(None)), )?; - + // Sync up to height 120 for _ in 0..120 { indexer.tick()?; } - + // Verify we're at height 120 assert_eq!(indexer.get_best_height()?, Some(120)); } @@ -759,10 +736,7 @@ fn test_indexed_height_exceeds_blockchain_height() -> Result<(), anyhow::Error> // Verify construction fails with InconsistentBlockchain assert!(result.is_err()); - assert!(matches!( - result, - Err(IndexerError::InconsistentBlockchain) - )); + assert!(matches!(result, Err(IndexerError::InconsistentBlockchain))); bitcoind.stop()?; clear_output(); @@ -783,16 +757,12 @@ fn test_checkpoint_already_exists_and_match() -> Result<(), anyhow::Error> { * Verify no errors. * Expected Result: Indexer initializes successfully. Best height = 120. No checkpoint conflict. Ready to continue syncing. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -810,15 +780,16 @@ fn test_checkpoint_already_exists_and_match() -> Result<(), anyhow::Error> { store.clone(), Some(IndexerSettings::new(Some(100))), )?; - + // Verify starts at checkpoint 100 assert_eq!(indexer.get_best_height()?, Some(100)); - + // Sync up to height 120 - for _ in 0..20 { // Changed from 21 to 20 + for _ in 0..20 { + // Changed from 21 to 20 indexer.tick()?; } - + // Verify we're at height 120 assert_eq!(indexer.get_best_height()?, Some(120)); } @@ -860,19 +831,15 @@ fn test_different_checkpoint_height_fails() -> Result<(), anyhow::Error> { * Run indexer with checkpoint 50, sync some blocks. * Stop indexer. * Attempt to construct new indexer with different checkpoint 100 using same storage. - * Expected Result: Construction fails with IndexerError::AlreadyIndexedWithDifferentCheckpointHeight. + * Expected Result: Construction fails with IndexerError::AlreadyIndexedWithDifferentCheckpointHeight. * Error log suggests wiping database or using original checkpoint. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; @@ -886,19 +853,19 @@ fn test_different_checkpoint_height_fails() -> Result<(), anyhow::Error> { { let bitcoin_client_for_indexer = BitcoinClient::new_from_config(&config.bitcoin)?; let indexer = Indexer::new( - bitcoin_client_for_indexer, + bitcoin_client_for_indexer, store.clone(), Some(IndexerSettings::new(Some(50))), )?; - + // Verify the indexer is at checkpoint height 50 assert_eq!(indexer.get_best_height()?, Some(50)); - + // Sync a few more blocks indexer.tick()?; indexer.tick()?; indexer.tick()?; - + // Verify we've synced past the checkpoint let best_height = indexer.get_best_height()?; assert!(best_height.is_some()); @@ -938,21 +905,17 @@ fn test_database_corrupted_missing_block_hash_for_height() -> Result<(), anyhow: * Attempt to construct indexer. * Expected Result: Construction fails with IndexerError::DatabaseCorrupted. Error indicates storage inconsistency. */ - + clear_output(); - + let config = settings::load::()?; let bitcoind_config = bitcoind::config::BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None, - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; let wallet = bitcoin_client.init_wallet("test_wallet")?; - + // Create storage path and store let store_path = format!( "test_output/get_best_block_height_test/{}", @@ -960,12 +923,12 @@ fn test_database_corrupted_missing_block_hash_for_height() -> Result<(), anyhow: ); // Create the directory before initializing Storage std::fs::create_dir_all(&store_path)?; - + // Build absolute path and normalize to forward slashes for Storage backend let current_dir = std::env::current_dir()?; let absolute_store_path = current_dir.join(&store_path); let path_str = absolute_store_path.to_string_lossy().replace('\\', "/"); - + let storage_config = storage_backend::storage_config::StorageConfig::new(path_str, None); let storage = std::rc::Rc::new(storage_backend::storage::Storage::new(&storage_config)?); let store = std::rc::Rc::new(IndexerStore::new(storage.clone())?); @@ -981,7 +944,7 @@ fn test_database_corrupted_missing_block_hash_for_height() -> Result<(), anyhow: store.clone(), Some(IndexerSettings::new(Some(80))), )?; - + // Verify the indexer is at height 80 assert_eq!(indexer.get_best_height()?, Some(80)); } @@ -989,11 +952,11 @@ fn test_database_corrupted_missing_block_hash_for_height() -> Result<(), anyhow: // Step 2: Manually corrupt storage by deleting block hash entry for height 80 // while keeping best height metadata intact let corrupted_key = format!("indexer/block/height/80"); - + // Delete the block hash at height 80 directly from storage //use storage_backend::storage::KeyValueStore; - storage.delete(&corrupted_key)?; - + storage.remove(&corrupted_key, None)?; + // Verify that best height is still 80 but hash is missing assert_eq!(store.get_best_height()?, Some(80)); assert_eq!(store.get_block_hash_by_height(80)?, None); @@ -1008,12 +971,9 @@ fn test_database_corrupted_missing_block_hash_for_height() -> Result<(), anyhow: // Expected Result: Construction fails with IndexerError::DatabaseCorrupted assert!(result.is_err()); - assert!(matches!( - result, - Err(IndexerError::DatabaseCorrupted) - )); + assert!(matches!(result, Err(IndexerError::DatabaseCorrupted))); bitcoind.stop()?; clear_output(); Ok(()) -} \ No newline at end of file +} diff --git a/tests/reorg_regtest_test.rs b/tests/reorg_regtest_test.rs index 8f90512..0dcb9e2 100644 --- a/tests/reorg_regtest_test.rs +++ b/tests/reorg_regtest_test.rs @@ -22,15 +22,13 @@ fn reorganization_test() -> Result<(), anyhow::Error> { None => tracing::Level::INFO, }; - tracing_subscriber::fmt().with_max_level(log_level).init(); + let _ = tracing_subscriber::fmt() + .with_max_level(log_level) + .try_init(); let bitcoind_config = BitcoindConfig::default(); - let bitcoind = Bitcoind::new( - bitcoind_config, - config.bitcoin.clone(), - None - ); + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); bitcoind.start()?; @@ -123,3 +121,82 @@ fn reorganization_test() -> Result<(), anyhow::Error> { Ok(()) } + +#[test] +fn reorg_marks_last_three_blocks_as_orphan() -> Result<(), anyhow::Error> { + clear_output(); + + let config = settings::load::()?; + + let log_level = match config.log_level { + Some(level) => level.parse().unwrap_or(tracing::Level::ERROR), + None => tracing::Level::INFO, + }; + + let _ = tracing_subscriber::fmt() + .with_max_level(log_level) + .try_init(); + + let bitcoind_config = BitcoindConfig::default(); + + let bitcoind = Bitcoind::new(bitcoind_config, config.bitcoin.clone(), None); + + bitcoind.start()?; + + let bitcoin_client = BitcoinClient::new_from_config(&config.bitcoin)?; + let wallet = bitcoin_client.init_wallet("reorg_orphans_wallet")?; + let indexer_store = get_indexer_store(); + let bitcoin_client_for_indexer = BitcoinClient::new_from_config(&config.bitcoin)?; + let indexer = Indexer::new(bitcoin_client_for_indexer, indexer_store.clone(), None)?; + + info!("Mining 100 blocks to wallet"); + bitcoin_client.mine_blocks_to_address(100, &wallet)?; + + info!("Indexing 100 blocks"); + for _ in 0..100 { + indexer.tick()?; + } + + info!("Checking that the indexer and blockchain are at height 100"); + assert_eq!(indexer.get_best_height()?, Some(100)); + assert_eq!(bitcoin_client.get_best_block()?, 100); + + // Capture the hashes of the last 3 blocks before invalidation. + let block_98 = bitcoin_client + .get_block_by_height(&98)? + .expect("block at height 98 must exist"); + let block_99 = bitcoin_client + .get_block_by_height(&99)? + .expect("block at height 99 must exist"); + let block_100 = bitcoin_client + .get_block_by_height(&100)? + .expect("block at height 100 must exist"); + + info!("Invalidating the last 3 blocks (heights 98, 99 and 100)"); + bitcoin_client.invalidate_block(&block_100.hash)?; + bitcoin_client.invalidate_block(&block_99.hash)?; + bitcoin_client.invalidate_block(&block_98.hash)?; + + info!("Ticking once to let the indexer detect the rollback"); + indexer.tick()?; + + info!("Checking that the indexer and blockchain best height is now 97"); + assert_eq!(bitcoin_client.get_best_block()?, 97); + assert_eq!(indexer.get_best_height()?, Some(97)); + + info!("Checking that blocks 98, 99 and 100 are marked as orphan"); + for height in 98..=100 { + let block = indexer + .get_block_by_height(height)? + .expect("block must exist in indexer store"); + assert!( + block.orphan, + "block at height {} should be marked as orphan", + height + ); + } + + clear_output(); + + Ok(()) +} diff --git a/tests/store_test.rs b/tests/store_test.rs index 933594b..1153c5c 100644 --- a/tests/store_test.rs +++ b/tests/store_test.rs @@ -175,10 +175,10 @@ fn get_tx_info_test() -> Result<(), anyhow::Error> { // 1) Save block_1 and check get_tx_info method, transaction with tx_id should exist indexer_store.save_new_best_block(&block_1, 0)?; let tx_info = indexer_store.get_tx_info(&tx_id)?.unwrap(); - assert_eq!(tx_info.tx.compute_txid(), tx_id); - assert_eq!(tx_info.block_info.height, block_1.height); - assert_eq!(tx_info.block_info.orphan, false); - assert_eq!(tx_info.block_info.hash, block_1.hash); + assert_eq!(tx_info.tx.as_ref().unwrap().compute_txid(), tx_id); + assert_eq!(tx_info.block_info.as_ref().unwrap().height, block_1.height); + assert_eq!(tx_info.block_info.as_ref().unwrap().orphan, false); + assert_eq!(tx_info.block_info.as_ref().unwrap().hash, block_1.hash); assert_eq!(tx_info.confirmations, 1); // Creating a new block for height 1 @@ -200,10 +200,10 @@ fn get_tx_info_test() -> Result<(), anyhow::Error> { indexer_store.save_new_best_block(&new_block_1, 0)?; let tx_info = indexer_store.get_tx_info(&tx_id)?.unwrap(); - assert_eq!(tx_info.tx.compute_txid(), tx_id); - assert_eq!(tx_info.block_info.height, block_1.height); - assert_eq!(tx_info.block_info.orphan, true); - assert_eq!(tx_info.block_info.hash, block_1.hash); + assert_eq!(tx_info.tx.as_ref().unwrap().compute_txid(), tx_id); + assert_eq!(tx_info.block_info.as_ref().unwrap().height, block_1.height); + assert_eq!(tx_info.block_info.as_ref().unwrap().orphan, true); + assert_eq!(tx_info.block_info.as_ref().unwrap().hash, block_1.hash); // Create new block let block_hash_1 = @@ -220,10 +220,16 @@ fn get_tx_info_test() -> Result<(), anyhow::Error> { // 3) Insert new_block_1_again and check get_tx_info, transaction tx_id should exist again and not be orphan anymore. It was included in a new block at the same height indexer_store.save_new_best_block(&new_block_1_again, 0)?; let tx_info = indexer_store.get_tx_info(&tx_id)?.unwrap(); - assert_eq!(tx_info.tx.compute_txid(), tx_id); - assert_eq!(tx_info.block_info.height, new_block_1_again.height); - assert_eq!(tx_info.block_info.orphan, false); - assert_eq!(tx_info.block_info.hash, new_block_1_again.hash); + assert_eq!(tx_info.tx.as_ref().unwrap().compute_txid(), tx_id); + assert_eq!( + tx_info.block_info.as_ref().unwrap().height, + new_block_1_again.height + ); + assert_eq!(tx_info.block_info.as_ref().unwrap().orphan, false); + assert_eq!( + tx_info.block_info.as_ref().unwrap().hash, + new_block_1_again.hash + ); clear_output(); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 4caad61..6ba3cfe 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -5,10 +5,10 @@ use bitcoin::{ PublicKey, }; use bitcoin_indexer::store::IndexerStore; -use std::rc::Rc; -use storage_backend::{storage::Storage, storage_config::StorageConfig}; use std::net::TcpListener; +use std::rc::Rc; use std::time::{Duration, Instant}; +use storage_backend::{storage::Storage, storage_config::StorageConfig}; pub fn generate_random_string() -> String { use rand::Rng; @@ -21,7 +21,7 @@ pub fn generate_random_string() -> String { pub fn wait_for_port_available(timeout_secs: u64) -> bool { let start = Instant::now(); let timeout = Duration::from_secs(timeout_secs); - + while start.elapsed() < timeout { // Try to bind to the port - if successful, it's available if TcpListener::bind("127.0.0.1:18443").is_ok() { @@ -40,7 +40,7 @@ pub fn get_indexer_store() -> Rc { .join("test_output") .join("get_best_block_height_test") .join(generate_random_string()); - + // Create directory with retries for Windows file system consistency #[cfg(target_os = "windows")] { @@ -53,19 +53,22 @@ pub fn get_indexer_store() -> Rc { attempts += 1; std::thread::sleep(std::time::Duration::from_millis(50)); } - Err(e) => panic!("Failed to create directory after {} attempts: {}", MAX_ATTEMPTS, e), + Err(e) => panic!( + "Failed to create directory after {} attempts: {}", + MAX_ATTEMPTS, e + ), } } } - + #[cfg(not(target_os = "windows"))] std::fs::create_dir_all(&absolute_path).expect("Failed to create directory"); - + // Convert to string and normalize to forward slashes for RocksDB let path_str = absolute_path.to_string_lossy().replace('\\', "/"); - + let config = StorageConfig::new(path_str.clone(), None); - + // Create Storage with retries for Windows file system consistency #[cfg(target_os = "windows")] { @@ -79,14 +82,20 @@ pub fn get_indexer_store() -> Rc { } Err(e) if attempts < MAX_ATTEMPTS - 1 => { attempts += 1; - eprintln!("Attempt {} failed to create Storage at {}: {}. Retrying...", attempts, path_str, e); + eprintln!( + "Attempt {} failed to create Storage at {}: {}. Retrying...", + attempts, path_str, e + ); std::thread::sleep(std::time::Duration::from_millis(100)); } - Err(e) => panic!("Failed to create Storage after {} attempts at {}: {}", MAX_ATTEMPTS, path_str, e), + Err(e) => panic!( + "Failed to create Storage after {} attempts at {}: {}", + MAX_ATTEMPTS, path_str, e + ), } } } - + #[cfg(not(target_os = "windows"))] { let store = Rc::new(Storage::new(&config).unwrap()); @@ -98,7 +107,7 @@ pub fn get_indexer_store() -> Rc { pub fn clear_output() { // Try once, retry once on Windows-like systems with brief delay match std::fs::remove_dir_all("test_output") { - Ok(_) => {}, + Ok(_) => {} Err(_) => { std::thread::sleep(std::time::Duration::from_millis(50)); let _ = std::fs::remove_dir_all("test_output");