From c8113ec0e557302ed3ecf5559b509c83d911e2cb Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Tue, 1 Jun 2021 20:41:30 +0200 Subject: [PATCH] Increment wallet key manager index during recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, when recovery is performed the key manager index is not incremented as outputs are found. This means that when new transactions are performed the key manager produces previously used spending keys for the new outputs, reusing private keys. We also did not apply the correct script private key to the recovered output. This wasn’t a problem because we are using `Nop` scripts but for any script that requires this key to be correct we would have had a problem. This PR adds the logic that when an output is rewound during recovery that we find the key manager index for the recovered spending key, update the key manager index to one more than the highest index found during recovery and also use this index to apply the correct script private key to the recovered output. This PR also contains a but of neatness refactoring in the Output Manager Service. --- .../tari_console_wallet/src/init/mod.rs | 5 +- .../src/ui/state/app_state.rs | 7 +- base_layer/key_manager/src/key_manager.rs | 10 +- .../src/output_manager_service/config.rs | 3 + .../src/output_manager_service/error.rs | 2 + .../src/output_manager_service/handle.rs | 14 +- .../master_key_manager.rs | 222 +++++++++ .../wallet/src/output_manager_service/mod.rs | 8 +- .../output_manager_service/recovery/mod.rs | 25 + .../recovery/standard_outputs_recoverer.rs | 152 ++++++ .../src/output_manager_service/resources.rs | 52 ++ .../src/output_manager_service/service.rs | 470 ++++++------------ .../storage/database.rs | 10 + .../storage/memory_db.rs | 14 + .../storage/sqlite_db.rs | 30 ++ .../{protocols => tasks}/mod.rs | 4 +- .../txo_validation_task.rs} | 10 +- .../src/utxo_scanner_service/utxo_scanning.rs | 72 +-- .../tests/output_manager_service/service.rs | 8 +- base_layer/wallet_ffi/src/callback_handler.rs | 4 +- base_layer/wallet_ffi/src/lib.rs | 2 +- integration_tests/package-lock.json | 27 +- 22 files changed, 760 insertions(+), 391 deletions(-) create mode 100644 base_layer/wallet/src/output_manager_service/master_key_manager.rs create mode 100644 base_layer/wallet/src/output_manager_service/recovery/mod.rs create mode 100644 base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs create mode 100644 base_layer/wallet/src/output_manager_service/resources.rs rename base_layer/wallet/src/output_manager_service/{protocols => tasks}/mod.rs (94%) rename base_layer/wallet/src/output_manager_service/{protocols/txo_validation_protocol.rs => tasks/txo_validation_task.rs} (99%) diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 534e6e1cde8..693395c3d23 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -50,10 +50,7 @@ use tari_shutdown::ShutdownSignal; use tari_wallet::{ base_node_service::config::BaseNodeServiceConfig, error::{WalletError, WalletStorageError}, - output_manager_service::{ - config::OutputManagerServiceConfig, - protocols::txo_validation_protocol::TxoValidationType, - }, + output_manager_service::{config::OutputManagerServiceConfig, TxoValidationType}, storage::{database::WalletDatabase, sqlite_utilities::initialize_sqlite_database_backends}, transaction_service::{ config::{TransactionRoutingMechanism, TransactionServiceConfig}, diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index 8ff089365c0..eec338d68a1 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -55,12 +55,7 @@ use tari_shutdown::ShutdownSignal; use tari_wallet::{ base_node_service::{handle::BaseNodeEventReceiver, service::BaseNodeState}, contacts_service::storage::database::Contact, - output_manager_service::{ - handle::OutputManagerEventReceiver, - protocols::txo_validation_protocol::TxoValidationType, - service::Balance, - TxId, - }, + output_manager_service::{handle::OutputManagerEventReceiver, service::Balance, TxId, TxoValidationType}, transaction_service::{ handle::TransactionEventReceiver, storage::models::{CompletedTransaction, TransactionStatus}, diff --git a/base_layer/key_manager/src/key_manager.rs b/base_layer/key_manager/src/key_manager.rs index 6717c411f89..2a6cafe326d 100644 --- a/base_layer/key_manager/src/key_manager.rs +++ b/base_layer/key_manager/src/key_manager.rs @@ -52,7 +52,7 @@ where K: SecretKey pub struct KeyManager { master_key: K, pub branch_seed: String, - pub primary_key_index: u64, + primary_key_index: u64, digest_type: PhantomData, } @@ -134,6 +134,14 @@ where pub fn master_key(&self) -> &K { &self.master_key } + + pub fn key_index(&self) -> u64 { + self.primary_key_index + } + + pub fn update_key_index(&mut self, new_index: u64) { + self.primary_key_index = new_index; + } } #[cfg(test)] diff --git a/base_layer/wallet/src/output_manager_service/config.rs b/base_layer/wallet/src/output_manager_service/config.rs index d1d918ca0cb..bf66bc69b3b 100644 --- a/base_layer/wallet/src/output_manager_service/config.rs +++ b/base_layer/wallet/src/output_manager_service/config.rs @@ -21,6 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use std::time::Duration; +use tari_key_manager::mnemonic::MnemonicLanguage; #[derive(Clone, Debug)] pub struct OutputManagerServiceConfig { @@ -28,6 +29,7 @@ pub struct OutputManagerServiceConfig { pub max_utxo_query_size: usize, pub prevent_fee_gt_amount: bool, pub peer_dial_retry_timeout: Duration, + pub seed_word_language: MnemonicLanguage, } impl Default for OutputManagerServiceConfig { @@ -37,6 +39,7 @@ impl Default for OutputManagerServiceConfig { max_utxo_query_size: 5000, prevent_fee_gt_amount: true, peer_dial_retry_timeout: Duration::from_secs(20), + seed_word_language: MnemonicLanguage::English, } } } diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 71d448a0e20..b7ab7fd6ba0 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -107,6 +107,8 @@ pub enum OutputManagerError { ScriptError(#[from] ScriptError), #[error("Master secret key does not match persisted key manager state")] MasterSecretKeyMismatch, + #[error("Private Key is not found in the current Key Chain")] + KeyNotFoundInKeyChain, } #[derive(Debug, Error, PartialEq)] diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 2ce11fa7dfe..8e4e2592ac7 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -23,9 +23,9 @@ use crate::{ output_manager_service::{ error::OutputManagerError, - protocols::txo_validation_protocol::TxoValidationType, service::Balance, storage::{database::PendingTransactionOutputs, models::KnownOneSidedPaymentScript}, + tasks::TxoValidationType, TxId, }, types::ValidationRetryStrategy, @@ -71,7 +71,7 @@ pub enum OutputManagerRequest { RemoveEncryption, GetPublicRewindKeys, FeeEstimate((MicroTari, MicroTari, u64, u64)), - RewindOutputs(Vec, u64), + ScanForRecoverableOutputs(Vec, u64), ScanOutputs(Vec, u64), UpdateMinedHeight(u64, u64), AddKnownOneSidedPaymentScript(KnownOneSidedPaymentScript), @@ -103,7 +103,7 @@ impl fmt::Display for OutputManagerRequest { GetCoinbaseTransaction(_) => write!(f, "GetCoinbaseTransaction"), GetPublicRewindKeys => write!(f, "GetPublicRewindKeys"), FeeEstimate(_) => write!(f, "FeeEstimate"), - RewindOutputs(_, _) => write!(f, "RewindAndImportOutputs"), + ScanForRecoverableOutputs(_, _) => write!(f, "ScanForRecoverableOutputs"), ScanOutputs(_, _) => write!(f, "ScanRewindAndImportOutputs"), UpdateMinedHeight(_, _) => write!(f, "UpdateMinedHeight"), AddKnownOneSidedPaymentScript(_) => write!(f, "AddKnownOneSidedPaymentScript"), @@ -137,7 +137,7 @@ pub enum OutputManagerResponse { EncryptionRemoved, PublicRewindKeys(Box), FeeEstimate(MicroTari), - RewindOutputs(Vec), + RewoundOutputs(Vec), ScanOutputs(Vec), MinedHeightUpdated, AddKnownOneSidedPaymentScript, @@ -452,17 +452,17 @@ impl OutputManagerHandle { } } - pub async fn rewind_outputs( + pub async fn scan_for_recoverable_outputs( &mut self, outputs: Vec, height: u64, ) -> Result, OutputManagerError> { match self .handle - .call(OutputManagerRequest::RewindOutputs(outputs, height)) + .call(OutputManagerRequest::ScanForRecoverableOutputs(outputs, height)) .await?? { - OutputManagerResponse::RewindOutputs(outputs) => Ok(outputs), + OutputManagerResponse::RewoundOutputs(outputs) => Ok(outputs), _ => Err(OutputManagerError::UnexpectedApiResponse), } } diff --git a/base_layer/wallet/src/output_manager_service/master_key_manager.rs b/base_layer/wallet/src/output_manager_service/master_key_manager.rs new file mode 100644 index 00000000000..7b315569dcf --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/master_key_manager.rs @@ -0,0 +1,222 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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::{ + output_manager_service::{ + error::OutputManagerError, + handle::PublicRewindKeys, + storage::database::{KeyManagerState, OutputManagerBackend, OutputManagerDatabase}, + }, + types::KeyDigest, +}; +use futures::lock::Mutex; +use log::*; +use tari_core::transactions::{ + transaction_protocol::RewindData, + types::{PrivateKey, PublicKey}, +}; +use tari_crypto::{keys::PublicKey as PublicKeyTrait, range_proof::REWIND_USER_MESSAGE_LENGTH}; +use tari_key_manager::{ + key_manager::KeyManager, + mnemonic::{from_secret_key, MnemonicLanguage}, +}; + +const LOG_TARGET: &str = "wallet::output_manager_service::master_key_manager"; + +const KEY_MANAGER_COINBASE_BRANCH_KEY: &str = "coinbase"; +const KEY_MANAGER_COINBASE_SCRIPT_BRANCH_KEY: &str = "coinbase_script"; +const KEY_MANAGER_SCRIPT_BRANCH_KEY: &str = "script"; +const KEY_MANAGER_RECOVERY_VIEWONLY_BRANCH_KEY: &str = "recovery_viewonly"; +const KEY_MANAGER_RECOVERY_BLINDING_BRANCH_KEY: &str = "recovery_blinding"; +const KEY_MANAGER_MAX_SEARCH_DEPTH: u64 = 1_000_000; + +pub(crate) struct MasterKeyManager +where TBackend: OutputManagerBackend + 'static +{ + utxo_key_manager: Mutex>, + utxo_script_key_manager: Mutex>, + coinbase_key_manager: Mutex>, + coinbase_script_key_manager: Mutex>, + rewind_data: RewindData, + db: OutputManagerDatabase, +} + +impl MasterKeyManager +where TBackend: OutputManagerBackend + 'static +{ + pub async fn new( + master_secret_key: PrivateKey, + db: OutputManagerDatabase, + ) -> Result { + // Check to see if there is any persisted state. If there is confirm that the provided master secret key matches + let key_manager_state = match db.get_key_manager_state().await? { + None => { + let starting_state = KeyManagerState { + master_key: master_secret_key, + branch_seed: "".to_string(), + primary_key_index: 0, + }; + db.set_key_manager_state(starting_state.clone()).await?; + starting_state + }, + Some(km) => { + if km.master_key != master_secret_key { + return Err(OutputManagerError::MasterSecretKeyMismatch); + } + km + }, + }; + + let utxo_key_manager = KeyManager::::from( + key_manager_state.master_key.clone(), + key_manager_state.branch_seed, + key_manager_state.primary_key_index, + ); + + let utxo_script_key_manager = KeyManager::::from( + key_manager_state.master_key.clone(), + KEY_MANAGER_SCRIPT_BRANCH_KEY.to_string(), + key_manager_state.primary_key_index, + ); + + let coinbase_key_manager = KeyManager::::from( + key_manager_state.master_key.clone(), + KEY_MANAGER_COINBASE_BRANCH_KEY.to_string(), + 0, + ); + + let coinbase_script_key_manager = KeyManager::::from( + key_manager_state.master_key.clone(), + KEY_MANAGER_COINBASE_SCRIPT_BRANCH_KEY.to_string(), + 0, + ); + + let rewind_key_manager = KeyManager::::from( + key_manager_state.master_key.clone(), + KEY_MANAGER_RECOVERY_VIEWONLY_BRANCH_KEY.to_string(), + 0, + ); + let rewind_key = rewind_key_manager.derive_key(0)?.k; + + let rewind_blinding_key_manager = KeyManager::::from( + key_manager_state.master_key, + KEY_MANAGER_RECOVERY_BLINDING_BRANCH_KEY.to_string(), + 0, + ); + let rewind_blinding_key = rewind_blinding_key_manager.derive_key(0)?.k; + + let rewind_data = RewindData { + rewind_key, + rewind_blinding_key, + proof_message: [0u8; REWIND_USER_MESSAGE_LENGTH], + }; + + Ok(Self { + utxo_key_manager: Mutex::new(utxo_key_manager), + utxo_script_key_manager: Mutex::new(utxo_script_key_manager), + coinbase_key_manager: Mutex::new(coinbase_key_manager), + coinbase_script_key_manager: Mutex::new(coinbase_script_key_manager), + rewind_data, + db, + }) + } + + pub fn rewind_data(&self) -> &RewindData { + &self.rewind_data + } + + /// Return the next pair of (spending_key, script_private_key) from the key managers. These will always be generated + /// in tandem and at corresponding increments + pub async fn get_next_spend_and_script_key(&self) -> Result<(PrivateKey, PrivateKey), OutputManagerError> { + let mut km = self.utxo_key_manager.lock().await; + let key = km.next_key()?; + + let mut skm = self.utxo_script_key_manager.lock().await; + let script_key = skm.next_key()?; + + self.db.increment_key_index().await?; + Ok((key.k, script_key.k)) + } + + pub async fn get_script_key_at_index(&self, index: u64) -> Result { + let skm = self.utxo_script_key_manager.lock().await; + let script_key = skm.derive_key(index)?; + Ok(script_key.k) + } + + pub async fn get_coinbase_spend_and_script_key_for_height( + &self, + height: u64, + ) -> Result<(PrivateKey, PrivateKey), OutputManagerError> { + let km = self.coinbase_key_manager.lock().await; + let spending_key = km.derive_key(height)?; + + let mut skm = self.coinbase_script_key_manager.lock().await; + let script_key = skm.next_key()?; + Ok((spending_key.k, script_key.k)) + } + + /// Return the Seed words for the current Master Key set in the Key Manager + pub async fn get_seed_words(&self, language: &MnemonicLanguage) -> Result, OutputManagerError> { + Ok(from_secret_key( + self.utxo_key_manager.lock().await.master_key(), + language, + )?) + } + + /// Return the public rewind keys + pub fn get_rewind_public_keys(&self) -> PublicRewindKeys { + PublicRewindKeys { + rewind_public_key: PublicKey::from_secret_key(&self.rewind_data.rewind_key), + rewind_blinding_public_key: PublicKey::from_secret_key(&self.rewind_data.rewind_blinding_key), + } + } + + /// Search the current key manager key chain to find the index of the specified key. + pub async fn find_utxo_key_index(&self, key: PrivateKey) -> Result { + let utxo_key_manager = self.utxo_key_manager.lock().await; + let current_index = (*utxo_key_manager).key_index(); + + for i in 0u64..current_index + KEY_MANAGER_MAX_SEARCH_DEPTH { + if (*utxo_key_manager).derive_key(i)?.k == key { + trace!(target: LOG_TARGET, "Key found in Key Chain at index {}", i); + return Ok(i); + } + } + + Err(OutputManagerError::KeyNotFoundInKeyChain) + } + + /// If the supplied index is higher than the current UTXO key chain indices then they will be updated. + pub async fn update_current_index_if_higher(&self, index: u64) -> Result<(), OutputManagerError> { + let mut utxo_key_manager = self.utxo_key_manager.lock().await; + let mut utxo_script_key_manager = self.utxo_script_key_manager.lock().await; + let current_index = (*utxo_key_manager).key_index(); + if index > current_index { + (*utxo_key_manager).update_key_index(index); + (*utxo_script_key_manager).update_key_index(index); + self.db.set_key_index(index).await?; + trace!(target: LOG_TARGET, "Updated UTXO Key Index to {}", index); + } + Ok(()) + } +} diff --git a/base_layer/wallet/src/output_manager_service/mod.rs b/base_layer/wallet/src/output_manager_service/mod.rs index 675db7770e1..39d01580913 100644 --- a/base_layer/wallet/src/output_manager_service/mod.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -48,10 +48,16 @@ use tokio::sync::broadcast; pub mod config; pub mod error; pub mod handle; -pub mod protocols; +mod master_key_manager; +mod recovery; +pub mod resources; #[allow(unused_assignments)] pub mod service; pub mod storage; +mod tasks; + +pub(crate) use master_key_manager::MasterKeyManager; +pub use tasks::TxoValidationType; const LOG_TARGET: &str = "wallet::output_manager_service::initializer"; diff --git a/base_layer/wallet/src/output_manager_service/recovery/mod.rs b/base_layer/wallet/src/output_manager_service/recovery/mod.rs new file mode 100644 index 00000000000..ed327537dd3 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/recovery/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +mod standard_outputs_recoverer; + +pub(crate) use standard_outputs_recoverer::StandardUtxoRecoverer; diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs new file mode 100644 index 00000000000..dc064527ed0 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -0,0 +1,152 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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::output_manager_service::{ + error::OutputManagerError, + storage::{ + database::{OutputManagerBackend, OutputManagerDatabase}, + models::DbUnblindedOutput, + }, + MasterKeyManager, +}; +use blake2::Digest; +use log::*; +use std::sync::Arc; +use tari_core::transactions::{ + transaction::{TransactionOutput, UnblindedOutput}, + types::{CryptoFactories, PrivateKey, PublicKey}, +}; +use tari_crypto::{ + common::Blake256, + inputs, + keys::PublicKey as PublicKeyTrait, + tari_utilities::{hex::Hex, ByteArray}, +}; + +const LOG_TARGET: &str = "wallet::output_manager_service::recovery"; + +pub(crate) struct StandardUtxoRecoverer { + master_key_manager: Arc>, + factories: CryptoFactories, + db: OutputManagerDatabase, +} + +impl StandardUtxoRecoverer +where TBackend: OutputManagerBackend + 'static +{ + pub fn new( + master_key_manager: Arc>, + factories: CryptoFactories, + db: OutputManagerDatabase, + ) -> Self { + Self { + master_key_manager, + factories, + db, + } + } + + /// Attempt to rewind all of the given transaction outputs into unblinded outputs. If they can be rewound then add + /// them to the database and increment the key manager index + pub async fn scan_and_recover_outputs( + &mut self, + outputs: Vec, + height: u64, + ) -> Result, OutputManagerError> { + let mut rewound_outputs: Vec = outputs + .into_iter() + .filter_map(|output| { + output + .full_rewind_range_proof( + &self.factories.range_proof, + &self.master_key_manager.rewind_data().rewind_key, + &self.master_key_manager.rewind_data().rewind_blinding_key, + ) + .ok() + .map(|v| (v, output.features, output.script, output.script_offset_public_key)) + }) + .map(|(output, features, script, script_offset_public_key)| { + let beta_hash = Blake256::new() + .chain(script.as_bytes()) + .chain(features.to_bytes()) + .chain(script_offset_public_key.as_bytes()) + .result() + .to_vec(); + let beta = PrivateKey::from_bytes(beta_hash.as_slice()) + .expect("Should be able to construct a private key from a hash"); + UnblindedOutput::new( + output.committed_value, + output.blinding_factor.clone() - beta, + Some(features), + script, + inputs!(PublicKey::from_secret_key(&output.blinding_factor)), + height, + output.blinding_factor, + script_offset_public_key, + ) + }) + .collect(); + + for output in rewound_outputs.iter_mut() { + self.update_outputs_script_private_key_and_update_key_manager_index(output) + .await?; + + let db_output = DbUnblindedOutput::from_unblinded_output(output.clone(), &self.factories)?; + self.db.add_unspent_output(db_output).await?; + + trace!( + target: LOG_TARGET, + "Output {} with value {} with {} recovered", + output + .as_transaction_input(&self.factories.commitment)? + .commitment + .to_hex(), + output.value, + output.features, + ); + } + + Ok(rewound_outputs) + } + + /// Find the key manager index that corresponds to the spending key in the rewound output, if found then modify + /// output to contain correct associated script private key and update the key manager to the highest index it has + /// seen so far. + async fn update_outputs_script_private_key_and_update_key_manager_index( + &mut self, + output: &mut UnblindedOutput, + ) -> Result<(), OutputManagerError> { + let found_index = self + .master_key_manager + .find_utxo_key_index(output.spending_key.clone()) + .await?; + + self.master_key_manager + .update_current_index_if_higher(found_index) + .await?; + + let script_private_key = self.master_key_manager.get_script_key_at_index(found_index).await?; + output.input_data = inputs!(PublicKey::from_secret_key(&script_private_key)); + output.script_private_key = script_private_key; + Ok(()) + } +} diff --git a/base_layer/wallet/src/output_manager_service/resources.rs b/base_layer/wallet/src/output_manager_service/resources.rs new file mode 100644 index 00000000000..d6e17b570b3 --- /dev/null +++ b/base_layer/wallet/src/output_manager_service/resources.rs @@ -0,0 +1,52 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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::{ + output_manager_service::{ + config::OutputManagerServiceConfig, + handle::OutputManagerEventSender, + storage::database::{OutputManagerBackend, OutputManagerDatabase}, + MasterKeyManager, + }, + transaction_service::handle::TransactionServiceHandle, +}; +use std::sync::Arc; +use tari_comms::{connectivity::ConnectivityRequester, types::CommsPublicKey}; +use tari_core::{consensus::ConsensusConstants, transactions::types::CryptoFactories}; +use tari_shutdown::ShutdownSignal; + +/// This struct is a collection of the common resources that a async task in the service requires. +#[derive(Clone)] +pub(crate) struct OutputManagerResources +where TBackend: OutputManagerBackend + 'static +{ + pub config: OutputManagerServiceConfig, + pub db: OutputManagerDatabase, + pub transaction_service: TransactionServiceHandle, + pub factories: CryptoFactories, + pub base_node_public_key: Option, + pub event_publisher: OutputManagerEventSender, + pub master_key_manager: Arc>, + pub consensus_constants: ConsensusConstants, + pub connectivity_manager: ConnectivityRequester, + pub shutdown_signal: ShutdownSignal, +} diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 328dba0982d..15771b34b8a 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -25,26 +25,30 @@ use crate::{ output_manager_service::{ config::OutputManagerServiceConfig, error::{OutputManagerError, OutputManagerProtocolError, OutputManagerStorageError}, - handle::{OutputManagerEventSender, OutputManagerRequest, OutputManagerResponse, PublicRewindKeys}, - protocols::txo_validation_protocol::{TxoValidationProtocol, TxoValidationType}, + handle::{OutputManagerEventSender, OutputManagerRequest, OutputManagerResponse}, + recovery::StandardUtxoRecoverer, + resources::OutputManagerResources, storage::{ - database::{KeyManagerState, OutputManagerBackend, OutputManagerDatabase, PendingTransactionOutputs}, + database::{OutputManagerBackend, OutputManagerDatabase, PendingTransactionOutputs}, models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, }, + tasks::{TxoValidationTask, TxoValidationType}, + MasterKeyManager, TxId, }, transaction_service::handle::TransactionServiceHandle, - types::{HashDigest, KeyDigest, ValidationRetryStrategy}, + types::{HashDigest, ValidationRetryStrategy}, }; use diesel::result::{DatabaseErrorKind, Error as DieselError}; use digest::Digest; -use futures::{pin_mut, stream::FuturesUnordered, StreamExt}; +use futures::{pin_mut, StreamExt}; use log::*; use rand::{rngs::OsRng, RngCore}; use std::{ cmp::Ordering, collections::HashMap, fmt::{self, Display}, + sync::Arc, time::Duration, }; use tari_comms::{ @@ -64,7 +68,7 @@ use tari_core::{ TransactionOutput, UnblindedOutput, }, - transaction_protocol::{sender::TransactionSenderMessage, RewindData}, + transaction_protocol::sender::TransactionSenderMessage, types::{CryptoFactories, PrivateKey, PublicKey}, CoinbaseBuilder, ReceiverTransactionProtocol, @@ -75,31 +79,17 @@ use tari_crypto::{ common::Blake256, inputs, keys::{DiffieHellmanSharedSecret, PublicKey as PublicKeyTrait, SecretKey}, - range_proof::REWIND_USER_MESSAGE_LENGTH, script, script::TariScript, tari_utilities::{hex::Hex, ByteArray}, }; -use tari_key_manager::{ - key_manager::KeyManager, - mnemonic::{from_secret_key, MnemonicLanguage}, -}; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; -use tokio::{ - sync::{broadcast, Mutex}, - task::JoinHandle, -}; +use tokio::sync::broadcast; const LOG_TARGET: &str = "wallet::output_manager_service"; const LOG_TARGET_STRESS: &str = "stress_test::output_manager_service"; -const KEY_MANAGER_COINBASE_BRANCH_KEY: &str = "coinbase"; -const KEY_MANAGER_COINBASE_SCRIPT_BRANCH_KEY: &str = "coinbase_script"; -const KEY_MANAGER_SCRIPT_BRANCH_KEY: &str = "script"; -const KEY_MANAGER_RECOVERY_VIEWONLY_BRANCH_KEY: &str = "recovery_viewonly"; -const KEY_MANAGER_RECOVERY_BLINDING_BRANCH_KEY: &str = "recovery_blinding"; - /// This service will manage a wallet's available outputs and the key manager that produces the keys for these outputs. /// The service will assemble transactions to be sent from the wallets available outputs and provide keys to receive /// outputs. When the outputs are detected on the blockchain the Transaction service will call this Service to confirm @@ -108,10 +98,6 @@ pub struct OutputManagerService where TBackend: OutputManagerBackend + 'static { resources: OutputManagerResources, - key_manager: Mutex>, - script_key_manager: Mutex>, - coinbase_key_manager: Mutex>, - coinbase_script_key_manager: Mutex>, request_stream: Option>>, base_node_update_publisher: broadcast::Sender, @@ -138,73 +124,12 @@ where TBackend: OutputManagerBackend + 'static connectivity_manager: ConnectivityRequester, master_secret_key: CommsSecretKey, ) -> Result, OutputManagerError> { - // Check to see if there is any persisted state. If there is confirm that the provided master secret key matches - let key_manager_state = match db.get_key_manager_state().await? { - None => { - let starting_state = KeyManagerState { - master_key: master_secret_key, - branch_seed: "".to_string(), - primary_key_index: 0, - }; - db.set_key_manager_state(starting_state.clone()).await?; - starting_state - }, - Some(km) => { - if km.master_key != master_secret_key { - return Err(OutputManagerError::MasterSecretKeyMismatch); - } - km - }, - }; - - let coinbase_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), - KEY_MANAGER_COINBASE_BRANCH_KEY.to_string(), - 0, - ); - - let coinbase_script_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), - KEY_MANAGER_COINBASE_SCRIPT_BRANCH_KEY.to_string(), - 0, - ); - - let key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), - key_manager_state.branch_seed, - key_manager_state.primary_key_index, - ); - - let script_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), - KEY_MANAGER_SCRIPT_BRANCH_KEY.to_string(), - key_manager_state.primary_key_index, - ); - - let rewind_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), - KEY_MANAGER_RECOVERY_VIEWONLY_BRANCH_KEY.to_string(), - 0, - ); - let rewind_key = rewind_key_manager.derive_key(0)?.k; - - let rewind_blinding_key_manager = KeyManager::::from( - key_manager_state.master_key, - KEY_MANAGER_RECOVERY_BLINDING_BRANCH_KEY.to_string(), - 0, - ); - let rewind_blinding_key = rewind_blinding_key_manager.derive_key(0)?.k; - - let rewind_data = RewindData { - rewind_key, - rewind_blinding_key, - proof_message: [0u8; REWIND_USER_MESSAGE_LENGTH], - }; - // Clear any encumberances for transactions that were being negotiated but did not complete to become official // Pending Transactions. db.clear_short_term_encumberances().await?; + let master_key_manager = MasterKeyManager::new(master_secret_key, db.clone()).await?; + let resources = OutputManagerResources { config, db, @@ -212,7 +137,7 @@ where TBackend: OutputManagerBackend + 'static factories, base_node_public_key: None, event_publisher, - rewind_data, + master_key_manager: Arc::new(master_key_manager), consensus_constants, connectivity_manager, shutdown_signal, @@ -222,10 +147,6 @@ where TBackend: OutputManagerBackend + 'static Ok(OutputManagerService { resources, - key_manager: Mutex::new(key_manager), - script_key_manager: Mutex::new(script_key_manager), - coinbase_key_manager: Mutex::new(coinbase_key_manager), - coinbase_script_key_manager: Mutex::new(coinbase_script_key_manager), request_stream: Some(request_stream), base_node_update_publisher, base_node_service, @@ -242,16 +163,13 @@ where TBackend: OutputManagerBackend + 'static let mut shutdown = self.resources.shutdown_signal.clone(); - let mut txo_validation_handles: FuturesUnordered>> = - FuturesUnordered::new(); - info!(target: LOG_TARGET, "Output Manager Service started"); loop { futures::select! { request_context = request_stream.select_next_some() => { trace!(target: LOG_TARGET, "Handling Service API Request"); let (request, reply_tx) = request_context.split(); - let response = self.handle_request(request, &mut txo_validation_handles).await.map_err(|e| { + let response = self.handle_request(request).await.map_err(|e| { warn!(target: LOG_TARGET, "Error handling request: {:?}", e); e }); @@ -260,13 +178,6 @@ where TBackend: OutputManagerBackend + 'static e }); }, - join_result = txo_validation_handles.select_next_some() => { - trace!(target: LOG_TARGET, "TXO Validation protocol has ended with result {:?}", join_result); - match join_result { - Ok(join_result_inner) => self.complete_utxo_validation_protocol(join_result_inner).await, - Err(e) => error!(target: LOG_TARGET, "Error resolving TXO Validation protocol: {:?}", e), - }; - } _ = shutdown => { info!(target: LOG_TARGET, "Output manager service shutting down because it received the shutdown signal"); break; @@ -285,7 +196,6 @@ where TBackend: OutputManagerBackend + 'static async fn handle_request( &mut self, request: OutputManagerRequest, - txo_validation_handles: &mut FuturesUnordered>>, ) -> Result { trace!(target: LOG_TARGET, "Handling Service Request: {}", request); match request { @@ -365,13 +275,18 @@ where TBackend: OutputManagerBackend + 'static .collect(); Ok(OutputManagerResponse::UnspentOutputs(outputs)) }, - OutputManagerRequest::GetSeedWords => self.get_seed_words().await.map(OutputManagerResponse::SeedWords), + OutputManagerRequest::GetSeedWords => self + .resources + .master_key_manager + .get_seed_words(&self.resources.config.seed_word_language) + .await + .map(OutputManagerResponse::SeedWords), OutputManagerRequest::SetBaseNodePublicKey(pk) => self .set_base_node_public_key(pk) .await .map(|_| OutputManagerResponse::BaseNodePublicKeySet), OutputManagerRequest::ValidateUtxos(validation_type, retries) => self - .validate_outputs(validation_type, retries, txo_validation_handles) + .validate_outputs(validation_type, retries) .map(OutputManagerResponse::UtxoValidationStarted), OutputManagerRequest::GetInvalidOutputs => { let outputs = self @@ -402,12 +317,16 @@ where TBackend: OutputManagerBackend + 'static .map_err(OutputManagerError::OutputManagerStorageError), OutputManagerRequest::GetPublicRewindKeys => Ok(OutputManagerResponse::PublicRewindKeys(Box::new( - self.get_rewind_public_keys(), + self.resources.master_key_manager.get_rewind_public_keys(), ))), - OutputManagerRequest::RewindOutputs(outputs, height) => self - .rewind_outputs(outputs, height) - .await - .map(OutputManagerResponse::RewindOutputs), + OutputManagerRequest::ScanForRecoverableOutputs(outputs, height) => StandardUtxoRecoverer::new( + self.resources.master_key_manager.clone(), + self.resources.factories.clone(), + self.resources.db.clone(), + ) + .scan_and_recover_outputs(outputs, height) + .await + .map(OutputManagerResponse::RewoundOutputs), OutputManagerRequest::ScanOutputs(outputs, height) => self .scan_outputs_for_one_sided_payments(outputs, height) .await @@ -430,14 +349,13 @@ where TBackend: OutputManagerBackend + 'static &mut self, validation_type: TxoValidationType, retry_strategy: ValidationRetryStrategy, - txo_validation_handles: &mut FuturesUnordered>>, ) -> Result { match self.resources.base_node_public_key.as_ref() { None => Err(OutputManagerError::NoBaseNodeKeysProvided), Some(pk) => { let id = OsRng.next_u64(); - let utxo_validation_protocol = TxoValidationProtocol::new( + let utxo_validation_task = TxoValidationTask::new( id, validation_type, retry_strategy, @@ -446,31 +364,28 @@ where TBackend: OutputManagerBackend + 'static self.base_node_update_publisher.subscribe(), ); - let join_handle = tokio::spawn(utxo_validation_protocol.execute()); - txo_validation_handles.push(join_handle); + tokio::spawn(async move { + match utxo_validation_task.execute().await { + Ok(id) => { + info!( + target: LOG_TARGET, + "UTXO Validation Protocol (Id: {}) completed successfully", id + ); + }, + Err(OutputManagerProtocolError { id, error }) => { + warn!( + target: LOG_TARGET, + "Error completing UTXO Validation Protocol (Id: {}): {:?}", id, error + ); + }, + } + }); Ok(id) }, } } - async fn complete_utxo_validation_protocol(&mut self, join_result: Result) { - match join_result { - Ok(id) => { - info!( - target: LOG_TARGET, - "UTXO Validation Protocol (Id: {}) completed successfully", id - ); - }, - Err(OutputManagerProtocolError { id, error }) => { - warn!( - target: LOG_TARGET, - "Error completing UTXO Validation Protocol (Id: {}): {:?}", id, error - ); - }, - } - } - /// Add an unblinded output to the unspent outputs list pub async fn add_output(&mut self, output: UnblindedOutput) -> Result<(), OutputManagerError> { debug!( @@ -502,7 +417,11 @@ where TBackend: OutputManagerBackend + 'static return Err(OutputManagerError::InvalidScriptHash); } - let (spending_key, script_private_key) = self.get_next_spend_and_script_key().await?; + let (spending_key, script_private_key) = self + .resources + .master_key_manager + .get_next_spend_and_script_key() + .await?; let output = DbUnblindedOutput::from_unblinded_output( UnblindedOutput::new( @@ -533,65 +452,12 @@ where TBackend: OutputManagerBackend + 'static spending_key, OutputFeatures::default(), &self.resources.factories, - &self.resources.rewind_data, + self.resources.master_key_manager.rewind_data(), ); Ok(rtp) } - /// Request a Coinbase transaction for a specific block height. All existing pending transactions with - /// this blockheight will be cancelled. - /// The key will be derived from the coinbase specific keychain using the blockheight as an index. The coinbase - /// keychain is based on the wallets master_key and the "coinbase" branch. - async fn get_coinbase_transaction( - &mut self, - tx_id: TxId, - reward: MicroTari, - fees: MicroTari, - block_height: u64, - ) -> Result { - self.resources - .db - .cancel_pending_transaction_at_block_height(block_height) - .await?; - - let (spending_key, script_key) = self.get_coinbase_spend_and_script_key_for_height(block_height).await?; - - let nonce = PrivateKey::random(&mut OsRng); - let (tx, _) = CoinbaseBuilder::new(self.resources.factories.clone()) - .with_block_height(block_height) - .with_fees(fees) - .with_spend_key(spending_key.clone()) - .with_script_key(script_key.clone()) - .with_nonce(nonce) - .with_rewind_data(self.resources.rewind_data.clone()) - .build_with_reward(&self.resources.consensus_constants, reward)?; - - let output = DbUnblindedOutput::from_unblinded_output( - UnblindedOutput::new( - reward + fees, - spending_key, - Some(OutputFeatures::create_coinbase( - block_height + self.resources.consensus_constants.coinbase_lock_height(), - )), - script!(Nop), - inputs!(PublicKey::from_secret_key(&script_key)), - block_height, - script_key, - PublicKey::default(), - ), - &self.resources.factories, - )?; - - self.resources - .db - .accept_incoming_pending_transaction(tx_id, output, Some(block_height)) - .await?; - - self.confirm_encumberance(tx_id).await?; - Ok(tx) - } - /// Confirm the reception of an expected transaction output. This will be called by the Transaction Service when it /// detects the output on the blockchain pub async fn confirm_received_transaction_output( @@ -709,10 +575,14 @@ where TBackend: OutputManagerBackend + 'static // If the input values > the amount to be sent + fee_without_change then we will need to include a change // output if total > amount + fee_without_change { - let (spending_key, script_private_key) = self.get_next_spend_and_script_key().await?; + let (spending_key, script_private_key) = self + .resources + .master_key_manager + .get_next_spend_and_script_key() + .await?; change_keys = Some((spending_key.clone(), script_private_key.clone())); builder.with_change_secret(spending_key); - builder.with_rewindable_outputs(self.resources.rewind_data.clone()); + builder.with_rewindable_outputs(self.resources.master_key_manager.rewind_data().clone()); builder.with_change_script( script!(Nop), inputs!(PublicKey::from_secret_key(&script_private_key)), @@ -765,6 +635,63 @@ where TBackend: OutputManagerBackend + 'static Ok(stp) } + /// Request a Coinbase transaction for a specific block height. All existing pending transactions with + /// this blockheight will be cancelled. + /// The key will be derived from the coinbase specific keychain using the blockheight as an index. The coinbase + /// keychain is based on the wallets master_key and the "coinbase" branch. + async fn get_coinbase_transaction( + &mut self, + tx_id: TxId, + reward: MicroTari, + fees: MicroTari, + block_height: u64, + ) -> Result { + self.resources + .db + .cancel_pending_transaction_at_block_height(block_height) + .await?; + + let (spending_key, script_key) = self + .resources + .master_key_manager + .get_coinbase_spend_and_script_key_for_height(block_height) + .await?; + + let nonce = PrivateKey::random(&mut OsRng); + let (tx, _) = CoinbaseBuilder::new(self.resources.factories.clone()) + .with_block_height(block_height) + .with_fees(fees) + .with_spend_key(spending_key.clone()) + .with_script_key(script_key.clone()) + .with_nonce(nonce) + .with_rewind_data(self.resources.master_key_manager.rewind_data().clone()) + .build_with_reward(&self.resources.consensus_constants, reward)?; + + let output = DbUnblindedOutput::from_unblinded_output( + UnblindedOutput::new( + reward + fees, + spending_key, + Some(OutputFeatures::create_coinbase( + block_height + self.resources.consensus_constants.coinbase_lock_height(), + )), + script!(Nop), + inputs!(PublicKey::from_secret_key(&script_key)), + block_height, + script_key, + PublicKey::default(), + ), + &self.resources.factories, + )?; + + self.resources + .db + .accept_incoming_pending_transaction(tx_id, output, Some(block_height)) + .await?; + + self.confirm_encumberance(tx_id).await?; + Ok(tx) + } + async fn create_pay_to_self_transaction( &mut self, amount: MicroTari, @@ -796,7 +723,11 @@ where TBackend: OutputManagerBackend + 'static ); } - let (spending_key, script_private_key) = self.get_next_spend_and_script_key().await?; + let (spending_key, script_private_key) = self + .resources + .master_key_manager + .get_next_spend_and_script_key() + .await?; let utxo = DbUnblindedOutput::from_unblinded_output( UnblindedOutput::new( amount, @@ -818,10 +749,14 @@ where TBackend: OutputManagerBackend + 'static let fee = Fee::calculate(fee_per_gram, 1, inputs.len(), 1); let change_value = total.saturating_sub(amount).saturating_sub(fee); if change_value > 0.into() { - let (spending_key, script_private_key) = self.get_next_spend_and_script_key().await?; + let (spending_key, script_private_key) = self + .resources + .master_key_manager + .get_next_spend_and_script_key() + .await?; change_keys = Some((spending_key.clone(), script_private_key.clone())); builder.with_change_secret(spending_key); - builder.with_rewindable_outputs(self.resources.rewind_data.clone()); + builder.with_rewindable_outputs(self.resources.master_key_manager.rewind_data().clone()); builder.with_change_script( script!(Nop), inputs!(PublicKey::from_secret_key(&script_private_key)), @@ -1155,7 +1090,7 @@ where TBackend: OutputManagerBackend + 'static .with_fee_per_gram(fee_per_gram) .with_offset(offset.clone()) .with_private_nonce(nonce.clone()) - .with_rewindable_outputs(self.resources.rewind_data.clone()); + .with_rewindable_outputs(self.resources.master_key_manager.rewind_data().clone()); trace!(target: LOG_TARGET, "Add inputs to coin split transaction."); for uo in inputs.iter() { @@ -1179,7 +1114,11 @@ where TBackend: OutputManagerBackend + 'static change_output }; - let (spending_key, script_private_key) = self.get_next_spend_and_script_key().await?; + let (spending_key, script_private_key) = self + .resources + .master_key_manager + .get_next_spend_and_script_key() + .await?; let script_offset_private_key = PrivateKey::random(&mut OsRng); let utxo = DbUnblindedOutput::from_unblinded_output( @@ -1219,81 +1158,8 @@ where TBackend: OutputManagerBackend + 'static Ok((tx_id, tx, fee, utxos_total_value)) } - /// Return the Seed words for the current Master Key set in the Key Manager - pub async fn get_seed_words(&self) -> Result, OutputManagerError> { - Ok(from_secret_key( - self.key_manager.lock().await.master_key(), - &MnemonicLanguage::English, - )?) - } - - /// Return the public rewind keys - fn get_rewind_public_keys(&self) -> PublicRewindKeys { - PublicRewindKeys { - rewind_public_key: PublicKey::from_secret_key(&self.resources.rewind_data.rewind_key), - rewind_blinding_public_key: PublicKey::from_secret_key(&self.resources.rewind_data.rewind_blinding_key), - } - } - - /// Attempt to rewind all of the given transaction outputs into unblinded outputs - async fn rewind_outputs( - &mut self, - outputs: Vec, - height: u64, - ) -> Result, OutputManagerError> { - let rewind_data = &self.resources.rewind_data; - - let rewound_outputs: Vec = outputs - .into_iter() - .filter_map(|output| { - output - .full_rewind_range_proof( - &self.resources.factories.range_proof, - &rewind_data.rewind_key, - &rewind_data.rewind_blinding_key, - ) - .ok() - .map(|v| (v, output.features, output.script, output.script_offset_public_key)) - }) - .map(|(output, features, script, script_offset_public_key)| { - let beta_hash = Blake256::new() - .chain(script.as_bytes()) - .chain(features.to_bytes()) - .chain(script_offset_public_key.as_bytes()) - .result() - .to_vec(); - let beta = PrivateKey::from_bytes(beta_hash.as_slice()) - .expect("Should be able to construct a private key from a hash"); - // TODO actually increment the keymanager with found private keys so that we don't reuse private keys - // once recovery is complete and use the correct script private key - UnblindedOutput::new( - output.committed_value, - output.blinding_factor.clone() - beta, - Some(features), - script!(Nop), - inputs!(PublicKey::from_secret_key(&output.blinding_factor)), - height, - output.blinding_factor, - script_offset_public_key, - ) - }) - .collect(); - for output in &rewound_outputs { - trace!( - target: LOG_TARGET, - "Output {} with value {} with {} recovered", - output - .as_transaction_input(&self.resources.factories.commitment)? - .commitment - .to_hex(), - output.value, - output.features, - ); - } - - Ok(rewound_outputs) - } - + /// Persist a one-sided payment script for a Comms Public/Private key. These are the scripts that this wallet knows + /// to look for when scanning for one-sided payments async fn add_known_script(&mut self, known_script: KnownOneSidedPaymentScript) -> Result<(), OutputManagerError> { debug!(target: LOG_TARGET, "Adding new script to output manager service"); // It is not a problem if the script has already been persisted @@ -1344,49 +1210,34 @@ where TBackend: OutputManagerBackend + 'static .to_vec(); let beta = PrivateKey::from_bytes(beta_hash.as_slice())?; - if rewound.is_ok() { - let rewound_output = rewound.unwrap(); - rewound_outputs.push(UnblindedOutput::new( - rewound_output.committed_value, - rewound_output.blinding_factor.clone() - beta, + if let Ok(rewound_result) = rewound { + let rewound_output = UnblindedOutput::new( + rewound_result.committed_value, + rewound_result.blinding_factor.clone() - beta, Some(output.features), known_one_sided_payment_scripts[i].script.clone(), known_one_sided_payment_scripts[i].input.clone(), height, known_one_sided_payment_scripts[i].private_key.clone(), output.script_offset_public_key, - )) + ); + let db_output = + DbUnblindedOutput::from_unblinded_output(rewound_output.clone(), &self.resources.factories)?; + + rewound_outputs.push(rewound_output); + self.resources.db.add_unspent_output(db_output).await?; + trace!( + target: LOG_TARGET, + "One-sided payment Output {} with value {} recovered", + output.commitment.to_hex(), + rewound_result.committed_value, + ); } } } Ok(rewound_outputs) } - - /// Return the next pair of (spending_key, script_private_key) from the key managers. These will always be generated - /// in tandem and at corresponding increments - async fn get_next_spend_and_script_key(&self) -> Result<(PrivateKey, PrivateKey), OutputManagerError> { - let mut km = self.key_manager.lock().await; - let key = km.next_key()?; - - let mut skm = self.script_key_manager.lock().await; - let script_key = skm.next_key()?; - - self.resources.db.increment_key_index().await?; - Ok((key.k, script_key.k)) - } - - async fn get_coinbase_spend_and_script_key_for_height( - &self, - height: u64, - ) -> Result<(PrivateKey, PrivateKey), OutputManagerError> { - let km = self.coinbase_key_manager.lock().await; - let spending_key = km.derive_key(height)?; - - let mut skm = self.coinbase_script_key_manager.lock().await; - let script_key = skm.next_key()?; - Ok((spending_key.k, script_key.k)) - } } /// Different UTXO selection strategies for choosing which UTXO's are used to fulfill a transaction @@ -1445,23 +1296,6 @@ impl fmt::Display for Balance { } } -/// This struct is a collection of the common resources that a async task in the service requires. -#[derive(Clone)] -pub struct OutputManagerResources -where TBackend: OutputManagerBackend + 'static -{ - pub config: OutputManagerServiceConfig, - pub db: OutputManagerDatabase, - pub transaction_service: TransactionServiceHandle, - pub factories: CryptoFactories, - pub base_node_public_key: Option, - pub event_publisher: OutputManagerEventSender, - pub rewind_data: RewindData, - pub consensus_constants: ConsensusConstants, - pub connectivity_manager: ConnectivityRequester, - pub shutdown_signal: ShutdownSignal, -} - fn hash_secret_key(key: &PrivateKey) -> Vec { HashDigest::new().chain(key.as_bytes()).result().to_vec() } 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 fc9d0122ed0..5d34a8b2d39 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database.rs @@ -80,6 +80,8 @@ pub trait OutputManagerBackend: Send + Sync + Clone { /// This method will increment the currently stored key index for the key manager config. Increment this after each /// key is generated fn increment_key_index(&self) -> Result<(), OutputManagerStorageError>; + /// This method will set the currently stored key index for the key manager + fn set_key_index(&self, index: u64) -> Result<(), OutputManagerStorageError>; /// If an unspent output is detected as invalid (i.e. not available on the blockchain) then it should be moved to /// the invalid outputs collection. The function will return the last recorded TxId associated with this output. fn invalidate_unspent_output(&self, output: &DbUnblindedOutput) -> Result, OutputManagerStorageError>; @@ -220,6 +222,14 @@ where T: OutputManagerBackend + 'static Ok(()) } + pub async fn set_key_index(&self, index: u64) -> Result<(), OutputManagerStorageError> { + let db_clone = self.db.clone(); + tokio::task::spawn_blocking(move || db_clone.set_key_index(index)) + .await + .map_err(|err| OutputManagerStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(()) + } + pub async fn add_unspent_output(&self, output: DbUnblindedOutput) -> Result<(), OutputManagerStorageError> { let db_clone = self.db.clone(); tokio::task::spawn_blocking(move || { diff --git a/base_layer/wallet/src/output_manager_service/storage/memory_db.rs b/base_layer/wallet/src/output_manager_service/storage/memory_db.rs index 002378b76e4..5fac552b5f0 100644 --- a/base_layer/wallet/src/output_manager_service/storage/memory_db.rs +++ b/base_layer/wallet/src/output_manager_service/storage/memory_db.rs @@ -381,6 +381,20 @@ impl OutputManagerBackend for OutputManagerMemoryDatabase { Ok(()) } + fn set_key_index(&self, index: u64) -> Result<(), OutputManagerStorageError> { + let mut db = acquire_write_lock!(self.db); + + if db.key_manager_state.is_none() { + return Err(OutputManagerStorageError::KeyManagerNotInitialized); + } + db.key_manager_state = db.key_manager_state.clone().map(|mut state| { + state.primary_key_index = index; + state + }); + + Ok(()) + } + fn invalidate_unspent_output(&self, output: &DbUnblindedOutput) -> Result, OutputManagerStorageError> { let mut db = acquire_write_lock!(self.db); match db 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 ea956fb7ffa..20a55d75ead 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 @@ -573,6 +573,14 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { Ok(()) } + fn set_key_index(&self, index: u64) -> Result<(), OutputManagerStorageError> { + let conn = self.database_connection.acquire_lock(); + + KeyManagerStateSql::set_index(index, &(*conn))?; + + Ok(()) + } + fn invalidate_unspent_output(&self, output: &DbUnblindedOutput) -> Result, OutputManagerStorageError> { let conn = self.database_connection.acquire_lock(); let output = OutputSql::find_by_commitment(&output.commitment.to_vec(), &conn)?; @@ -1385,6 +1393,28 @@ impl KeyManagerStateSql { Err(_) => return Err(OutputManagerStorageError::KeyManagerNotInitialized), }) } + + pub fn set_index(index: u64, conn: &SqliteConnection) -> Result<(), OutputManagerStorageError> { + match KeyManagerStateSql::get_state(conn) { + Ok(km) => { + let update = KeyManagerStateUpdateSql { + master_key: None, + branch_seed: None, + primary_key_index: Some(index as i64), + }; + let num_updated = diesel::update(key_manager_states::table.filter(key_manager_states::id.eq(&km.id))) + .set(update) + .execute(conn)?; + if num_updated == 0 { + return Err(OutputManagerStorageError::UnexpectedResult( + "Database update error".to_string(), + )); + } + Ok(()) + }, + Err(_) => Err(OutputManagerStorageError::KeyManagerNotInitialized), + } + } } #[derive(AsChangeset)] diff --git a/base_layer/wallet/src/output_manager_service/protocols/mod.rs b/base_layer/wallet/src/output_manager_service/tasks/mod.rs similarity index 94% rename from base_layer/wallet/src/output_manager_service/protocols/mod.rs rename to base_layer/wallet/src/output_manager_service/tasks/mod.rs index e0ac5c45cae..0c28ca2c90a 100644 --- a/base_layer/wallet/src/output_manager_service/protocols/mod.rs +++ b/base_layer/wallet/src/output_manager_service/tasks/mod.rs @@ -20,4 +20,6 @@ // 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. -pub mod txo_validation_protocol; +mod txo_validation_task; + +pub use txo_validation_task::{TxoValidationTask, TxoValidationType}; diff --git a/base_layer/wallet/src/output_manager_service/protocols/txo_validation_protocol.rs b/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs similarity index 99% rename from base_layer/wallet/src/output_manager_service/protocols/txo_validation_protocol.rs rename to base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs index 80ea822cbcf..afa2bd75c84 100644 --- a/base_layer/wallet/src/output_manager_service/protocols/txo_validation_protocol.rs +++ b/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs @@ -24,7 +24,7 @@ use crate::{ output_manager_service::{ error::{OutputManagerError, OutputManagerProtocolError}, handle::OutputManagerEvent, - service::OutputManagerResources, + resources::OutputManagerResources, storage::{database::OutputManagerBackend, models::DbUnblindedOutput}, }, types::ValidationRetryStrategy, @@ -41,11 +41,11 @@ use tari_core::{ use tari_crypto::tari_utilities::{hash::Hashable, hex::Hex}; use tokio::{sync::broadcast, time::delay_for}; -const LOG_TARGET: &str = "wallet::output_manager_service::protocols::utxo_validation_protocol"; +const LOG_TARGET: &str = "wallet::output_manager_service::utxo_validation_task"; const MAX_RETRY_DELAY: Duration = Duration::from_secs(300); -pub struct TxoValidationProtocol +pub struct TxoValidationTask where TBackend: OutputManagerBackend + 'static { id: u64, @@ -59,11 +59,11 @@ where TBackend: OutputManagerBackend + 'static } /// This protocol defines the process of submitting our current UTXO set to the Base Node to validate it. -impl TxoValidationProtocol +impl TxoValidationTask where TBackend: OutputManagerBackend + 'static { #[allow(clippy::too_many_arguments)] - pub fn new( + pub(crate) fn new( id: u64, validation_type: TxoValidationType, retry_strategy: ValidationRetryStrategy, diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs index 6539d54ceae..c12a6c70a8d 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs @@ -22,11 +22,7 @@ use crate::{ error::WalletError, - output_manager_service::{ - error::{OutputManagerError, OutputManagerStorageError}, - handle::OutputManagerHandle, - TxId, - }, + output_manager_service::{handle::OutputManagerHandle, TxId}, storage::{ database::{WalletBackend, WalletDatabase}, sqlite_db::WalletSqliteDatabase, @@ -531,38 +527,38 @@ where TBackend: WalletBackend + 'static // ToDo fix this,m this should come from the syncing node. let height = 0; iteration_count += 1; - let unblinded_outputs = match self.mode { + let (standard_outputs, one_sided_outputs) = match self.mode { UtxoScannerMode::Recovery => { - let mut unblinded_outputs = self + let standard_outputs = self + .resources + .output_manager_service + .scan_for_recoverable_outputs(outputs.clone(), height) + .await?; + let one_sided_outputs = self .resources .output_manager_service - .rewind_outputs(outputs.clone(), height) + .scan_outputs_for_one_sided_payments(outputs.clone(), height) .await?; - unblinded_outputs.append( - &mut self - .resources - .output_manager_service - .scan_outputs_for_one_sided_payments(outputs.clone(), height) - .await?, - ); - unblinded_outputs + + (standard_outputs, one_sided_outputs) }, - UtxoScannerMode::Scanning => { + UtxoScannerMode::Scanning => ( + vec![], self.resources .output_manager_service .scan_outputs_for_one_sided_payments(outputs.clone(), height) - .await? - }, + .await?, + ), }; - if unblinded_outputs.is_empty() { + if standard_outputs.is_empty() && one_sided_outputs.is_empty() { continue; } let source_public_key = self.resources.node_identity.public_key().clone(); - for uo in unblinded_outputs { + for uo in standard_outputs { match self - .import_unblinded_utxo_to_services( + .import_unblinded_utxo_to_transaction_service( uo.clone(), &source_public_key, format!("Recovered on {}.", Utc::now().naive_utc()), @@ -573,9 +569,23 @@ where TBackend: WalletBackend + 'static num_recovered = num_recovered.saturating_add(1); total_amount += uo.value; }, - Err(WalletError::OutputManagerError(OutputManagerError::OutputManagerStorageError( - OutputManagerStorageError::DuplicateOutput, - ))) => warn!(target: LOG_TARGET, "Recovered output already in database"), + Err(e) => return Err(UtxoScannerError::UtxoImportError(e.to_string())), + } + } + + for uo in one_sided_outputs { + match self + .import_unblinded_utxo_to_transaction_service( + uo.clone(), + &source_public_key, + format!("Detected one-sided transaction on {}.", Utc::now().naive_utc()), + ) + .await + { + Ok(_) => { + num_recovered = num_recovered.saturating_add(1); + total_amount += uo.value; + }, Err(e) => return Err(UtxoScannerError::UtxoImportError(e.to_string())), } } @@ -670,20 +680,14 @@ where TBackend: WalletBackend + 'static let _ = self.event_sender.send(event); } - /// Import an external spendable UTXO into the wallet. The output will be added to the Output Manager and made - /// spendable. A faux incoming transaction will be created to provide a record of the event. The TxId of the - /// generated transaction is returned. - pub async fn import_unblinded_utxo_to_services( + /// A faux incoming transaction will be created to provide a record of the event of importing a UTXO. The TxId of + /// the generated transaction is returned. + pub async fn import_unblinded_utxo_to_transaction_service( &mut self, unblinded_output: UnblindedOutput, source_public_key: &CommsPublicKey, message: String, ) -> Result { - self.resources - .output_manager_service - .add_output(unblinded_output.clone()) - .await?; - let tx_id = self .resources .transaction_service diff --git a/base_layer/wallet/tests/output_manager_service/service.rs b/base_layer/wallet/tests/output_manager_service/service.rs index de3450c9d08..18e71785868 100644 --- a/base_layer/wallet/tests/output_manager_service/service.rs +++ b/base_layer/wallet/tests/output_manager_service/service.rs @@ -68,7 +68,6 @@ use tari_wallet::{ config::OutputManagerServiceConfig, error::{OutputManagerError, OutputManagerStorageError}, handle::{OutputManagerEvent, OutputManagerHandle}, - protocols::txo_validation_protocol::TxoValidationType, service::OutputManagerService, storage::{ database::{DbKey, DbKeyValuePair, DbValue, OutputManagerBackend, OutputManagerDatabase, WriteOperation}, @@ -77,6 +76,7 @@ use tari_wallet::{ sqlite_db::OutputManagerSqliteDatabase, }, TxId, + TxoValidationType, }, storage::sqlite_utilities::run_migration_and_create_sqlite_connection, transaction_service::handle::TransactionServiceHandle, @@ -1244,12 +1244,6 @@ fn coin_split_with_change(backend: T) assert_eq!(coin_split_tx.body.outputs().len(), split_count + 1); assert_eq!(fee, Fee::calculate(fee_per_gram, 1, 2, split_count + 1)); assert_eq!(amount, val2 + val3); - - // check they are rewindable - let uo = runtime - .block_on(oms.rewind_outputs(vec![coin_split_tx.body.outputs()[3].clone()], 0)) - .expect("Should be able to rewind outputs"); - assert!(uo[0].value == MicroTari::from(1000) || uo[0].value == MicroTari::from(3950)) } #[test] diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index fe0b1bccb7c..c84127f7201 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -56,8 +56,8 @@ use tari_shutdown::ShutdownSignal; use tari_wallet::{ output_manager_service::{ handle::{OutputManagerEvent, OutputManagerEventReceiver}, - protocols::txo_validation_protocol::TxoValidationType, TxId, + TxoValidationType, }, transaction_service::{ handle::{TransactionEvent, TransactionEventReceiver}, @@ -603,7 +603,7 @@ mod test { use tari_crypto::keys::{PublicKey as PublicKeyTrait, SecretKey}; use tari_shutdown::Shutdown; use tari_wallet::{ - output_manager_service::{handle::OutputManagerEvent, protocols::txo_validation_protocol::TxoValidationType}, + output_manager_service::{handle::OutputManagerEvent, TxoValidationType}, test_utils::make_wallet_databases, transaction_service::{ handle::TransactionEvent, diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 9cf222f592e..035eef0b978 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -178,7 +178,7 @@ use tari_utilities::{hex, hex::Hex}; use tari_wallet::{ contacts_service::storage::database::Contact, error::{WalletError, WalletStorageError}, - output_manager_service::protocols::txo_validation_protocol::TxoValidationType, + output_manager_service::TxoValidationType, storage::{ database::WalletDatabase, sqlite_db::WalletSqliteDatabase, diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index 44d4fe7dfd8..e63d2117d85 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -820,6 +820,20 @@ "node": ">= 8" } }, + "node_modules/csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cucumber": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cucumber/-/cucumber-6.0.5.tgz", @@ -2700,8 +2714,7 @@ "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/minipass": { "version": "3.1.3", @@ -4840,6 +4853,13 @@ "which": "^2.0.1" } }, + "csv-parser": { + "version": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "requires": { + "minimist": "^1.2.0" + } + }, "cucumber": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cucumber/-/cucumber-6.0.5.tgz", @@ -6272,8 +6292,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "3.1.3",