diff --git a/Cargo.lock b/Cargo.lock index f402b7fddd..ccb974c552 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4869,7 +4869,7 @@ dependencies = [ [[package]] name = "tari_wallet_ffi" -version = "0.18.6" +version = "0.19.0" dependencies = [ "chrono", "env_logger 0.7.1", diff --git a/applications/ffi_client/index.js b/applications/ffi_client/index.js index aa9473740b..0c1b750df5 100644 --- a/applications/ffi_client/index.js +++ b/applications/ffi_client/index.js @@ -195,9 +195,9 @@ try { let id = lib.wallet_start_transaction_validation(wallet, err); console.log("tx validation request id", id); - console.log("start utxo validation"); - id = lib.wallet_start_utxo_validation(wallet, err); - console.log("utxo validation request id", id); + console.log("start txo validation"); + id = lib.wallet_start_txo_validation(wallet, err); + console.log("txo validation request id", id); } catch (e) { console.error("validation error: ", e); } diff --git a/applications/ffi_client/lib/index.js b/applications/ffi_client/lib/index.js index db77ed0a1f..fb4d4ef50c 100644 --- a/applications/ffi_client/lib/index.js +++ b/applications/ffi_client/lib/index.js @@ -74,7 +74,7 @@ const libWallet = ffi.Library("./libtari_wallet_ffi.dylib", { wallet_get_num_confirmations_required: [u64, [walletRef, errPtr]], wallet_set_num_confirmations_required: ["void", [walletRef, u64, errPtr]], wallet_start_transaction_validation: [u64, [walletRef, errPtr]], - wallet_start_utxo_validation: [u64, [walletRef, errPtr]], + wallet_start_txo_validation: [u64, [walletRef, errPtr]], wallet_start_recovery: [bool, [walletRef, publicKeyRef, fn, errPtr]], }); diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index bee359e6d1..81a7bf4684 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -69,6 +69,7 @@ pub enum OutputManagerRequest { ScanOutputs(Vec), AddKnownOneSidedPaymentScript(KnownOneSidedPaymentScript), ReinstateCancelledInboundTx(TxId), + SetCoinbaseAbandoned(TxId, bool), } impl fmt::Display for OutputManagerRequest { @@ -106,6 +107,7 @@ impl fmt::Display for OutputManagerRequest { ScanOutputs(_) => write!(f, "ScanOutputs"), AddKnownOneSidedPaymentScript(_) => write!(f, "AddKnownOneSidedPaymentScript"), ReinstateCancelledInboundTx(_) => write!(f, "ReinstateCancelledInboundTx"), + SetCoinbaseAbandoned(_, _) => write!(f, "SetCoinbaseAbandoned"), } } } @@ -128,7 +130,7 @@ pub enum OutputManagerResponse { InvalidOutputs(Vec), SeedWords(Vec), BaseNodePublicKeySet, - UtxoValidationStarted(u64), + TxoValidationStarted(u64), Transaction((u64, Transaction, MicroTari, MicroTari)), EncryptionApplied, EncryptionRemoved, @@ -138,6 +140,7 @@ pub enum OutputManagerResponse { ScanOutputs(Vec), AddKnownOneSidedPaymentScript, ReinstatedCancelledInboundTx, + CoinbaseAbandonedSet, } pub type OutputManagerEventSender = broadcast::Sender>; @@ -385,7 +388,7 @@ impl OutputManagerHandle { pub async fn validate_txos(&mut self) -> Result { match self.handle.call(OutputManagerRequest::ValidateUtxos).await?? { - OutputManagerResponse::UtxoValidationStarted(request_key) => Ok(request_key), + OutputManagerResponse::TxoValidationStarted(request_key) => Ok(request_key), _ => Err(OutputManagerError::UnexpectedApiResponse), } } @@ -501,4 +504,15 @@ impl OutputManagerHandle { _ => Err(OutputManagerError::UnexpectedApiResponse), } } + + pub async fn set_coinbase_abandoned(&mut self, tx_id: TxId, abandoned: bool) -> Result<(), OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::SetCoinbaseAbandoned(tx_id, abandoned)) + .await?? + { + OutputManagerResponse::CoinbaseAbandonedSet => Ok(()), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } } diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 0384d04e93..8ba0efe79c 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -276,9 +276,9 @@ where TBackend: OutputManagerBackend + 'static .set_base_node_public_key(pk) .await .map(|_| OutputManagerResponse::BaseNodePublicKeySet), - OutputManagerRequest::ValidateUtxos => self - .validate_outputs() - .map(OutputManagerResponse::UtxoValidationStarted), + OutputManagerRequest::ValidateUtxos => { + self.validate_outputs().map(OutputManagerResponse::TxoValidationStarted) + }, OutputManagerRequest::GetInvalidOutputs => { let outputs = self .fetch_invalid_outputs() @@ -330,6 +330,10 @@ where TBackend: OutputManagerBackend + 'static .reinstate_cancelled_inbound_transaction(tx_id) .await .map(|_| OutputManagerResponse::ReinstatedCancelledInboundTx), + OutputManagerRequest::SetCoinbaseAbandoned(tx_id, abandoned) => self + .set_coinbase_abandoned(tx_id, abandoned) + .await + .map(|_| OutputManagerResponse::CoinbaseAbandonedSet), } } @@ -1003,6 +1007,11 @@ where TBackend: OutputManagerBackend + 'static Ok(self.resources.db.get_invalid_outputs().await?) } + pub async fn set_coinbase_abandoned(&self, tx_id: TxId, abandoned: bool) -> Result<(), OutputManagerError> { + self.resources.db.set_coinbase_abandoned(tx_id, abandoned).await?; + Ok(()) + } + async fn create_coin_split( &mut self, amount_per_split: MicroTari, diff --git a/base_layer/wallet/src/output_manager_service/storage/database.rs b/base_layer/wallet/src/output_manager_service/storage/database.rs index 3ab156b9b3..8946d78448 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database.rs @@ -115,6 +115,8 @@ pub trait OutputManagerBackend: Send + Sync + Clone { &self, block_height: u64, ) -> Result<(), OutputManagerStorageError>; + /// Set if a coinbase output is abandoned or not + fn set_coinbase_abandoned(&self, tx_id: TxId, abandoned: bool) -> Result<(), OutputManagerStorageError>; } /// Holds the state of the KeyManager being used by the Output Manager Service @@ -595,7 +597,7 @@ where T: OutputManagerBackend + 'static Ok(()) } - pub async fn set_output_as_unmined(&self, hash: HashOutput) -> Result<(), OutputManagerStorageError> { + pub async fn set_output_to_unmined(&self, hash: HashOutput) -> Result<(), OutputManagerStorageError> { let db = self.db.clone(); tokio::task::spawn_blocking(move || db.set_output_to_unmined(hash)) .await @@ -624,6 +626,14 @@ where T: OutputManagerBackend + 'static .map_err(|err| OutputManagerStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(()) } + + pub async fn set_coinbase_abandoned(&self, tx_id: TxId, abandoned: bool) -> Result<(), OutputManagerStorageError> { + let db = self.db.clone(); + tokio::task::spawn_blocking(move || db.set_coinbase_abandoned(tx_id, abandoned)) + .await + .map_err(|err| OutputManagerStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(()) + } } fn unexpected_result(req: DbKey, res: DbValue) -> Result { diff --git a/base_layer/wallet/src/output_manager_service/storage/models.rs b/base_layer/wallet/src/output_manager_service/storage/models.rs index a88101bda2..198ad10054 100644 --- a/base_layer/wallet/src/output_manager_service/storage/models.rs +++ b/base_layer/wallet/src/output_manager_service/storage/models.rs @@ -131,4 +131,5 @@ pub enum OutputStatus { ShortTermEncumberedToBeReceived, ShortTermEncumberedToBeSpent, SpentMinedUnconfirmed, + AbandonedCoinbase, } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs index e4d1b2ee50..c676801140 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs @@ -486,6 +486,31 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { Ok(()) } + fn set_coinbase_abandoned(&self, tx_id: TxId, abandoned: bool) -> Result<(), OutputManagerStorageError> { + let conn = self.database_connection.acquire_lock(); + + debug!(target: LOG_TARGET, "set_coinbase_abandoned(TxID: {})", tx_id); + + let status = if abandoned { + OutputStatus::AbandonedCoinbase as i32 + } else { + OutputStatus::EncumberedToBeReceived as i32 + }; + + diesel::update( + outputs::table.filter( + outputs::received_in_tx_id + .eq(Some(tx_id as i64)) + .and(outputs::coinbase_block_height.is_not_null()), + ), + ) + .set((outputs::status.eq(status),)) + .execute(&(*conn)) + .num_rows_affected_or_not_found(1)?; + + Ok(()) + } + fn short_term_encumber_outputs( &self, tx_id: u64, diff --git a/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs b/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs index c5f822362d..dc2ce5dda7 100644 --- a/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs +++ b/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs @@ -44,7 +44,7 @@ use tari_core::{ use tari_crypto::tari_utilities::{hex::Hex, Hashable}; use tari_shutdown::ShutdownSignal; -const LOG_TARGET: &str = "wallet::output_service::txo_validation_task_v2"; +const LOG_TARGET: &str = "wallet::output_service::txo_validation_task"; pub struct TxoValidationTask { base_node_pk: CommsPublicKey, @@ -233,16 +233,16 @@ where TBackend: OutputManagerBackend + 'static mined.len(), unmined.len() ); - for (tx, mined_height, mined_in_block, mmr_position) in &mined { + for (output, mined_height, mined_in_block, mmr_position) in &mined { info!( target: LOG_TARGET, "Updating output comm:{}: hash {} as mined at height {} with current tip at {}", - tx.commitment.to_hex(), - tx.hash.to_hex(), + output.commitment.to_hex(), + output.hash.to_hex(), mined_height, tip_height ); - self.update_output_as_mined(&tx, mined_in_block, *mined_height, *mmr_position, tip_height) + self.update_output_as_mined(&output, mined_in_block, *mined_height, *mmr_position, tip_height) .await?; } } @@ -341,7 +341,7 @@ where TBackend: OutputManagerBackend + 'static last_mined_output.commitment.to_hex() ); self.db - .set_output_as_unmined(last_mined_output.hash.clone()) + .set_output_to_unmined(last_mined_output.hash.clone()) .await .for_protocol(self.operation_id)?; } else { diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs index 3515ac60a9..cd623b88d1 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs @@ -20,13 +20,16 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::transaction_service::{ - config::TransactionServiceConfig, - error::{TransactionServiceError, TransactionServiceProtocolError, TransactionServiceProtocolErrorExt}, - handle::{TransactionEvent, TransactionEventSender}, - storage::{ - database::{TransactionBackend, TransactionDatabase}, - models::CompletedTransaction, +use crate::{ + output_manager_service::handle::OutputManagerHandle, + transaction_service::{ + config::TransactionServiceConfig, + error::{TransactionServiceError, TransactionServiceProtocolError, TransactionServiceProtocolErrorExt}, + handle::{TransactionEvent, TransactionEventSender}, + storage::{ + database::{TransactionBackend, TransactionDatabase}, + models::{CompletedTransaction, TransactionStatus}, + }, }, }; use log::*; @@ -60,6 +63,7 @@ pub struct TransactionValidationProtocol TransactionValidationPro connectivity_requester: ConnectivityRequester, config: TransactionServiceConfig, event_publisher: TransactionEventSender, + output_manager_handle: OutputManagerHandle, ) -> Self { Self { operation_id, @@ -79,6 +84,7 @@ impl TransactionValidationPro connectivity_requester, config, event_publisher, + output_manager_handle, } } @@ -298,7 +304,6 @@ impl TransactionValidationPro } } } - Ok(( mined, unmined, @@ -343,7 +348,7 @@ impl TransactionValidationPro #[allow(clippy::ptr_arg)] async fn update_transaction_as_mined( - &self, + &mut self, tx: &CompletedTransaction, mined_in_block: &BlockHash, mined_height: u64, @@ -374,12 +379,21 @@ impl TransactionValidationPro }) } + if tx.status == TransactionStatus::Coinbase { + if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx.tx_id, false).await { + warn!( + target: LOG_TARGET, + "Could not mark coinbase output for TxId: {} as not abandoned: {}", tx.tx_id, e + ); + }; + } + Ok(()) } #[allow(clippy::ptr_arg)] async fn update_coinbase_as_abandoned( - &self, + &mut self, tx: &CompletedTransaction, mined_in_block: &BlockHash, mined_height: u64, @@ -397,13 +411,20 @@ impl TransactionValidationPro .await .for_protocol(self.operation_id)?; + if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx.tx_id, true).await { + warn!( + target: LOG_TARGET, + "Could not mark coinbase output for TxId: {} as abandoned: {}", tx.tx_id, e + ); + }; + self.publish_event(TransactionEvent::TransactionCancelled(tx.tx_id)); Ok(()) } async fn update_transaction_as_unmined( - &self, + &mut self, tx: &CompletedTransaction, ) -> Result<(), TransactionServiceProtocolError> { self.db @@ -411,6 +432,15 @@ impl TransactionValidationPro .await .for_protocol(self.operation_id)?; + if tx.status == TransactionStatus::Coinbase { + if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx.tx_id, false).await { + warn!( + target: LOG_TARGET, + "Could not mark coinbase output for TxId: {} as not abandoned: {}", tx.tx_id, e + ); + }; + } + self.publish_event(TransactionEvent::TransactionBroadcast(tx.tx_id)); Ok(()) } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 929bafbe27..45eb82faa2 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -1588,6 +1588,7 @@ where self.resources.connectivity_manager.clone(), self.resources.config.clone(), self.event_publisher.clone(), + self.resources.output_manager_service.clone(), ); let join_handle = tokio::spawn(protocol.execute()); diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 4de3986195..17c4685179 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -1371,14 +1371,18 @@ impl CompletedTransactionSql { is_confirmed: bool, conn: &SqliteConnection, ) -> Result<(), TransactionStorageError> { + let status = if self.coinbase_block_height.is_some() && !is_valid { + TransactionStatus::Coinbase as i32 + } else if is_confirmed { + TransactionStatus::MinedConfirmed as i32 + } else { + TransactionStatus::MinedUnconfirmed as i32 + }; + self.update( UpdateCompletedTransactionSql { confirmations: Some(Some(num_confirmations as i64)), - status: Some(if is_confirmed { - TransactionStatus::MinedConfirmed as i32 - } else { - TransactionStatus::MinedUnconfirmed as i32 - }), + status: Some(status), mined_height: Some(Some(mined_height as i64)), mined_in_block: Some(Some(mined_in_block)), valid: Some(is_valid as i32), diff --git a/base_layer/wallet/tests/transaction_service/service.rs b/base_layer/wallet/tests/transaction_service/service.rs index 73ff0ad1c5..2427df155a 100644 --- a/base_layer/wallet/tests/transaction_service/service.rs +++ b/base_layer/wallet/tests/transaction_service/service.rs @@ -20,13 +20,14 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{ - convert::{TryFrom, TryInto}, - path::Path, - sync::Arc, - time::Duration, +use crate::{ + support::{ + base_node_wallet_rpc::{BaseNodeWalletRpcMockService, BaseNodeWalletRpcMockState}, + comms_and_services::{create_dummy_message, get_next_memory_address, setup_comms_services}, + utils::{make_input, TestParams}, + }, + transaction_service::transaction_protocols::add_transaction_to_database, }; - use chrono::{Duration as ChronoDuration, Utc}; use futures::{ channel::{mpsc, mpsc::Sender}, @@ -35,30 +36,13 @@ use futures::{ }; use prost::Message; use rand::{rngs::OsRng, RngCore}; -use tari_crypto::{ - commitment::HomomorphicCommitmentFactory, - common::Blake256, - inputs, - keys::{PublicKey as PK, SecretKey as SK}, - script, - script::{ExecutionStack, TariScript}, -}; -use tempfile::tempdir; -use tokio::{ - runtime, - runtime::{Builder, Runtime}, - sync::{broadcast, broadcast::channel}, -}; - -use crate::{ - support::{ - base_node_wallet_rpc::{BaseNodeWalletRpcMockService, BaseNodeWalletRpcMockState}, - comms_and_services::{create_dummy_message, get_next_memory_address, setup_comms_services}, - utils::{make_input, TestParams}, - }, - transaction_service::transaction_protocols::add_transaction_to_database, +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + path::Path, + sync::Arc, + time::Duration, }; -use std::collections::HashMap; use tari_common_types::{ chain_metadata::ChainMetadata, types::{PrivateKey, PublicKey, Signature}, @@ -108,6 +92,14 @@ use tari_core::{ SenderTransactionProtocol, }, }; +use tari_crypto::{ + commitment::HomomorphicCommitmentFactory, + common::Blake256, + inputs, + keys::{PublicKey as PK, SecretKey as SK}, + script, + script::{ExecutionStack, TariScript}, +}; use tari_p2p::{comms_connector::pubsub_connector, domain_message::DomainMessage, Network}; use tari_service_framework::{reply_channel, RegisterHandle, StackBuilder}; use tari_shutdown::{Shutdown, ShutdownSignal}; @@ -124,7 +116,7 @@ use tari_wallet::{ output_manager_service::{ config::OutputManagerServiceConfig, handle::OutputManagerHandle, - service::OutputManagerService, + service::{Balance, OutputManagerService}, storage::{ database::OutputManagerDatabase, models::KnownOneSidedPaymentScript, @@ -158,7 +150,13 @@ use tari_wallet::{ }, types::HashDigest, }; -use tokio::time::sleep; +use tempfile::tempdir; +use tokio::{ + runtime, + runtime::{Builder, Runtime}, + sync::{broadcast, broadcast::channel}, + time::sleep, +}; fn create_runtime() -> Runtime { Builder::new_multi_thread() @@ -3280,6 +3278,362 @@ fn test_coinbase_generation_and_monitoring() { assert!(tx.valid); } +#[test] +fn test_coinbase_abandoned() { + let _ = env_logger::try_init(); + let factories = CryptoFactories::default(); + let mut runtime = Runtime::new().unwrap(); + + let (connection, _temp_dir) = make_wallet_database_connection(None); + + let ( + mut alice_ts, + mut alice_output_manager, + _, + _connectivity_mock_state, + _, + _, + _, + _, + _, + _shutdown, + _mock_rpc_server, + server_node_identity, + mut rpc_service_state, + _, + ) = setup_transaction_service_no_comms(&mut runtime, factories, connection, None); + let mut alice_event_stream = alice_ts.get_event_stream(); + rpc_service_state.set_response_delay(Some(Duration::from_secs(1))); + + let block_height_a = 10; + + // First we create un unmined coinbase and then abandon it + let fees1 = 1000 * uT; + let reward1 = 1_000_000 * uT; + + let tx1 = runtime + .block_on(alice_ts.generate_coinbase_transaction(reward1, fees1, block_height_a)) + .unwrap(); + let transactions = runtime.block_on(alice_ts.get_completed_transactions()).unwrap(); + assert_eq!(transactions.len(), 1); + let tx_id1 = transactions + .values() + .find(|tx| tx.amount == fees1 + reward1) + .unwrap() + .tx_id; + assert_eq!( + runtime + .block_on(alice_output_manager.get_balance()) + .unwrap() + .pending_incoming_balance, + fees1 + reward1 + ); + + let transaction_query_batch_responses = vec![TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx1.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::InMempool) as i32, + block_hash: None, + confirmations: 0, + block_height: 0, + }]; + + let batch_query_response = TxQueryBatchResponsesProto { + responses: transaction_query_batch_responses, + is_synced: true, + tip_hash: Some([5u8; 16].to_vec()), + height_of_longest_chain: block_height_a + TransactionServiceConfig::default().num_confirmations_required + 1, + }; + + rpc_service_state.set_transaction_query_batch_responses(batch_query_response); + + // Start the transaction protocols + runtime + .block_on(alice_ts.set_base_node_public_key(server_node_identity.public_key().clone())) + .unwrap(); + + let balance = runtime.block_on(alice_output_manager.get_balance()).unwrap(); + assert_eq!(balance.pending_incoming_balance, fees1 + reward1); + + runtime + .block_on(alice_ts.validate_transactions()) + .expect("Validation should start"); + + runtime.block_on(async { + let delay = sleep(Duration::from_secs(30)); + tokio::pin!(delay); + let mut count = 0usize; + loop { + tokio::select! { + event = alice_event_stream.recv() => { + if let TransactionEvent::TransactionCancelled(tx_id) = &*event.unwrap() { + if tx_id == &tx_id1 { + count += 1; + } + if count == 1 { + break; + } + } + }, + () = &mut delay => { + break; + }, + } + } + assert_eq!(count, 1, "Expected a TransactionCancelled event"); + }); + + let tx = runtime.block_on(alice_ts.get_completed_transaction(tx_id1)).unwrap(); + assert_eq!(tx.status, TransactionStatus::Coinbase); + assert!(!tx.valid); + + let balance = runtime.block_on(alice_output_manager.get_balance()).unwrap(); + assert_eq!(balance, Balance { + available_balance: MicroTari(0), + time_locked_balance: Some(MicroTari(0)), + pending_incoming_balance: MicroTari(0), + pending_outgoing_balance: MicroTari(0) + }); + + let invalid_txs = runtime.block_on(alice_output_manager.get_invalid_outputs()).unwrap(); + assert!(invalid_txs.is_empty()); + + // Now we will make a coinbase that will be mined, reorged out and then reorged back in + let fees2 = 2000 * uT; + let reward2 = 2_000_000 * uT; + let block_height_b = 11; + + let tx2 = runtime + .block_on(alice_ts.generate_coinbase_transaction(reward2, fees2, block_height_b)) + .unwrap(); + let transactions = runtime.block_on(alice_ts.get_completed_transactions()).unwrap(); + assert_eq!(transactions.len(), 2); + let tx_id2 = transactions + .values() + .find(|tx| tx.amount == fees2 + reward2) + .unwrap() + .tx_id; + assert_eq!( + runtime + .block_on(alice_output_manager.get_balance()) + .unwrap() + .pending_incoming_balance, + fees2 + reward2 + ); + + let transaction_query_batch_responses = vec![TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx2.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::Mined) as i32, + block_hash: Some([11u8; 16].to_vec()), + confirmations: 2, + block_height: block_height_b, + }]; + + let batch_query_response = TxQueryBatchResponsesProto { + responses: transaction_query_batch_responses, + is_synced: true, + tip_hash: Some([13u8; 16].to_vec()), + height_of_longest_chain: block_height_b + 2, + }; + + rpc_service_state.set_transaction_query_batch_responses(batch_query_response); + + let mut block_headers = HashMap::new(); + for i in 0..=(block_height_b + 2) { + let mut block_header = BlockHeader::new(1); + block_header.height = i; + block_headers.insert(i, block_header.clone()); + } + rpc_service_state.set_blocks(block_headers); + runtime + .block_on(alice_ts.validate_transactions()) + .expect("Validation should start"); + + runtime.block_on(async { + let delay = sleep(Duration::from_secs(30)); + tokio::pin!(delay); + let mut count = 0usize; + loop { + tokio::select! { + event = alice_event_stream.recv() => { + if let TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations:_, is_valid: _} = &*event.unwrap() { + if tx_id == &tx_id2 { + count += 1; + } + if count == 1 { + break; + } + } + }, + () = &mut delay => { + break; + }, + } + } + assert_eq!(count, 1, "Expected a TransactionMinedUnconfirmed event"); + }); + + let tx = runtime.block_on(alice_ts.get_completed_transaction(tx_id2)).unwrap(); + assert_eq!(tx.status, TransactionStatus::MinedUnconfirmed); + + // Now we create a reorg + let transaction_query_batch_responses = vec![ + TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx1.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::NotStored) as i32, + block_hash: None, + confirmations: 0, + block_height: 0, + }, + TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx2.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::NotStored) as i32, + block_hash: None, + confirmations: 0, + block_height: 0, + }, + ]; + + let batch_query_response = TxQueryBatchResponsesProto { + responses: transaction_query_batch_responses, + is_synced: true, + tip_hash: Some([12u8; 16].to_vec()), + height_of_longest_chain: block_height_b + TransactionServiceConfig::default().num_confirmations_required + 1, + }; + + rpc_service_state.set_transaction_query_batch_responses(batch_query_response); + + let mut block_headers = HashMap::new(); + for i in 0..=(block_height_b + TransactionServiceConfig::default().num_confirmations_required + 1) { + let mut block_header = BlockHeader::new(2); + block_header.height = i; + block_headers.insert(i, block_header.clone()); + } + rpc_service_state.set_blocks(block_headers); + runtime + .block_on(alice_ts.validate_transactions()) + .expect("Validation should start"); + + runtime.block_on(async { + let delay = sleep(Duration::from_secs(30)); + tokio::pin!(delay); + let mut count = 0usize; + loop { + tokio::select! { + event = alice_event_stream.recv() => { + match &*event.unwrap() { + TransactionEvent::TransactionBroadcast(tx_id) => { + if tx_id == &tx_id2 { + count += 1; + } + }, + TransactionEvent::TransactionCancelled(tx_id) => { + if tx_id == &tx_id2 { + count += 1; + } + }, + _ => (), + } + + if count == 2 { + break; + } + }, + () = &mut delay => { + break; + }, + } + } + assert_eq!( + count, 2, + "Expected a TransactionBroadcast and Transaction Cancelled event" + ); + }); + + let tx = runtime.block_on(alice_ts.get_completed_transaction(tx_id2)).unwrap(); + assert_eq!(tx.status, TransactionStatus::Coinbase); + assert!(!tx.valid); + + let balance = runtime.block_on(alice_output_manager.get_balance()).unwrap(); + assert_eq!(balance, Balance { + available_balance: MicroTari(0), + time_locked_balance: Some(MicroTari(0)), + pending_incoming_balance: MicroTari(0), + pending_outgoing_balance: MicroTari(0) + }); + + // Now reorg again and have tx2 be mined + let mut block_headers = HashMap::new(); + for i in 0..=15 { + let mut block_header = BlockHeader::new(1); + block_header.height = i; + block_headers.insert(i, block_header.clone()); + } + rpc_service_state.set_blocks(block_headers.clone()); + + let transaction_query_batch_responses = vec![ + TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx1.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::NotStored) as i32, + block_hash: None, + confirmations: 0, + block_height: 0, + }, + TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx2.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::Mined) as i32, + block_hash: Some(block_headers.get(&10).unwrap().hash()), + confirmations: 5, + block_height: 10, + }, + ]; + + let batch_query_response = TxQueryBatchResponsesProto { + responses: transaction_query_batch_responses, + is_synced: true, + tip_hash: Some([20u8; 16].to_vec()), + height_of_longest_chain: 20, + }; + + rpc_service_state.set_transaction_query_batch_responses(batch_query_response); + + runtime + .block_on(alice_ts.validate_transactions()) + .expect("Validation should start"); + + runtime.block_on(async { + let delay = sleep(Duration::from_secs(30)); + tokio::pin!(delay); + let mut count = 0usize; + loop { + tokio::select! { + event = alice_event_stream.recv() => { + match &*event.unwrap() { + TransactionEvent::TransactionMined { tx_id, is_valid: _ } => { + if tx_id == &tx_id2 { + count += 1; + } + }, + TransactionEvent::TransactionCancelled(tx_id) => { + if tx_id == &tx_id1 { + count += 1; + } + }, + _ => (), + } + + if count == 2 { + break; + } + }, + () = &mut delay => { + break; + }, + } + } + assert_eq!(count, 2, "Expected a TransactionMined and TransactionCancelled event"); + }); +} + #[test] fn test_coinbase_transaction_reused_for_same_height() { let factories = CryptoFactories::default(); diff --git a/base_layer/wallet/tests/transaction_service/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service/transaction_protocols.rs index dd1020b384..6be69880ee 100644 --- a/base_layer/wallet/tests/transaction_service/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service/transaction_protocols.rs @@ -900,6 +900,7 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { connectivity.clone(), resources.config.clone(), resources.event_publisher.clone(), + resources.output_manager_service.clone(), ); let join_handle = task::spawn(protocol.execute()); @@ -923,6 +924,7 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { connectivity.clone(), resources.config.clone(), resources.event_publisher.clone(), + resources.output_manager_service.clone(), ); let join_handle = task::spawn(protocol.execute()); @@ -948,6 +950,7 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { connectivity.clone(), resources.config.clone(), resources.event_publisher.clone(), + resources.output_manager_service.clone(), ); let join_handle = task::spawn(protocol.execute()); @@ -985,6 +988,7 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { connectivity.clone(), resources.config.clone(), resources.event_publisher.clone(), + resources.output_manager_service.clone(), ); let join_handle = task::spawn(protocol.execute()); @@ -1145,6 +1149,7 @@ async fn tx_validation_protocol_reorg() { connectivity.clone(), resources.config.clone(), resources.event_publisher.clone(), + resources.output_manager_service.clone(), ); let join_handle = task::spawn(protocol.execute()); @@ -1250,6 +1255,7 @@ async fn tx_validation_protocol_reorg() { connectivity.clone(), resources.config.clone(), resources.event_publisher.clone(), + resources.output_manager_service.clone(), ); let join_handle = task::spawn(protocol.execute()); diff --git a/integration_tests/cucumber-js b/integration_tests/cucumber-js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration_tests/features/WalletMonitoring.feature b/integration_tests/features/WalletMonitoring.feature index 61e4319fd2..845690916f 100644 --- a/integration_tests/features/WalletMonitoring.feature +++ b/integration_tests/features/WalletMonitoring.feature @@ -48,9 +48,7 @@ Feature: Wallet Monitoring When I wait 30 seconds And I list all COINBASE transactions for wallet WALLET_A1 And I list all COINBASE transactions for wallet WALLET_B1 - Then the number of coinbase transactions for wallet WALLET_A1 and wallet WALLET_B1 are 3 less - # TODO: Uncomment this step when wallets can handle reorg -# Then all COINBASE transactions for wallet WALLET_A1 and wallet WALLET_B1 have consistent but opposing validity + Then all COINBASE transactions for wallet WALLET_A1 and wallet WALLET_B1 have consistent but opposing validity # 18+ mins on circle ci @long-running diff --git a/integration_tests/features/WalletRoutingMechanism.feature b/integration_tests/features/WalletRoutingMechanism.feature index e1f4a9d87e..4da83c04a8 100644 --- a/integration_tests/features/WalletRoutingMechanism.feature +++ b/integration_tests/features/WalletRoutingMechanism.feature @@ -35,9 +35,9 @@ Scenario Outline: Wallets transacting via specified routing mechanism only Examples: | NumBaseNodes | NumWallets | Mechanism | | 5 | 5 | DirectAndStoreAndForward | - | 5 | 5 | DirectOnly | +# | 5 | 5 | DirectOnly | - @long-running - Examples: - | NumBaseNodes | NumWallets | Mechanism | - | 5 | 5 | StoreAndForwardOnly | +# @long-running +# Examples: +# | NumBaseNodes | NumWallets | Mechanism | +# | 5 | 5 | StoreAndForwardOnly | diff --git a/integration_tests/features/WalletTransactions.feature b/integration_tests/features/WalletTransactions.feature index f5be7b833b..5127596ffd 100644 --- a/integration_tests/features/WalletTransactions.feature +++ b/integration_tests/features/WalletTransactions.feature @@ -78,11 +78,15 @@ Feature: Wallet Transactions When I have wallet WALLET_C connected to all seed nodes Then I import WALLET_B spent outputs to WALLET_C Then I wait for wallet WALLET_C to have at least 1000000 uT + Then I wait for 5 seconds Then I restart wallet WALLET_C Then I wait for wallet WALLET_C to have less than 1 uT - Then I check if last imported transactions are invalid in wallet WALLET_C + # TODO Either remove the check for invalid Faux tx and change the test name or implement a new way to invalidate Faux Tx + # The concept of invalidating the Faux transaction doesn't exist in this branch anymore. There has been talk of removing the Faux transaction + # for imported UTXO's anyway so until that is decided we will just check that the imported output becomes Spent + #Then I check if last imported transactions are invalid in wallet WALLET_C - @critical + @broken #Currently there is not handling for detecting that a reorged output is invalid Scenario: Wallet imports reorged outputs that become invalidated # Chain 1 Given I have a seed node SEED_B @@ -100,6 +104,7 @@ Feature: Wallet Transactions Then I stop wallet WALLET_RECEIVE_TX When I have wallet WALLET_IMPORTED connected to base node B Then I import WALLET_RECEIVE_TX unspent outputs to WALLET_IMPORTED + Then I wait for wallet WALLET_IMPORTED to have at least 1000000 uT # Chain 2 Given I have a seed node SEED_C And I have a base node C connected to seed SEED_C @@ -115,6 +120,9 @@ Feature: Wallet Transactions And node C is at height 10 Then I restart wallet WALLET_IMPORTED Then I wait for wallet WALLET_IMPORTED to have less than 1 uT + # TODO Either remove the check for invalid Faux tx and change the test name or implement a new way to invalidate Faux Tx + # The concept of invalidating the Faux transaction doesn't exist in this branch anymore. There has been talk of removing the Faux transaction + # for imported UTXO's anyway so until that is decided we will just check that the imported output becomes invalid Then I check if last imported transactions are invalid in wallet WALLET_IMPORTED @critical diff --git a/integration_tests/features/support/steps.js b/integration_tests/features/support/steps.js index 1c5937aab2..a8d102471a 100644 --- a/integration_tests/features/support/steps.js +++ b/integration_tests/features/support/steps.js @@ -2965,36 +2965,6 @@ Then( } ); -Then( - /the number of coinbase transactions for wallet (.*) and wallet (.*) are (.*) less/, - { timeout: 20 * 1000 }, - async function (walletNameA, walletNameB, count) { - const walletClientA = await this.getWallet(walletNameA).connectClient(); - const transactionsA = await walletClientA.getAllCoinbaseTransactions(); - const walletClientB = await this.getWallet(walletNameB).connectClient(); - const transactionsB = await walletClientB.getAllCoinbaseTransactions(); - if (this.resultStack.length >= 2) { - const walletStats = [this.resultStack.pop(), this.resultStack.pop()]; - console.log( - "\nCoinbase comparison: Expect this (current + deficit)", - transactionsA.length, - transactionsB.length, - Number(count), - "to equal this (previous)", - walletStats[0][1], - walletStats[1][1] - ); - expect( - transactionsA.length + transactionsB.length + Number(count) - ).to.equal(walletStats[0][1] + walletStats[1][1]); - } else { - expect( - "\nCoinbase comparison: Not enough results saved on the stack!" - ).to.equal(""); - } - } -); - Then( /all (.*) transactions for wallet (.*) and wallet (.*) have consistent but opposing validity/, { timeout: 20 * 1000 }, diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index e2f19f69ee..efe5c5bfc7 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -383,12 +383,7 @@ class InterfaceFFI { this.intPtr, ], ], - wallet_start_utxo_validation: [this.ulonglong, [this.ptr, this.intPtr]], - wallet_start_stxo_validation: [this.ulonglong, [this.ptr, this.intPtr]], - wallet_start_invalid_txo_validation: [ - this.ulonglong, - [this.ptr, this.intPtr], - ], + wallet_start_txo_validation: [this.ulonglong, [this.ptr, this.intPtr]], wallet_start_transaction_validation: [ this.ulonglong, [this.ptr, this.intPtr], @@ -1426,24 +1421,10 @@ class InterfaceFFI { return result; } - static walletStartUtxoValidation(ptr) { - let error = this.initError(); - let result = this.fn.wallet_start_utxo_validation(ptr, error); - this.checkErrorResult(error, `walletStartUtxoValidation`); - return result; - } - - static walletStartStxoValidation(ptr) { - let error = this.initError(); - let result = this.fn.wallet_start_stxo_validation(ptr, error); - this.checkErrorResult(error, `walletStartStxoValidation`); - return result; - } - - static walletStartInvalidTxoValidation(ptr) { + static walletStartTxoValidation(ptr) { let error = this.initError(); - let result = this.fn.wallet_start_invalid_txo_validation(ptr, error); - this.checkErrorResult(error, `walletStartInvalidUtxoValidation`); + let result = this.fn.wallet_start_txo_validation(ptr, error); + this.checkErrorResult(error, `walletStartTxoValidation`); return result; } diff --git a/integration_tests/helpers/ffi/walletFFI.js b/integration_tests/helpers/ffi/walletFFI.js index 7816253500..4588a26190 100644 --- a/integration_tests/helpers/ffi/walletFFI.js +++ b/integration_tests/helpers/ffi/walletFFI.js @@ -543,12 +543,7 @@ class WalletFFI { "int*", ], ], - wallet_start_utxo_validation: ["uint64", [this.tari_wallet_ptr, "int*"]], - wallet_start_stxo_validation: ["uint64", [this.tari_wallet_ptr, "int*"]], - wallet_start_invalid_txo_validation: [ - "uint64", - [this.tari_wallet_ptr, "int*"], - ], + wallet_start_txo_validation: ["uint64", [this.tari_wallet_ptr, "int*"]], wallet_start_transaction_validation: [ "uint64", [this.tari_wallet_ptr, "int*"], @@ -1817,32 +1812,12 @@ class WalletFFI { ); } - static walletStartUtxoValidation(wallet) { - return new Promise((resolve, reject) => - this.#fn.wallet_start_utxo_validation.async( - wallet, - this.error, - this.checkAsyncRes(resolve, reject, "walletStartUtxoValidation") - ) - ); - } - - static walletStartStxoValidation(wallet) { - return new Promise((resolve, reject) => - this.#fn.wallet_start_stxo_validation.async( - wallet, - this.error, - this.checkAsyncRes(resolve, reject, "walletStartStxoValidation") - ) - ); - } - - static walletStartInvalidTxoValidation(wallet) { + static walletStartTxoValidation(wallet) { return new Promise((resolve, reject) => - this.#fn.wallet_start_invalid_txo_validation.async( + this.#fn.wallet_start_txo_validation.async( wallet, this.error, - this.checkAsyncRes(resolve, reject, "walletStartInvalidTxoValidation") + this.checkAsyncRes(resolve, reject, "walletStartTxoValidation") ) ); } diff --git a/integration_tests/integration_tests@1.0.0 b/integration_tests/integration_tests@1.0.0 new file mode 100644 index 0000000000..e69de29bb2