From c7cdad284e2887fc5a3ea47977b5d06242528a2a Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Tue, 23 Nov 2021 11:06:19 +0200 Subject: [PATCH 01/29] docs: ignore RFC code blocks (#3603) Description --- The code blocks in the covenants RFC were failing automated rust tests in CI --- RFC/src/RFC-0250_Covenants.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/RFC/src/RFC-0250_Covenants.md b/RFC/src/RFC-0250_Covenants.md index e3eb65cc15..48891d1188 100644 --- a/RFC/src/RFC-0250_Covenants.md +++ b/RFC/src/RFC-0250_Covenants.md @@ -291,7 +291,7 @@ before being executed. For instance, -``` +```ignore xor( filter_output_hash_eq(Hash(0e0411c70df0ea4243a363fcbf161ebe6e2c1f074faf1c6a316a386823c3753c)), filter_relative_height(10), @@ -300,7 +300,7 @@ xor( is represented in hex bytes as `23 30 01 a8b3f48e39449e89f7ff699b3eb2b080a2479b09a600a19d8ba48d765fe5d47d 35 07 0a`. Let's unpack that as follows: -``` +```ignore 23 // xor - consume two covenant args 30 // filter_output_hash_eq - consume a hash arg 01 // 32-byte hash @@ -365,7 +365,7 @@ one or more outputs. Spend within 10 blocks or burn -``` +```ignore not(filter_relative_height(10)) ``` @@ -377,13 +377,13 @@ the miner. Output features as detailed in [RFC-310-AssetImplementation] (early draft stages, still to be finalised) contain the NFT details. This covenant preserves both the covenant protecting the token, and the token itself. -``` +```ignore filter_fields_preserved([field::features, field::covenant]) ``` ### Side-chain checkpointing -``` +```ignore and( filter_field_int_eq(field::feature_flags, 16) // SIDECHAIN CHECKPOINT = 16 filter_fields_preserved([field::features, field::covenant, field::script]) @@ -392,7 +392,7 @@ and( ### Restrict spending to a particular commitment if not spent within 100 blocks -``` +```ignore or( not(filter_relative_height(100)), filter_fields_hashed_eq([field::commmitment], Hash(xxxx)) @@ -401,7 +401,7 @@ or( ### Output must preserve covenant, features and script or be burnt -``` +```ignore xor( filter_fields_preserved([field::features, field::covenant, field::script]), and( @@ -413,7 +413,7 @@ xor( ### Commission for NFT transfer -``` +```ignore // Must be different outputs xor( and( From befa6215741c37c3c40f7088cdccb4221750a033 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Tue, 23 Nov 2021 11:43:12 +0200 Subject: [PATCH 02/29] feat: use CipherSeed wallet birthday for recovery start point (#3602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description --- This PR makes use of the wallet birthday encoded into the wallet’s CipherSeed as a starting point for wallet recovery. This is instead of starting at the genesis block and will reduce the amount of work the wallet needs to do significantly by exclude all the blocks before its birthday. This PR implements a new RPC method on the BaseNodeSyncRpcService service called `get_height_at_time` which accepts a Unix Epoch time. The base node will then use a binary search strategy to determine what the block height was at that time. When a fresh Wallet Recovery is started if there isn’t a current progress metadata already stored in the database the wallet will calculate the unix epoch time of two days prior CipherSeeds birthday. This is to account for any timezone issues and does not add much in terms of work to the process. The wallet will then request the height at this time, request the header for that height and will be able to start the recovery process from that point. How Has This Been Tested? --- Tests provided for RPC service and CipherSeed birthday db storage. UTXO Recovery tested manually --- base_layer/core/src/base_node/sync/rpc/mod.rs | 3 + .../core/src/base_node/sync/rpc/service.rs | 55 ++++++++++++++ base_layer/core/tests/base_node_rpc.rs | 76 +++++++++++++++++-- base_layer/key_manager/src/cipher_seed.rs | 12 ++- base_layer/key_manager/src/key_manager.rs | 2 +- base_layer/wallet/src/storage/database.rs | 20 +++++ base_layer/wallet/src/storage/sqlite_db.rs | 6 ++ .../src/utxo_scanner_service/utxo_scanning.rs | 43 ++++++++--- base_layer/wallet/tests/wallet/mod.rs | 37 ++++++++- .../features/WalletRecovery.feature | 1 - 10 files changed, 236 insertions(+), 19 deletions(-) diff --git a/base_layer/core/src/base_node/sync/rpc/mod.rs b/base_layer/core/src/base_node/sync/rpc/mod.rs index 3c0d5ea698..1fc9ec13ac 100644 --- a/base_layer/core/src/base_node/sync/rpc/mod.rs +++ b/base_layer/core/src/base_node/sync/rpc/mod.rs @@ -90,6 +90,9 @@ pub trait BaseNodeSyncService: Send + Sync + 'static { #[rpc(method = 8)] async fn sync_utxos(&self, request: Request) -> Result, RpcStatus>; + + #[rpc(method = 9)] + async fn get_height_at_time(&self, request: Request) -> Result, RpcStatus>; } #[cfg(feature = "base_node")] diff --git a/base_layer/core/src/base_node/sync/rpc/service.rs b/base_layer/core/src/base_node/sync/rpc/service.rs index 9e20df058a..8a48cb3ce1 100644 --- a/base_layer/core/src/base_node/sync/rpc/service.rs +++ b/base_layer/core/src/base_node/sync/rpc/service.rs @@ -462,4 +462,59 @@ impl BaseNodeSyncService for BaseNodeSyncRpcServ Ok(Streaming::new(rx)) } + + async fn get_height_at_time(&self, request: Request) -> Result, RpcStatus> { + let requested_epoch_time: u64 = request.into_message(); + + let tip_header = self + .db() + .fetch_tip_header() + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))?; + let mut left_height = 0u64; + let mut right_height = tip_header.height(); + + while left_height <= right_height { + let mut mid_height = (left_height + right_height) / 2; + + if mid_height == 0 { + return Ok(Response::new(0u64)); + } + // If the two bounds are adjacent then perform the test between the right and left sides + if left_height == mid_height { + mid_height = right_height; + } + + let mid_header = self + .db() + .fetch_header(mid_height) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| { + RpcStatus::not_found(format!("Header not found during search at height {}", mid_height)) + })?; + let before_mid_header = self + .db() + .fetch_header(mid_height - 1) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| { + RpcStatus::not_found(format!("Header not found during search at height {}", mid_height - 1)) + })?; + + if requested_epoch_time < mid_header.timestamp.as_u64() && + requested_epoch_time >= before_mid_header.timestamp.as_u64() + { + return Ok(Response::new(before_mid_header.height)); + } else if mid_height == right_height { + return Ok(Response::new(right_height)); + } else if requested_epoch_time <= mid_header.timestamp.as_u64() { + right_height = mid_height; + } else { + left_height = mid_height; + } + } + + Ok(Response::new(0u64)) + } } diff --git a/base_layer/core/tests/base_node_rpc.rs b/base_layer/core/tests/base_node_rpc.rs index 23e2f267fb..7643b5ca28 100644 --- a/base_layer/core/tests/base_node_rpc.rs +++ b/base_layer/core/tests/base_node_rpc.rs @@ -42,7 +42,7 @@ // 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; +use std::{convert::TryFrom, sync::Arc, time::Duration}; use randomx_rs::RandomXFlag; use tempfile::{tempdir, TempDir}; @@ -61,6 +61,8 @@ use tari_core::{ }, rpc::{BaseNodeWalletRpcService, BaseNodeWalletService}, state_machine_service::states::{ListeningInfo, StateInfo, StatusInfo}, + sync::rpc::BaseNodeSyncRpcService, + BaseNodeSyncService, }, blocks::ChainBlock, consensus::{ConsensusManager, ConsensusManagerBuilder, NetworkConsensus}, @@ -80,7 +82,7 @@ use tari_core::{ }; use crate::helpers::{ - block_builders::{chain_block, create_genesis_block_with_coinbase_value}, + block_builders::{chain_block, chain_block_with_new_coinbase, create_genesis_block_with_coinbase_value}, nodes::{BaseNodeBuilder, NodeInterfaces}, }; @@ -88,6 +90,7 @@ mod helpers; async fn setup() -> ( BaseNodeWalletRpcService, + BaseNodeSyncRpcService, NodeInterfaces, RpcRequestMock, ConsensusManager, @@ -118,13 +121,15 @@ async fn setup() -> ( }); let request_mock = RpcRequestMock::new(base_node.comms.peer_manager()); - let service = BaseNodeWalletRpcService::new( + let wallet_service = BaseNodeWalletRpcService::new( base_node.blockchain_db.clone().into(), base_node.mempool_handle.clone(), base_node.state_machine_handle.clone(), ); + let base_node_service = BaseNodeSyncRpcService::new(base_node.blockchain_db.clone().into()); ( - service, + wallet_service, + base_node_service, base_node, request_mock, consensus_manager, @@ -138,7 +143,7 @@ async fn setup() -> ( #[allow(clippy::identity_op)] async fn test_base_node_wallet_rpc() { // Testing the submit_transaction() and transaction_query() rpc calls - let (service, mut base_node, request_mock, consensus_manager, block0, utxo0, _temp_dir) = setup().await; + let (service, _, mut base_node, request_mock, consensus_manager, block0, utxo0, _temp_dir) = setup().await; let (txs1, utxos1) = schema_to_transaction(&[txn_schema!(from: vec![utxo0.clone()], to: vec![1 * T, 1 * T])]); let tx1 = (*txs1[0]).clone(); @@ -290,3 +295,64 @@ async fn test_base_node_wallet_rpc() { .any(|u| u.as_transaction_output(&factories).unwrap().commitment == output.commitment)); } } + +#[tokio::test] +async fn test_get_height_at_time() { + let factories = CryptoFactories::default(); + + let (_, service, base_node, request_mock, consensus_manager, block0, _utxo0, _temp_dir) = setup().await; + + let mut prev_block = block0.clone(); + let mut times = Vec::new(); + times.push(prev_block.header().timestamp); + for _ in 0..10 { + tokio::time::sleep(Duration::from_secs(2)).await; + let new_block = base_node + .blockchain_db + .prepare_new_block(chain_block_with_new_coinbase(&prev_block, vec![], &consensus_manager, &factories).0) + .unwrap(); + + prev_block = base_node + .blockchain_db + .add_block(Arc::new(new_block)) + .unwrap() + .assert_added(); + times.push(prev_block.header().timestamp); + } + + let req = request_mock.request_with_context(Default::default(), times[0].as_u64() - 100); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 0); + + let req = request_mock.request_with_context(Default::default(), times[0].as_u64()); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 0); + + let req = request_mock.request_with_context(Default::default(), times[0].as_u64() + 1); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 0); + + let req = request_mock.request_with_context(Default::default(), times[7].as_u64()); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 7); + + let req = request_mock.request_with_context(Default::default(), times[7].as_u64() - 1); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 6); + + let req = request_mock.request_with_context(Default::default(), times[7].as_u64() + 1); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 7); + + let req = request_mock.request_with_context(Default::default(), times[10].as_u64()); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 10); + + let req = request_mock.request_with_context(Default::default(), times[10].as_u64() - 1); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 9); + + let req = request_mock.request_with_context(Default::default(), times[10].as_u64() + 1); + let resp = service.get_height_at_time(req).await.unwrap().into_message(); + assert_eq!(resp, 10); +} diff --git a/base_layer/key_manager/src/cipher_seed.rs b/base_layer/key_manager/src/cipher_seed.rs index 543071041d..dfffe7fe98 100644 --- a/base_layer/key_manager/src/cipher_seed.rs +++ b/base_layer/key_manager/src/cipher_seed.rs @@ -85,7 +85,7 @@ pub const CIPHER_SEED_MAC_BYTES: usize = 5; pub struct CipherSeed { version: u8, birthday: u16, - pub entropy: [u8; CIPHER_SEED_ENTROPY_BYTES], + entropy: [u8; CIPHER_SEED_ENTROPY_BYTES], salt: [u8; CIPHER_SEED_SALT_BYTES], } @@ -108,7 +108,7 @@ impl CipherSeed { pub fn encipher(&self, passphrase: Option) -> Result, KeyManagerError> { let mut plaintext = self.birthday.to_le_bytes().to_vec(); - plaintext.append(&mut self.entropy.clone().to_vec()); + plaintext.append(&mut self.entropy().clone().to_vec()); let passphrase = passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string()); @@ -236,6 +236,14 @@ impl CipherSeed { Ok(()) } + + pub fn entropy(&self) -> [u8; CIPHER_SEED_ENTROPY_BYTES] { + self.entropy + } + + pub fn birthday(&self) -> u16 { + self.birthday + } } impl Drop for CipherSeed { diff --git a/base_layer/key_manager/src/key_manager.rs b/base_layer/key_manager/src/key_manager.rs index 3ca14c409a..b95e750374 100644 --- a/base_layer/key_manager/src/key_manager.rs +++ b/base_layer/key_manager/src/key_manager.rs @@ -74,7 +74,7 @@ where /// Derive a new private key from master key: derived_key=SHA256(master_key||branch_seed||index) pub fn derive_key(&self, key_index: u64) -> Result, ByteArrayError> { - let concatenated = format!("{}{}", self.seed.entropy.to_vec().to_hex(), key_index.to_string()); + let concatenated = format!("{}{}", self.seed.entropy().to_vec().to_hex(), key_index.to_string()); match K::from_bytes(D::digest(&concatenated.into_bytes()).as_slice()) { Ok(k) => Ok(DerivedKey { k, key_index }), Err(e) => Err(e), diff --git a/base_layer/wallet/src/storage/database.rs b/base_layer/wallet/src/storage/database.rs index b0f9021f3b..ae22123969 100644 --- a/base_layer/wallet/src/storage/database.rs +++ b/base_layer/wallet/src/storage/database.rs @@ -55,6 +55,7 @@ pub enum DbKey { MasterSeed, PassphraseHash, EncryptionSalt, + WalletBirthday, } pub enum DbValue { @@ -67,6 +68,7 @@ pub enum DbValue { MasterSeed(CipherSeed), PassphraseHash(String), EncryptionSalt(String), + WalletBirthday(String), } #[derive(Clone)] @@ -306,6 +308,22 @@ where T: WalletBackend + 'static .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(c) } + + pub async fn get_wallet_birthday(&self) -> Result { + let db_clone = self.db.clone(); + + let result = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::WalletBirthday) { + Ok(None) => Err(WalletStorageError::ValueNotFound(DbKey::WalletBirthday)), + Ok(Some(DbValue::WalletBirthday(b))) => Ok(b + .parse::() + .map_err(|_| WalletStorageError::ConversionError("Could not parse wallet birthday".to_string()))?), + Ok(Some(other)) => unexpected_result(DbKey::WalletBirthday, other), + Err(e) => log_error(DbKey::WalletBirthday, e), + }) + .await + .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(result) + } } impl Display for DbKey { @@ -319,6 +337,7 @@ impl Display for DbKey { DbKey::BaseNodeChainMetadata => f.write_str(&"Last seen Chain metadata from base node".to_string()), DbKey::PassphraseHash => f.write_str(&"PassphraseHash".to_string()), DbKey::EncryptionSalt => f.write_str(&"EncryptionSalt".to_string()), + DbKey::WalletBirthday => f.write_str(&"WalletBirthday".to_string()), } } } @@ -335,6 +354,7 @@ impl Display for DbValue { DbValue::BaseNodeChainMetadata(v) => f.write_str(&format!("Last seen Chain metadata from base node:{}", v)), DbValue::PassphraseHash(h) => f.write_str(&format!("PassphraseHash: {}", h)), DbValue::EncryptionSalt(s) => f.write_str(&format!("EncryptionSalt: {}", s)), + DbValue::WalletBirthday(b) => f.write_str(&format!("WalletBirthday: {}", b)), } } } diff --git a/base_layer/wallet/src/storage/sqlite_db.rs b/base_layer/wallet/src/storage/sqlite_db.rs index 27295998fa..4f9c2aa364 100644 --- a/base_layer/wallet/src/storage/sqlite_db.rs +++ b/base_layer/wallet/src/storage/sqlite_db.rs @@ -83,7 +83,9 @@ impl WalletSqliteDatabase { match cipher.as_ref() { None => { let seed_bytes = seed.encipher(None)?; + let birthday = seed.birthday(); WalletSettingSql::new(DbKey::MasterSeed.to_string(), seed_bytes.to_hex()).set(conn)?; + WalletSettingSql::new(DbKey::WalletBirthday.to_string(), birthday.to_string()).set(conn)?; }, Some(cipher) => { let seed_bytes = seed.encipher(None)?; @@ -305,6 +307,9 @@ impl WalletSqliteDatabase { DbKey::EncryptionSalt => { return Err(WalletStorageError::OperationNotSupported); }, + DbKey::WalletBirthday => { + return Err(WalletStorageError::OperationNotSupported); + }, }; if start.elapsed().as_millis() > 0 { trace!( @@ -346,6 +351,7 @@ impl WalletBackend for WalletSqliteDatabase { DbKey::BaseNodeChainMetadata => self.get_chain_metadata(&conn)?.map(DbValue::BaseNodeChainMetadata), DbKey::PassphraseHash => WalletSettingSql::get(key.to_string(), &conn)?.map(DbValue::PassphraseHash), DbKey::EncryptionSalt => WalletSettingSql::get(key.to_string(), &conn)?.map(DbValue::EncryptionSalt), + DbKey::WalletBirthday => WalletSettingSql::get(key.to_string(), &conn)?.map(DbValue::WalletBirthday), }; if start.elapsed().as_millis() > 0 { trace!( 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 e6fb3f1011..e807208b9f 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs @@ -338,14 +338,15 @@ where TBackend: WalletBackend + 'static } async fn get_start_utxo_mmr_pos(&self, client: &mut BaseNodeSyncRpcClient) -> Result { - let metadata = self.get_metadata().await?.unwrap_or_default(); - if metadata.height_hash.is_empty() { - // Set a value in here so that if the recovery fails on the genesis block the client will know a - // recover was started. Important on Console wallet that otherwise makes this decision based on the - // presence of the data file - self.set_metadata(metadata).await?; - return Ok(0); - } + let metadata = match self.get_metadata().await? { + None => { + let birthday_metadata = self.get_birthday_metadata(client).await?; + self.set_metadata(birthday_metadata.clone()).await?; + return Ok(birthday_metadata.utxo_index); + }, + Some(m) => m, + }; + // if it's none, we return 0 above. let request = FindChainSplitRequest { block_hashes: vec![metadata.height_hash], @@ -635,6 +636,30 @@ where TBackend: WalletBackend + 'static self.peer_index += 1; peer } + + async fn get_birthday_metadata( + &self, + client: &mut BaseNodeSyncRpcClient, + ) -> Result { + let birthday = self.resources.db.get_wallet_birthday().await?; + // Calculate the unix epoch time of two days before the wallet birthday. This is to avoid any weird time zone + // issues + let epoch_time = (birthday.saturating_sub(2) as u64) * 60 * 60 * 24; + let block_height = client.get_height_at_time(epoch_time).await?; + let header = client.get_header_by_height(block_height).await?; + let header = BlockHeader::try_from(header).map_err(|_| UtxoScannerError::ConversionError)?; + + info!( + target: LOG_TARGET, + "Fresh wallet recovery starting at Block {}", block_height + ); + Ok(ScanningMetadata { + total_amount: Default::default(), + number_of_utxos: 0, + utxo_index: header.output_mmr_size, + height_hash: header.hash(), + }) + } } pub struct UtxoScannerService @@ -783,7 +808,7 @@ fn convert_response_to_transaction_outputs( Ok((outputs, current_utxo_index)) } -#[derive(Default, Serialize, Deserialize)] +#[derive(Clone, Default, Serialize, Deserialize)] struct ScanningMetadata { pub total_amount: MicroTari, pub number_of_utxos: u64, diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index bd6e6f8d79..fb79579c02 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -46,7 +46,7 @@ use tari_core::transactions::{ transaction_entities::OutputFeatures, CryptoFactories, }; -use tari_key_manager::cipher_seed::CipherSeed; +use tari_key_manager::{cipher_seed::CipherSeed, mnemonic::Mnemonic}; use tari_p2p::{initialization::P2pConfig, transport::TransportType, Network, DEFAULT_DNS_NAME_SERVER}; use tari_shutdown::{Shutdown, ShutdownSignal}; use tari_test_utils::random; @@ -69,6 +69,7 @@ use tari_wallet::{ handle::TransactionEvent, storage::sqlite_db::TransactionServiceSqliteDatabase, }, + utxo_scanner_service::utxo_scanning::UtxoScannerService, Wallet, WalletConfig, WalletSqlite, @@ -774,3 +775,37 @@ fn test_db_file_locking() { assert!(run_migration_and_create_sqlite_connection(&wallet_path, 16).is_ok()); } + +#[tokio::test] +async fn test_recovery_birthday() { + let dir = tempdir().unwrap(); + let factories = CryptoFactories::default(); + let shutdown = Shutdown::new(); + + let seed_words: Vec = [ + "cactus", "pool", "fuel", "skull", "chair", "casino", "season", "disorder", "flat", "crash", "wrist", + "whisper", "decorate", "narrow", "oxygen", "remember", "minor", "among", "happy", "cricket", "embark", "blue", + "ship", "sick", + ] + .to_vec() + .iter() + .map(|w| w.to_string()) + .collect(); + + let recovery_seed = CipherSeed::from_mnemonic(seed_words.as_slice(), None).unwrap(); + let birthday = recovery_seed.birthday(); + + let wallet = create_wallet( + dir.path(), + "wallet_db", + factories.clone(), + shutdown.to_signal(), + None, + Some(recovery_seed), + ) + .await + .unwrap(); + + let db_birthday = wallet.db.get_wallet_birthday().await.unwrap(); + assert_eq!(birthday, db_birthday); +} diff --git a/integration_tests/features/WalletRecovery.feature b/integration_tests/features/WalletRecovery.feature index d003de4718..5b4484e708 100644 --- a/integration_tests/features/WalletRecovery.feature +++ b/integration_tests/features/WalletRecovery.feature @@ -1,7 +1,6 @@ @wallet-recovery @wallet Feature: Wallet Recovery - Scenario: Wallet recovery with connected base node staying online Given I have a seed node NODE And I have 1 base nodes connected to all seed nodes From cc846cdf0495082d4e11939b311e57f05fd7425e Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Tue, 23 Nov 2021 12:14:33 +0200 Subject: [PATCH 03/29] test: improve cucumber scenario robustness (#3599) Description --- Improved cucumber scenario robustness by explicitly waiting on the command mode to finish before proceeding with the next step for: - `Scenario: Wallet imports spent outputs that become invalidated` - `Scenario: Wallet imports reorged outputs that become invalidated` Motivation and Context --- The above-mentioned tests failed now and again, at least 1 in 10. How Has This Been Tested? --- Repeatedly running these tests 10 times on two different computers. - `npm test -- --name "Wallet imports spent outputs that become invalidated"` - `npm test -- --name "Wallet imports reorged outputs that become invalidated"` --- integration_tests/helpers/walletProcess.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integration_tests/helpers/walletProcess.js b/integration_tests/helpers/walletProcess.js index 549b4993d2..aad8ed7048 100644 --- a/integration_tests/helpers/walletProcess.js +++ b/integration_tests/helpers/walletProcess.js @@ -252,6 +252,7 @@ class WalletProcess { } async exportSpentOutputs() { + await this.stop(); const args = [ "--init", "--base-path", @@ -262,11 +263,13 @@ class WalletProcess { "--command", "export-spent-utxos --csv-file exported_outputs.csv", ]; + let output = { buffer: "" }; outputProcess = __dirname + "/../temp/out/tari_console_wallet"; - await this.run(outputProcess, args, true); + await this.run(outputProcess, args, true, "\n", output, true); } async exportUnspentOutputs() { + await this.stop(); const args = [ "--init", "--base-path", @@ -277,8 +280,9 @@ class WalletProcess { "--command", "export-utxos --csv-file exported_outputs.csv", ]; + let output = { buffer: "" }; outputProcess = __dirname + "/../temp/out/tari_console_wallet"; - await this.run(outputProcess, args, true); + await this.run(outputProcess, args, true, "\n", output, true); } async readExportedOutputs() { From 3b3da21830fc098b8038b30b1d947e98f9198ede Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Tue, 23 Nov 2021 12:49:21 +0200 Subject: [PATCH 04/29] feat!: expose reason for transaction cancellation for callback in wallet_ffi (#3601) Description --- Exposes reason for transaction cancellation to wallet_ffi via the transaction cancellation callback. Additionally removes outdated docs and fixes Clippy errors. Motivation and Context --- How Has This Been Tested? --- cargo test --all --- .../src/ui/state/wallet_event_monitor.rs | 2 +- base_layer/p2p/src/services/liveness/state.rs | 4 +- .../wallet/src/transaction_service/handle.rs | 3 +- .../src/transaction_service/protocols/mod.rs | 11 ++++ .../transaction_broadcast_protocol.rs | 38 +++++++++---- .../protocols/transaction_receive_protocol.rs | 7 ++- .../protocols/transaction_send_protocol.rs | 6 ++- .../transaction_validation_protocol.rs | 3 +- .../wallet/src/transaction_service/service.rs | 6 ++- .../tests/transaction_service/service.rs | 14 ++--- .../transaction_protocols.rs | 4 +- base_layer/wallet/tests/wallet/mod.rs | 2 +- base_layer/wallet_ffi/src/callback_handler.rs | 12 ++--- .../wallet_ffi/src/callback_handler_tests.rs | 18 +++++-- base_layer/wallet_ffi/src/lib.rs | 54 +++++++------------ base_layer/wallet_ffi/wallet.h | 21 ++++++-- integration_tests/helpers/ffi/ffiInterface.js | 2 +- integration_tests/helpers/ffi/wallet.js | 4 +- 18 files changed, 130 insertions(+), 81 deletions(-) diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index dca471ee9b..c1b51207d0 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -100,7 +100,7 @@ impl WalletEventMonitor { self.trigger_balance_refresh(); notifier.transaction_mined(tx_id); }, - TransactionEvent::TransactionCancelled(tx_id) => { + TransactionEvent::TransactionCancelled(tx_id, _) => { self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); notifier.transaction_cancelled(tx_id); diff --git a/base_layer/p2p/src/services/liveness/state.rs b/base_layer/p2p/src/services/liveness/state.rs index 4e89d8e91f..3fc0478dba 100644 --- a/base_layer/p2p/src/services/liveness/state.rs +++ b/base_layer/p2p/src/services/liveness/state.rs @@ -152,7 +152,7 @@ impl LivenessState { self.failed_pings .entry(node_id) .and_modify(|v| { - *v = *v + 1; + *v += 1; }) .or_insert(1); } @@ -167,7 +167,7 @@ impl LivenessState { /// a latency sample is added and calculated. The given `peer` must match the recorded peer pub fn record_pong(&mut self, nonce: u64, sent_by: &NodeId) -> Option { self.inc_pongs_received(); - self.failed_pings.remove_entry(&sent_by); + self.failed_pings.remove_entry(sent_by); let (node_id, _) = self.inflight_pings.get(&nonce)?; if node_id == sent_by { diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 0f91f8fee8..6e914aebde 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -36,6 +36,7 @@ use tari_service_framework::reply_channel::SenderService; use crate::transaction_service::{ error::TransactionServiceError, + protocols::TxRejection, storage::models::{CompletedTransaction, InboundTransaction, OutboundTransaction, WalletTransaction}, }; @@ -150,7 +151,7 @@ pub enum TransactionEvent { TransactionDirectSendResult(TxId, bool), TransactionCompletedImmediately(TxId), TransactionStoreForwardSendResult(TxId, bool), - TransactionCancelled(TxId), + TransactionCancelled(TxId, TxRejection), TransactionBroadcast(TxId), TransactionImported(TxId), TransactionMined { diff --git a/base_layer/wallet/src/transaction_service/protocols/mod.rs b/base_layer/wallet/src/transaction_service/protocols/mod.rs index 15bdb1dd1c..664aab6146 100644 --- a/base_layer/wallet/src/transaction_service/protocols/mod.rs +++ b/base_layer/wallet/src/transaction_service/protocols/mod.rs @@ -20,6 +20,17 @@ // 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. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum TxRejection { + Unknown, // 0 + UserCancelled, // 1 + Timeout, // 2 + DoubleSpend, // 3 + Orphan, // 4 + TimeLocked, // 5 + InvalidTransaction, // 6 +} + pub mod transaction_broadcast_protocol; pub mod transaction_receive_protocol; pub mod transaction_send_protocol; diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs index 7bcf3ccb3e..4983a7d140 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs @@ -48,6 +48,7 @@ use crate::{ transaction_service::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, + protocols::TxRejection, service::TransactionServiceResources, storage::{database::TransactionBackend, models::CompletedTransaction}, }, @@ -215,10 +216,31 @@ where self.cancel_transaction().await; + let reason = match response.rejection_reason { + TxSubmissionRejectionReason::None | TxSubmissionRejectionReason::ValidationFailed => { + TransactionServiceError::MempoolRejectionInvalidTransaction + }, + TxSubmissionRejectionReason::DoubleSpend => TransactionServiceError::MempoolRejectionDoubleSpend, + TxSubmissionRejectionReason::Orphan => TransactionServiceError::MempoolRejectionOrphan, + TxSubmissionRejectionReason::TimeLocked => TransactionServiceError::MempoolRejectionTimeLocked, + _ => TransactionServiceError::UnexpectedBaseNodeResponse, + }; + + let cancellation_event_reason = match reason { + TransactionServiceError::MempoolRejectionInvalidTransaction => TxRejection::InvalidTransaction, + TransactionServiceError::MempoolRejectionDoubleSpend => TxRejection::DoubleSpend, + TransactionServiceError::MempoolRejectionOrphan => TxRejection::Orphan, + TransactionServiceError::MempoolRejectionTimeLocked => TxRejection::TimeLocked, + _ => TxRejection::Unknown, + }; + let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::TransactionCancelled(self.tx_id))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + self.tx_id, + cancellation_event_reason, + ))) .map_err(|e| { trace!( target: LOG_TARGET, @@ -228,15 +250,6 @@ where e }); - let reason = match response.rejection_reason { - TxSubmissionRejectionReason::None | TxSubmissionRejectionReason::ValidationFailed => { - TransactionServiceError::MempoolRejectionInvalidTransaction - }, - TxSubmissionRejectionReason::DoubleSpend => TransactionServiceError::MempoolRejectionDoubleSpend, - TxSubmissionRejectionReason::Orphan => TransactionServiceError::MempoolRejectionOrphan, - TxSubmissionRejectionReason::TimeLocked => TransactionServiceError::MempoolRejectionTimeLocked, - _ => TransactionServiceError::UnexpectedBaseNodeResponse, - }; return Err(TransactionServiceProtocolError::new(self.tx_id, reason)); } else if response.rejection_reason == TxSubmissionRejectionReason::AlreadyMined { info!( @@ -342,7 +355,10 @@ where let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::TransactionCancelled(self.tx_id))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + self.tx_id, + TxRejection::InvalidTransaction, + ))) .map_err(|e| { trace!( target: LOG_TARGET, diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index 2be90c9b48..1ce1e4ba9c 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -39,7 +39,7 @@ use tari_common_types::transaction::{TransactionDirection, TransactionStatus, Tx use tari_comms::types::CommsPublicKey; use tokio::sync::{mpsc, oneshot}; -use crate::connectivity_service::WalletConnectivityInterface; +use crate::{connectivity_service::WalletConnectivityInterface, transaction_service::protocols::TxRejection}; use tari_common_types::types::HashOutput; use tari_core::transactions::{ transaction_entities::Transaction, @@ -504,7 +504,10 @@ where let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::TransactionCancelled(self.id))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + self.id, + TxRejection::Timeout, + ))) .map_err(|e| { trace!( target: LOG_TARGET, diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 34d68644df..5812f2ac58 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -26,6 +26,7 @@ use crate::{ config::TransactionRoutingMechanism, error::{TransactionServiceError, TransactionServiceProtocolError}, handle::{TransactionEvent, TransactionServiceResponse}, + protocols::TxRejection, service::TransactionServiceResources, storage::{ database::TransactionBackend, @@ -826,7 +827,10 @@ where let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::TransactionCancelled(self.id))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + self.id, + TxRejection::Timeout, + ))) .map_err(|e| { trace!( target: LOG_TARGET, 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 639a246e7a..bf34834081 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 @@ -64,6 +64,7 @@ pub struct TransactionValidationProtocol { - if let TransactionEvent::TransactionCancelled(_) = &*event.unwrap() { + if let TransactionEvent::TransactionCancelled(..) = &*event.unwrap() { cancelled = true; break; } @@ -2380,7 +2380,7 @@ fn test_transaction_cancellation() { loop { tokio::select! { event = alice_event_stream.recv() => { - if let TransactionEvent::TransactionCancelled(_) = &*event.unwrap() { + if let TransactionEvent::TransactionCancelled(..) = &*event.unwrap() { cancelled = true; break; } @@ -3524,7 +3524,7 @@ fn test_coinbase_abandoned() { loop { tokio::select! { event = alice_event_stream.recv() => { - if let TransactionEvent::TransactionCancelled(tx_id) = &*event.unwrap() { + if let TransactionEvent::TransactionCancelled(tx_id, _) = &*event.unwrap() { if tx_id == &tx_id1 { count += 1; } @@ -3684,7 +3684,7 @@ fn test_coinbase_abandoned() { count += 1; } }, - TransactionEvent::TransactionCancelled(tx_id) => { + TransactionEvent::TransactionCancelled(tx_id, _) => { if tx_id == &tx_id2 { count += 1; } @@ -3771,7 +3771,7 @@ fn test_coinbase_abandoned() { count += 1; } }, - TransactionEvent::TransactionCancelled(tx_id) => { + TransactionEvent::TransactionCancelled(tx_id, _) => { if tx_id == &tx_id1 { count += 1; } @@ -4755,7 +4755,7 @@ fn test_transaction_timeout_cancellation() { loop { tokio::select! { event = carol_event_stream.recv() => { - if let TransactionEvent::TransactionCancelled(t) = &*event.unwrap() { + if let TransactionEvent::TransactionCancelled(t, _) = &*event.unwrap() { if t == &tx_id { transaction_cancelled = true; break; @@ -5074,7 +5074,7 @@ fn transaction_service_tx_broadcast() { loop { tokio::select! { event = alice_event_stream.recv() => { - if let TransactionEvent::TransactionCancelled(tx_id) = &*event.unwrap(){ + if let TransactionEvent::TransactionCancelled(tx_id, _) = &*event.unwrap(){ if tx_id == &tx_id2 { tx2_cancelled = true; break; diff --git a/base_layer/wallet/tests/transaction_service/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service/transaction_protocols.rs index 102c744769..e38cf6f022 100644 --- a/base_layer/wallet/tests/transaction_service/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service/transaction_protocols.rs @@ -370,7 +370,7 @@ async fn tx_broadcast_protocol_submit_rejection() { loop { tokio::select! { event = event_stream.recv() => { - if let TransactionEvent::TransactionCancelled(_) = &*event.unwrap() { + if let TransactionEvent::TransactionCancelled(..) = &*event.unwrap() { cancelled = true; } }, @@ -547,7 +547,7 @@ async fn tx_broadcast_protocol_submit_success_followed_by_rejection() { loop { tokio::select! { event = event_stream.recv() => { - if let TransactionEvent::TransactionCancelled(_) = &*event.unwrap() { + if let TransactionEvent::TransactionCancelled(..) = &*event.unwrap() { cancelled = true; } }, diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index fb79579c02..d54c307c01 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -633,7 +633,7 @@ fn test_store_and_forward_send_tx() { event = carol_event_stream.recv() => { match &*event.unwrap() { TransactionEvent::ReceivedTransaction(_) => tx_recv = true, - TransactionEvent::TransactionCancelled(_) => tx_cancelled = true, + TransactionEvent::TransactionCancelled(..) => tx_cancelled = true, _ => (), } if tx_recv && tx_cancelled { diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index fffbe5800e..9418b38e1b 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -88,7 +88,7 @@ where TBackend: TransactionBackend + 'static callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(TxId, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(TxId, bool), - callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction), + callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_txo_validation_complete: unsafe extern "C" fn(u64, u8), callback_balance_updated: unsafe extern "C" fn(*mut Balance), callback_transaction_validation_complete: unsafe extern "C" fn(u64, u8), @@ -123,7 +123,7 @@ where TBackend: TransactionBackend + 'static callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(TxId, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(TxId, bool), - callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction), + callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_txo_validation_complete: unsafe extern "C" fn(TxId, u8), callback_balance_updated: unsafe extern "C" fn(*mut Balance), callback_transaction_validation_complete: unsafe extern "C" fn(TxId, u8), @@ -242,8 +242,8 @@ where TBackend: TransactionBackend + 'static self.receive_store_and_forward_send_result(tx_id, result); self.trigger_balance_refresh().await; }, - TransactionEvent::TransactionCancelled(tx_id) => { - self.receive_transaction_cancellation(tx_id).await; + TransactionEvent::TransactionCancelled(tx_id, reason) => { + self.receive_transaction_cancellation(tx_id, reason as u64).await; self.trigger_balance_refresh().await; }, TransactionEvent::TransactionBroadcast(tx_id) => { @@ -425,7 +425,7 @@ where TBackend: TransactionBackend + 'static } } - async fn receive_transaction_cancellation(&mut self, tx_id: TxId) { + async fn receive_transaction_cancellation(&mut self, tx_id: TxId, reason: u64) { let mut transaction = None; if let Ok(tx) = self.db.get_cancelled_completed_transaction(tx_id).await { transaction = Some(tx); @@ -451,7 +451,7 @@ where TBackend: TransactionBackend + 'static ); let boxing = Box::into_raw(Box::new(tx)); unsafe { - (self.callback_transaction_cancellation)(boxing); + (self.callback_transaction_cancellation)(boxing, reason); } }, } diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index e544fab345..4d90f2b3ee 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -63,6 +63,7 @@ mod test { }; use crate::{callback_handler::CallbackHandler, output_manager_service_mock::MockOutputManagerService}; + use tari_wallet::transaction_service::protocols::TxRejection; struct CallbackState { pub received_tx_callback_called: bool, @@ -168,7 +169,7 @@ mod test { drop(lock); } - unsafe extern "C" fn tx_cancellation_callback(tx: *mut CompletedTransaction) { + unsafe extern "C" fn tx_cancellation_callback(tx: *mut CompletedTransaction, _reason: u64) { let mut lock = CALLBACK_STATE.lock().unwrap(); match (*tx).tx_id { 3 => lock.tx_cancellation_callback_called_inbound = true, @@ -415,7 +416,10 @@ mod test { mock_output_manager_service_state.set_balance(balance.clone()); // Balance updated should be detected with following event, total = 4 times transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionCancelled(3u64))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + 3u64, + TxRejection::UserCancelled, + ))) .unwrap(); let start = Instant::now(); while start.elapsed().as_secs() < 10 { @@ -431,11 +435,17 @@ mod test { assert_eq!(callback_balance_updated, 4); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionCancelled(4u64))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + 4u64, + TxRejection::UserCancelled, + ))) .unwrap(); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionCancelled(5u64))) + .send(Arc::new(TransactionEvent::TransactionCancelled( + 5u64, + TxRejection::UserCancelled, + ))) .unwrap(); oms_event_sender diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index d5ec154748..2a65bf8fd5 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -72,35 +72,6 @@ //! 6. This wallet will then monitor the Base Layer to see when the transaction is mined which means the //! `CompletedTransaction` status will become `Mined` and the funds will then move from the `PendingIncomingBalance` //! to the `AvailableBalance`. -//! -//! ## Using the test functions -//! The above two flows both require a second wallet for this wallet to interact with. Because we do not yet have a live -//! Test Net and the communications layer is not quite ready the library supplies four functions to help simulate the -//! second wallets role in these flows. The following will describe how to use these functions to produce the flows. -//! -//! ### Send Transaction with test functions -//! 1. Send Transaction as above to produce a `PendingOutboundTransaction`. -//! 2. Call the `complete_sent_transaction(...)` function with the tx_id of the sent transaction to simulate a reply. -//! This will move the `PendingOutboundTransaction` to become a `CompletedTransaction` with the `Completed` status. -//! 3. Call the 'broadcast_transaction(...)` function with the tx_id of the sent transaction and its status will move -//! from 'Completed' to 'Broadcast' which means it has been broadcast to the Base Layer Mempool but not mined yet. -//! from 'Completed' to 'Broadcast' which means it has been broadcast to the Base Layer Mempool but not mined yet. -//! 4. Call the `mined_transaction(...)` function with the tx_id of the sent transaction which will change -//! the status of the `CompletedTransaction` from `Broadcast` to `Mined`. The pending funds will also become -//! finalized as spent and available funds respectively. -//! -//! ### Receive Transaction with test functions -//! Under normal operation another wallet would initiate a Receive Transaction flow by sending you a transaction. We -//! will use the `receive_test_transaction(...)` function to initiate the flow: -//! -//! 1. Calling `receive_test_transaction(...)` will produce an `InboundTransaction`, the amount of the transaction will -//! appear under the `PendingIncomingBalance`. -//! 2. To simulate detecting the `InboundTransaction` being broadcast to the Base Layer Mempool call -//! `broadcast_transaction(...)` function. This will change the `InboundTransaction` to a -//! `CompletedTransaction` with the `Broadcast` status. The funds will still reflect in the pending balance. -//! 3. Call the `mined_transaction(...)` function with the tx_id of the received transaction which will -//! change the status of the `CompletedTransaction` from `Broadcast` to `Mined`. The pending funds will also -//! become finalized as spent and available funds respectively #![recursion_limit = "1024"] @@ -3044,9 +3015,24 @@ unsafe fn init_logging( /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will /// be called when a Broadcast transaction is detected as mined but not yet confirmed. -/// `callback_discovery_process_complete` - The callback function pointer matching the function signature. This will be -/// called when a `send_transacion(..)` call is made to a peer whose address is not known and a discovery process must -/// be conducted. The outcome of the discovery process is relayed via this callback +/// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called +/// when a direct send is completed. The first parameter is the transaction id and the second is whether if was +/// successful or not. +/// `callback_store_and_forward_send_result` - The callback function pointer matching the function +/// signature. This is called when a direct send is completed. The first parameter is the transaction id and the second +/// is whether if was successful or not. +/// `callback_transaction_cancellation` - The callback function pointer matching +/// the function signature. This is called when a transaction is cancelled. The first parameter is a pointer to the +/// cancelled transaction, the second is a reason as to why said transaction failed that is mapped to the `TxRejection` +/// enum: pub enum TxRejection { +/// Unknown, // 0 +/// UserCancelled, // 1 +/// Timeout, // 2 +/// DoubleSpend, // 3 +/// Orphan, // 4 +/// TimeLocked, // 5 +/// InvalidTransaction, // 6 +/// } /// `callback_txo_validation_complete` - The callback function pointer matching the function signature. This is called /// when a TXO validation process is completed. The request_key is used to identify which request this /// callback references and the second parameter is a u8 that represent the ClassbackValidationResults enum. @@ -3085,7 +3071,7 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(c_ulonglong, bool), - callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction), + callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), callback_txo_validation_complete: unsafe extern "C" fn(u64, u8), callback_balance_updated: unsafe extern "C" fn(*mut TariBalance), callback_transaction_validation_complete: unsafe extern "C" fn(u64, u8), @@ -5850,7 +5836,7 @@ mod test { // assert!(true); //optimized out by compiler } - unsafe extern "C" fn tx_cancellation_callback(tx: *mut TariCompletedTransaction) { + unsafe extern "C" fn tx_cancellation_callback(tx: *mut TariCompletedTransaction, _reason: u64) { assert!(!tx.is_null()); assert_eq!( type_of((*tx).clone()), diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index c4be5b890c..d7266603e2 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -423,9 +423,22 @@ void comms_config_destroy(struct TariCommsConfig *wc); /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will /// be called when a Broadcast transaction is detected as mined but not yet confirmed. -/// `callback_discovery_process_complete` - The callback function pointer matching the function signature. This will be -/// called when a `send_transacion(..)` call is made to a peer whose address is not known and a discovery process must -/// be conducted. The outcome of the discovery process is relayed via this callback +/// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called +/// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. +/// `callback_store_and_forward_send_result` - The callback function pointer matching the function signature. This is called +/// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. +/// `callback_transaction_cancellation` - The callback function pointer matching the function signature. This is called +/// when a transaction is cancelled. The first parameter is a pointer to the cancelled transaction, the second is a reason as to +/// why said transaction failed that is mapped to the `TxRejection` enum: +/// pub enum TxRejection { +/// Unknown, // 0 +/// UserCancelled, // 1 +/// Timeout, // 2 +/// DoubleSpend, // 3 +/// Orphan, // 4 +/// TimeLocked, // 5 +/// InvalidTransaction, // 6 +/// } /// `callback_txo_validation_complete` - The callback function pointer matching the function signature. This is called /// when a TXO validation process is completed. The request_key is used to identify which request this /// callback references and the second parameter is a u8 that represent the CallbackValidationResults enum. @@ -469,7 +482,7 @@ struct TariWallet *wallet_create(struct TariCommsConfig *config, void (*callback_transaction_mined_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), void (*callback_direct_send_result)(unsigned long long, bool), void (*callback_store_and_forward_send_result)(unsigned long long, bool), - void (*callback_transaction_cancellation)(struct TariCompletedTransaction *), + void (*callback_transaction_cancellation)(struct TariCompletedTransaction *, unsigned long long), void (*callback_txo_validation_complete)(unsigned long long, unsigned char), void (*callback_balance_updated)(struct TariBalance *), void (*callback_transaction_validation_complete)(unsigned long long, unsigned char), diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index 78430ac90e..607252a07e 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -1129,7 +1129,7 @@ class InterfaceFFI { } static createCallbackTransactionCancellation(fn) { - return ffi.Callback(this.void, [this.ptr], fn); + return ffi.Callback(this.void, [this.ptr, this.ulonglong], fn); } static createCallbackTxoValidationComplete(fn) { return ffi.Callback(this.void, [this.ulonglong, this.uchar], fn); diff --git a/integration_tests/helpers/ffi/wallet.js b/integration_tests/helpers/ffi/wallet.js index 0797ad81d5..ff1b783ca1 100644 --- a/integration_tests/helpers/ffi/wallet.js +++ b/integration_tests/helpers/ffi/wallet.js @@ -245,11 +245,11 @@ class Wallet { this.minedunconfirmed += 1; }; - onTransactionCancellation = (ptr) => { + onTransactionCancellation = (ptr, reason) => { let tx = new CompletedTransaction(); tx.pointerAssign(ptr); console.log( - `${new Date().toISOString()} Transaction with txID ${tx.getTransactionID()} was cancelled` + `${new Date().toISOString()} Transaction with txID ${tx.getTransactionID()} was cancelled with reason code ${reason}.` ); tx.destroy(); this.cancelled += 1; From 65157b00237a7cd6b3b68d84f958ed33da3a7297 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Tue, 23 Nov 2021 13:25:18 +0200 Subject: [PATCH 05/29] feat: add ban peers metric (#3605) Description --- Add metric for peer banning Motivation and Context --- Visibility into whether a node is banning peers How Has This Been Tested? --- Simple change, code compiles --- comms/src/connectivity/manager.rs | 3 +++ comms/src/connectivity/metrics.rs | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/comms/src/connectivity/manager.rs b/comms/src/connectivity/manager.rs index ad75db44b0..e5c0e46bee 100644 --- a/comms/src/connectivity/manager.rs +++ b/comms/src/connectivity/manager.rs @@ -733,6 +733,9 @@ impl ConnectivityManagerActor { self.peer_manager.ban_peer_by_node_id(node_id, duration, reason).await?; + #[cfg(feature = "metrics")] + super::metrics::banned_peers_counter(node_id).inc(); + self.publish_event(ConnectivityEvent::PeerBanned(node_id.clone())); if let Some(conn) = self.pool.get_connection_mut(node_id) { diff --git a/comms/src/connectivity/metrics.rs b/comms/src/connectivity/metrics.rs index 1470b4c9c7..0cc90578d0 100644 --- a/comms/src/connectivity/metrics.rs +++ b/comms/src/connectivity/metrics.rs @@ -20,9 +20,9 @@ // 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::connection_manager::ConnectionDirection; +use crate::{connection_manager::ConnectionDirection, peer_manager::NodeId}; use once_cell::sync::Lazy; -use tari_metrics::{IntGauge, IntGaugeVec}; +use tari_metrics::{IntCounter, IntCounterVec, IntGauge, IntGaugeVec}; pub fn connections(direction: ConnectionDirection) -> IntGauge { static METER: Lazy = Lazy::new(|| { @@ -43,3 +43,16 @@ pub fn uptime() -> IntGauge { METER.clone() } + +pub fn banned_peers_counter(peer: &NodeId) -> IntCounter { + static METER: Lazy = Lazy::new(|| { + tari_metrics::register_int_counter_vec( + "comms::connectivity::banned_peers", + "The number of peer bans by peer", + &["peer_id"], + ) + .unwrap() + }); + + METER.with_label_values(&[peer.to_string().as_str()]) +} From fff45db4bd1a6fa436ad3525e96ae08f26a856e8 Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Tue, 23 Nov 2021 15:23:49 +0200 Subject: [PATCH 06/29] fix: seed word parsing (#3607) Description --- Moved `detect_language` into `MnemonicLanguage` and made it public. Prevented a `TariSeedWords` object from becoming invalid in the event an invalid or inconsistent word was attempted to be pushed to it in wallet_ffi. Differentiated between an invalid word and an inconsistent word. Added word to the string of WordNotFound error. Motivation and Context --- General fixes How Has This Been Tested? --- cargo test --all --- base_layer/key_manager/src/error.rs | 4 +- base_layer/key_manager/src/mnemonic.rs | 110 +++++++++++++------------ base_layer/wallet_ffi/src/enums.rs | 1 + base_layer/wallet_ffi/src/lib.rs | 72 ++++++++++++---- 4 files changed, 115 insertions(+), 72 deletions(-) diff --git a/base_layer/key_manager/src/error.rs b/base_layer/key_manager/src/error.rs index 14b79ea058..3d67c59eb1 100644 --- a/base_layer/key_manager/src/error.rs +++ b/base_layer/key_manager/src/error.rs @@ -51,8 +51,8 @@ pub enum MnemonicError { defined natural languages" )] UnknownLanguage, - #[error("Only 2048 words for each language was selected to form Mnemonic word lists")] - WordNotFound, + #[error("Word not found: `{0}`")] + WordNotFound(String), #[error("A mnemonic word does not exist for the requested index")] IndexOutOfBounds, #[error("A problem encountered constructing a secret key from bytes or mnemonic sequence: `{0}`")] diff --git a/base_layer/key_manager/src/mnemonic.rs b/base_layer/key_manager/src/mnemonic.rs index 6e7e6ef8f3..9338e82217 100644 --- a/base_layer/key_manager/src/mnemonic.rs +++ b/base_layer/key_manager/src/mnemonic.rs @@ -48,7 +48,7 @@ impl MnemonicLanguage { /// Detects the mnemonic language of a specific word by searching all defined mnemonic word lists pub fn from(mnemonic_word: &str) -> Result { let words = vec![mnemonic_word.to_string()]; - detect_language(&words) + MnemonicLanguage::detect_language(&words) } /// Returns an iterator for the MnemonicLanguage enum group to allow iteration over all defined languages @@ -77,6 +77,51 @@ impl MnemonicLanguage { MnemonicLanguage::Spanish => MNEMONIC_SPANISH_WORDS.len(), } } + + /// Detects the language of a list of words + pub fn detect_language(words: &[String]) -> Result { + let count = words.iter().len(); + match count.cmp(&1) { + Ordering::Less => { + return Err(MnemonicError::UnknownLanguage); + }, + Ordering::Equal => { + let word = words.get(0).ok_or(MnemonicError::EncodeInvalidLength)?; + for language in MnemonicLanguage::iterator() { + if find_mnemonic_index_from_word(word, language).is_ok() { + return Ok(*language); + } + } + return Err(MnemonicError::UnknownLanguage); + }, + Ordering::Greater => { + for word in words { + let mut languages = Vec::with_capacity(MnemonicLanguage::iterator().len()); + // detect all languages in which a word falls into + for language in MnemonicLanguage::iterator() { + if find_mnemonic_index_from_word(word, language).is_ok() { + languages.push(*language); + } + } + // check if at least one of the languages is consistent for all other words against languages + // yielded from the initial word for this iteration + for language in languages { + let mut consistent = true; + for compare in words { + if compare != word && find_mnemonic_index_from_word(compare, &language).is_err() { + consistent = false; + } + } + if consistent { + return Ok(language); + } + } + } + }, + } + + Err(MnemonicError::UnknownLanguage) + } } /// Finds and returns the index of a specific word in a mnemonic word list defined by the specified language @@ -106,7 +151,7 @@ fn find_mnemonic_index_from_word(word: &str, language: &MnemonicLanguage) -> Res } match search_result { Ok(v) => Ok(v), - Err(_err) => Err(MnemonicError::WordNotFound), + Err(_err) => Err(MnemonicError::WordNotFound(word.to_string())), } } @@ -154,54 +199,10 @@ pub fn from_bytes(bytes: Vec, language: &MnemonicLanguage) -> Result Result { - let count = words.iter().len(); - match count.cmp(&1) { - Ordering::Less => { - return Err(MnemonicError::UnknownLanguage); - }, - Ordering::Equal => { - let word = words.get(0).ok_or(MnemonicError::EncodeInvalidLength)?; - for language in MnemonicLanguage::iterator() { - if find_mnemonic_index_from_word(word, language).is_ok() { - return Ok(*language); - } - } - return Err(MnemonicError::UnknownLanguage); - }, - Ordering::Greater => { - for word in words { - let mut languages = Vec::with_capacity(MnemonicLanguage::iterator().len()); - // detect all languages in which a word falls into - for language in MnemonicLanguage::iterator() { - if find_mnemonic_index_from_word(word, language).is_ok() { - languages.push(*language); - } - } - // check if at least one of the languages is consistent for all other words against languages yielded - // from the initial word for this iteration - for language in languages { - let mut consistent = true; - for compare in words { - if compare != word && find_mnemonic_index_from_word(compare, &language).is_err() { - consistent = false; - } - } - if consistent { - return Ok(language); - } - } - } - }, - } - - Err(MnemonicError::UnknownLanguage) -} - /// Generates a vector of bytes that represent the provided mnemonic sequence of words, the language of the mnemonic /// sequence is detected pub fn to_bytes(mnemonic_seq: &[String]) -> Result, MnemonicError> { - let language = self::detect_language(mnemonic_seq)?; + let language = MnemonicLanguage::detect_language(mnemonic_seq)?; to_bytes_with_language(mnemonic_seq, &language) } @@ -336,7 +337,10 @@ mod test { "opera".to_string(), "abandon".to_string(), ]; - assert_eq!(detect_language(&words1), Ok(MnemonicLanguage::English)); + assert_eq!( + MnemonicLanguage::detect_language(&words1), + Ok(MnemonicLanguage::English) + ); // English/Spanish + English/French + Italian/Spanish let words2 = vec![ @@ -346,7 +350,7 @@ mod test { "abandon".to_string(), "tipico".to_string(), ]; - assert_eq!(detect_language(&words2).is_err(), true); + assert_eq!(MnemonicLanguage::detect_language(&words2).is_err(), true); // bounds check (last word is invalid) let words3 = vec![ @@ -356,16 +360,16 @@ mod test { "abandon".to_string(), "topazio".to_string(), ]; - assert_eq!(detect_language(&words3).is_err(), true); + assert_eq!(MnemonicLanguage::detect_language(&words3).is_err(), true); // building up a word list: English/French + French -> French let mut words = Vec::with_capacity(3); words.push("concert".to_string()); - assert_eq!(detect_language(&words), Ok(MnemonicLanguage::English)); + assert_eq!(MnemonicLanguage::detect_language(&words), Ok(MnemonicLanguage::English)); words.push("abandon".to_string()); - assert_eq!(detect_language(&words), Ok(MnemonicLanguage::English)); + assert_eq!(MnemonicLanguage::detect_language(&words), Ok(MnemonicLanguage::English)); words.push("barbier".to_string()); - assert_eq!(detect_language(&words), Ok(MnemonicLanguage::French)); + assert_eq!(MnemonicLanguage::detect_language(&words), Ok(MnemonicLanguage::French)); } #[test] diff --git a/base_layer/wallet_ffi/src/enums.rs b/base_layer/wallet_ffi/src/enums.rs index 4d54dfc38e..bb1d90b598 100644 --- a/base_layer/wallet_ffi/src/enums.rs +++ b/base_layer/wallet_ffi/src/enums.rs @@ -27,4 +27,5 @@ pub enum SeedWordPushResult { SeedPhraseComplete, InvalidSeedPhrase, InvalidObject, + NoLanguageMatch, } diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 2a65bf8fd5..566ec880d4 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -1020,11 +1020,13 @@ pub unsafe extern "C" fn seed_words_get_at( /// /// ## Returns /// 'c_uchar' - Returns a u8 version of the `SeedWordPushResult` enum indicating whether the word was not a valid seed -/// word, if the push was successful and whether the push was successful and completed the full Seed Phrase +/// word, if the push was successful and whether the push was successful and completed the full Seed Phrase. +/// `seed_words` is only modified in the event of a `SuccessfulPush`. /// '0' -> InvalidSeedWord /// '1' -> SuccessfulPush /// '2' -> SeedPhraseComplete /// '3' -> InvalidSeedPhrase +/// '4' -> NoLanguageMatch, /// # Safety /// The ```string_destroy``` method must be called when finished with a string from rust to prevent a memory leak #[no_mangle] @@ -1082,28 +1084,64 @@ pub unsafe extern "C" fn seed_words_push_word( }, } - if MnemonicLanguage::from(word_string.as_str()).is_err() { - log::error!(target: LOG_TARGET, "{} is not a valid mnemonic seed word", word_string); - return SeedWordPushResult::InvalidSeedWord as u8; + // Seed words is currently empty, this is the first word + if (*seed_words).0.is_empty() { + (*seed_words).0.push(word_string); + return SeedWordPushResult::SuccessfulPush as u8; } - (*seed_words).0.push(word_string); - if (*seed_words).0.len() >= 24 { - return if let Err(e) = CipherSeed::from_mnemonic(&(*seed_words).0, None) { + // Try push to a temporary copy first to prevent existing object becoming invalid + let mut temp = (*seed_words).0.clone(); + + if let Ok(language) = MnemonicLanguage::detect_language(&temp) { + temp.push(word_string.clone()); + // Check words in temp are still consistent for a language, note that detected language can change + // depending on word added + if MnemonicLanguage::detect_language(&temp).is_ok() { + if temp.len() >= 24 { + if let Err(e) = CipherSeed::from_mnemonic(&temp, None) { + log::error!( + target: LOG_TARGET, + "Problem building valid private seed from seed phrase: {:?}", + e + ); + error = LibWalletError::from(WalletError::KeyManagerError(e)).code; + ptr::swap(error_out, &mut error as *mut c_int); + return SeedWordPushResult::InvalidSeedPhrase as u8; + }; + } + + (*seed_words).0.push(word_string); + + // Note: test for a validity was already done so we can just check length here + if (*seed_words).0.len() < 24 { + SeedWordPushResult::SuccessfulPush as u8 + } else { + SeedWordPushResult::SeedPhraseComplete as u8 + } + } else { log::error!( target: LOG_TARGET, - "Problem building valid private seed from seed phrase: {:?}", - e + "Words in seed phrase do not match any language after trying to add word: `{:?}`, previously words \ + were detected to be in: `{:?}`", + word_string, + language ); - error = LibWalletError::from(WalletError::KeyManagerError(e)).code; - ptr::swap(error_out, &mut error as *mut c_int); - SeedWordPushResult::InvalidSeedPhrase as u8 - } else { - SeedWordPushResult::SeedPhraseComplete as u8 - }; + SeedWordPushResult::NoLanguageMatch as u8 + } + } else { + // Seed words are invalid, shouldn't normally be reachable + log::error!( + target: LOG_TARGET, + "Words in seed phrase do not match any language prior to adding word: `{:?}`", + word_string + ); + let error_msg = "Invalid seed words object, no language can be detected."; + log::error!(target: LOG_TARGET, "{}", error_msg); + error = LibWalletError::from(InterfaceError::InvalidArgument(error_msg.to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + SeedWordPushResult::InvalidObject as u8 } - - SeedWordPushResult::SuccessfulPush as u8 } /// Frees memory for a TariSeedWords From bb94ea23ad72d7bddac9a105b5f254c91f2a0386 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Tue, 23 Nov 2021 16:59:33 +0200 Subject: [PATCH 07/29] fix: improve handling of old base nodes and reorgs in wallet recovery (#3608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description --- This PR adds two quick improvements on the wallet recovery: - Gracefully handle the case where the base node is older and doesn’t support the `get_height_at_time` RPC call yet. - If a reorg is detected during recovery the process used to be restarted from the genesis block. Now it will restart from the wallet birthday How Has This Been Tested? --- Manually --- .../src/utxo_scanner_service/utxo_scanning.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 e807208b9f..b00ebc839d 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs @@ -358,8 +358,9 @@ where TBackend: WalletBackend + 'static Err(RpcError::RequestFailed(err)) if err.as_status_code().is_not_found() => { warn!(target: LOG_TARGET, "Reorg detected: {}", err); // The node does not know of the last hash we scanned, thus we had a chain split. - // We now start at 0 again. - Ok(0) + // We now start at the wallet birthday again + let birthday_metdadata = self.get_birthday_metadata(client).await?; + Ok(birthday_metdadata.utxo_index) }, Err(err) => Err(err.into()), } @@ -645,7 +646,16 @@ where TBackend: WalletBackend + 'static // Calculate the unix epoch time of two days before the wallet birthday. This is to avoid any weird time zone // issues let epoch_time = (birthday.saturating_sub(2) as u64) * 60 * 60 * 24; - let block_height = client.get_height_at_time(epoch_time).await?; + let block_height = match client.get_height_at_time(epoch_time).await { + Ok(b) => b, + Err(e) => { + warn!( + target: LOG_TARGET, + "Problem requesting `height_at_time` from Base Node: {}", e + ); + 0 + }, + }; let header = client.get_header_by_height(block_height).await?; let header = BlockHeader::try_from(header).map_err(|_| UtxoScannerError::ConversionError)?; From c262e61622a4e0de278a117a34c564590b3df4d2 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Tue, 23 Nov 2021 16:30:59 +0100 Subject: [PATCH 08/29] chore: public_address for TCP node has higher priority from env/config (#3600) Description --- The tcp `public_address` read from env/config (if set) instead of `base_node_id.json`. I've added a comment to the newly generated `base_node_id.json` that this value is overwritten. I've chosen json5 so it's backwards compatible. How Has This Been Tested? --- Manually. --- Cargo.lock | 241 +++++++++++++----- applications/tari_app_utilities/Cargo.toml | 1 + .../src/identity_management.rs | 39 ++- applications/tari_base_node/src/bootstrap.rs | 12 +- .../tari_console_wallet/src/init/mod.rs | 5 +- common/src/configuration/global.rs | 11 +- common/src/configuration/utils.rs | 7 +- integration_tests/helpers/baseNodeProcess.js | 3 +- integration_tests/package-lock.json | 5 +- integration_tests/package.json | 1 + 10 files changed, 238 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec15c0129c..d1f677ff1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,7 +14,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -23,7 +23,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -46,7 +46,7 @@ dependencies = [ "cfg-if 1.0.0", "cipher 0.3.0", "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -84,7 +84,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ "cipher 0.2.5", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -94,7 +94,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ "cipher 0.2.5", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -353,8 +353,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" dependencies = [ "crypto-mac", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", ] [[package]] @@ -363,8 +375,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding", - "generic-array", + "block-padding 0.2.1", + "generic-array 0.14.4", ] [[package]] @@ -373,7 +385,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f337a3e6da609650eb74e02bc9fac7b735049f7623ab12f2e4c719316fcc7e80" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -383,7 +395,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9b14fd8a4739e6548d4b6018696cf991dcf8c6effd9ef9eb33b29b8a650972" dependencies = [ "block-cipher", - "block-padding", + "block-padding 0.2.1", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -400,7 +421,7 @@ checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" dependencies = [ "byteorder", "cipher 0.2.5", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -443,6 +464,12 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "bytemuck" version = "1.7.2" @@ -505,7 +532,7 @@ checksum = "1285caf81ea1f1ece6b24414c521e625ad0ec94d880625c20f2e65d8d3f78823" dependencies = [ "byteorder", "cipher 0.2.5", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -624,7 +651,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -633,7 +660,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -980,7 +1007,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ - "generic-array", + "generic-array 0.14.4", "subtle", ] @@ -1046,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "rand_core 0.5.1", "serde 1.0.130", "subtle", @@ -1060,7 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "packed_simd_2", "rand_core 0.6.3", "serde 1.0.130", @@ -1196,7 +1223,7 @@ checksum = "b24e7c748888aa2fa8bce21d8c64a52efc810663285315ac7476f7197a982fae" dependencies = [ "byteorder", "cipher 0.2.5", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -1238,13 +1265,22 @@ dependencies = [ "migrations_macros", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.4", ] [[package]] @@ -1395,6 +1431,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "887dc6b1e1f9ef25ac52649e19ce610a2520b4c55b48429030782b6c8a70e78a" +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "fast-float" version = "0.2.0" @@ -1692,6 +1734,15 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -1752,7 +1803,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" dependencies = [ - "opaque-debug", + "opaque-debug 0.3.0", "polyval 0.4.5", ] @@ -1762,7 +1813,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" dependencies = [ - "opaque-debug", + "opaque-debug 0.3.0", "polyval 0.5.3", ] @@ -1839,7 +1890,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha-1 0.9.8", ] [[package]] @@ -2091,6 +2142,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb2522ff59fbfefb955e9bd44d04d5e5c2d0e8865bfc2c3d1ab3916183ef5ee" +dependencies = [ + "pest", + "pest_derive", + "serde 1.0.130", +] + [[package]] name = "jsonrpc" version = "0.12.0" @@ -2342,6 +2404,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.0.1" @@ -2363,9 +2431,9 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ - "block-buffer", - "digest", - "opaque-debug", + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -2505,7 +2573,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "752a61cd890ff691b4411423d23816d5866dd5621e4d1c5687a53b94b5a979d8" dependencies = [ - "generic-array", + "generic-array 0.14.4", "multihash-derive", "unsigned-varint", ] @@ -2787,6 +2855,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -3029,6 +3103,40 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2 1.0.32", + "quote 1.0.10", + "syn 1.0.81", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1 0.8.2", +] + [[package]] name = "petgraph" version = "0.5.1" @@ -3049,7 +3157,7 @@ dependencies = [ "base64 0.12.3", "bitfield", "block-modes", - "block-padding", + "block-padding 0.2.1", "blowfish", "buf_redux", "byteorder", @@ -3062,10 +3170,10 @@ dependencies = [ "crc24", "derive_builder", "des", - "digest", + "digest 0.9.0", "ed25519-dalek", "flate2", - "generic-array", + "generic-array 0.14.4", "hex", "lazy_static 1.4.0", "log", @@ -3077,7 +3185,7 @@ dependencies = [ "rand 0.7.3", "ripemd160", "rsa", - "sha-1", + "sha-1 0.9.8", "sha2", "sha3", "signature", @@ -3154,7 +3262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.0", "universal-hash", ] @@ -3165,7 +3273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" dependencies = [ "cpuid-bool", - "opaque-debug", + "opaque-debug 0.3.0", "universal-hash", ] @@ -3177,7 +3285,7 @@ checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.0", "universal-hash", ] @@ -3665,9 +3773,9 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ - "block-buffer", - "digest", - "opaque-debug", + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -3708,7 +3816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3648b669b10afeab18972c105e284a7b953a669b0be3514c27f9b17acab2f9cd" dependencies = [ "byteorder", - "digest", + "digest 0.9.0", "lazy_static 1.4.0", "num-bigint-dig", "num-integer", @@ -4039,17 +4147,29 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + [[package]] name = "sha-1" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -4058,11 +4178,11 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", - "opaque-debug", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -4071,10 +4191,10 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "keccak", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -4415,6 +4535,7 @@ dependencies = [ "config", "dirs-next 1.0.2", "futures 0.3.17", + "json5", "log", "qrcode", "rand 0.8.4", @@ -4484,7 +4605,7 @@ dependencies = [ "byteorder", "clear_on_drop", "curve25519-dalek-ng", - "digest", + "digest 0.9.0", "merlin", "rand 0.8.4", "rand_core 0.6.3", @@ -4534,7 +4655,7 @@ dependencies = [ name = "tari_common_types" version = "0.21.2" dependencies = [ - "digest", + "digest 0.9.0", "futures 0.3.17", "lazy_static 1.4.0", "rand 0.8.4", @@ -4557,7 +4678,7 @@ dependencies = [ "cidr", "clear_on_drop", "data-encoding", - "digest", + "digest 0.9.0", "env_logger 0.7.1", "futures 0.3.17", "lazy_static 1.4.0", @@ -4606,7 +4727,7 @@ dependencies = [ "clap", "diesel", "diesel_migrations", - "digest", + "digest 0.9.0", "env_logger 0.7.1", "futures 0.3.17", "futures-test", @@ -4664,7 +4785,7 @@ dependencies = [ "bitflags 1.3.2", "chrono", "crossterm 0.17.7", - "digest", + "digest 0.9.0", "futures 0.3.17", "log", "opentelemetry", @@ -4714,7 +4835,7 @@ dependencies = [ "croaring", "decimal-rs", "derive_more", - "digest", + "digest 0.9.0", "env_logger 0.7.1", "fs2", "futures 0.3.17", @@ -4768,7 +4889,7 @@ dependencies = [ "cbindgen", "clear_on_drop", "curve25519-dalek-ng", - "digest", + "digest 0.9.0", "lazy_static 1.4.0", "merlin", "rand 0.8.4", @@ -4803,7 +4924,7 @@ dependencies = [ "chrono", "clear_on_drop", "crc32fast", - "digest", + "digest 0.9.0", "rand 0.8.4", "sha2", "strum 0.22.0", @@ -4896,7 +5017,7 @@ dependencies = [ "blake2", "criterion", "croaring", - "digest", + "digest 0.9.0", "log", "rand 0.8.4", "serde 1.0.130", @@ -5093,7 +5214,7 @@ dependencies = [ "crossbeam-channel 0.3.9", "diesel", "diesel_migrations", - "digest", + "digest 0.9.0", "env_logger 0.7.1", "fs2", "futures 0.3.17", @@ -5747,7 +5868,7 @@ dependencies = [ "httparse", "log", "rand 0.8.4", - "sha-1", + "sha-1 0.9.8", "thiserror", "url 2.2.2", "utf-8", @@ -5761,7 +5882,7 @@ checksum = "0028f5982f23ecc9a1bc3008ead4c664f843ed5d78acd3d213b99ff50c441bc2" dependencies = [ "byteorder", "cipher 0.2.5", - "opaque-debug", + "opaque-debug 0.3.0", ] [[package]] @@ -5866,7 +5987,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ - "generic-array", + "generic-array 0.14.4", "subtle", ] diff --git a/applications/tari_app_utilities/Cargo.toml b/applications/tari_app_utilities/Cargo.toml index a943e35544..ae6d9c9d1f 100644 --- a/applications/tari_app_utilities/Cargo.toml +++ b/applications/tari_app_utilities/Cargo.toml @@ -16,6 +16,7 @@ futures = { version = "^0.3.16", default-features = false, features = ["alloc"] qrcode = { version = "0.12" } dirs-next = "1.0.2" serde_json = "1.0" +json5 = "0.2.2" log = { version = "0.4.8", features = ["std"] } rand = "0.8" tokio = { version = "1.11", features = ["signal"] } diff --git a/applications/tari_app_utilities/src/identity_management.rs b/applications/tari_app_utilities/src/identity_management.rs index f8bb2893d7..7dc6458b5f 100644 --- a/applications/tari_app_utilities/src/identity_management.rs +++ b/applications/tari_app_utilities/src/identity_management.rs @@ -22,8 +22,11 @@ use log::*; use rand::rngs::OsRng; -use std::{clone::Clone, fs, path::Path, string::ToString, sync::Arc}; -use tari_common::{configuration::bootstrap::prompt, exit_codes::ExitCodes}; +use std::{clone::Clone, fs, path::Path, str::FromStr, string::ToString, sync::Arc}; +use tari_common::{ + configuration::{bootstrap::prompt, utils::get_local_ip}, + exit_codes::ExitCodes, +}; use tari_common_types::types::PrivateKey; use tari_comms::{multiaddr::Multiaddr, peer_manager::PeerFeatures, NodeIdentity}; use tari_crypto::{ @@ -44,12 +47,18 @@ pub const LOG_TARGET: &str = "tari_application"; /// A NodeIdentity wrapped in an atomic reference counter on success, the exit code indicating the reason on failure pub fn setup_node_identity>( identity_file: P, - public_address: &Multiaddr, + public_address: &Option, create_id: bool, peer_features: PeerFeatures, ) -> Result, ExitCodes> { match load_identity(&identity_file) { - Ok(id) => Ok(Arc::new(id)), + Ok(id) => match public_address { + Some(public_address) => { + id.set_public_address(public_address.clone()); + Ok(Arc::new(id)) + }, + None => Ok(Arc::new(id)), + }, Err(e) => { if !create_id { let prompt = prompt("Node identity does not exist.\nWould you like to to create one (Y/n)?"); @@ -117,7 +126,7 @@ pub fn load_identity>(path: P) -> Result { e.to_string() ) })?; - let id = NodeIdentity::from_json(&id_str).map_err(|e| { + let id = json5::from_str::(&id_str).map_err(|e| { format!( "The node identity file, {}, has an error. {}", path.as_ref().to_str().unwrap_or("?"), @@ -141,11 +150,20 @@ pub fn load_identity>(path: P) -> Result { /// Result containing the node identity, string will indicate reason on error pub fn create_new_identity>( path: P, - public_addr: Multiaddr, + public_addr: Option, features: PeerFeatures, ) -> Result { let private_key = PrivateKey::random(&mut OsRng); - let node_identity = NodeIdentity::new(private_key, public_addr, features); + let node_identity = NodeIdentity::new( + private_key, + match public_addr { + Some(public_addr) => public_addr, + None => format!("{}/tcp/18141", get_local_ip().ok_or("Can't get local ip address")?) + .parse() + .map_err(|e: ::Err| e.to_string())?, + }, + features, + ); save_as_json(path, &node_identity)?; Ok(node_identity) } @@ -203,7 +221,12 @@ pub fn save_as_json, T: MessageFormat>(path: P, object: &T) -> Re fs::create_dir_all(p).map_err(|e| format!("Could not save json to data folder. {}", e.to_string()))?; } } - fs::write(path.as_ref(), json.as_bytes()).map_err(|e| { + let json_with_comment = format!( + "// The public address is overwritten by the config/environment variable \ + (TARI_BASE_NODE____PUBLIC_ADDRESS)\n{}", + json + ); + fs::write(path.as_ref(), json_with_comment.as_bytes()).map_err(|e| { format!( "Error writing json file, {}. {}", path.as_ref().to_str().unwrap_or(""), diff --git a/applications/tari_base_node/src/bootstrap.rs b/applications/tari_base_node/src/bootstrap.rs index 94f00af43e..516e93d2f1 100644 --- a/applications/tari_base_node/src/bootstrap.rs +++ b/applications/tari_base_node/src/bootstrap.rs @@ -58,6 +58,7 @@ use tari_p2p::{ initialization::{P2pConfig, P2pInitializer}, peer_seeds::SeedPeer, services::liveness::{LivenessConfig, LivenessInitializer}, + transport::TransportType, }; use tari_service_framework::{ServiceHandles, StackBuilder}; use tari_shutdown::ShutdownSignal; @@ -184,11 +185,16 @@ where B: BlockchainBackend + 'static let comms = comms.add_protocol_extension(mempool_protocol); let comms = Self::setup_rpc_services(comms, &handles, self.db.into(), config); - let comms = initialization::spawn_comms_using_transport(comms, transport_type).await?; + let comms = initialization::spawn_comms_using_transport(comms, transport_type.clone()).await?; // Save final node identity after comms has initialized. This is required because the public_address can be // changed by comms during initialization when using tor. - identity_management::save_as_json(&config.base_node_identity_file, &*comms.node_identity()) - .map_err(|e| anyhow!("Failed to save node identity: {:?}", e))?; + match transport_type { + TransportType::Tcp { .. } => {}, // Do not overwrite TCP public_address in the base_node_id! + _ => { + identity_management::save_as_json(&config.base_node_identity_file, &*comms.node_identity()) + .map_err(|e| anyhow!("Failed to save node identity: {:?}", e))?; + }, + }; if let Some(hs) = comms.hidden_service() { identity_management::save_as_json(&config.base_node_tor_identity_file, hs.tor_identity()) .map_err(|e| anyhow!("Failed to save tor identity: {:?}", e))?; diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 0cb9d96338..f974ed4b9f 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -299,7 +299,10 @@ pub async fn init_wallet( ); let node_address = match wallet_db.get_node_address().await? { - None => config.public_address.clone(), + None => config + .public_address + .clone() + .ok_or_else(|| ExitCodes::ConfigError("node public address error".to_string()))?, Some(a) => a, }; diff --git a/common/src/configuration/global.rs b/common/src/configuration/global.rs index 990b9acd13..04086be1f0 100644 --- a/common/src/configuration/global.rs +++ b/common/src/configuration/global.rs @@ -75,7 +75,7 @@ pub struct GlobalConfig { pub pruned_mode_cleanup_interval: u64, pub core_threads: Option, pub base_node_identity_file: PathBuf, - pub public_address: Multiaddr, + pub public_address: Option, pub grpc_enabled: bool, pub grpc_base_node_address: Multiaddr, pub grpc_console_wallet_address: Multiaddr, @@ -328,13 +328,12 @@ fn convert_node_config( // Public address let key = config_string("base_node", net_str, "public_address"); - let public_address = cfg - .get_str(&key) - .map_err(|e| ConfigurationError::new(&key, &e.to_string())) - .and_then(|addr| { + let public_address = optional(cfg.get_str(&key))? + .map(|addr| { addr.parse::() .map_err(|e| ConfigurationError::new(&key, &e.to_string())) - })?; + }) + .transpose()?; // GPRC enabled let key = config_string("base_node", net_str, "grpc_enabled"); diff --git a/common/src/configuration/utils.rs b/common/src/configuration/utils.rs index 1c1f59c38a..7b0f2c4cf0 100644 --- a/common/src/configuration/utils.rs +++ b/common/src/configuration/utils.rs @@ -226,11 +226,6 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { default_subdir("config/base_node_id.json", Some(&bootstrap.base_path)), ) .unwrap(); - cfg.set_default( - "base_node.weatherwax.public_address", - format!("{}/tcp/18141", local_ip_addr), - ) - .unwrap(); cfg.set_default("base_node.weatherwax.allow_test_addresses", false) .unwrap(); @@ -435,7 +430,7 @@ fn set_transport_defaults(cfg: &mut Config) -> Result<(), config::ConfigError> { Ok(()) } -fn get_local_ip() -> Option { +pub fn get_local_ip() -> Option { use std::net::IpAddr; get_if_addrs::get_if_addrs().ok().and_then(|if_addrs| { diff --git a/integration_tests/helpers/baseNodeProcess.js b/integration_tests/helpers/baseNodeProcess.js index 097285d607..4de9f1e79e 100644 --- a/integration_tests/helpers/baseNodeProcess.js +++ b/integration_tests/helpers/baseNodeProcess.js @@ -6,6 +6,7 @@ const BaseNodeClient = require("./baseNodeClient"); const { getFreePort } = require("./util"); const dateFormat = require("dateformat"); const { createEnv } = require("./config"); +const JSON5 = require("json5"); let outputProcess; class BaseNodeProcess { @@ -78,7 +79,7 @@ class BaseNodeProcess { } } - this.nodeInfo = JSON.parse( + this.nodeInfo = JSON5.parse( fs.readFileSync(this.baseDir + "/" + this.nodeFile, "utf8") ); } diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index 059a7730c4..90c7663a38 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -17,6 +17,7 @@ "dateformat": "^3.0.3", "glob": "^7.2.0", "hex64": "^0.4.0", + "json5": "^2.2.0", "jszip": "^3.7.1", "nvm": "^0.0.4", "sha3": "^2.1.3", @@ -2964,8 +2965,8 @@ }, "node_modules/json5": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -6495,8 +6496,8 @@ }, "json5": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "requires": { "minimist": "^1.2.5" } diff --git a/integration_tests/package.json b/integration_tests/package.json index 7c040ec972..539b4771e8 100644 --- a/integration_tests/package.json +++ b/integration_tests/package.json @@ -46,6 +46,7 @@ "dateformat": "^3.0.3", "glob": "^7.2.0", "hex64": "^0.4.0", + "json5": "^2.2.0", "jszip": "^3.7.1", "nvm": "^0.0.4", "sha3": "^2.1.3", From 603380f1466e7004a8d54da92b0971112fb0b963 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 24 Nov 2021 08:20:16 +0200 Subject: [PATCH 09/29] docs: update covenants links (#3614) Description --- Fixes up a few links that weren't working in RFC-0250, and a few typos. Auto formatting also removed some whitespace. --- RFC/src/RFC-0250_Covenants.md | 148 ++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/RFC/src/RFC-0250_Covenants.md b/RFC/src/RFC-0250_Covenants.md index 48891d1188..15aecebdb2 100644 --- a/RFC/src/RFC-0250_Covenants.md +++ b/RFC/src/RFC-0250_Covenants.md @@ -49,11 +49,12 @@ technological merits of the potential system outlined herein. ## Goals This Request for Comment (RFC) presents a proposal for introducing _covenants_ into the Tari base layer protocol. Tari -Covenents aims to provide restrictions on the _future_ spending of subsequent transactions to enable a number of powerful +Covenants aims to provide restrictions on the _future_ spending of subsequent transactions to enable a number of powerful use-cases, such as + - [vaults] - side-chain checkpointing transactions, -- commission on NFT transfers, and +- commission on NFT transfers, and - many others not thought of here. ## Related Requests for Comment @@ -65,42 +66,44 @@ use-cases, such as The Tari protocol already provides programmable consensus, through [TariScript], that restricts whether a [UTXO] may be included as an input to a transaction (a.k.a spent). The scope of information [TariScript] is inherently limited, -by the [TariScript Opcodes] and the input data provided by a spender. Once the requirements of the script are met, -a spender may generate [UTXO]s of their choosing, within the constraints of [MimbleWimble]. +by the [TariScript Opcodes] and the input data provided by a spender. Once the requirements of the script are met, +a spender may generate [UTXO]s of their choosing, within the constraints of [MimbleWimble]. -This RFC aims to expand the capabilities of Tari protocol by adding _additional requirements_, called covenants -that allow the owner(s) of a [UTXO] to control the composition of a _subsequent_ transaction. +This RFC aims to expand the capabilities of Tari protocol by adding _additional requirements_, called covenants +that allow the owner(s) of a [UTXO] to control the composition of a _subsequent_ transaction. Covenants are not a new idea and have been proposed and implemented in various forms by others. For example, -- [Bitcoin-NG covenents] put forward the `CheckOutputVerify` script opcode. -- [Handshake] has implemented covenants to add the [UTXO] state of their auctioning process. + +- [Bitcoin-NG covenants] put forward the `CheckOutputVerify` script opcode. +- [Handshake] has implemented covenants to add the [UTXO] state of their auctioning process. - [Elements Covenants] -## Covenants in MimbleWimble +## Covenants in MimbleWimble -In block chains like Bitcoin, a block contains discrete transactions containing inputs and outputs. A covenant +In blockchains like Bitcoin, a block contains discrete transactions containing inputs and outputs. A covenant in Bitcoin would be able to interrogate those outputs _belonging to the input_ to ensure that they adhere to rules. In [MimbleWimble], the body of a block and transaction can be expressed in an identical data structure. This is indeed the case in the [Tari codebase], which defines a structure called `AggregateBody` containing inputs -and outputs (and kernels) for transactions and blocks. This is innate to [MimbleWimble], so even if we were -to put a "box" around these inputs/outputs there is nothing to stop someone from including inputs and +and outputs (and kernels) for transactions and blocks. This is innate to [MimbleWimble], so even if we were +to put a "box" around these inputs/outputs there is nothing to stop someone from including inputs and outputs from other boxes as long as balance is maintained. This results in an interesting dilemma: how do we allow rules that dictate how future outputs look only armed with the knowledge that the rule must apply to one or more outputs? -In this RFC, we propose a covenant scheme that allows the [UTXO] originator to express a _filter_ that must be +In this RFC, we propose a covenant scheme that allows the [UTXO] originator to express a _filter_ that must be satisfied for a subsequent spending transaction to be considered valid. ## Assumptions The following assumptions are made: + 1. Duplicate commitments within a block are disallowed by consensus _prior_ to covenant execution, 2. all outputs in the output set are valid, and -3. all inputs are valid spends, save for covenent checks. +3. all inputs are valid spends, save for covenant checks. ## Protocol modifications @@ -114,7 +117,7 @@ Modifications to the existing protocol and consensus are as follows: ### Transaction input and output changes A `covenant` field would need to be added to the `TransactionOutput` and `TransactionInput` structs -and committed to in their hashes. +and committed to in their hashes. ### Covenant definition @@ -122,22 +125,23 @@ We define a clear notation for covenants that mirrors the [miniscript] project. #### Execution Context and Scope -Covenants execute within a limited read-only context and scope. This is both to reduce complexity (and therefore -the possibility of bugs) and maintain reasonable performance. +Covenants execute within a limited read-only context and scope. This is both to reduce complexity (and therefore +the possibility of bugs) and maintain reasonable performance. A covenant's context is limited to: -- a immutable reference to the current input, -- a vector of immutable _references_ to outputs in the current block/transaction (called, the output set), + +- an immutable reference to the current input, +- a vector of immutable _references_ to outputs in the current block/transaction (called the output set), - the current input's mined height, and - the current block height. -Each output's covenant is executed with this context, filtering on the output set and returning the result. +Each output's covenant is executed with this context, filtering on the output set and returning the result. The output set given to each covenant at execution MUST be the same set for all covenants and MUST never be -influenced by other covenants. The stateless and immutable nature of this scheme has the benefit of being +influenced by other covenants. The stateless and immutable nature of this scheme has the benefit of being able to execute covenants in parallel. -A covenant passes if at least one output in the set is matched. Allowing more than one output to match allows for -covenants that restrict the characteristics of multiple outputs. A covenant that matches zero outputs _fails_ +A covenant passes if at least one output in the set is matched. Allowing more than one output to match allows for +covenants that restrict the characteristics of multiple outputs. A covenant that matches zero outputs _fails_ which invalidates the transaction/block. If a covenant is empty (zero bytes) the `identity` operation is implied and therefore, no actual execution need occur. @@ -149,7 +153,7 @@ enum CovenantArg { // byte code: 0x01 // data size: 32 bytes Hash([u8; 32]), - // byte code: 0x02 + // byte code: 0x02 // data size: 32 bytes PublicKey(RistrettoPublicKey), // byte code: 0x03 @@ -162,16 +166,16 @@ enum CovenantArg { // data size: variable Script(TariScript), // byte code: 0x06 - // data size: variable + // data size: variable Covenant(Covenant), // byte code: 0x07 - // data size: variable + // data size: variable VarInt(VarInt), // byte code: 0x08 - // data size: 1 byte + // data size: 1 byte Field(FieldKey), // byte code: 0x09 - // data size: variable + // data size: variable Fields(Vec), } ``` @@ -181,24 +185,23 @@ enum CovenantArg { Fields from each output in the output set may be brought into a covenant filter. The available fields are defined as follows: - -| Tag Name | Byte Code | Returns | -|---------------------------------------|---------------|---------------------------------------| -| `field::commitment` | 0x00 | output.commitment | -| `field::script` | 0x01 | output.script | -| `field::sender_offset_public_key` | 0x02 | output.sender_offset_public_key | -| `field::covenant` | 0x03 | output.covenant | -| `field::features` | 0x04 | output.features | -| `field::features_flags` | 0x05 | output.features.flags | -| `field::features_maturity` | 0x06 | output.features.maturity | -| `field::features_unique_id` | 0x07 | output.features.unique_id | -| `field::features_parent_public_key` | 0x08 | output.features.parent_public_key | -| `field::features_metadata` | 0x09 | output.features.metadata | +| Tag Name | Byte Code | Returns | +| ----------------------------------- | --------- | --------------------------------- | +| `field::commitment` | 0x00 | output.commitment | +| `field::script` | 0x01 | output.script | +| `field::sender_offset_public_key` | 0x02 | output.sender_offset_public_key | +| `field::covenant` | 0x03 | output.covenant | +| `field::features` | 0x04 | output.features | +| `field::features_flags` | 0x05 | output.features.flags | +| `field::features_maturity` | 0x06 | output.features.maturity | +| `field::features_unique_id` | 0x07 | output.features.unique_id | +| `field::features_parent_public_key` | 0x08 | output.features.parent_public_key | +| `field::features_metadata` | 0x09 | output.features.metadata | Each field tag returns a consensus encoded byte representation of the value contained in the field. -How those bytes are interpreted depends on the covenant. For instance, `filter_fields_hashed_eq` will +How those bytes are interpreted depends on the covenant. For instance, `filter_fields_hashed_eq` will concatenate the bytes and hash the result whereas `filter_field_int_eq` will interpret the bytes as a -little-endian 64-byte unsigned integer. +little-endian 64-bit unsigned integer. #### Set operations @@ -225,7 +228,7 @@ args: [Covenant, Covenant] ##### xor(A, B) -The symmetric difference (\\(A \triangle B\\)) of the resulting output set for covenant rules \\(A\\) and \\(B\\). +The symmetric difference (\\(A \triangle B\\)) of the resulting output set for covenant rules \\(A\\) and \\(B\\). This is, outputs that match either \\(A\\) or \\(B\\) but not both. op_byte: 0x23
@@ -278,7 +281,7 @@ args: [Fields, VarInt] ##### filter_relative_height(height) -Checks the block height that current [UTXO] (i.e. the current input) was mined plus `height` is greater than or +Checks the block height that the current [UTXO] (i.e. the current input) was mined plus `height` is greater than or equal to the current block height. If so, the `identity()` is returned, otherwise `empty()`. op_byte: 0x34
@@ -286,10 +289,10 @@ args: [VarInt] #### Encoding / Decoding -Convenants can be encoded to/decoded from bytes as a token stream. Each token is consumed and interpreted serially +Covenants can be encoded to/decoded from bytes as a token stream. Each token is consumed and interpreted serially before being executed. -For instance, +For instance, ```ignore xor( @@ -300,6 +303,7 @@ xor( is represented in hex bytes as `23 30 01 a8b3f48e39449e89f7ff699b3eb2b080a2479b09a600a19d8ba48d765fe5d47d 35 07 0a`. Let's unpack that as follows: + ```ignore 23 // xor - consume two covenant args 30 // filter_output_hash_eq - consume a hash arg @@ -308,7 +312,7 @@ a8b3f48e39449e89f7ff699b3eb2b080a2479b09a600a19d8ba48d765fe5d47d // data // end filter_output_hash_eq 35 // 2nd covenant - filter_relative_height 07 // varint -0A // 10 +0A // 10 // end varint, filter_relative_height, xor ``` @@ -320,31 +324,31 @@ of field identifiers to consume. To mitigate misuse, the maximum allowed argumen A covenant and therefore the block/transaction MUST be regarded as invalid if: -1. an unrecognised byte code is encountered +1. an unrecognised bytecode is encountered 2. the end of the byte stream is reached unexpectedly 3. there are bytes remaining on the stream after interpreting 4. an invalid argument type is encountered 5. the `Fields` type encounters more than 9 arguments (i.e. the number of fields tags available) -6. the depth of the calls exceeds 16. +6. the depth of the calls exceeds 16. ### Consensus changes The covenant is executed once all other validations, including [TariScript], are complete. This ensures that -invalid transactions in a block cannot influence the results. +invalid transactions in a block cannot influence the results. -## Considerations +## Considerations ### Complexity -This introduces additional validation complexity. We avoid stacks, loop, and conditionals (covenants are basically -one conditional), there are overheads both in terms of complexity and performance as a trade-off for the -power given by covenants. +This introduces additional validation complexity. We avoid stacks, loops, and conditionals (covenants are basically +one conditional), there are overheads both in terms of complexity and performance as a trade-off for the +power given by covenants. The worst case complexity for covenant validation is `O(num_inputs*num_outputs)`, although as mentioned above -validation for each input can be executed in parallel. To compensate for the additional workload the network +validation for each input can be executed in parallel. To compensate for the additional workload the network encounters, use of covenants should incur heavily-weighted fees to discourage needlessly using them. -### Cut-through +### Cut-through The same arguments made in the [TariScript RFC](./RFC-0201_TariScript.md#cut-through) for the need to prevent [cut-through] apply to covenants. @@ -355,8 +359,8 @@ The same arguments made in the [TariScript RFC](./RFC-0201_TariScript.md#fodder- ### Security -As all outputs in a block are in the scope of an input to be checked, any unrelated/malicious output in a block -_could_ pass an unrelated covenant rule if given the chance. A secure covenant is one that _uniquely_ identifies +As all outputs in a block are in the scope of an input to be checked, any unrelated/malicious output in a block +_could_ pass an unrelated covenant rule if given the chance. A secure covenant is one that _uniquely_ identifies one or more outputs. ## Examples @@ -374,8 +378,8 @@ the miner. ### NFT transfer -Output features as detailed in [RFC-310-AssetImplementation] (early draft stages, still to be finalised) contain the -NFT details. This covenant preserves both the covenant protecting the token, and the token itself. +Output features as detailed in [RFC-310-AssetImplementation] (early draft stages, still to be finalised) contain the +NFT details. This covenant preserves both the covenant protecting the token, and the token itself. ```ignore filter_fields_preserved([field::features, field::covenant]) @@ -407,7 +411,7 @@ xor( and( filter_field_int_eq(field::features_flags, 128), // FLAG_BURN = 128 filter_fields_hashed_eq([field::commitment, field::script], Hash(...)), - ), + ), ) ``` @@ -434,13 +438,15 @@ xor( - `filter_script_match()` - `filter_covenant_match()` -[commitment]: (./Glossary.md#commitment) -[Tari codebase]: (https://github.com/tari-project/tari) -[Handshake]: https://handshake.org/files/handshake.txt +[commitment]: ./Glossary.md#commitment +[tari codebase]: https://github.com/tari-project/tari +[handshake]: https://handshake.org/files/handshake.txt [cut-through]: https://tlu.tarilabs.com/protocols/grin-protocol-overview/MainReport.html#cut-through -[RFC-0310_AssetImplementation]: https://github.com/tari-project/tari/pull/3340 -[Bitcoin-NG covenants](https://maltemoeser.de/paper/covenants.pdf) -[UTXO](./Glossary.md#unspent-transaction-outputs) -[miniscript](https://medium.com/blockstream/miniscript-bitcoin-scripting-3aeff3853620) -[Elements Covenants](https://blockstream.com/2016/11/02/en-covenants-in-elements-alpha/) -[vaults](https://hackingdistributed.com/2016/02/26/how-to-implement-secure-bitcoin-vaults/) +[rfc-0310_assetimplementation]: https://github.com/tari-project/tari/pull/3340 +[bitcoin-ng covenants]: https://maltemoeser.de/paper/covenants.pdf +[utxo]: ./Glossary.md#unspent-transaction-outputs +[miniscript]: https://medium.com/blockstream/miniscript-bitcoin-scripting-3aeff3853620 +[elements covenants]: https://blockstream.com/2016/11/02/en-covenants-in-elements-alpha/ +[vaults]: https://hackingdistributed.com/2016/02/26/how-to-implement-secure-bitcoin-vaults/ +[tariscript]: ./Glossary.md#tariscript +[mimblewimble]: ./Glossary.md#mimblewimble From 7432c628e5f9cc4d0806a1327da92188f450f750 Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Wed, 24 Nov 2021 08:57:57 +0200 Subject: [PATCH 10/29] feat: display network for console wallet (#3611) Description --- Visually display the network that the wallet has been configured for. console_wallet_network Motivation and Context --- As above. How Has This Been Tested? --- Manually --- .../src/ui/components/menu.rs | 42 +++++++++++++------ .../src/ui/state/app_state.rs | 4 ++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/applications/tari_console_wallet/src/ui/components/menu.rs b/applications/tari_console_wallet/src/ui/components/menu.rs index e1b105da97..c83195ff0b 100644 --- a/applications/tari_console_wallet/src/ui/components/menu.rs +++ b/applications/tari_console_wallet/src/ui/components/menu.rs @@ -3,7 +3,7 @@ use tari_app_utilities::consts; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Color, Style}, text::{Span, Spans}, widgets::{Block, Paragraph}, Frame, @@ -18,14 +18,15 @@ impl Menu { } impl Component for Menu { - fn draw(&mut self, f: &mut Frame, area: Rect, _app_state: &AppState) + fn draw(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) where B: Backend { let columns = Layout::default() .direction(Direction::Horizontal) .constraints( [ Constraint::Ratio(1, 5), - Constraint::Ratio(3, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(2, 5), Constraint::Ratio(1, 5), ] .as_ref(), @@ -36,14 +37,26 @@ impl Component for Menu { Span::styled(" Version: ", Style::default().fg(Color::White)), Span::styled(consts::APP_VERSION_NUMBER, Style::default().fg(Color::Magenta)), Span::raw(" "), - match cfg!(feature = "avx2") { - true => Span::styled("Avx2", Style::default().fg(Color::LightGreen)), - false => Span::styled( - "Avx2", - Style::default().fg(Color::LightRed).add_modifier(Modifier::CROSSED_OUT), - ), - }, + /* In the event this is needed in future, should be put into its' own span + * match cfg!(feature = "avx2") { + * true => Span::styled("Avx2", Style::default().fg(Color::LightGreen)), + * false => Span::styled( + * "Avx2", + * Style::default().fg(Color::LightRed).add_modifier(Modifier::CROSSED_OUT), + * ), + * }, + */ + ]); + + let network = Spans::from(vec![ + Span::styled(" Network: ", Style::default().fg(Color::White)), + Span::styled( + app_state.get_network().to_string(), + Style::default().fg(Color::LightGreen), + ), + Span::raw(" "), ]); + let tabs = Spans::from(vec![ Span::styled("LeftArrow: ", Style::default().fg(Color::White)), Span::styled("Previous Tab ", Style::default().fg(Color::Magenta)), @@ -51,16 +64,19 @@ impl Component for Menu { Span::styled("Tab/RightArrow: ", Style::default().fg(Color::White)), Span::styled("Next Tab ", Style::default().fg(Color::Magenta)), ]); + let quit = Spans::from(vec![ Span::styled(" F10/Ctrl-Q: ", Style::default().fg(Color::White)), Span::styled("Quit ", Style::default().fg(Color::Magenta)), ]); - let paragraph = Paragraph::new(version).block(Block::default()); + let paragraph = Paragraph::new(network).block(Block::default()); f.render_widget(paragraph, columns[0]); - let paragraph = Paragraph::new(tabs).block(Block::default()); + let paragraph = Paragraph::new(version).block(Block::default()); f.render_widget(paragraph, columns[1]); - let paragraph = Paragraph::new(quit).block(Block::default()); + let paragraph = Paragraph::new(tabs).block(Block::default()); f.render_widget(paragraph, columns[2]); + let paragraph = Paragraph::new(quit).block(Block::default()); + f.render_widget(paragraph, columns[3]); } } 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 9f6abcddaa..3d8665fe07 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -473,6 +473,10 @@ impl AppState { Igor => MicroTari(5), } } + + pub fn get_network(&self) -> Network { + self.node_config.network + } } pub struct AppStateInner { updated: bool, From a632e5b051912331f43b6679e156ffb31b346f84 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Wed, 24 Nov 2021 08:32:00 +0100 Subject: [PATCH 11/29] chore: add pub key in the dailes notify (#3612) Description --- Add node pub key (force sync peers in the daily job) to notify. --- applications/daily_tests/cron_jobs.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/applications/daily_tests/cron_jobs.js b/applications/daily_tests/cron_jobs.js index a8038b9123..572163a552 100644 --- a/applications/daily_tests/cron_jobs.js +++ b/applications/daily_tests/cron_jobs.js @@ -76,7 +76,12 @@ ${logLines.join("\n")} } async function runBaseNodeSyncTest(syncType) { - notify(`🚀 ${syncType} basenode sync check has begun 🚀`); + const node_pub_key = + "b0c1f788f137ba0cdc0b61e89ee43b80ebf5cca4136d3229561bf11eba347849"; + const node_address = "/ip4/3.8.193.254/tcp/18189"; + notify( + `🚀 ${syncType} basenode sync (from ${node_pub_key}) check has begun 🚀` + ); const baseDir = __dirname + "/temp/base-node-sync"; @@ -97,9 +102,7 @@ async function runBaseNodeSyncTest(syncType) { log: LOG_FILE, syncType, baseDir, - forceSyncPeers: [ - "b0c1f788f137ba0cdc0b61e89ee43b80ebf5cca4136d3229561bf11eba347849::/ip4/3.8.193.254/tcp/18189", - ], + forceSyncPeers: [`${node_pub_key}::${node_address}`], }); notify( From 970f8113c495688aee47a3bce1eaebe067547c52 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Wed, 24 Nov 2021 09:08:08 +0100 Subject: [PATCH 12/29] feat: add page for detailed mempool in explorer (#3613) Description --- Add mempool TX detailed view. I've run prettier on the source that I've edited. How Has This Been Tested? --- Manually. ![image](https://user-images.githubusercontent.com/35243812/143129920-b7882bd1-3058-4c19-8324-ff04ff898879.png) --- applications/tari_explorer/app.js | 129 +++++++++++-------- applications/tari_explorer/routes/mempool.js | 19 +++ applications/tari_explorer/views/index.hbs | 2 +- applications/tari_explorer/views/mempool.hbs | 100 ++++++++++++++ 4 files changed, 193 insertions(+), 57 deletions(-) create mode 100644 applications/tari_explorer/routes/mempool.js create mode 100644 applications/tari_explorer/views/mempool.hbs diff --git a/applications/tari_explorer/app.js b/applications/tari_explorer/app.js index 9007a468f3..0a71eaebdf 100644 --- a/applications/tari_explorer/app.js +++ b/applications/tari_explorer/app.js @@ -1,77 +1,94 @@ -const createError = require('http-errors') -const express = require('express') -const path = require('path') -const cookieParser = require('cookie-parser') -const logger = require('morgan') -const asciichart = require('asciichart') +const createError = require("http-errors"); +const express = require("express"); +const path = require("path"); +const cookieParser = require("cookie-parser"); +const logger = require("morgan"); +const asciichart = require("asciichart"); -var indexRouter = require('./routes/index') -var blocksRouter = require('./routes/blocks') +var indexRouter = require("./routes/index"); +var blocksRouter = require("./routes/blocks"); +var mempoolRouter = require("./routes/mempool"); -var hbs = require('hbs') -hbs.registerHelper('hex', function (buffer) { +var hbs = require("hbs"); +hbs.registerHelper("hex", function (buffer) { + return buffer ? Buffer.from(buffer).toString("hex") : ""; +}); +hbs.registerHelper("json", function (obj) { + return Buffer.from(JSON.stringify(obj)).toString("base64"); +}); - return buffer ? buffer.toString('hex') : '' -}) +hbs.registerHelper("timestamp", function (timestamp) { + var dateObj = new Date(timestamp.seconds * 1000); + const day = dateObj.getUTCDate(); + const month = dateObj.getUTCMonth() + 1; + const year = dateObj.getUTCFullYear(); + const hours = dateObj.getUTCHours(); + const minutes = dateObj.getUTCMinutes(); + const seconds = dateObj.getSeconds(); -hbs.registerHelper('timestamp', function (timestamp) { - var dateObj = new Date(timestamp.seconds * 1000) - const day = dateObj.getUTCDate() - const month = dateObj.getUTCMonth() + 1 - const year = dateObj.getUTCFullYear() - const hours = dateObj.getUTCHours() - const minutes = dateObj.getUTCMinutes() - const seconds = dateObj.getSeconds() + return ( + year.toString() + + "-" + + month.toString().padStart(2, "0") + + "-" + + day.toString().padStart(2, "0") + + " " + + hours.toString().padStart(2, "0") + + ":" + + minutes.toString().padStart(2, "0") + + ":" + + seconds.toString().padStart(2, "0") + ); +}); - return year.toString() + '-' + - month.toString().padStart(2, '0') + '-' + - day.toString().padStart(2, '0') + ' ' + - hours.toString().padStart(2, '0') + ':' + - minutes.toString().padStart(2, '0') + ':' + - seconds.toString().padStart(2, '0') -}) +hbs.registerHelper("percentbar", function (a, b) { + var percent = (a / (a + b)) * 100; + var barWidth = percent / 10; + var bar = "**********".slice(0, barWidth); + var space = "...........".slice(0, 10 - barWidth); + return bar + space + " " + parseInt(percent) + "% "; +}); -hbs.registerHelper('percentbar', function (a, b) { - var percent = a / (a + b) * 100 - var barWidth = percent / 10 - var bar = '**********'.slice(0, barWidth) - var space = '...........'.slice(0, 10 - barWidth) - return bar + space + ' ' + parseInt(percent) + '% ' -}) +hbs.registerHelper("chart", function (data, height) { + return asciichart.plot(data, { + height: height, + }); +}); -hbs.registerHelper('chart', function(data, height) { - return asciichart.plot(data, {height: height}) -}) - -var app = express() +var app = express(); // view engine setup -app.set('views', path.join(__dirname, 'views')) -app.set('view engine', 'hbs') +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "hbs"); -app.use(logger('dev')) -app.use(express.json()) -app.use(express.urlencoded({ extended: false })) -app.use(cookieParser()) -app.use(express.static(path.join(__dirname, 'public'))) +app.use(logger("dev")); +app.use(express.json()); +app.use( + express.urlencoded({ + extended: false, + }) +); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, "public"))); -app.use('/', indexRouter) -app.use('/blocks', blocksRouter) +app.use("/", indexRouter); +app.use("/blocks", blocksRouter); +app.use("/mempool", mempoolRouter); // catch 404 and forward to error handler app.use(function (req, res, next) { - next(createError(404)) -}) + next(createError(404)); +}); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development - res.locals.message = err.message - res.locals.error = req.app.get('env') === 'development' ? err : {} + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page - res.status(err.status || 500) - res.render('error') -}) + res.status(err.status || 500); + res.render("error"); +}); -module.exports = app +module.exports = app; diff --git a/applications/tari_explorer/routes/mempool.js b/applications/tari_explorer/routes/mempool.js new file mode 100644 index 0000000000..e512055840 --- /dev/null +++ b/applications/tari_explorer/routes/mempool.js @@ -0,0 +1,19 @@ +var { createClient } = require("../baseNodeClient"); + +var express = require("express"); +var router = express.Router(); + +/* GET mempool page. */ +router.get("/:tx", async function (req, res, next) { + let tx = JSON.parse(Buffer.from(req.params.tx, "base64")); + console.log("========== stringify 2 ========"); + console.log(tx.inputs); + console.log("==============="); + res.render("Mempool", { + inputs: tx.inputs, + outputs: tx.outputs, + kernels: tx.kernels, + }); +}); + +module.exports = router; diff --git a/applications/tari_explorer/views/index.hbs b/applications/tari_explorer/views/index.hbs index 4ee0a5bef1..0b1e07ec48 100644 --- a/applications/tari_explorer/views/index.hbs +++ b/applications/tari_explorer/views/index.hbs @@ -136,7 +136,7 @@ {{#each mempool}} {{#with this.transaction.body}} - {{#each this.kernels}}{{hex this.excess_sig.signature}}{{/each}} + {{#each this.kernels}}{{hex this.excess_sig.signature}}{{/each}} {{this.total_fees}} {{this.outputs.length}} {{this.kernels.length}} diff --git a/applications/tari_explorer/views/mempool.hbs b/applications/tari_explorer/views/mempool.hbs new file mode 100644 index 0000000000..71bea60c43 --- /dev/null +++ b/applications/tari_explorer/views/mempool.hbs @@ -0,0 +1,100 @@ +Back +

Inputs

+ + + + + + + + + + + + + + {{#each this.inputs}} + + + + + + + + + + {{/each}} + +
FeaturesCommitmentHashScriptInput DataScript SignatureSender Offset Public Key
Flags
+ {{features.flags}}
Maturity
+ {{features.maturity}}
{{hex commitment}}{{hex hash}}{{hex script}}{{hex input_data}}Public Nonce Commitment
+ {{hex script_signature.public_nonce_commitment}}
Signature U
+ {{hex script_signature.signature_u}}
Signature V
+ {{hex script_signature.signature_v}}
{{hex sender_offset_public_key}}
+ +
+

Outputs

+ + + + + + + + + + + + + + {{#each this.outputs}} + + + + + + + + + + {{/each}} + +
FeaturesCommitmentRange ProofHashScriptSender Offset Public KeyMetadata Signature
Flags
+ {{features.flags}}
Maturity
+ {{features.maturity}}
{{hex commitment}}{{hex + range_proof + }}{{hex hash}}{{hex script}}{{hex sender_offset_public_key}}Public Nonce Commitment
+ {{hex metadata_signature.public_nonce_commitment}}
Signature U
+ {{hex metadata_signature.signature_u}}
Signature V
+ {{hex metadata_signature.signature_v}}
+ +
+

Kernels

+ + + + + + + + + + + + + {{#each this.kernels}} + + + + + + + + + {{/each}} + +
FeaturesFeeLock HeightExcessExcess SignatureHash
{{features}}{{fee}}{{lock_height}}{{hex excess}}Nonce + {{hex excess_sig.public_nonce}}
Sig + {{hex excess_sig.signature}}
{{hex hash}}
\ No newline at end of file From 2ac07577a4c0d0f2454484f625285a07b2cd0b98 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Wed, 24 Nov 2021 12:16:46 +0200 Subject: [PATCH 13/29] feat: implement dht pooled db connection (#3596) Description --- Implemented the pooled SQLite db connection for the DHT storage in favour of the Rust based async read-write lock, which makes use of the much faster SQLite3 Write-ahead Log (WAL) mode. Motivation and Context --- Recent changes to the common implementation for SQLite pooled connections managed in the background via Diesel and SQLite3 enabled this improvement. How Has This Been Tested? --- - Unit tests - Cucumber tests - System-level tests [_**Edit:** Running a twice more intensive stress test than before (2x sender wallets with 3x 1000 transactions on one computer to 3x receiver wallets on another computer vs. 1x sender wallet with 3x 1000 transactions to 3x receiver wallets) the disk resource utilization was 90+% down from previous levels - ~100% disk utilization on both systems during all phases of the transactions life cycle down to <~5% with occasional spikes up to ~80%. All transactions reached mined confirmed in all 5 participating wallets.)_ --- Cargo.lock | 1 + base_layer/core/tests/base_node_rpc.rs | 4 +- base_layer/key_manager/src/mnemonic.rs | 4 +- .../wallet/src/contacts_service/error.rs | 4 +- base_layer/wallet/src/error.rs | 6 - .../src/output_manager_service/error.rs | 2 +- .../wallet/tests/contacts_service/mod.rs | 27 +- base_layer/wallet/tests/wallet/mod.rs | 1 - common_sqlite/src/error.rs | 6 - comms/dht/Cargo.toml | 1 + comms/dht/src/actor.rs | 26 +- comms/dht/src/dedup/dedup_cache.rs | 152 ++++----- comms/dht/src/dht.rs | 3 +- comms/dht/src/storage/connection.rs | 105 +++--- comms/dht/src/storage/database.rs | 54 ++- comms/dht/src/storage/error.rs | 3 + comms/dht/src/store_forward/database/mod.rs | 316 ++++++++---------- comms/dht/src/store_forward/service.rs | 53 ++- 18 files changed, 336 insertions(+), 432 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1f677ff1d..730b9ef7f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4746,6 +4746,7 @@ dependencies = [ "serde_derive", "serde_repr", "tari_common", + "tari_common_sqlite", "tari_comms", "tari_comms_rpc_macros", "tari_crypto", diff --git a/base_layer/core/tests/base_node_rpc.rs b/base_layer/core/tests/base_node_rpc.rs index 7643b5ca28..1b928bf911 100644 --- a/base_layer/core/tests/base_node_rpc.rs +++ b/base_layer/core/tests/base_node_rpc.rs @@ -45,6 +45,7 @@ use std::{convert::TryFrom, sync::Arc, time::Duration}; use randomx_rs::RandomXFlag; +use tari_crypto::tari_utilities::epoch_time::EpochTime; use tempfile::{tempdir, TempDir}; use tari_common::configuration::Network; @@ -303,8 +304,7 @@ async fn test_get_height_at_time() { let (_, service, base_node, request_mock, consensus_manager, block0, _utxo0, _temp_dir) = setup().await; let mut prev_block = block0.clone(); - let mut times = Vec::new(); - times.push(prev_block.header().timestamp); + let mut times: Vec = vec![prev_block.header().timestamp]; for _ in 0..10 { tokio::time::sleep(Duration::from_secs(2)).await; let new_block = base_node diff --git a/base_layer/key_manager/src/mnemonic.rs b/base_layer/key_manager/src/mnemonic.rs index 9338e82217..8df3f90734 100644 --- a/base_layer/key_manager/src/mnemonic.rs +++ b/base_layer/key_manager/src/mnemonic.rs @@ -350,7 +350,7 @@ mod test { "abandon".to_string(), "tipico".to_string(), ]; - assert_eq!(MnemonicLanguage::detect_language(&words2).is_err(), true); + assert!(MnemonicLanguage::detect_language(&words2).is_err()); // bounds check (last word is invalid) let words3 = vec![ @@ -360,7 +360,7 @@ mod test { "abandon".to_string(), "topazio".to_string(), ]; - assert_eq!(MnemonicLanguage::detect_language(&words3).is_err(), true); + assert!(MnemonicLanguage::detect_language(&words3).is_err()); // building up a word list: English/French + French -> French let mut words = Vec::with_capacity(3); diff --git a/base_layer/wallet/src/contacts_service/error.rs b/base_layer/wallet/src/contacts_service/error.rs index b06a02f9c0..3d9dd19987 100644 --- a/base_layer/wallet/src/contacts_service/error.rs +++ b/base_layer/wallet/src/contacts_service/error.rs @@ -25,7 +25,7 @@ use diesel::result::Error as DieselError; use tari_service_framework::reply_channel::TransportChannelError; use thiserror::Error; -#[derive(Debug, Error, PartialEq)] +#[derive(Debug, Error)] #[allow(clippy::large_enum_variant)] pub enum ContactsServiceError { #[error("Contact is not found")] @@ -38,7 +38,7 @@ pub enum ContactsServiceError { TransportChannelError(#[from] TransportChannelError), } -#[derive(Debug, Error, PartialEq)] +#[derive(Debug, Error)] pub enum ContactsServiceStorageError { #[error("This write operation is not supported for provided DbKey")] OperationNotSupported, diff --git a/base_layer/wallet/src/error.rs b/base_layer/wallet/src/error.rs index 61c879570f..7aba0ca426 100644 --- a/base_layer/wallet/src/error.rs +++ b/base_layer/wallet/src/error.rs @@ -170,9 +170,3 @@ impl From for ExitCodes { } } } - -impl PartialEq for WalletStorageError { - fn eq(&self, other: &Self) -> bool { - self == other - } -} diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 1e7eeb84e8..afef1156c3 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -120,7 +120,7 @@ pub enum OutputManagerError { InvalidMessageError(String), } -#[derive(Debug, Error, PartialEq)] +#[derive(Debug, Error)] pub enum OutputManagerStorageError { #[error("Tried to insert an output that already exists in the database")] DuplicateOutput, diff --git a/base_layer/wallet/tests/contacts_service/mod.rs b/base_layer/wallet/tests/contacts_service/mod.rs index ed5ad5033c..b96aa8e8a7 100644 --- a/base_layer/wallet/tests/contacts_service/mod.rs +++ b/base_layer/wallet/tests/contacts_service/mod.rs @@ -87,18 +87,21 @@ pub fn test_contacts_service() { let (_secret_key, public_key) = PublicKey::random_keypair(&mut OsRng); let contact = runtime.block_on(contacts_service.get_contact(public_key.clone())); - assert_eq!( - contact, - Err(ContactsServiceError::ContactsServiceStorageError( - ContactsServiceStorageError::ValueNotFound(DbKey::Contact(public_key.clone())) - )) - ); - assert_eq!( - runtime.block_on(contacts_service.remove_contact(public_key.clone())), - Err(ContactsServiceError::ContactsServiceStorageError( - ContactsServiceStorageError::ValueNotFound(DbKey::Contact(public_key)) - )) - ); + match contact { + Ok(_) => panic!("There should be an error here"), + Err(ContactsServiceError::ContactsServiceStorageError(ContactsServiceStorageError::ValueNotFound(val))) => { + assert_eq!(val, DbKey::Contact(public_key.clone())) + }, + _ => panic!("There should be a specific error here"), + } + let result = runtime.block_on(contacts_service.remove_contact(public_key.clone())); + match result { + Ok(_) => panic!("There should be an error here"), + Err(ContactsServiceError::ContactsServiceStorageError(ContactsServiceStorageError::ValueNotFound(val))) => { + assert_eq!(val, DbKey::Contact(public_key)) + }, + _ => panic!("There should be a specific error here"), + } let _ = runtime .block_on(contacts_service.remove_contact(contacts[0].public_key.clone())) diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index d54c307c01..c33481f3c2 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -69,7 +69,6 @@ use tari_wallet::{ handle::TransactionEvent, storage::sqlite_db::TransactionServiceSqliteDatabase, }, - utxo_scanner_service::utxo_scanning::UtxoScannerService, Wallet, WalletConfig, WalletSqlite, diff --git a/common_sqlite/src/error.rs b/common_sqlite/src/error.rs index c47859ce16..cd2b8e967a 100644 --- a/common_sqlite/src/error.rs +++ b/common_sqlite/src/error.rs @@ -27,9 +27,3 @@ pub enum SqliteStorageError { #[error("Diesel R2d2 error")] DieselR2d2Error(String), } - -impl PartialEq for SqliteStorageError { - fn eq(&self, other: &Self) -> bool { - self == other - } -} diff --git a/comms/dht/Cargo.toml b/comms/dht/Cargo.toml index 5b15421607..6119216687 100644 --- a/comms/dht/Cargo.toml +++ b/comms/dht/Cargo.toml @@ -16,6 +16,7 @@ tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch tari_utilities = { version = "^0.3" } tari_shutdown = { version = "^0.21", path = "../../infrastructure/shutdown" } tari_storage = { version = "^0.21", path = "../../infrastructure/storage" } +tari_common_sqlite = { path = "../../common_sqlite" } anyhow = "1.0.32" bitflags = "1.2.0" diff --git a/comms/dht/src/actor.rs b/comms/dht/src/actor.rs index 3d93e3abc8..b1e16f18e8 100644 --- a/comms/dht/src/actor.rs +++ b/comms/dht/src/actor.rs @@ -254,7 +254,6 @@ impl DhtActor { let offline_ts = self .database .get_metadata_value::>(DhtMetadataKey::OfflineTimestamp) - .await .ok() .flatten(); info!( @@ -284,25 +283,24 @@ impl DhtActor { }, _ = dedup_cache_trim_ticker.tick() => { - if let Err(err) = self.msg_hash_dedup_cache.trim_entries().await { + if let Err(err) = self.msg_hash_dedup_cache.trim_entries() { error!(target: LOG_TARGET, "Error when trimming message dedup cache: {:?}", err); } }, _ = self.shutdown_signal.wait() => { info!(target: LOG_TARGET, "DhtActor is shutting down because it received a shutdown signal."); - self.mark_shutdown_time().await; + self.mark_shutdown_time(); break Ok(()); }, } } } - async fn mark_shutdown_time(&self) { + fn mark_shutdown_time(&self) { if let Err(err) = self .database .set_metadata_value(DhtMetadataKey::OfflineTimestamp, Utc::now()) - .await { warn!(target: LOG_TARGET, "Failed to mark offline time: {:?}", err); } @@ -323,7 +321,7 @@ impl DhtActor { } => { let msg_hash_cache = self.msg_hash_dedup_cache.clone(); Box::pin(async move { - match msg_hash_cache.add_body_hash(message_hash, received_from).await { + match msg_hash_cache.add_body_hash(message_hash, received_from) { Ok(hit_count) => { let _ = reply_tx.send(hit_count); }, @@ -341,7 +339,7 @@ impl DhtActor { GetMsgHashHitCount(hash, reply_tx) => { let msg_hash_cache = self.msg_hash_dedup_cache.clone(); Box::pin(async move { - let hit_count = msg_hash_cache.get_hit_count(hash).await?; + let hit_count = msg_hash_cache.get_hit_count(hash)?; let _ = reply_tx.send(hit_count); Ok(()) }) @@ -366,14 +364,14 @@ impl DhtActor { GetMetadata(key, reply_tx) => { let db = self.database.clone(); Box::pin(async move { - let _ = reply_tx.send(db.get_metadata_value_bytes(key).await.map_err(Into::into)); + let _ = reply_tx.send(db.get_metadata_value_bytes(key).map_err(Into::into)); Ok(()) }) }, SetMetadata(key, value, reply_tx) => { let db = self.database.clone(); Box::pin(async move { - match db.set_metadata_value_bytes(key, value).await { + match db.set_metadata_value_bytes(key, value) { Ok(_) => { debug!(target: LOG_TARGET, "Dht metadata '{}' set", key); let _ = reply_tx.send(Ok(())); @@ -727,8 +725,8 @@ mod test { use tari_test_utils::random; async fn db_connection() -> DbConnection { - let conn = DbConnection::connect_memory(random::string(8)).await.unwrap(); - conn.migrate().await.unwrap(); + let conn = DbConnection::connect_memory(random::string(8)).unwrap(); + conn.migrate().unwrap(); conn } @@ -838,7 +836,6 @@ mod test { let num_hits = actor .msg_hash_dedup_cache .add_body_hash(key.clone(), CommsPublicKey::default()) - .await .unwrap(); assert_eq!(num_hits, 1); } @@ -847,7 +844,6 @@ mod test { let num_hits = actor .msg_hash_dedup_cache .add_body_hash(key.clone(), CommsPublicKey::default()) - .await .unwrap(); assert_eq!(num_hits, 2); } @@ -855,7 +851,7 @@ mod test { let dedup_cache_db = actor.msg_hash_dedup_cache.clone(); // The cleanup ticker starts when the actor is spawned; the first cleanup event will fire fairly soon after the // task is running on a thread. To remove this race condition, we trim the cache in the test. - let num_trimmed = dedup_cache_db.trim_entries().await.unwrap(); + let num_trimmed = dedup_cache_db.trim_entries().unwrap(); assert_eq!(num_trimmed, 10); actor.spawn(); @@ -877,7 +873,7 @@ mod test { } // Trim the database of excess entries - dedup_cache_db.trim_entries().await.unwrap(); + dedup_cache_db.trim_entries().unwrap(); // Verify that the last half of the signatures have been removed and can be re-inserted into cache for key in signatures.iter().take(capacity * 2).skip(capacity) { diff --git a/comms/dht/src/dedup/dedup_cache.rs b/comms/dht/src/dedup/dedup_cache.rs index 2b21fd38bb..b6b2d9338c 100644 --- a/comms/dht/src/dedup/dedup_cache.rs +++ b/comms/dht/src/dedup/dedup_cache.rs @@ -58,10 +58,8 @@ impl DedupCacheDatabase { /// Adds the body hash to the cache, returning the number of hits (inclusive) that have been recorded for this body /// hash - pub async fn add_body_hash(&self, body_hash: Vec, public_key: CommsPublicKey) -> Result { - let hit_count = self - .insert_body_hash_or_update_stats(body_hash.to_hex(), public_key.to_hex()) - .await?; + pub fn add_body_hash(&self, body_hash: Vec, public_key: CommsPublicKey) -> Result { + let hit_count = self.insert_body_hash_or_update_stats(body_hash.to_hex(), public_key.to_hex())?; if hit_count == 0 { warn!( @@ -72,96 +70,80 @@ impl DedupCacheDatabase { Ok(hit_count) } - pub async fn get_hit_count(&self, body_hash: Vec) -> Result { - let hit_count = self - .connection - .with_connection_async(move |conn| { - dedup_cache::table - .select(dedup_cache::number_of_hits) - .filter(dedup_cache::body_hash.eq(&body_hash.to_hex())) - .get_result::(conn) - .optional() - .map_err(Into::into) - }) - .await?; + pub fn get_hit_count(&self, body_hash: Vec) -> Result { + let conn = self.connection.get_pooled_connection()?; + let hit_count = dedup_cache::table + .select(dedup_cache::number_of_hits) + .filter(dedup_cache::body_hash.eq(&body_hash.to_hex())) + .get_result::(&conn) + .optional()?; Ok(hit_count.unwrap_or(0) as u32) } /// Trims the dedup cache to the configured limit by removing the oldest entries - pub async fn trim_entries(&self) -> Result { + pub fn trim_entries(&self) -> Result { let capacity = self.capacity as i64; - self.connection - .with_connection_async(move |conn| { - let mut num_removed = 0; - let msg_count = dedup_cache::table - .select(dsl::count(dedup_cache::id)) - .first::(conn)?; - // Hysteresis added to minimize database impact - if msg_count > capacity { - let remove_count = msg_count - capacity; - num_removed = diesel::sql_query( - "DELETE FROM dedup_cache WHERE id IN (SELECT id FROM dedup_cache ORDER BY last_hit_at ASC \ - LIMIT $1)", - ) - .bind::(remove_count) - .execute(conn)?; - } - debug!( - target: LOG_TARGET, - "Message dedup cache: count {}, capacity {}, removed {}", msg_count, capacity, num_removed, - ); - Ok(num_removed) - }) - .await + let mut num_removed = 0; + let conn = self.connection.get_pooled_connection()?; + let msg_count = dedup_cache::table + .select(dsl::count(dedup_cache::id)) + .first::(&conn)?; + // Hysteresis added to minimize database impact + if msg_count > capacity { + let remove_count = msg_count - capacity; + num_removed = diesel::sql_query( + "DELETE FROM dedup_cache WHERE id IN (SELECT id FROM dedup_cache ORDER BY last_hit_at ASC LIMIT $1)", + ) + .bind::(remove_count) + .execute(&conn)?; + } + debug!( + target: LOG_TARGET, + "Message dedup cache: count {}, capacity {}, removed {}", msg_count, capacity, num_removed, + ); + Ok(num_removed) } /// Insert new row into the table or updates an existing row. Returns the number of hits for this body hash. - async fn insert_body_hash_or_update_stats( - &self, - body_hash: String, - public_key: String, - ) -> Result { - self.connection - .with_connection_async(move |conn| { - let insert_result = diesel::insert_into(dedup_cache::table) - .values(( - dedup_cache::body_hash.eq(&body_hash), - dedup_cache::sender_public_key.eq(&public_key), - dedup_cache::number_of_hits.eq(1), - dedup_cache::last_hit_at.eq(Utc::now().naive_utc()), - )) - .execute(conn); - match insert_result { - Ok(1) => Ok(1), - Ok(n) => Err(StorageError::UnexpectedResult(format!( - "Expected exactly one row to be inserted. Got {}", - n - ))), - Err(diesel::result::Error::DatabaseError(kind, e_info)) => match kind { - DatabaseErrorKind::UniqueViolation => { - // Update hit stats for the message - diesel::update(dedup_cache::table.filter(dedup_cache::body_hash.eq(&body_hash))) - .set(( - dedup_cache::sender_public_key.eq(&public_key), - dedup_cache::number_of_hits.eq(dedup_cache::number_of_hits + 1), - dedup_cache::last_hit_at.eq(Utc::now().naive_utc()), - )) - .execute(conn)?; - // TODO: Diesel support for RETURNING statements would remove this query, but is not - // available for Diesel + SQLite yet - let hits = dedup_cache::table - .select(dedup_cache::number_of_hits) - .filter(dedup_cache::body_hash.eq(&body_hash)) - .get_result::(conn)?; + fn insert_body_hash_or_update_stats(&self, body_hash: String, public_key: String) -> Result { + let conn = self.connection.get_pooled_connection()?; + let insert_result = diesel::insert_into(dedup_cache::table) + .values(( + dedup_cache::body_hash.eq(&body_hash), + dedup_cache::sender_public_key.eq(&public_key), + dedup_cache::number_of_hits.eq(1), + dedup_cache::last_hit_at.eq(Utc::now().naive_utc()), + )) + .execute(&conn); + match insert_result { + Ok(1) => Ok(1), + Ok(n) => Err(StorageError::UnexpectedResult(format!( + "Expected exactly one row to be inserted. Got {}", + n + ))), + Err(diesel::result::Error::DatabaseError(kind, e_info)) => match kind { + DatabaseErrorKind::UniqueViolation => { + // Update hit stats for the message + diesel::update(dedup_cache::table.filter(dedup_cache::body_hash.eq(&body_hash))) + .set(( + dedup_cache::sender_public_key.eq(&public_key), + dedup_cache::number_of_hits.eq(dedup_cache::number_of_hits + 1), + dedup_cache::last_hit_at.eq(Utc::now().naive_utc()), + )) + .execute(&conn)?; + // TODO: Diesel support for RETURNING statements would remove this query, but is not + // TODO: available for Diesel + SQLite yet + let hits = dedup_cache::table + .select(dedup_cache::number_of_hits) + .filter(dedup_cache::body_hash.eq(&body_hash)) + .get_result::(&conn)?; - Ok(hits as u32) - }, - _ => Err(diesel::result::Error::DatabaseError(kind, e_info).into()), - }, - Err(e) => Err(e.into()), - } - }) - .await + Ok(hits as u32) + }, + _ => Err(diesel::result::Error::DatabaseError(kind, e_info).into()), + }, + Err(e) => Err(e.into()), + } } } diff --git a/comms/dht/src/dht.rs b/comms/dht/src/dht.rs index 54f049a3da..06741eb256 100644 --- a/comms/dht/src/dht.rs +++ b/comms/dht/src/dht.rs @@ -132,11 +132,10 @@ impl Dht { saf_response_signal_sender, connectivity, discovery_sender, - event_publisher: event_publisher.clone(), + event_publisher, }; let conn = DbConnection::connect_and_migrate(dht.config.database_url.clone()) - .await .map_err(DhtInitializationError::DatabaseMigrationFailed)?; dht.network_discovery_service(shutdown_signal.clone()).spawn(); diff --git a/comms/dht/src/storage/connection.rs b/comms/dht/src/storage/connection.rs index ee99f8b560..3a37647ec6 100644 --- a/comms/dht/src/storage/connection.rs +++ b/comms/dht/src/storage/connection.rs @@ -21,16 +21,16 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::storage::error::StorageError; -use diesel::{Connection, SqliteConnection}; -use log::*; -use std::{ - io, - path::PathBuf, - sync::{Arc, Mutex}, +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + SqliteConnection, }; -use tokio::task; +use log::*; +use std::{io, path::PathBuf, time::Duration}; +use tari_common_sqlite::sqlite_connection_pool::SqliteConnectionPool; const LOG_TARGET: &str = "comms::dht::storage::connection"; +const SQLITE_POOL_SIZE: usize = 16; #[derive(Clone, Debug)] pub enum DbConnectionUrl { @@ -58,64 +58,53 @@ impl DbConnectionUrl { #[derive(Clone)] pub struct DbConnection { - inner: Arc>, + pool: SqliteConnectionPool, } impl DbConnection { #[cfg(test)] - pub async fn connect_memory(name: String) -> Result { - Self::connect_url(DbConnectionUrl::MemoryShared(name)).await + pub fn connect_memory(name: String) -> Result { + Self::connect_url(DbConnectionUrl::MemoryShared(name)) } - pub async fn connect_url(db_url: DbConnectionUrl) -> Result { + pub fn connect_url(db_url: DbConnectionUrl) -> Result { debug!(target: LOG_TARGET, "Connecting to database using '{:?}'", db_url); - let conn = task::spawn_blocking(move || { - let conn = SqliteConnection::establish(&db_url.to_url_string())?; - conn.execute("PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 60000;")?; - Result::<_, StorageError>::Ok(conn) - }) - .await??; - - Ok(Self::new(conn)) + + let mut pool = SqliteConnectionPool::new( + db_url.to_url_string(), + SQLITE_POOL_SIZE, + true, + true, + Duration::from_secs(60), + ); + pool.create_pool()?; + + Ok(Self::new(pool)) } - pub async fn connect_and_migrate(db_url: DbConnectionUrl) -> Result { - let conn = Self::connect_url(db_url).await?; - let output = conn.migrate().await?; + pub fn connect_and_migrate(db_url: DbConnectionUrl) -> Result { + let conn = Self::connect_url(db_url)?; + let output = conn.migrate()?; info!(target: LOG_TARGET, "DHT database migration: {}", output.trim()); Ok(conn) } - fn new(conn: SqliteConnection) -> Self { - Self { - inner: Arc::new(Mutex::new(conn)), - } + fn new(pool: SqliteConnectionPool) -> Self { + Self { pool } } - pub async fn migrate(&self) -> Result { - embed_migrations!("./migrations"); - - self.with_connection_async(|conn| { - let mut buf = io::Cursor::new(Vec::new()); - embedded_migrations::run_with_output(conn, &mut buf) - .map_err(|err| StorageError::DatabaseMigrationFailed(format!("Database migration failed {}", err)))?; - Ok(String::from_utf8_lossy(&buf.into_inner()).to_string()) - }) - .await + pub fn get_pooled_connection(&self) -> Result>, StorageError> { + self.pool.get_pooled_connection().map_err(StorageError::DieselR2d2Error) } - pub async fn with_connection_async(&self, f: F) -> Result - where - F: FnOnce(&SqliteConnection) -> Result + Send + 'static, - R: Send + 'static, - { - let conn_mutex = self.inner.clone(); - let ret = task::spawn_blocking(move || { - let lock = acquire_lock!(conn_mutex); - f(&*lock) - }) - .await??; - Ok(ret) + pub fn migrate(&self) -> Result { + embed_migrations!("./migrations"); + + let mut buf = io::Cursor::new(Vec::new()); + let conn = self.get_pooled_connection()?; + embedded_migrations::run_with_output(&conn, &mut buf) + .map_err(|err| StorageError::DatabaseMigrationFailed(format!("Database migration failed {}", err)))?; + Ok(String::from_utf8_lossy(&buf.into_inner()).to_string()) } } @@ -128,24 +117,20 @@ mod test { #[runtime::test] async fn connect_and_migrate() { - let conn = DbConnection::connect_memory(random::string(8)).await.unwrap(); - let output = conn.migrate().await.unwrap(); + let conn = DbConnection::connect_memory(random::string(8)).unwrap(); + let output = conn.migrate().unwrap(); assert!(output.starts_with("Running migration")); } #[runtime::test] async fn memory_connections() { let id = random::string(8); - let conn = DbConnection::connect_memory(id.clone()).await.unwrap(); - conn.migrate().await.unwrap(); - let conn = DbConnection::connect_memory(id).await.unwrap(); - let count: i32 = conn - .with_connection_async(|c| { - sql::("SELECT COUNT(*) FROM stored_messages") - .get_result(c) - .map_err(Into::into) - }) - .await + let conn = DbConnection::connect_memory(id.clone()).unwrap(); + conn.migrate().unwrap(); + let conn = DbConnection::connect_memory(id).unwrap(); + let conn = conn.get_pooled_connection().unwrap(); + let count: i32 = sql::("SELECT COUNT(*) FROM stored_messages") + .get_result(&conn) .unwrap(); assert_eq!(count, 0); } diff --git a/comms/dht/src/storage/database.rs b/comms/dht/src/storage/database.rs index c035eebb9a..94704de3b8 100644 --- a/comms/dht/src/storage/database.rs +++ b/comms/dht/src/storage/database.rs @@ -38,49 +38,39 @@ impl DhtDatabase { Self { connection } } - pub async fn get_metadata_value(&self, key: DhtMetadataKey) -> Result, StorageError> { - match self.get_metadata_value_bytes(key).await? { + pub fn get_metadata_value(&self, key: DhtMetadataKey) -> Result, StorageError> { + match self.get_metadata_value_bytes(key)? { Some(bytes) => T::from_binary(&bytes).map(Some).map_err(Into::into), None => Ok(None), } } - pub async fn get_metadata_value_bytes(&self, key: DhtMetadataKey) -> Result>, StorageError> { - self.connection - .with_connection_async(move |conn| { - dht_metadata::table - .filter(dht_metadata::key.eq(key.to_string())) - .first(conn) - .map(|rec: DhtMetadataEntry| Some(rec.value)) - .or_else(|err| match err { - diesel::result::Error::NotFound => Ok(None), - err => Err(err.into()), - }) + pub fn get_metadata_value_bytes(&self, key: DhtMetadataKey) -> Result>, StorageError> { + let conn = self.connection.get_pooled_connection()?; + dht_metadata::table + .filter(dht_metadata::key.eq(key.to_string())) + .first(&conn) + .map(|rec: DhtMetadataEntry| Some(rec.value)) + .or_else(|err| match err { + diesel::result::Error::NotFound => Ok(None), + err => Err(err.into()), }) - .await } - pub async fn set_metadata_value( - &self, - key: DhtMetadataKey, - value: T, - ) -> Result<(), StorageError> { + pub fn set_metadata_value(&self, key: DhtMetadataKey, value: T) -> Result<(), StorageError> { let bytes = value.to_binary()?; - self.set_metadata_value_bytes(key, bytes).await + self.set_metadata_value_bytes(key, bytes) } - pub async fn set_metadata_value_bytes(&self, key: DhtMetadataKey, value: Vec) -> Result<(), StorageError> { - self.connection - .with_connection_async(move |conn| { - diesel::replace_into(dht_metadata::table) - .values(NewDhtMetadataEntry { - key: key.to_string(), - value, - }) - .execute(conn) - .map(|_| ()) - .map_err(Into::into) + pub fn set_metadata_value_bytes(&self, key: DhtMetadataKey, value: Vec) -> Result<(), StorageError> { + let conn = self.connection.get_pooled_connection()?; + diesel::replace_into(dht_metadata::table) + .values(NewDhtMetadataEntry { + key: key.to_string(), + value, }) - .await + .execute(&conn) + .map(|_| ()) + .map_err(Into::into) } } diff --git a/comms/dht/src/storage/error.rs b/comms/dht/src/storage/error.rs index f5bf4f0596..5e89ab494d 100644 --- a/comms/dht/src/storage/error.rs +++ b/comms/dht/src/storage/error.rs @@ -20,6 +20,7 @@ // 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 tari_common_sqlite::error::SqliteStorageError; use tari_utilities::message_format::MessageFormatError; use thiserror::Error; use tokio::task; @@ -42,4 +43,6 @@ pub enum StorageError { MessageFormatError(#[from] MessageFormatError), #[error("Unexpected result: {0}")] UnexpectedResult(String), + #[error("Diesel R2d2 error: `{0}`")] + DieselR2d2Error(#[from] SqliteStorageError), } diff --git a/comms/dht/src/store_forward/database/mod.rs b/comms/dht/src/store_forward/database/mod.rs index 58ee06eb9c..74a004fb97 100644 --- a/comms/dht/src/store_forward/database/mod.rs +++ b/comms/dht/src/store_forward/database/mod.rs @@ -44,36 +44,30 @@ impl StoreAndForwardDatabase { } /// Inserts and returns Ok(true) if the item already existed and Ok(false) if it didn't - pub async fn insert_message_if_unique(&self, message: NewStoredMessage) -> Result { - self.connection - .with_connection_async(move |conn| { - match diesel::insert_into(stored_messages::table) - .values(message) - .execute(conn) - { - Ok(_) => Ok(false), - Err(diesel::result::Error::DatabaseError(kind, e_info)) => match kind { - DatabaseErrorKind::UniqueViolation => Ok(true), - _ => Err(diesel::result::Error::DatabaseError(kind, e_info).into()), - }, - Err(e) => Err(e.into()), - } - }) - .await + pub fn insert_message_if_unique(&self, message: NewStoredMessage) -> Result { + let conn = self.connection.get_pooled_connection()?; + match diesel::insert_into(stored_messages::table) + .values(message) + .execute(&conn) + { + Ok(_) => Ok(false), + Err(diesel::result::Error::DatabaseError(kind, e_info)) => match kind { + DatabaseErrorKind::UniqueViolation => Ok(true), + _ => Err(diesel::result::Error::DatabaseError(kind, e_info).into()), + }, + Err(e) => Err(e.into()), + } } - pub async fn remove_message(&self, message_ids: Vec) -> Result { - self.connection - .with_connection_async(move |conn| { - diesel::delete(stored_messages::table) - .filter(stored_messages::id.eq_any(message_ids)) - .execute(conn) - .map_err(Into::into) - }) - .await + pub fn remove_message(&self, message_ids: Vec) -> Result { + let conn = self.connection.get_pooled_connection()?; + diesel::delete(stored_messages::table) + .filter(stored_messages::id.eq_any(message_ids)) + .execute(&conn) + .map_err(Into::into) } - pub async fn find_messages_for_peer( + pub fn find_messages_for_peer( &self, public_key: &CommsPublicKey, node_id: &NodeId, @@ -82,85 +76,76 @@ impl StoreAndForwardDatabase { ) -> Result, StorageError> { let pk_hex = public_key.to_hex(); let node_id_hex = node_id.to_hex(); - self.connection - .with_connection_async::<_, Vec>(move |conn| { - let mut query = stored_messages::table - .select(stored_messages::all_columns) - .filter( - stored_messages::destination_pubkey - .eq(pk_hex) - .or(stored_messages::destination_node_id.eq(node_id_hex)), - ) - .filter(stored_messages::message_type.eq(DhtMessageType::None as i32)) - .into_boxed(); + let conn = self.connection.get_pooled_connection()?; + let mut query = stored_messages::table + .select(stored_messages::all_columns) + .filter( + stored_messages::destination_pubkey + .eq(pk_hex) + .or(stored_messages::destination_node_id.eq(node_id_hex)), + ) + .filter(stored_messages::message_type.eq(DhtMessageType::None as i32)) + .into_boxed(); - if let Some(since) = since { - query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); - } + if let Some(since) = since { + query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); + } - query - .order_by(stored_messages::stored_at.desc()) - .limit(limit) - .get_results(conn) - .map_err(Into::into) - }) - .await + query + .order_by(stored_messages::stored_at.desc()) + .limit(limit) + .get_results(&conn) + .map_err(Into::into) } - pub async fn find_anonymous_messages( + pub fn find_anonymous_messages( &self, since: Option>, limit: i64, ) -> Result, StorageError> { - self.connection - .with_connection_async(move |conn| { - let mut query = stored_messages::table - .select(stored_messages::all_columns) - .filter(stored_messages::origin_pubkey.is_null()) - .filter(stored_messages::destination_pubkey.is_null()) - .filter(stored_messages::is_encrypted.eq(true)) - .filter(stored_messages::message_type.eq(DhtMessageType::None as i32)) - .into_boxed(); + let conn = self.connection.get_pooled_connection()?; + let mut query = stored_messages::table + .select(stored_messages::all_columns) + .filter(stored_messages::origin_pubkey.is_null()) + .filter(stored_messages::destination_pubkey.is_null()) + .filter(stored_messages::is_encrypted.eq(true)) + .filter(stored_messages::message_type.eq(DhtMessageType::None as i32)) + .into_boxed(); - if let Some(since) = since { - query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); - } + if let Some(since) = since { + query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); + } - query - .order_by(stored_messages::stored_at.desc()) - .limit(limit) - .get_results(conn) - .map_err(Into::into) - }) - .await + query + .order_by(stored_messages::stored_at.desc()) + .limit(limit) + .get_results(&conn) + .map_err(Into::into) } - pub async fn find_join_messages( + pub fn find_join_messages( &self, since: Option>, limit: i64, ) -> Result, StorageError> { - self.connection - .with_connection_async(move |conn| { - let mut query = stored_messages::table - .select(stored_messages::all_columns) - .filter(stored_messages::message_type.eq(DhtMessageType::Join as i32)) - .into_boxed(); + let conn = self.connection.get_pooled_connection()?; + let mut query = stored_messages::table + .select(stored_messages::all_columns) + .filter(stored_messages::message_type.eq(DhtMessageType::Join as i32)) + .into_boxed(); - if let Some(since) = since { - query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); - } + if let Some(since) = since { + query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); + } - query - .order_by(stored_messages::stored_at.desc()) - .limit(limit) - .get_results(conn) - .map_err(Into::into) - }) - .await + query + .order_by(stored_messages::stored_at.desc()) + .limit(limit) + .get_results(&conn) + .map_err(Into::into) } - pub async fn find_messages_of_type_for_pubkey( + pub fn find_messages_of_type_for_pubkey( &self, public_key: &CommsPublicKey, message_type: DhtMessageType, @@ -168,87 +153,72 @@ impl StoreAndForwardDatabase { limit: i64, ) -> Result, StorageError> { let pk_hex = public_key.to_hex(); - self.connection - .with_connection_async(move |conn| { - let mut query = stored_messages::table - .select(stored_messages::all_columns) - .filter(stored_messages::destination_pubkey.eq(pk_hex)) - .filter(stored_messages::message_type.eq(message_type as i32)) - .into_boxed(); + let conn = self.connection.get_pooled_connection()?; + let mut query = stored_messages::table + .select(stored_messages::all_columns) + .filter(stored_messages::destination_pubkey.eq(pk_hex)) + .filter(stored_messages::message_type.eq(message_type as i32)) + .into_boxed(); - if let Some(since) = since { - query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); - } + if let Some(since) = since { + query = query.filter(stored_messages::stored_at.gt(since.naive_utc())); + } - query - .order_by(stored_messages::stored_at.desc()) - .limit(limit) - .get_results(conn) - .map_err(Into::into) - }) - .await + query + .order_by(stored_messages::stored_at.desc()) + .limit(limit) + .get_results(&conn) + .map_err(Into::into) } #[cfg(test)] - pub(crate) async fn get_all_messages(&self) -> Result, StorageError> { - self.connection - .with_connection_async(|conn| { - stored_messages::table - .select(stored_messages::all_columns) - .get_results(conn) - .map_err(Into::into) - }) - .await + pub(crate) fn get_all_messages(&self) -> Result, StorageError> { + let conn = self.connection.get_pooled_connection()?; + stored_messages::table + .select(stored_messages::all_columns) + .get_results(&conn) + .map_err(Into::into) } - pub(crate) async fn delete_messages_with_priority_older_than( + pub(crate) fn delete_messages_with_priority_older_than( &self, priority: StoredMessagePriority, since: NaiveDateTime, ) -> Result { - self.connection - .with_connection_async(move |conn| { - diesel::delete(stored_messages::table) - .filter(stored_messages::stored_at.lt(since)) - .filter(stored_messages::priority.eq(priority as i32)) - .execute(conn) - .map_err(Into::into) - }) - .await + let conn = self.connection.get_pooled_connection()?; + diesel::delete(stored_messages::table) + .filter(stored_messages::stored_at.lt(since)) + .filter(stored_messages::priority.eq(priority as i32)) + .execute(&conn) + .map_err(Into::into) } - pub(crate) async fn delete_messages_older_than(&self, since: NaiveDateTime) -> Result { - self.connection - .with_connection_async(move |conn| { - diesel::delete(stored_messages::table) - .filter(stored_messages::stored_at.lt(since)) - .execute(conn) - .map_err(Into::into) - }) - .await + pub(crate) fn delete_messages_older_than(&self, since: NaiveDateTime) -> Result { + let conn = self.connection.get_pooled_connection()?; + diesel::delete(stored_messages::table) + .filter(stored_messages::stored_at.lt(since)) + .execute(&conn) + .map_err(Into::into) } - pub(crate) async fn truncate_messages(&self, max_size: usize) -> Result { - self.connection - .with_connection_async(move |conn| { - let mut num_removed = 0; - let msg_count = stored_messages::table - .select(dsl::count(stored_messages::id)) - .first::(conn)? as usize; - if msg_count > max_size { - let remove_count = msg_count - max_size; - let message_ids: Vec = stored_messages::table - .select(stored_messages::id) - .order_by(stored_messages::stored_at.asc()) - .limit(remove_count as i64) - .get_results(conn)?; - num_removed = diesel::delete(stored_messages::table) - .filter(stored_messages::id.eq_any(message_ids)) - .execute(conn)?; - } - Ok(num_removed) - }) - .await + pub(crate) fn truncate_messages(&self, max_size: usize) -> Result { + let mut num_removed = 0; + let conn = self.connection.get_pooled_connection()?; + let msg_count = stored_messages::table + .select(dsl::count(stored_messages::id)) + .first::(&conn)? as usize; + if msg_count > max_size { + let remove_count = msg_count - max_size; + let message_ids: Vec = stored_messages::table + .select(stored_messages::id) + .order_by(stored_messages::stored_at.asc()) + .limit(remove_count as i64) + .get_results(&conn)?; + num_removed = diesel::delete(stored_messages::table) + .filter(stored_messages::id.eq_any(message_ids)) + .execute(&conn)?; + } + Ok(num_removed) } } @@ -260,8 +230,8 @@ mod test { #[runtime::test] async fn insert_messages() { - let conn = DbConnection::connect_memory(random::string(8)).await.unwrap(); - conn.migrate().await.unwrap(); + let conn = DbConnection::connect_memory(random::string(8)).unwrap(); + conn.migrate().unwrap(); let db = StoreAndForwardDatabase::new(conn); let mut msg1 = NewStoredMessage::default(); msg1.body_hash.push('1'); @@ -269,10 +239,10 @@ mod test { msg2.body_hash.push('2'); let mut msg3 = NewStoredMessage::default(); msg3.body_hash.push('2'); // Duplicate message - db.insert_message_if_unique(msg1.clone()).await.unwrap(); - db.insert_message_if_unique(msg2.clone()).await.unwrap(); - db.insert_message_if_unique(msg3.clone()).await.unwrap(); - let messages = db.get_all_messages().await.unwrap(); + db.insert_message_if_unique(msg1.clone()).unwrap(); + db.insert_message_if_unique(msg2.clone()).unwrap(); + db.insert_message_if_unique(msg3.clone()).unwrap(); + let messages = db.get_all_messages().unwrap(); assert_eq!(messages.len(), 2); assert_eq!(messages[0].body_hash, msg1.body_hash); assert_eq!(messages[1].body_hash, msg2.body_hash); @@ -280,8 +250,8 @@ mod test { #[runtime::test] async fn remove_messages() { - let conn = DbConnection::connect_memory(random::string(8)).await.unwrap(); - conn.migrate().await.unwrap(); + let conn = DbConnection::connect_memory(random::string(8)).unwrap(); + conn.migrate().unwrap(); let db = StoreAndForwardDatabase::new(conn); // Create 3 unique messages let mut msg1 = NewStoredMessage::default(); @@ -290,25 +260,25 @@ mod test { msg2.body_hash.push('2'); let mut msg3 = NewStoredMessage::default(); msg3.body_hash.push('3'); - db.insert_message_if_unique(msg1.clone()).await.unwrap(); - db.insert_message_if_unique(msg2.clone()).await.unwrap(); - db.insert_message_if_unique(msg3.clone()).await.unwrap(); - let messages = db.get_all_messages().await.unwrap(); + db.insert_message_if_unique(msg1.clone()).unwrap(); + db.insert_message_if_unique(msg2.clone()).unwrap(); + db.insert_message_if_unique(msg3.clone()).unwrap(); + let messages = db.get_all_messages().unwrap(); assert_eq!(messages.len(), 3); let msg1_id = messages[0].id; let msg2_id = messages[1].id; let msg3_id = messages[2].id; - db.remove_message(vec![msg1_id, msg3_id]).await.unwrap(); - let messages = db.get_all_messages().await.unwrap(); + db.remove_message(vec![msg1_id, msg3_id]).unwrap(); + let messages = db.get_all_messages().unwrap(); assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, msg2_id); } #[runtime::test] async fn truncate_messages() { - let conn = DbConnection::connect_memory(random::string(8)).await.unwrap(); - conn.migrate().await.unwrap(); + let conn = DbConnection::connect_memory(random::string(8)).unwrap(); + conn.migrate().unwrap(); let db = StoreAndForwardDatabase::new(conn); let mut msg1 = NewStoredMessage::default(); msg1.body_hash.push('1'); @@ -318,13 +288,13 @@ mod test { msg3.body_hash.push('3'); let mut msg4 = NewStoredMessage::default(); msg4.body_hash.push('4'); - db.insert_message_if_unique(msg1.clone()).await.unwrap(); - db.insert_message_if_unique(msg2.clone()).await.unwrap(); - db.insert_message_if_unique(msg3.clone()).await.unwrap(); - db.insert_message_if_unique(msg4.clone()).await.unwrap(); - let num_removed = db.truncate_messages(2).await.unwrap(); + db.insert_message_if_unique(msg1.clone()).unwrap(); + db.insert_message_if_unique(msg2.clone()).unwrap(); + db.insert_message_if_unique(msg3.clone()).unwrap(); + db.insert_message_if_unique(msg4.clone()).unwrap(); + let num_removed = db.truncate_messages(2).unwrap(); assert_eq!(num_removed, 2); - let messages = db.get_all_messages().await.unwrap(); + let messages = db.get_all_messages().unwrap(); assert_eq!(messages.len(), 2); assert_eq!(messages[0].body_hash, msg3.body_hash); assert_eq!(messages[1].body_hash, msg4.body_hash); diff --git a/comms/dht/src/store_forward/service.rs b/comms/dht/src/store_forward/service.rs index 91bff5037c..3e2e26797b 100644 --- a/comms/dht/src/store_forward/service.rs +++ b/comms/dht/src/store_forward/service.rs @@ -243,7 +243,7 @@ impl StoreAndForwardService { }, _ = cleanup_ticker.tick() => { - if let Err(err) = self.cleanup().await { + if let Err(err) = self.cleanup() { error!(target: LOG_TARGET, "Error when performing store and forward cleanup: {:?}", err); } }, @@ -267,7 +267,7 @@ impl StoreAndForwardService { use StoreAndForwardRequest::*; trace!(target: LOG_TARGET, "Request: {:?}", request); match request { - FetchMessages(query, reply_tx) => match self.handle_fetch_message_query(query).await { + FetchMessages(query, reply_tx) => match self.handle_fetch_message_query(query) { Ok(messages) => { let _ = reply_tx.send(Ok(messages)); }, @@ -282,7 +282,7 @@ impl StoreAndForwardService { InsertMessage(msg, reply_tx) => { let public_key = msg.destination_pubkey.clone(); let node_id = msg.destination_node_id.clone(); - match self.database.insert_message_if_unique(msg).await { + match self.database.insert_message_if_unique(msg) { Ok(existed) => { let pub_key = public_key .map(|p| format!("public key '{}'", p)) @@ -301,7 +301,7 @@ impl StoreAndForwardService { }, } }, - RemoveMessages(message_ids) => match self.database.remove_message(message_ids.clone()).await { + RemoveMessages(message_ids) => match self.database.remove_message(message_ids.clone()) { Ok(_) => trace!(target: LOG_TARGET, "Removed messages: {:?}", message_ids), Err(err) => error!(target: LOG_TARGET, "RemoveMessage failed because '{:?}'", err), }, @@ -319,7 +319,7 @@ impl StoreAndForwardService { } }, RemoveMessagesOlderThan(threshold) => { - match self.database.delete_messages_older_than(threshold.naive_utc()).await { + match self.database.delete_messages_older_than(threshold.naive_utc()) { Ok(_) => trace!(target: LOG_TARGET, "Removed messages older than {}", threshold), Err(err) => error!(target: LOG_TARGET, "RemoveMessage failed because '{:?}'", err), } @@ -453,7 +453,7 @@ impl StoreAndForwardService { } } - async fn handle_fetch_message_query(&self, query: FetchStoredMessageQuery) -> SafResult> { + fn handle_fetch_message_query(&self, query: FetchStoredMessageQuery) -> SafResult> { use SafResponseType::*; let limit = i64::try_from(self.config.max_returned_messages) .ok() @@ -461,47 +461,34 @@ impl StoreAndForwardService { .unwrap(); let db = &self.database; let messages = match query.response_type { - ForMe => { - db.find_messages_for_peer(&query.public_key, &query.node_id, query.since, limit) - .await? - }, - Join => db.find_join_messages(query.since, limit).await?, + ForMe => db.find_messages_for_peer(&query.public_key, &query.node_id, query.since, limit)?, + Join => db.find_join_messages(query.since, limit)?, Discovery => { - db.find_messages_of_type_for_pubkey(&query.public_key, DhtMessageType::Discovery, query.since, limit) - .await? + db.find_messages_of_type_for_pubkey(&query.public_key, DhtMessageType::Discovery, query.since, limit)? }, - Anonymous => db.find_anonymous_messages(query.since, limit).await?, + Anonymous => db.find_anonymous_messages(query.since, limit)?, }; Ok(messages) } - async fn cleanup(&mut self) -> SafResult<()> { + fn cleanup(&mut self) -> SafResult<()> { self.local_state .garbage_collect(self.config.max_inflight_request_age * 2); - let num_removed = self - .database - .delete_messages_with_priority_older_than( - StoredMessagePriority::Low, - since(self.config.low_priority_msg_storage_ttl), - ) - .await?; + let num_removed = self.database.delete_messages_with_priority_older_than( + StoredMessagePriority::Low, + since(self.config.low_priority_msg_storage_ttl), + )?; debug!(target: LOG_TARGET, "Cleaned {} old low priority messages", num_removed); - let num_removed = self - .database - .delete_messages_with_priority_older_than( - StoredMessagePriority::High, - since(self.config.high_priority_msg_storage_ttl), - ) - .await?; + let num_removed = self.database.delete_messages_with_priority_older_than( + StoredMessagePriority::High, + since(self.config.high_priority_msg_storage_ttl), + )?; debug!(target: LOG_TARGET, "Cleaned {} old high priority messages", num_removed); - let num_removed = self - .database - .truncate_messages(self.config.msg_storage_capacity) - .await?; + let num_removed = self.database.truncate_messages(self.config.msg_storage_capacity)?; if num_removed > 0 { debug!( target: LOG_TARGET, From df1be7e4249d6e3e0701c837bedbdc2ad6c9ff65 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Fri, 26 Nov 2021 08:50:05 +0200 Subject: [PATCH 14/29] feat: only trigger UTXO scanning when a new block event is received (#3620) Description --- Previously the UTXO scanner task was triggered on an interval. This PR updates this process to rather be triggered only when an existing task is not running and new Block event is received from the wallet Base Node monitoring service. It is only when this event is received when we know there will be new block data to scan. A new event is added to the Base Node Monitoring Service that only fires when a new block is detected to be used in the above functionality The Utxo Scanner is also refactored into separate files. To help in the review I have highlighted the new code below An unrelated bug is also fixed with handling of the wallet public address in the Console Wallet that broke the wallet. How Has This Been Tested? --- manually and by CI --- applications/daily_tests/cron_jobs.js | 2 +- .../tari_console_wallet/src/init/mod.rs | 7 +- .../tari_console_wallet/src/recovery.rs | 2 +- .../src/ui/state/wallet_event_monitor.rs | 4 +- .../wallet/src/base_node_service/handle.rs | 1 + .../wallet/src/base_node_service/monitor.rs | 32 +- base_layer/wallet/src/config.rs | 3 - .../src/output_manager_service/service.rs | 1 + .../wallet/src/transaction_service/service.rs | 3 +- .../wallet/src/utxo_scanner_service/mod.rs | 47 +-- .../src/utxo_scanner_service/service.rs | 192 +++++++++++ ...{utxo_scanning.rs => utxo_scanner_task.rs} | 323 ++---------------- .../uxto_scanner_service_builder.rs | 143 ++++++++ base_layer/wallet/src/wallet.rs | 4 +- base_layer/wallet/tests/wallet/mod.rs | 2 - base_layer/wallet_ffi/src/lib.rs | 3 +- common/config/presets/console_wallet.toml | 3 - common/src/configuration/global.rs | 7 - common/src/configuration/utils.rs | 1 - integration_tests/helpers/config.js | 1 - 20 files changed, 411 insertions(+), 370 deletions(-) create mode 100644 base_layer/wallet/src/utxo_scanner_service/service.rs rename base_layer/wallet/src/utxo_scanner_service/{utxo_scanning.rs => utxo_scanner_task.rs} (67%) create mode 100644 base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs diff --git a/applications/daily_tests/cron_jobs.js b/applications/daily_tests/cron_jobs.js index 572163a552..d3041c6250 100644 --- a/applications/daily_tests/cron_jobs.js +++ b/applications/daily_tests/cron_jobs.js @@ -60,7 +60,7 @@ async function runWalletRecoveryTest(instances) { }); notify( - `🙌 Wallet (Pubkey: ${identity.public_key} ) recovered to a block height of ${numScanned}, completed in ${timeDiffMinutes} minutes (${scannedRate} blocks/min). ${recoveredAmount} µT recovered for ${instances} instance(s).` + `🙌 Wallet (Pubkey: ${identity.public_key} ) recovered scanned ${numScanned} UTXO's, completed in ${timeDiffMinutes} minutes (${scannedRate} UTXOs/min). ${recoveredAmount} µT recovered for ${instances} instance(s).` ); } catch (err) { console.error(err); diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index f974ed4b9f..74c9271c19 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -29,6 +29,7 @@ use rustyline::Editor; use tari_app_utilities::utilities::create_transport_type; use tari_common::{exit_codes::ExitCodes, ConfigBootstrap, GlobalConfig}; use tari_comms::{ + multiaddr::Multiaddr, peer_manager::{Peer, PeerFeatures}, types::CommsSecretKey, NodeIdentity, @@ -299,10 +300,7 @@ pub async fn init_wallet( ); let node_address = match wallet_db.get_node_address().await? { - None => config - .public_address - .clone() - .ok_or_else(|| ExitCodes::ConfigError("node public address error".to_string()))?, + None => config.public_address.clone().unwrap_or_else(Multiaddr::empty), Some(a) => a, }; @@ -403,7 +401,6 @@ pub async fn init_wallet( config.buffer_size_console_wallet, )), Some(config.buffer_rate_limit_console_wallet), - Some(config.scan_for_utxo_interval), Some(updater_config), config.autoupdate_check_interval, ); diff --git a/applications/tari_console_wallet/src/recovery.rs b/applications/tari_console_wallet/src/recovery.rs index 852a9b817f..ab49446bc7 100644 --- a/applications/tari_console_wallet/src/recovery.rs +++ b/applications/tari_console_wallet/src/recovery.rs @@ -30,7 +30,7 @@ use tari_key_manager::mnemonic::Mnemonic; use tari_shutdown::Shutdown; use tari_wallet::{ storage::sqlite_db::WalletSqliteDatabase, - utxo_scanner_service::{handle::UtxoScannerEvent, utxo_scanning::UtxoScannerService}, + utxo_scanner_service::{handle::UtxoScannerEvent, service::UtxoScannerService}, WalletSqlite, }; diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index c1b51207d0..b47b306e9c 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -180,10 +180,8 @@ impl WalletEventMonitor { match result { Ok(msg) => { trace!(target: LOG_TARGET, "Wallet Event Monitor received base node event {:?}", msg); - match (*msg).clone() { - BaseNodeEvent::BaseNodeStateChanged(state) => { + if let BaseNodeEvent::BaseNodeStateChanged(state) = (*msg).clone() { self.trigger_base_node_state_refresh(state).await; - } } }, Err(broadcast::error::RecvError::Lagged(n)) => { diff --git a/base_layer/wallet/src/base_node_service/handle.rs b/base_layer/wallet/src/base_node_service/handle.rs index 7e318b4a8b..80b09a2936 100644 --- a/base_layer/wallet/src/base_node_service/handle.rs +++ b/base_layer/wallet/src/base_node_service/handle.rs @@ -44,6 +44,7 @@ pub enum BaseNodeServiceResponse { #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum BaseNodeEvent { BaseNodeStateChanged(BaseNodeState), + NewBlockDetected(u64), } /// The Base Node Service Handle is a struct that contains the interfaces used to communicate with a running diff --git a/base_layer/wallet/src/base_node_service/monitor.rs b/base_layer/wallet/src/base_node_service/monitor.rs index b5ae89902b..a79d04428f 100644 --- a/base_layer/wallet/src/base_node_service/monitor.rs +++ b/base_layer/wallet/src/base_node_service/monitor.rs @@ -88,7 +88,7 @@ where }, Err(e @ BaseNodeMonitorError::RpcFailed(_)) => { warn!(target: LOG_TARGET, "Connectivity failure to base node: {}", e); - self.map_state(move |_| BaseNodeState { + self.update_state(BaseNodeState { chain_metadata: None, is_synced: None, updated: None, @@ -134,7 +134,7 @@ where let tip_info = match interrupt(base_node_watch.changed(), client.get_tip_info()).await { Some(tip_info) => tip_info?, None => { - self.map_state(|_| Default::default()).await; + self.update_state(Default::default()).await; continue; }, }; @@ -165,7 +165,8 @@ where let is_synced = tip_info.is_synced; let height_of_longest_chain = chain_metadata.height_of_longest_chain(); - self.map_state(move |_| BaseNodeState { + + self.update_state(BaseNodeState { chain_metadata: Some(chain_metadata), is_synced: Some(is_synced), updated: Some(Utc::now().naive_utc()), @@ -184,7 +185,7 @@ where let delay = time::sleep(self.interval.saturating_sub(latency)); if interrupt(base_node_watch.changed(), delay).await.is_none() { - self.map_state(|_| Default::default()).await; + self.update_state(Default::default()).await; } } @@ -193,14 +194,23 @@ where Ok(()) } - async fn map_state(&self, transform: F) - where F: FnOnce(&BaseNodeState) -> BaseNodeState { - let new_state = { - let mut lock = self.state.write().await; - let new_state = transform(&*lock); - *lock = new_state.clone(); - new_state + async fn update_state(&self, new_state: BaseNodeState) { + let mut lock = self.state.write().await; + let (new_block_detected, height) = match (new_state.chain_metadata.clone(), (*lock).chain_metadata.clone()) { + (Some(new_metadata), Some(old_metadata)) => ( + new_metadata.height_of_longest_chain() != old_metadata.height_of_longest_chain(), + new_metadata.height_of_longest_chain(), + ), + (Some(new_metadata), _) => (true, new_metadata.height_of_longest_chain()), + (None, _) => (false, 0), }; + + if new_block_detected { + self.publish_event(BaseNodeEvent::NewBlockDetected(height)); + } + + *lock = new_state.clone(); + self.publish_event(BaseNodeEvent::BaseNodeStateChanged(new_state)); } diff --git a/base_layer/wallet/src/config.rs b/base_layer/wallet/src/config.rs index e176836453..2dc56f7d1d 100644 --- a/base_layer/wallet/src/config.rs +++ b/base_layer/wallet/src/config.rs @@ -43,7 +43,6 @@ pub struct WalletConfig { pub rate_limit: usize, pub network: NetworkConsensus, pub base_node_service_config: BaseNodeServiceConfig, - pub scan_for_utxo_interval: Duration, pub updater_config: Option, pub autoupdate_check_interval: Option, } @@ -59,7 +58,6 @@ impl WalletConfig { base_node_service_config: Option, buffer_size: Option, rate_limit: Option, - scan_for_utxo_interval: Option, updater_config: Option, autoupdate_check_interval: Option, ) -> Self { @@ -72,7 +70,6 @@ impl WalletConfig { rate_limit: rate_limit.unwrap_or(50), network, base_node_service_config: base_node_service_config.unwrap_or_default(), - scan_for_utxo_interval: scan_for_utxo_interval.unwrap_or_else(|| Duration::from_secs(43200)), updater_config, autoupdate_check_interval, } diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 49ae3bf44b..d7b61cfbb2 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -383,6 +383,7 @@ where } self.last_seen_tip_height = state.chain_metadata.map(|cm| cm.height_of_longest_chain()); }, + BaseNodeEvent::NewBlockDetected(_) => {}, } } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 136bd198a8..6b00d1e0fe 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -49,7 +49,7 @@ use crate::{ }, types::HashDigest, util::watch::Watch, - utxo_scanner_service::utxo_scanning::RECOVERY_KEY, + utxo_scanner_service::RECOVERY_KEY, }; use chrono::{NaiveDateTime, Utc}; use digest::Digest; @@ -708,6 +708,7 @@ where } self.last_seen_tip_height = state.chain_metadata.map(|cm| cm.height_of_longest_chain()); }, + BaseNodeEvent::NewBlockDetected(_) => {}, } } diff --git a/base_layer/wallet/src/utxo_scanner_service/mod.rs b/base_layer/wallet/src/utxo_scanner_service/mod.rs index e8b9db5aea..c787e31f64 100644 --- a/base_layer/wallet/src/utxo_scanner_service/mod.rs +++ b/base_layer/wallet/src/utxo_scanner_service/mod.rs @@ -1,38 +1,18 @@ -// 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::{ + base_node_service::handle::BaseNodeServiceHandle, connectivity_service::{WalletConnectivityHandle, WalletConnectivityInterface}, output_manager_service::handle::OutputManagerHandle, storage::database::{WalletBackend, WalletDatabase}, transaction_service::handle::TransactionServiceHandle, utxo_scanner_service::{ handle::UtxoScannerHandle, - utxo_scanning::{UtxoScannerMode, UtxoScannerService}, + service::UtxoScannerService, + uxto_scanner_service_builder::UtxoScannerMode, }, }; use futures::future; use log::*; -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use tari_comms::{connectivity::ConnectivityRequester, NodeIdentity}; use tari_core::transactions::CryptoFactories; use tari_service_framework::{async_trait, ServiceInitializationError, ServiceInitializer, ServiceInitializerContext}; @@ -40,12 +20,15 @@ use tokio::sync::broadcast; pub mod error; pub mod handle; -pub mod utxo_scanning; +pub mod service; +mod utxo_scanner_task; +mod uxto_scanner_service_builder; + +pub use utxo_scanner_task::RECOVERY_KEY; const LOG_TARGET: &str = "wallet::utxo_scanner_service::initializer"; pub struct UtxoScannerServiceInitializer { - interval: Duration, backend: Option>, factories: CryptoFactories, node_identity: Arc, @@ -54,14 +37,8 @@ pub struct UtxoScannerServiceInitializer { impl UtxoScannerServiceInitializer where T: WalletBackend + 'static { - pub fn new( - interval: Duration, - backend: WalletDatabase, - factories: CryptoFactories, - node_identity: Arc, - ) -> Self { + pub fn new(backend: WalletDatabase, factories: CryptoFactories, node_identity: Arc) -> Self { Self { - interval, backend: Some(backend), factories, node_identity, @@ -87,7 +64,6 @@ where T: WalletBackend + 'static .take() .expect("Cannot start Utxo scanner service without setting a storage backend"); let factories = self.factories.clone(); - let interval = self.interval; let node_identity = self.node_identity.clone(); context.spawn_when_ready(move |handles| async move { @@ -95,11 +71,11 @@ where T: WalletBackend + 'static let output_manager_service = handles.expect_handle::(); let comms_connectivity = handles.expect_handle::(); let wallet_connectivity = handles.expect_handle::(); + let base_node_service_handle = handles.expect_handle::(); let scanning_service = UtxoScannerService::::builder() .with_peers(vec![]) .with_retry_limit(2) - .with_scanning_interval(interval) .with_mode(UtxoScannerMode::Scanning) .build_with_resources( backend, @@ -111,6 +87,7 @@ where T: WalletBackend + 'static factories, handles.get_shutdown_signal(), event_sender, + base_node_service_handle, ) .run(); diff --git a/base_layer/wallet/src/utxo_scanner_service/service.rs b/base_layer/wallet/src/utxo_scanner_service/service.rs new file mode 100644 index 0000000000..9be47f6546 --- /dev/null +++ b/base_layer/wallet/src/utxo_scanner_service/service.rs @@ -0,0 +1,192 @@ +// 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::{ + base_node_service::handle::BaseNodeServiceHandle, + error::WalletError, + output_manager_service::handle::OutputManagerHandle, + storage::database::{WalletBackend, WalletDatabase}, + transaction_service::handle::TransactionServiceHandle, + utxo_scanner_service::{ + handle::UtxoScannerEvent, + utxo_scanner_task::UtxoScannerTask, + uxto_scanner_service_builder::{UtxoScannerMode, UtxoScannerServiceBuilder}, + }, +}; + +use crate::base_node_service::handle::BaseNodeEvent; +use futures::FutureExt; +use log::*; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tari_common_types::types::HashOutput; +use tari_comms::{connectivity::ConnectivityRequester, peer_manager::Peer, types::CommsPublicKey, NodeIdentity}; +use tari_core::transactions::{tari_amount::MicroTari, CryptoFactories}; +use tari_shutdown::{Shutdown, ShutdownSignal}; +use tokio::{ + sync::{broadcast, watch}, + task, +}; + +pub const LOG_TARGET: &str = "wallet::utxo_scanning"; + +pub struct UtxoScannerService +where TBackend: WalletBackend + 'static +{ + pub(crate) resources: UtxoScannerResources, + pub(crate) retry_limit: usize, + pub(crate) peer_seeds: Vec, + pub(crate) mode: UtxoScannerMode, + pub(crate) shutdown_signal: ShutdownSignal, + pub(crate) event_sender: broadcast::Sender, + pub(crate) base_node_service: BaseNodeServiceHandle, +} + +impl UtxoScannerService +where TBackend: WalletBackend + 'static +{ + #[allow(clippy::too_many_arguments)] + pub fn new( + peer_seeds: Vec, + retry_limit: usize, + mode: UtxoScannerMode, + resources: UtxoScannerResources, + shutdown_signal: ShutdownSignal, + event_sender: broadcast::Sender, + base_node_service: BaseNodeServiceHandle, + ) -> Self { + Self { + resources, + peer_seeds, + retry_limit, + mode, + shutdown_signal, + event_sender, + base_node_service, + } + } + + fn create_task(&self, shutdown_signal: ShutdownSignal) -> UtxoScannerTask { + UtxoScannerTask { + resources: self.resources.clone(), + peer_seeds: self.peer_seeds.clone(), + event_sender: self.event_sender.clone(), + retry_limit: self.retry_limit, + peer_index: 0, + num_retries: 1, + mode: self.mode.clone(), + shutdown_signal, + } + } + + pub fn builder() -> UtxoScannerServiceBuilder { + UtxoScannerServiceBuilder::default() + } + + pub fn get_event_receiver(&mut self) -> broadcast::Receiver { + self.event_sender.subscribe() + } + + pub async fn run(mut self) -> Result<(), WalletError> { + info!(target: LOG_TARGET, "UTXO scanning service starting"); + + if self.mode == UtxoScannerMode::Recovery { + let task = self.create_task(self.shutdown_signal.clone()); + task::spawn(async move { + if let Err(err) = task.run().await { + error!(target: LOG_TARGET, "Error scanning UTXOs: {}", err); + } + }); + return Ok(()); + } + + let mut main_shutdown = self.shutdown_signal.clone(); + let mut base_node_service_event_stream = self.base_node_service.get_event_stream(); + + loop { + let mut local_shutdown = Shutdown::new(); + let task = self.create_task(local_shutdown.to_signal()); + let mut task_join_handle = task::spawn(async move { + if let Err(err) = task.run().await { + error!(target: LOG_TARGET, "Error scanning UTXOs: {}", err); + } + }) + .fuse(); + + loop { + tokio::select! { + event = base_node_service_event_stream.recv() => { + match event { + Ok(e) => { + if let BaseNodeEvent::NewBlockDetected(h) = (*e).clone() { + debug!(target: LOG_TARGET, "New block event received: {}", h); + if local_shutdown.is_triggered() { + debug!(target: LOG_TARGET, "Starting new round of UTXO scanning"); + break; + } + } + }, + Err(e) => debug!(target: LOG_TARGET, "Lagging read on base node event broadcast channel: {}", e), + }; + }, + _ = &mut task_join_handle => { + debug!(target: LOG_TARGET, "UTXO scanning round completed"); + local_shutdown.trigger(); + } + _ = self.resources.current_base_node_watcher.changed() => { + debug!(target: LOG_TARGET, "Base node change detected."); + let peer = self.resources.current_base_node_watcher.borrow().as_ref().cloned(); + if let Some(peer) = peer { + self.peer_seeds = vec![peer.public_key]; + } + local_shutdown.trigger(); + }, + _ = main_shutdown.wait() => { + // this will stop the task if its running, and let that thread exit gracefully + local_shutdown.trigger(); + info!(target: LOG_TARGET, "UTXO scanning service shutting down because it received the shutdown signal"); + return Ok(()); + } + } + } + } + } +} + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct ScanningMetadata { + pub total_amount: MicroTari, + pub number_of_utxos: u64, + pub utxo_index: u64, + pub height_hash: HashOutput, +} + +#[derive(Clone)] +pub struct UtxoScannerResources { + pub db: WalletDatabase, + pub comms_connectivity: ConnectivityRequester, + pub current_base_node_watcher: watch::Receiver>, + pub output_manager_service: OutputManagerHandle, + pub transaction_service: TransactionServiceHandle, + pub node_identity: Arc, + pub factories: CryptoFactories, +} diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs similarity index 67% rename from base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs rename to base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index b00ebc839d..e6c1283641 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanning.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -20,33 +20,19 @@ // 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, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - time::{Duration, Instant}, -}; - use chrono::Utc; use futures::StreamExt; use log::*; -use serde::{Deserialize, Serialize}; -use tokio::{ - sync::{broadcast, watch}, - task, - time, - time::MissedTickBehavior, -}; -use tari_common_types::{transaction::TxId, types::HashOutput}; +use std::{ + convert::TryFrom, + time::{Duration, Instant}, +}; +use tari_common_types::transaction::TxId; use tari_comms::{ - connectivity::ConnectivityRequester, - peer_manager::{NodeId, Peer}, + peer_manager::NodeId, protocol::rpc::{RpcError, RpcStatus}, types::CommsPublicKey, - NodeIdentity, PeerConnection, }; use tari_core::{ @@ -59,22 +45,20 @@ use tari_core::{ transactions::{ tari_amount::MicroTari, transaction_entities::{TransactionOutput, UnblindedOutput}, - CryptoFactories, }, }; use tari_shutdown::ShutdownSignal; +use tokio::sync::broadcast; use crate::{ - connectivity_service::WalletConnectivityInterface, error::WalletError, - output_manager_service::handle::OutputManagerHandle, - storage::{ - database::{WalletBackend, WalletDatabase}, - sqlite_db::WalletSqliteDatabase, + storage::database::WalletBackend, + utxo_scanner_service::{ + error::UtxoScannerError, + handle::UtxoScannerEvent, + service::{ScanningMetadata, UtxoScannerResources}, + uxto_scanner_service_builder::UtxoScannerMode, }, - transaction_service::handle::TransactionServiceHandle, - utxo_scanner_service::{error::UtxoScannerError, handle::UtxoScannerEvent}, - WalletSqlite, }; pub const LOG_TARGET: &str = "wallet::utxo_scanning"; @@ -82,139 +66,17 @@ pub const LOG_TARGET: &str = "wallet::utxo_scanning"; pub const RECOVERY_KEY: &str = "recovery_data"; const SCANNING_KEY: &str = "scanning_data"; -#[derive(Debug, Clone, PartialEq)] -pub enum UtxoScannerMode { - Recovery, - Scanning, -} - -impl Default for UtxoScannerMode { - fn default() -> UtxoScannerMode { - UtxoScannerMode::Recovery - } -} - -#[derive(Debug, Default, Clone)] -pub struct UtxoScannerServiceBuilder { - retry_limit: usize, - peers: Vec, - mode: Option, - scanning_interval: Option, -} - -#[derive(Clone)] -struct UtxoScannerResources { - pub db: WalletDatabase, - pub comms_connectivity: ConnectivityRequester, - pub current_base_node_watcher: watch::Receiver>, - pub output_manager_service: OutputManagerHandle, - pub transaction_service: TransactionServiceHandle, - pub node_identity: Arc, - pub factories: CryptoFactories, -} - -impl UtxoScannerServiceBuilder { - /// Set the maximum number of times we retry recovery. A failed recovery is counted as _all_ peers have failed. - /// i.e. worst-case number of recovery attempts = number of sync peers * retry limit - pub fn with_retry_limit(&mut self, limit: usize) -> &mut Self { - self.retry_limit = limit; - self - } - - pub fn with_scanning_interval(&mut self, interval: Duration) -> &mut Self { - self.scanning_interval = Some(interval); - self - } - - pub fn with_peers(&mut self, peer_public_keys: Vec) -> &mut Self { - self.peers = peer_public_keys; - self - } - - pub fn with_mode(&mut self, mode: UtxoScannerMode) -> &mut Self { - self.mode = Some(mode); - self - } - - pub fn build_with_wallet( - &mut self, - wallet: &WalletSqlite, - shutdown_signal: ShutdownSignal, - ) -> UtxoScannerService { - let resources = UtxoScannerResources { - db: wallet.db.clone(), - comms_connectivity: wallet.comms.connectivity(), - current_base_node_watcher: wallet.wallet_connectivity.get_current_base_node_watcher(), - output_manager_service: wallet.output_manager_service.clone(), - transaction_service: wallet.transaction_service.clone(), - node_identity: wallet.comms.node_identity(), - factories: wallet.factories.clone(), - }; - - let (event_sender, _) = broadcast::channel(200); - - let interval = self - .scanning_interval - .unwrap_or_else(|| Duration::from_secs(60 * 60 * 12)); - UtxoScannerService::new( - self.peers.drain(..).collect(), - self.retry_limit, - self.mode.clone().unwrap_or_default(), - resources, - interval, - shutdown_signal, - event_sender, - ) - } - - #[allow(clippy::too_many_arguments)] - pub fn build_with_resources( - &mut self, - db: WalletDatabase, - comms_connectivity: ConnectivityRequester, - base_node_watcher: watch::Receiver>, - output_manager_service: OutputManagerHandle, - transaction_service: TransactionServiceHandle, - node_identity: Arc, - factories: CryptoFactories, - shutdown_signal: ShutdownSignal, - event_sender: broadcast::Sender, - ) -> UtxoScannerService { - let resources = UtxoScannerResources { - db, - comms_connectivity, - current_base_node_watcher: base_node_watcher, - output_manager_service, - transaction_service, - node_identity, - factories, - }; - let interval = self - .scanning_interval - .unwrap_or_else(|| Duration::from_secs(60 * 60 * 12)); - UtxoScannerService::new( - self.peers.drain(..).collect(), - self.retry_limit, - self.mode.clone().unwrap_or_default(), - resources, - interval, - shutdown_signal, - event_sender, - ) - } -} - -struct UtxoScannerTask +pub struct UtxoScannerTask where TBackend: WalletBackend + 'static { - resources: UtxoScannerResources, - event_sender: broadcast::Sender, - retry_limit: usize, - num_retries: usize, - peer_seeds: Vec, - peer_index: usize, - mode: UtxoScannerMode, - run_flag: Arc, + pub(crate) resources: UtxoScannerResources, + pub(crate) event_sender: broadcast::Sender, + pub(crate) retry_limit: usize, + pub(crate) num_retries: usize, + pub(crate) peer_seeds: Vec, + pub(crate) peer_index: usize, + pub(crate) mode: UtxoScannerMode, + pub(crate) shutdown_signal: ShutdownSignal, } impl UtxoScannerTask where TBackend: WalletBackend + 'static @@ -284,7 +146,7 @@ where TBackend: WalletBackend + 'static let start_index = self.get_start_utxo_mmr_pos(&mut client).await?; let tip_header = self.get_chain_tip_header(&mut client).await?; let output_mmr_size = tip_header.output_mmr_size; - if !self.run_flag.load(Ordering::Relaxed) { + if self.shutdown_signal.is_triggered() { // if running is set to false, we know its been canceled upstream so lets exit the loop return Ok((total_scanned, start_index, timer.elapsed())); } @@ -404,7 +266,7 @@ where TBackend: WalletBackend + 'static let mut last_utxo_index = 0u64; let mut iteration_count = 0u64; while let Some(response) = utxo_stream.next().await { - if !self.run_flag.load(Ordering::Relaxed) { + if self.shutdown_signal.is_triggered() { // if running is set to false, we know its been canceled upstream so lets exit the loop return Ok(total_scanned as u64); } @@ -581,10 +443,9 @@ where TBackend: WalletBackend + 'static Ok(tx_id) } - async fn run(mut self) -> Result<(), UtxoScannerError> { - self.run_flag.store(true, Ordering::Relaxed); + pub async fn run(mut self) -> Result<(), UtxoScannerError> { loop { - if !self.run_flag.load(Ordering::Relaxed) { + if self.shutdown_signal.is_triggered() { // if running is set to false, we know its been canceled upstream so lets exit the loop return Ok(()); } @@ -672,118 +533,6 @@ where TBackend: WalletBackend + 'static } } -pub struct UtxoScannerService -where TBackend: WalletBackend + 'static -{ - resources: UtxoScannerResources, - retry_limit: usize, - peer_seeds: Vec, - mode: UtxoScannerMode, - is_running: Arc, - scan_for_utxo_interval: Duration, - shutdown_signal: ShutdownSignal, - event_sender: broadcast::Sender, -} - -impl UtxoScannerService -where TBackend: WalletBackend + 'static -{ - #[allow(clippy::too_many_arguments)] - fn new( - peer_seeds: Vec, - retry_limit: usize, - mode: UtxoScannerMode, - resources: UtxoScannerResources, - scan_for_utxo_interval: Duration, - shutdown_signal: ShutdownSignal, - event_sender: broadcast::Sender, - ) -> Self { - Self { - resources, - peer_seeds, - retry_limit, - mode, - is_running: Arc::new(AtomicBool::new(false)), - scan_for_utxo_interval, - shutdown_signal, - event_sender, - } - } - - fn create_task(&self) -> UtxoScannerTask { - UtxoScannerTask { - resources: self.resources.clone(), - peer_seeds: self.peer_seeds.clone(), - event_sender: self.event_sender.clone(), - retry_limit: self.retry_limit, - peer_index: 0, - num_retries: 1, - mode: self.mode.clone(), - run_flag: self.is_running.clone(), - } - } - - pub fn builder() -> UtxoScannerServiceBuilder { - UtxoScannerServiceBuilder::default() - } - - pub fn get_event_receiver(&mut self) -> broadcast::Receiver { - self.event_sender.subscribe() - } - - pub async fn run(mut self) -> Result<(), WalletError> { - info!( - target: LOG_TARGET, - "UTXO scanning service starting (interval = {:.2?})", self.scan_for_utxo_interval - ); - - let mut shutdown = self.shutdown_signal.clone(); - let start_at = Instant::now() + Duration::from_secs(1); - let mut work_interval = time::interval_at(start_at.into(), self.scan_for_utxo_interval); - work_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); - loop { - tokio::select! { - _ = work_interval.tick() => { - let running_flag = self.is_running.clone(); - if !running_flag.load(Ordering::SeqCst) { - let task = self.create_task(); - debug!(target: LOG_TARGET, "UTXO scanning service starting scan for utxos"); - task::spawn(async move { - if let Err(err) = task.run().await { - error!(target: LOG_TARGET, "Error scanning UTXOs: {}", err); - } - //we make sure the flag is set to false here - running_flag.store(false, Ordering::Relaxed); - }); - if self.mode == UtxoScannerMode::Recovery { - return Ok(()); - } - } - }, - _ = self.resources.current_base_node_watcher.changed() => { - debug!(target: LOG_TARGET, "Base node change detected."); - let peer = self.resources.current_base_node_watcher.borrow().as_ref().cloned(); - - // If we are recovering we will stick to the initially provided seeds - if self.mode != UtxoScannerMode::Recovery { - if let Some(peer) = peer { - self.peer_seeds = vec![peer.public_key]; - } - } - - self.is_running.store(false, Ordering::Relaxed); - }, - _ = shutdown.wait() => { - // this will stop the task if its running, and let that thread exit gracefully - self.is_running.store(false, Ordering::Relaxed); - info!(target: LOG_TARGET, "UTXO scanning service shutting down because it received the shutdown signal"); - return Ok(()); - } - } - } - } -} - fn convert_response_to_transaction_outputs( response: Vec>, last_utxo_index: u64, @@ -794,12 +543,12 @@ fn convert_response_to_transaction_outputs( .collect::, _>>()?; let current_utxo_index = response - // Assumes correct ordering which is otherwise not required for this protocol - .last() - .ok_or_else(|| { - UtxoScannerError::BaseNodeResponseError("Invalid response from base node: response was empty".to_string()) - })? - .mmr_index; + // Assumes correct ordering which is otherwise not required for this protocol + .last() + .ok_or_else(|| { + UtxoScannerError::BaseNodeResponseError("Invalid response from base node: response was empty".to_string()) + })? + .mmr_index; if current_utxo_index < last_utxo_index { return Err(UtxoScannerError::BaseNodeResponseError( "Invalid response from base node: mmr index must be non-decreasing".to_string(), @@ -817,11 +566,3 @@ fn convert_response_to_transaction_outputs( .collect::, _>>()?; Ok((outputs, current_utxo_index)) } - -#[derive(Clone, Default, Serialize, Deserialize)] -struct ScanningMetadata { - pub total_amount: MicroTari, - pub number_of_utxos: u64, - pub utxo_index: u64, - pub height_hash: HashOutput, -} diff --git a/base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs b/base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs new file mode 100644 index 0000000000..e57c461b8b --- /dev/null +++ b/base_layer/wallet/src/utxo_scanner_service/uxto_scanner_service_builder.rs @@ -0,0 +1,143 @@ +// 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::{ + base_node_service::handle::BaseNodeServiceHandle, + connectivity_service::WalletConnectivityInterface, + output_manager_service::handle::OutputManagerHandle, + storage::{ + database::{WalletBackend, WalletDatabase}, + sqlite_db::WalletSqliteDatabase, + }, + transaction_service::handle::TransactionServiceHandle, + utxo_scanner_service::{ + handle::UtxoScannerEvent, + service::{UtxoScannerResources, UtxoScannerService}, + }, + WalletSqlite, +}; +use std::sync::Arc; +use tari_comms::{connectivity::ConnectivityRequester, peer_manager::Peer, types::CommsPublicKey, NodeIdentity}; +use tari_core::transactions::CryptoFactories; +use tari_shutdown::ShutdownSignal; +use tokio::sync::{broadcast, watch}; + +#[derive(Debug, Clone, PartialEq)] +pub enum UtxoScannerMode { + Recovery, + Scanning, +} + +impl Default for UtxoScannerMode { + fn default() -> UtxoScannerMode { + UtxoScannerMode::Recovery + } +} + +#[derive(Debug, Default, Clone)] +pub struct UtxoScannerServiceBuilder { + retry_limit: usize, + peers: Vec, + mode: Option, +} + +impl UtxoScannerServiceBuilder { + /// Set the maximum number of times we retry recovery. A failed recovery is counted as _all_ peers have failed. + /// i.e. worst-case number of recovery attempts = number of sync peers * retry limit + pub fn with_retry_limit(&mut self, limit: usize) -> &mut Self { + self.retry_limit = limit; + self + } + + pub fn with_peers(&mut self, peer_public_keys: Vec) -> &mut Self { + self.peers = peer_public_keys; + self + } + + pub fn with_mode(&mut self, mode: UtxoScannerMode) -> &mut Self { + self.mode = Some(mode); + self + } + + pub fn build_with_wallet( + &mut self, + wallet: &WalletSqlite, + shutdown_signal: ShutdownSignal, + ) -> UtxoScannerService { + let resources = UtxoScannerResources { + db: wallet.db.clone(), + comms_connectivity: wallet.comms.connectivity(), + current_base_node_watcher: wallet.wallet_connectivity.get_current_base_node_watcher(), + output_manager_service: wallet.output_manager_service.clone(), + transaction_service: wallet.transaction_service.clone(), + node_identity: wallet.comms.node_identity(), + factories: wallet.factories.clone(), + }; + + let (event_sender, _) = broadcast::channel(200); + + UtxoScannerService::new( + self.peers.drain(..).collect(), + self.retry_limit, + self.mode.clone().unwrap_or_default(), + resources, + shutdown_signal, + event_sender, + wallet.base_node_service.clone(), + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn build_with_resources( + &mut self, + db: WalletDatabase, + comms_connectivity: ConnectivityRequester, + base_node_watcher: watch::Receiver>, + output_manager_service: OutputManagerHandle, + transaction_service: TransactionServiceHandle, + node_identity: Arc, + factories: CryptoFactories, + shutdown_signal: ShutdownSignal, + event_sender: broadcast::Sender, + base_node_service: BaseNodeServiceHandle, + ) -> UtxoScannerService { + let resources = UtxoScannerResources { + db, + comms_connectivity, + current_base_node_watcher: base_node_watcher, + output_manager_service, + transaction_service, + node_identity, + factories, + }; + + UtxoScannerService::new( + self.peers.drain(..).collect(), + self.retry_limit, + self.mode.clone().unwrap_or_default(), + resources, + shutdown_signal, + event_sender, + base_node_service, + ) + } +} diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 169dfc2703..35d926a3c9 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -80,7 +80,7 @@ use crate::{ TransactionServiceInitializer, }, types::KeyDigest, - utxo_scanner_service::{handle::UtxoScannerHandle, UtxoScannerServiceInitializer}, + utxo_scanner_service::{handle::UtxoScannerHandle, UtxoScannerServiceInitializer, RECOVERY_KEY}, }; use tari_common_types::transaction::TxId; use tari_key_manager::cipher_seed::CipherSeed; @@ -188,7 +188,6 @@ where )) .add_initializer(WalletConnectivityInitializer::new(config.base_node_service_config)) .add_initializer(UtxoScannerServiceInitializer::new( - config.scan_for_utxo_interval, wallet_database.clone(), factories.clone(), node_identity.clone(), @@ -498,7 +497,6 @@ where /// Utility function to find out if there is data in the database indicating that there is an incomplete recovery /// process in progress pub async fn is_recovery_in_progress(&self) -> Result { - use crate::utxo_scanner_service::utxo_scanning::RECOVERY_KEY; Ok(self.db.get_client_key_value(RECOVERY_KEY.to_string()).await?.is_some()) } } diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index c33481f3c2..7a16a844ca 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -154,7 +154,6 @@ async fn create_wallet( None, None, None, - None, ); let metadata = ChainMetadata::new(std::i64::MAX as u64, Vec::new(), 0, 0, 0); @@ -704,7 +703,6 @@ async fn test_import_utxo() { None, None, None, - None, ); let mut alice_wallet = Wallet::start( config, diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 566ec880d4..ad0153f422 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -153,7 +153,7 @@ use tari_wallet::{ models::{CompletedTransaction, InboundTransaction, OutboundTransaction}, }, }, - utxo_scanner_service::utxo_scanning::{UtxoScannerService, RECOVERY_KEY}, + utxo_scanner_service::{service::UtxoScannerService, RECOVERY_KEY}, Wallet, WalletConfig, WalletSqlite, @@ -3217,7 +3217,6 @@ pub unsafe extern "C" fn wallet_create( None, None, None, - None, ); let mut recovery_lookup = match runtime.block_on(wallet_database.get_client_key_value(RECOVERY_KEY.to_owned())) { diff --git a/common/config/presets/console_wallet.toml b/common/config/presets/console_wallet.toml index 957fceece1..ea7a7a4636 100644 --- a/common/config/presets/console_wallet.toml +++ b/common/config/presets/console_wallet.toml @@ -69,9 +69,6 @@ base_node_update_publisher_channel_size = 500 # (options: "DirectOnly", "StoreAndForwardOnly", DirectAndStoreAndForward". default: "DirectAndStoreAndForward"). #transaction_routing_mechanism = "DirectAndStoreAndForward" -# UTXO scanning service interval (default = 12 hours, i.e. 60 * 60 * 12 seconds) -scan_for_utxo_interval = 180 - # When running the console wallet in command mode, use these values to determine what "stage" and timeout to wait # for sent transactions. # The stages are: diff --git a/common/src/configuration/global.rs b/common/src/configuration/global.rs index 04086be1f0..34ba629e92 100644 --- a/common/src/configuration/global.rs +++ b/common/src/configuration/global.rs @@ -101,7 +101,6 @@ pub struct GlobalConfig { pub fetch_utxos_timeout: Duration, pub service_request_timeout: Duration, pub base_node_query_timeout: Duration, - pub scan_for_utxo_interval: Duration, pub saf_expiry_duration: Duration, pub transaction_broadcast_monitoring_timeout: Duration, pub transaction_chain_monitoring_timeout: Duration, @@ -445,11 +444,6 @@ fn convert_node_config( cfg.get_int(key) .map_err(|e| ConfigurationError::new(key, &e.to_string()))? as u64, ); - let key = "wallet.scan_for_utxo_interval"; - let scan_for_utxo_interval = Duration::from_secs( - cfg.get_int(key) - .map_err(|e| ConfigurationError::new(key, &e.to_string()))? as u64, - ); let key = "wallet.saf_expiry_duration"; let saf_expiry_duration = Duration::from_secs(optional(cfg.get_int(key))?.unwrap_or(10800) as u64); @@ -755,7 +749,6 @@ fn convert_node_config( fetch_utxos_timeout, service_request_timeout, base_node_query_timeout, - scan_for_utxo_interval, saf_expiry_duration, transaction_broadcast_monitoring_timeout, transaction_chain_monitoring_timeout, diff --git a/common/src/configuration/utils.rs b/common/src/configuration/utils.rs index 7b0f2c4cf0..78aaa5c73e 100644 --- a/common/src/configuration/utils.rs +++ b/common/src/configuration/utils.rs @@ -118,7 +118,6 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("wallet.base_node_service_refresh_interval", 5).unwrap(); cfg.set_default("wallet.base_node_service_request_max_age", 60).unwrap(); cfg.set_default("wallet.balance_enquiry_cooldown_period", 1).unwrap(); - cfg.set_default("wallet.scan_for_utxo_interval", 60 * 60 * 12).unwrap(); cfg.set_default("wallet.transaction_broadcast_monitoring_timeout", 60) .unwrap(); cfg.set_default("wallet.transaction_chain_monitoring_timeout", 60) diff --git a/integration_tests/helpers/config.js b/integration_tests/helpers/config.js index a022190d3f..23653699b8 100644 --- a/integration_tests/helpers/config.js +++ b/integration_tests/helpers/config.js @@ -102,7 +102,6 @@ function baseEnvs(peerSeeds = [], forceSyncPeers = []) { TARI_MINING_NODE__NUM_MINING_THREADS: "1", TARI_MINING_NODE__MINE_ON_TIP_ONLY: true, TARI_MINING_NODE__VALIDATE_TIP_TIMEOUT_SEC: 1, - TARI_WALLET__SCAN_FOR_UTXO_INTERVAL: 5, }; if (forceSyncPeers.length > 0) { envs.TARI_BASE_NODE__LOCALNET__FORCE_SYNC_PEERS = forceSyncPeers.join(","); From 1003f918c5f7b46a94cb07620b82afc411917a05 Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Fri, 26 Nov 2021 09:50:13 +0200 Subject: [PATCH 15/29] feat!: add tcp bypass settings for tor in wallet_ffi (#3615) Description --- This PR allows the TCP bypass settings for Tor to be set via wallet_ffi instead of being hard-coded. Motivation and Context --- As above. How Has This Been Tested? --- cargo test --all nvm use 12.22.6 && node_modules/.bin/cucumber-js --profile "ci" --tags "not @long-running and not @broken and @wallet-ffi" --- base_layer/wallet_ffi/src/lib.rs | 35 ++++++++++++++----- base_layer/wallet_ffi/wallet.h | 1 + comms/dht/src/macros.rs | 1 + comms/src/tor/hidden_service/proxy_opts.rs | 1 + integration_tests/helpers/ffi/ffiInterface.js | 2 ++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index ad0153f422..26ec4fdbc4 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -2576,7 +2576,8 @@ pub unsafe extern "C" fn transport_tcp_create( /// `control_server_address` - The pointer to a char array /// `tor_cookie` - The pointer to a ByteVector containing the contents of the tor cookie file, can be null /// `tor_port` - The tor port -/// `socks_username` - The pointer to a char array containing the socks username, can be null +/// `tor_proxy_bypass_for_outbound` - Whether tor will use a direct tcp connection for a given bypass address instead of +/// the tor proxy if tcp is available, if not it has no effect /// `socks_password` - The pointer to a char array containing the socks password, can be null /// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions /// as an out parameter. @@ -2592,6 +2593,7 @@ pub unsafe extern "C" fn transport_tor_create( control_server_address: *const c_char, tor_cookie: *const ByteVector, tor_port: c_ushort, + tor_proxy_bypass_for_outbound: bool, socks_username: *const c_char, socks_password: *const c_char, error_out: *mut c_int, @@ -2665,8 +2667,7 @@ pub unsafe extern "C" fn transport_tor_create( socks_address_override: None, socks_auth: authentication, tor_proxy_bypass_addresses: vec![], - // Prefer performance - tor_proxy_bypass_for_outbound_tcp: true, + tor_proxy_bypass_for_outbound_tcp: tor_proxy_bypass_for_outbound, }; let transport = TariTransportType::Tor(tor_config); @@ -5974,6 +5975,7 @@ mod test { let transport = transport_memory_create(); let _address = transport_memory_get_address(transport, error_ptr); assert_eq!(error, 0); + transport_type_destroy(transport); } } @@ -5984,8 +5986,9 @@ mod test { let error_ptr = &mut error as *mut c_int; let address_listener = CString::new("/ip4/127.0.0.1/tcp/0").unwrap(); let address_listener_str: *const c_char = CString::into_raw(address_listener) as *const c_char; - let _transport = transport_tcp_create(address_listener_str, error_ptr); + let transport = transport_tcp_create(address_listener_str, error_ptr); assert_eq!(error, 0); + transport_type_destroy(transport); } } @@ -5995,16 +5998,32 @@ mod test { let mut error = 0; let error_ptr = &mut error as *mut c_int; let address_control = CString::new("/ip4/127.0.0.1/tcp/8080").unwrap(); + let mut bypass = false; let address_control_str: *const c_char = CString::into_raw(address_control) as *const c_char; - let _transport = transport_tor_create( + let mut transport = transport_tor_create( address_control_str, - ptr::null_mut(), + ptr::null(), + 8080, + bypass, + ptr::null(), + ptr::null(), + error_ptr, + ); + assert_eq!(error, 0); + transport_type_destroy(transport); + + bypass = true; + transport = transport_tor_create( + address_control_str, + ptr::null(), 8080, - ptr::null_mut(), - ptr::null_mut(), + bypass, + ptr::null(), + ptr::null(), error_ptr, ); assert_eq!(error, 0); + transport_type_destroy(transport); } } diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index d7266603e2..661d861103 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -90,6 +90,7 @@ struct TariTransportType *transport_tor_create( const char *control_server_address, struct ByteVector *tor_cookie, unsigned short tor_port, + bool tor_proxy_bypass_for_outbound, const char *socks_username, const char *socks_password, int *error_out); diff --git a/comms/dht/src/macros.rs b/comms/dht/src/macros.rs index 3911e96e99..a4303dd37d 100644 --- a/comms/dht/src/macros.rs +++ b/comms/dht/src/macros.rs @@ -23,6 +23,7 @@ /// This macro unlocks a Mutex or RwLock. If the lock is /// poisoned (i.e. panic while unlocked) the last value /// before the panic is used. +#[allow(unused_macros)] macro_rules! acquire_lock { ($e:expr, $m:ident) => { match $e.$m() { diff --git a/comms/src/tor/hidden_service/proxy_opts.rs b/comms/src/tor/hidden_service/proxy_opts.rs index bc71c449df..ed252f0124 100644 --- a/comms/src/tor/hidden_service/proxy_opts.rs +++ b/comms/src/tor/hidden_service/proxy_opts.rs @@ -63,6 +63,7 @@ fn is_tcp_address(addr: &Multiaddr) -> bool { let protocol = iter.next(); matches!(protocol, Some(Tcp(_))) } + #[cfg(test)] mod test { use super::*; diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index 607252a07e..9507ad18f9 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -86,6 +86,7 @@ class InterfaceFFI { this.string, this.ptr, this.ushort, + this.bool, this.string, this.string, this.intPtr, @@ -608,6 +609,7 @@ class InterfaceFFI { control_server_address, tor_cookie, tor_port, + true, socks_username, socks_password, error From 086cd0c30116c9d6f8d7140716181b3a9360242a Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Fri, 26 Nov 2021 09:46:25 +0100 Subject: [PATCH 16/29] chore: change status line (#3610) Description --- Added NodeId to status line. And status line is printed with more info (currently RandomX) every 2 minutes. How Has This Been Tested? --- Manually. --- .../tari_base_node/src/command_handler.rs | 30 ++-- applications/tari_base_node/src/main.rs | 18 ++- applications/tari_base_node/src/parser.rs | 134 +++++++++--------- .../states/events_and_states.rs | 41 +++--- .../horizon_state_synchronization.rs | 8 +- 5 files changed, 129 insertions(+), 102 deletions(-) diff --git a/applications/tari_base_node/src/command_handler.rs b/applications/tari_base_node/src/command_handler.rs index 5037f5555b..82c4d42e84 100644 --- a/applications/tari_base_node/src/command_handler.rs +++ b/applications/tari_base_node/src/command_handler.rs @@ -90,6 +90,7 @@ pub struct CommandHandler { mempool_service: LocalMempoolService, state_machine_info: watch::Receiver, software_updater: SoftwareUpdaterHandle, + last_time_full: Instant, } impl CommandHandler { @@ -110,10 +111,11 @@ impl CommandHandler { mempool_service: ctx.local_mempool(), state_machine_info: ctx.get_state_machine_info_channel(), software_updater: ctx.software_updater(), + last_time_full: Instant::now(), } } - pub fn status(&self, output: StatusOutput) { + pub fn status(&mut self, output: StatusOutput) { let state_info = self.state_machine_info.clone(); let mut node = self.node_service.clone(); let mut mempool = self.mempool_service.clone(); @@ -123,12 +125,17 @@ impl CommandHandler { let mut rpc_server = self.rpc_server.clone(); let config = self.config.clone(); let consensus_rules = self.consensus_rules.clone(); + let mut full_log = false; + if self.last_time_full.elapsed() > Duration::from_secs(120) { + self.last_time_full = Instant::now(); + full_log = true; + } self.executor.spawn(async move { let mut status_line = StatusLine::new(); status_line.add_field("", format!("v{}", consts::APP_VERSION_NUMBER)); status_line.add_field("", config.network); - status_line.add_field("State", state_info.borrow().state_info.short_desc()); + status_line.add_field("State", state_info.borrow().state_info.short_desc(full_log)); let metadata = node.get_metadata().await.unwrap(); let height = metadata.height_of_longest_chain(); @@ -183,15 +190,16 @@ impl CommandHandler { .unwrap_or_else(|| "∞".to_string()), ), ); - - status_line.add_field( - "RandomX", - format!( - "#{} with flags {:?}", - state_info.borrow().randomx_vm_cnt, - state_info.borrow().randomx_vm_flags - ), - ); + if full_log { + status_line.add_field( + "RandomX", + format!( + "#{} with flags {:?}", + state_info.borrow().randomx_vm_cnt, + state_info.borrow().randomx_vm_flags + ), + ); + } let target = "base_node::app::status"; match output { diff --git a/applications/tari_base_node/src/main.rs b/applications/tari_base_node/src/main.rs index e5817d1268..c7ef20e702 100644 --- a/applications/tari_base_node/src/main.rs +++ b/applications/tari_base_node/src/main.rs @@ -127,6 +127,7 @@ use tari_core::chain_storage::ChainStorageError; use tari_shutdown::{Shutdown, ShutdownSignal}; use tokio::{ runtime, + sync::Mutex, task, time::{self}, }; @@ -258,7 +259,7 @@ async fn run_node(node_config: Arc, bootstrap: ConfigBootstrap) -> } // Run, node, run! - let command_handler = Arc::new(CommandHandler::new(runtime::Handle::current(), &ctx)); + let command_handler = Arc::new(Mutex::new(CommandHandler::new(runtime::Handle::current(), &ctx))); if bootstrap.non_interactive_mode { task::spawn(status_loop(command_handler, shutdown)); println!("Node started in non-interactive mode (pid = {})", process::id()); @@ -367,7 +368,7 @@ fn status_interval(start_time: Instant) -> time::Sleep { time::sleep(duration) } -async fn status_loop(command_handler: Arc, shutdown: Shutdown) { +async fn status_loop(command_handler: Arc>, shutdown: Shutdown) { let start_time = Instant::now(); let mut shutdown_signal = shutdown.to_signal(); loop { @@ -379,7 +380,7 @@ async fn status_loop(command_handler: Arc, shutdown: Shutdown) { } _ = interval => { - command_handler.status(StatusOutput::Log); + command_handler.lock().await.status(StatusOutput::Log); }, } } @@ -407,7 +408,12 @@ async fn cli_loop(parser: Parser, mut shutdown: Shutdown) { let mut shutdown_signal = shutdown.to_signal(); let start_time = Instant::now(); - let mut software_update_notif = command_handler.get_software_updater().new_update_notifier().clone(); + let mut software_update_notif = command_handler + .lock() + .await + .get_software_updater() + .new_update_notifier() + .clone(); loop { let interval = status_interval(start_time); tokio::select! { @@ -415,7 +421,7 @@ async fn cli_loop(parser: Parser, mut shutdown: Shutdown) { match res { Ok((line, mut rustyline)) => { if let Some(p) = rustyline.helper_mut() { - p.handle_command(line.as_str(), &mut shutdown); + p.handle_command(line.as_str(), &mut shutdown).await; } if !shutdown.is_triggered() { read_command_fut.set(read_command(rustyline).fuse()); @@ -440,7 +446,7 @@ async fn cli_loop(parser: Parser, mut shutdown: Shutdown) { } } _ = interval => { - command_handler.status(StatusOutput::Full); + command_handler.lock().await.status(StatusOutput::Full); }, _ = shutdown_signal.wait() => { break; diff --git a/applications/tari_base_node/src/parser.rs b/applications/tari_base_node/src/parser.rs index 7ef6717dd2..8cfd139556 100644 --- a/applications/tari_base_node/src/parser.rs +++ b/applications/tari_base_node/src/parser.rs @@ -48,6 +48,7 @@ use tari_core::{ }; use tari_crypto::tari_utilities::hex; use tari_shutdown::Shutdown; +use tokio::sync::Mutex; /// Enum representing commands used by the basenode #[derive(Clone, Copy, PartialEq, Debug, Display, EnumIter, EnumString)] @@ -94,7 +95,7 @@ pub enum BaseNodeCommand { pub struct Parser { commands: Vec, hinter: HistoryHinter, - command_handler: Arc, + command_handler: Arc>, } /// This will go through all instructions and look for potential matches @@ -128,7 +129,7 @@ impl Hinter for Parser { impl Parser { /// creates a new parser struct - pub fn new(command_handler: Arc) -> Self { + pub fn new(command_handler: Arc>) -> Self { Parser { commands: BaseNodeCommand::iter().map(|x| x.to_string()).collect(), hinter: HistoryHinter {}, @@ -142,7 +143,7 @@ impl Parser { } /// This will parse the provided command and execute the task - pub fn handle_command(&mut self, command_str: &str, shutdown: &mut Shutdown) { + pub async fn handle_command(&mut self, command_str: &str, shutdown: &mut Shutdown) { if command_str.trim().is_empty() { return; } @@ -150,7 +151,7 @@ impl Parser { let mut args = command_str.split_whitespace(); match args.next().unwrap_or("help").parse() { Ok(command) => { - self.process_command(command, args, shutdown); + self.process_command(command, args, shutdown).await; }, Err(_) => { println!("{} is not a valid command, please enter a valid command", command_str); @@ -159,12 +160,12 @@ impl Parser { } } - pub fn get_command_handler(&self) -> Arc { + pub fn get_command_handler(&self) -> Arc> { self.command_handler.clone() } /// Function to process commands - fn process_command<'a, I: Iterator>( + async fn process_command<'a, I: Iterator>( &mut self, command: BaseNodeCommand, mut args: I, @@ -180,94 +181,94 @@ impl Parser { ); }, Status => { - self.command_handler.status(StatusOutput::Full); + self.command_handler.lock().await.status(StatusOutput::Full); }, GetStateInfo => { - self.command_handler.state_info(); + self.command_handler.lock().await.state_info(); }, Version => { - self.command_handler.print_version(); + self.command_handler.lock().await.print_version(); }, CheckForUpdates => { - self.command_handler.check_for_updates(); + self.command_handler.lock().await.check_for_updates(); }, GetChainMetadata => { - self.command_handler.get_chain_meta(); + self.command_handler.lock().await.get_chain_meta(); }, GetDbStats => { - self.command_handler.get_blockchain_db_stats(); + self.command_handler.lock().await.get_blockchain_db_stats(); }, DialPeer => { - self.process_dial_peer(args); + self.process_dial_peer(args).await; }, PingPeer => { - self.process_ping_peer(args); + self.process_ping_peer(args).await; }, DiscoverPeer => { - self.process_discover_peer(args); + self.process_discover_peer(args).await; }, GetPeer => { - self.process_get_peer(args); + self.process_get_peer(args).await; }, ListPeers => { - self.process_list_peers(args); + self.process_list_peers(args).await; }, ResetOfflinePeers => { - self.command_handler.reset_offline_peers(); + self.command_handler.lock().await.reset_offline_peers(); }, RewindBlockchain => { - self.process_rewind_blockchain(args); + self.process_rewind_blockchain(args).await; }, CheckDb => { - self.command_handler.check_db(); + self.command_handler.lock().await.check_db(); }, PeriodStats => { - self.process_period_stats(args); + self.process_period_stats(args).await; }, HeaderStats => { - self.process_header_stats(args); + self.process_header_stats(args).await; }, BanPeer => { - self.process_ban_peer(args, true); + self.process_ban_peer(args, true).await; }, UnbanPeer => { - self.process_ban_peer(args, false); + self.process_ban_peer(args, false).await; }, UnbanAllPeers => { - self.command_handler.unban_all_peers(); + self.command_handler.lock().await.unban_all_peers(); }, ListBannedPeers => { - self.command_handler.list_banned_peers(); + self.command_handler.lock().await.list_banned_peers(); }, ListConnections => { - self.command_handler.list_connections(); + self.command_handler.lock().await.list_connections(); }, ListHeaders => { - self.process_list_headers(args); + self.process_list_headers(args).await; }, BlockTiming | CalcTiming => { - self.process_block_timing(args); + self.process_block_timing(args).await; }, GetBlock => { - self.process_get_block(args); + self.process_get_block(args).await; }, SearchUtxo => { - self.process_search_utxo(args); + self.process_search_utxo(args).await; }, SearchKernel => { - self.process_search_kernel(args); + self.process_search_kernel(args).await; }, GetMempoolStats => { - self.command_handler.get_mempool_stats(); + self.command_handler.lock().await.get_mempool_stats(); }, GetMempoolState => { - self.command_handler.get_mempool_state(); + self.command_handler.lock().await.get_mempool_state(); }, Whoami => { - self.command_handler.whoami(); + self.command_handler.lock().await.whoami(); }, GetNetworkStats => { - self.command_handler.get_network_stats(); + self.command_handler.lock().await.get_network_stats(); }, Exit | Quit => { println!("Shutting down..."); @@ -426,7 +427,7 @@ impl Parser { } /// Function to process the get-block command - fn process_get_block<'a, I: Iterator>(&self, mut args: I) { + async fn process_get_block<'a, I: Iterator>(&self, mut args: I) { let height_or_hash = match args.next() { Some(s) => s .parse::() @@ -451,8 +452,8 @@ impl Parser { }; match height_or_hash { - Some(Either::Left(height)) => self.command_handler.get_block(height, format), - Some(Either::Right(hash)) => self.command_handler.get_block_by_hash(hash, format), + Some(Either::Left(height)) => self.command_handler.lock().await.get_block(height, format), + Some(Either::Right(hash)) => self.command_handler.lock().await.get_block_by_hash(hash, format), None => { println!("Invalid block height or hash provided. Height must be an integer."); self.print_help(BaseNodeCommand::GetBlock); @@ -461,7 +462,7 @@ impl Parser { } /// Function to process the search utxo command - fn process_search_utxo<'a, I: Iterator>(&self, mut args: I) { + async fn process_search_utxo<'a, I: Iterator>(&self, mut args: I) { // let command_arg = args.take(4).collect::>(); let hex = args.next(); if hex.is_none() { @@ -476,11 +477,11 @@ impl Parser { return; }, }; - self.command_handler.search_utxo(commitment) + self.command_handler.lock().await.search_utxo(commitment) } /// Function to process the search kernel command - fn process_search_kernel<'a, I: Iterator>(&self, mut args: I) { + async fn process_search_kernel<'a, I: Iterator>(&self, mut args: I) { // let command_arg = args.take(4).collect::>(); let hex = args.next(); if hex.is_none() { @@ -511,11 +512,11 @@ impl Parser { }; let kernel_sig = Signature::new(public_nonce, signature); - self.command_handler.search_kernel(kernel_sig) + self.command_handler.lock().await.search_kernel(kernel_sig) } /// Function to process the discover-peer command - fn process_discover_peer<'a, I: Iterator>(&mut self, mut args: I) { + async fn process_discover_peer<'a, I: Iterator>(&mut self, mut args: I) { let dest_pubkey = match args.next().and_then(parse_emoji_id_or_public_key) { Some(v) => Box::new(v), None => { @@ -525,10 +526,10 @@ impl Parser { }, }; - self.command_handler.discover_peer(dest_pubkey) + self.command_handler.lock().await.discover_peer(dest_pubkey) } - fn process_get_peer<'a, I: Iterator>(&mut self, mut args: I) { + async fn process_get_peer<'a, I: Iterator>(&mut self, mut args: I) { let (original_str, partial) = match args .next() .map(|s| { @@ -549,18 +550,18 @@ impl Parser { }, }; - self.command_handler.get_peer(partial, original_str) + self.command_handler.lock().await.get_peer(partial, original_str) } /// Function to process the list-peers command - fn process_list_peers<'a, I: Iterator>(&mut self, mut args: I) { + async fn process_list_peers<'a, I: Iterator>(&mut self, mut args: I) { let filter = args.next().map(ToOwned::to_owned); - self.command_handler.list_peers(filter) + self.command_handler.lock().await.list_peers(filter) } /// Function to process the dial-peer command - fn process_dial_peer<'a, I: Iterator>(&mut self, mut args: I) { + async fn process_dial_peer<'a, I: Iterator>(&mut self, mut args: I) { let dest_node_id = match args .next() .and_then(parse_emoji_id_or_public_key_or_node_id) @@ -574,11 +575,11 @@ impl Parser { }, }; - self.command_handler.dial_peer(dest_node_id) + self.command_handler.lock().await.dial_peer(dest_node_id) } /// Function to process the dial-peer command - fn process_ping_peer<'a, I: Iterator>(&mut self, mut args: I) { + async fn process_ping_peer<'a, I: Iterator>(&mut self, mut args: I) { let dest_node_id = match args .next() .and_then(parse_emoji_id_or_public_key_or_node_id) @@ -592,11 +593,11 @@ impl Parser { }, }; - self.command_handler.ping_peer(dest_node_id) + self.command_handler.lock().await.ping_peer(dest_node_id) } /// Function to process the ban-peer command - fn process_ban_peer<'a, I: Iterator>(&mut self, mut args: I, must_ban: bool) { + async fn process_ban_peer<'a, I: Iterator>(&mut self, mut args: I, must_ban: bool) { let node_id = match args .next() .and_then(parse_emoji_id_or_public_key_or_node_id) @@ -618,11 +619,11 @@ impl Parser { .map(Duration::from_secs) .unwrap_or_else(|| Duration::from_secs(std::u64::MAX)); - self.command_handler.ban_peer(node_id, duration, must_ban) + self.command_handler.lock().await.ban_peer(node_id, duration, must_ban) } /// Function to process the list-headers command - fn process_list_headers<'a, I: Iterator>(&self, mut args: I) { + async fn process_list_headers<'a, I: Iterator>(&self, mut args: I) { let start = args.next().map(u64::from_str).map(Result::ok).flatten(); let end = args.next().map(u64::from_str).map(Result::ok).flatten(); if start.is_none() { @@ -632,11 +633,11 @@ impl Parser { return; } let start = start.unwrap(); - self.command_handler.list_headers(start, end) + self.command_handler.lock().await.list_headers(start, end) } /// Function to process the calc-timing command - fn process_block_timing<'a, I: Iterator>(&self, mut args: I) { + async fn process_block_timing<'a, I: Iterator>(&self, mut args: I) { let start = args.next().map(u64::from_str).map(Result::ok).flatten(); let end = args.next().map(u64::from_str).map(Result::ok).flatten(); @@ -646,14 +647,14 @@ impl Parser { println!("Number of headers must be at least 2."); self.print_help(command); } else { - self.command_handler.block_timing(start, end) + self.command_handler.lock().await.block_timing(start, end) } } else { self.print_help(command); } } - fn process_period_stats<'a, I: Iterator>(&self, args: I) { + async fn process_period_stats<'a, I: Iterator>(&self, args: I) { let command_arg = args.map(|arg| arg.to_string()).take(3).collect::>(); if command_arg.len() != 3 { println!("Prints out certain stats to of the block chain, use as follows: "); @@ -684,10 +685,13 @@ impl Parser { return; }, }; - self.command_handler.period_stats(period_end, period_ticker_end, period) + self.command_handler + .lock() + .await + .period_stats(period_end, period_ticker_end, period) } - fn process_header_stats<'a, I: Iterator>(&self, mut args: I) { + async fn process_header_stats<'a, I: Iterator>(&self, mut args: I) { let start_height = try_or_print!(args .next() .ok_or_else(|| { @@ -713,14 +717,16 @@ impl Parser { _ => Err("Invalid pow algo"), })); self.command_handler + .lock() + .await .save_header_stats(start_height, end_height, filename, algo) } - fn process_rewind_blockchain<'a, I: Iterator>(&self, mut args: I) { + async fn process_rewind_blockchain<'a, I: Iterator>(&self, mut args: I) { let new_height = try_or_print!(args .next() .ok_or("new_height argument required") .and_then(|s| u64::from_str(s).map_err(|_| "new_height must be an integer."))); - self.command_handler.rewind_blockchain(new_height); + self.command_handler.lock().await.rewind_blockchain(new_height); } } diff --git a/base_layer/core/src/base_node/state_machine_service/states/events_and_states.rs b/base_layer/core/src/base_node/state_machine_service/states/events_and_states.rs index c85f8403d7..aaf4e814f8 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/events_and_states.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/events_and_states.rs @@ -165,25 +165,30 @@ pub enum StateInfo { } impl StateInfo { - pub fn short_desc(&self) -> String { + pub fn short_desc(&self, full_log: bool) -> String { use StateInfo::*; match self { StartUp => "Starting up".to_string(), HeaderSync(None) => "Starting header sync".to_string(), HeaderSync(Some(info)) => format!("Syncing headers: {}", info.sync_progress_string()), - HorizonSync(info) => match info.status { + HorizonSync(info) => match info.status.clone() { HorizonSyncStatus::Starting => "Starting horizon sync".to_string(), - HorizonSyncStatus::Kernels(current, total) => format!( - "Syncing kernels: {}/{} ({:.0}%)", + HorizonSyncStatus::Kernels(current, total, node) => format!( + "Syncing kernels: {}/{} ({:.0}%){}", current, total, - current as f64 / total as f64 * 100.0 + current as f64 / total as f64 * 100.0, + match full_log { + true => format!(" {}", node), + false => "".to_string(), + } ), - HorizonSyncStatus::Outputs(current, total) => format!( - "Syncing outputs: {}/{} ({:.0}%)", + HorizonSyncStatus::Outputs(current, total, node) => format!( + "Syncing outputs: {}/{} ({:.0}%) from {}", current, total, - current as f64 / total as f64 * 100.0 + current as f64 / total as f64 * 100.0, + node ), HorizonSyncStatus::Finalizing => "Finalizing horizon sync".to_string(), }, @@ -315,14 +320,16 @@ impl Display for HorizonSyncInfo { fmt.write_str(&format!("{}\n", peer))?; } - match self.status { + match self.status.clone() { HorizonSyncStatus::Starting => fmt.write_str("Starting horizon state synchronization"), - HorizonSyncStatus::Kernels(current, total) => { - fmt.write_str(&format!("Horizon syncing kernels: {}/{}\n", current, total)) - }, - HorizonSyncStatus::Outputs(current, total) => { - fmt.write_str(&format!("Horizon syncing outputs: {}/{}\n", current, total)) - }, + HorizonSyncStatus::Kernels(current, total, node) => fmt.write_str(&format!( + "Horizon syncing kernels: {}/{} from {}\n", + current, total, node + )), + HorizonSyncStatus::Outputs(current, total, node) => fmt.write_str(&format!( + "Horizon syncing outputs: {}/{} from {}\n", + current, total, node + )), HorizonSyncStatus::Finalizing => fmt.write_str("Finalizing horizon state synchronization"), } } @@ -330,7 +337,7 @@ impl Display for HorizonSyncInfo { #[derive(Clone, Debug, PartialEq)] pub enum HorizonSyncStatus { Starting, - Kernels(u64, u64), - Outputs(u64, u64), + Kernels(u64, u64, NodeId), + Outputs(u64, u64, NodeId), Finalizing, } diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs index 9b9f344040..05385f876a 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs @@ -154,7 +154,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { let info = HorizonSyncInfo::new( vec![self.sync_peer.node_id().clone()], - HorizonSyncStatus::Kernels(local_num_kernels, remote_num_kernels), + HorizonSyncStatus::Kernels(local_num_kernels, remote_num_kernels, self.sync_peer.node_id().clone()), ); self.shared.set_state_info(StateInfo::HorizonSync(info)); @@ -247,7 +247,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { if mmr_position % 100 == 0 || mmr_position == self.num_kernels { let info = HorizonSyncInfo::new( vec![self.sync_peer.node_id().clone()], - HorizonSyncStatus::Kernels(mmr_position, self.num_kernels), + HorizonSyncStatus::Kernels(mmr_position, self.num_kernels, self.sync_peer.node_id().clone()), ); self.shared.set_state_info(StateInfo::HorizonSync(info)); } @@ -278,7 +278,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { let info = HorizonSyncInfo::new( vec![self.sync_peer.node_id().clone()], - HorizonSyncStatus::Outputs(local_num_outputs, self.num_outputs), + HorizonSyncStatus::Outputs(local_num_outputs, self.num_outputs, self.sync_peer.node_id().clone()), ); self.shared.set_state_info(StateInfo::HorizonSync(info)); @@ -509,7 +509,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { if mmr_position % 100 == 0 || mmr_position == self.num_outputs { let info = HorizonSyncInfo::new( vec![self.sync_peer.node_id().clone()], - HorizonSyncStatus::Outputs(mmr_position, self.num_outputs), + HorizonSyncStatus::Outputs(mmr_position, self.num_outputs, self.sync_peer.node_id().clone()), ); self.shared.set_state_info(StateInfo::HorizonSync(info)); } From 414f4cd2a3b136cbc6eda09b1bf6bc6f0a949660 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Fri, 26 Nov 2021 11:41:36 +0200 Subject: [PATCH 17/29] test: increase timeout in cucumber (#3621) Description --- Increased the timeout to obtain a merge mining proxy process, which includes the time for testing the monerod URL `/get_height` response. Motivation and Context --- Some tests dependent on merge mining timed out in cucumber before a suitable `monerod` server could be find. How Has This Been Tested? --- Cucumber --- integration_tests/features/support/steps.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration_tests/features/support/steps.js b/integration_tests/features/support/steps.js index 723da3e09e..1dc1374b04 100644 --- a/integration_tests/features/support/steps.js +++ b/integration_tests/features/support/steps.js @@ -650,6 +650,7 @@ Given( Given( /I have a merge mining proxy (.*) connected to (.*) and (.*) with origin submission disabled/, + { timeout: 120 * 1000 }, // The timeout must make provision for testing the monerod URL /get_height response async function (mmProxy, node, wallet) { const baseNode = this.getNode(node); const walletNode = this.getWallet(wallet); @@ -668,6 +669,7 @@ Given( Given( /I have a merge mining proxy (.*) connected to (.*) and (.*) with origin submission enabled/, + { timeout: 120 * 1000 }, // The timeout must make provision for testing the monerod URL /get_height response async function (mmProxy, node, wallet) { const baseNode = this.getNode(node); const walletNode = this.getWallet(wallet); From b09acd1442ff6d0ff58530df0698eaa0934a2b61 Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Fri, 26 Nov 2021 15:11:22 +0200 Subject: [PATCH 18/29] fix!: console wallet grpc_console_wallet_addresss config (#3619) Description --- This PR changes `grpc_console_wallet_address` to `grpc_address` and moves it into the `wallet` section of the config. It also allows a grpc address to be specified via the `--grpc-address` command line argument for the console_wallet (using standard multiaddr formatting). Additionally, fixes a bug due to a missing config default. How Has This Been Tested? --- cargo test --all nvm use 12.22.6 && node_modules/.bin/cucumber-js --profile "ci" --tags "not @long-running and not @broken" manually (`cargo run --bin tari_console_wallet -- --grpc-address /ip4/127.0.0.1/tcp/18144` + bloomRPC client) --- README.md | 33 +++++++++++-------- applications/launchpad/docker_rig/config.toml | 4 --- .../launchpad/docker_rig/docker-compose.yml | 6 ++-- .../tari_app_utilities/src/initialization.rs | 15 +++++++++ common/config/presets/base_node.toml | 6 ---- common/config/presets/console_wallet.toml | 5 ++- common/config/presets/mining_node.toml | 3 +- common/src/configuration/bootstrap.rs | 3 ++ common/src/configuration/global.rs | 8 ++--- common/src/configuration/utils.rs | 16 +-------- integration_tests/features/support/steps.js | 2 +- integration_tests/helpers/config.js | 2 +- integration_tests/helpers/walletProcess.js | 2 +- 13 files changed, 54 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index b34b179d2a..b8d3a63517 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,13 @@ First you'll need to make sure you have a full development environment set up: - Build Tools - [CMake](https://cmake.org/download/) (Used for RandomX) - - - Either: + + - Either: - Microsoft Visual Studio Version 2019 or later - C++ CMake tools for Windows - MSVC build tools (latest version for your platform ARM, ARM64 or x64.x86) - Spectre-mitigated libs (latest version for your platform ARM, ARM64 or x64.x86) - + or - [Build Tools for Visual Studio 2019](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16) @@ -432,15 +432,19 @@ will be created in the `~/tari_weatherwax_testnet/config` (on Linux) or `%USERPR directory. With the main configuration file, in addition to the settings already present, the following must also be enabled for -the Tari Base Node and the Tari Console Wallet, if they are not enabled already. Under section **`base_node.weatherwax`**: +the Tari Base Node and the Tari Console Wallet, if they are not enabled already. Under sections **`base_node.weatherwax`** and **`wallet`** respectively: +``` +[wallet] + +grpc_address = "127.0.0.1:18143" +``` ``` [base_node.weatherwax] transport = "tor" allow_test_addresses = false grpc_enabled = true grpc_base_node_address = "127.0.0.1:18142" -grpc_console_wallet_address = "127.0.0.1:18143" ``` For Tari Stratum Transcoder: @@ -468,7 +472,7 @@ For the Tari Mining Node there are some additional settings under section **`min #base_node_grpc_address = "127.0.0.1:18142" # GRPC address of console wallet -# Default: value from `base_node.grpc_console_wallet_address` +# Default: value from `wallet.grpc_address` #wallet_grpc_address = "127.0.0.1:18143" # Start mining only when base node is bootstrapped @@ -492,14 +496,14 @@ For pooled SHA3 mining: # Stratum Mode configuration # mining_pool_address = "miningcore.tari.com:3052" -# mining_wallet_address = "YOUR_WALLET_PUBLIC_KEY" +# mining_wallet_address = "YOUR_WALLET_PUBLIC_KEY" # mining_worker_name = "worker1" ``` -Uncomment `mining_pool_address` and `mining_wallet_address`. Adjust the values to your intended configuration. +Uncomment `mining_pool_address` and `mining_wallet_address`. Adjust the values to your intended configuration. `mining_worker_name` is an optional configuration field allowing you to name your worker. #### Perform SHA3 mining -* For SHA3 mining: +* For SHA3 mining: Tor and the required Tari applications must be started and preferably in this order: - Tor: @@ -545,7 +549,7 @@ and performing mining: * Pool Operators: Tor and the required Tari applications must be started in this order: - Tor: - + - Linux/OSX: Execute `start_tor.sh`. - Windows: `Start Tor Serviecs` menu item or `start_tor` shortcut in the Tari installation folder. @@ -566,7 +570,7 @@ and performing mining: - Tari Mining Node: - Linux/OSX: As per [Runtime links](#runtime-links). - Windows: As per [Runtime links](#runtime-links) or `Start Mining Node` menu item - or `start_tari_mining_node` shortcut in the Tari installation folder. + or `start_tari_mining_node` shortcut in the Tari installation folder. ### Tari merge mining @@ -603,14 +607,17 @@ directory. With the main configuration file, in addition to the settings already present, the following must also be enabled if they are not enabled already: -- For the Tari Base Node and the Tari Console Wallet, under section **`base_node.weatherwax`** +- For the Tari Base Node and the Tari Console Wallet, under sections **`base_node.weatherwax`** and **`wallet`** respectively + ``` + [wallet] + grpc_address = "127.0.0.1:18143" + ``` ``` [base_node.weatherwax] transport = "tor" allow_test_addresses = false grpc_enabled = true grpc_base_node_address = "127.0.0.1:18142" - grpc_console_wallet_address = "127.0.0.1:18143" ``` And then depending on if you are using solo mining or self-select mining you will use one of the following: diff --git a/applications/launchpad/docker_rig/config.toml b/applications/launchpad/docker_rig/config.toml index 20f902026b..9d97586e67 100644 --- a/applications/launchpad/docker_rig/config.toml +++ b/applications/launchpad/docker_rig/config.toml @@ -192,9 +192,6 @@ grpc_enabled = true # The socket to expose for the gRPC base node server. This value is ignored if grpc_enabled is false. # Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") grpc_base_node_address = "0.0.0.0:18142" -# The socket to expose for the gRPC wallet server. This value is ignored if grpc_enabled is false. -# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") -grpc_console_wallet_address = "0.0.0.0:18143" # A path to the file that stores your node identity and secret key base_node_identity_file = "config/base_node_id.json" @@ -283,7 +280,6 @@ allow_test_addresses = false # Enable the gRPC server for the base node. Set this to true if you want to enable third-party wallet software grpc_enabled = true grpc_base_node_address = "127.0.0.1:18142" -grpc_console_wallet_address = "127.0.0.1:18143" base_node_identity_file = "config/igor/base_node_id.json" console_wallet_identity_file = "config/igor/console_wallet_id.json" diff --git a/applications/launchpad/docker_rig/docker-compose.yml b/applications/launchpad/docker_rig/docker-compose.yml index e062458ffa..7dde2f30fa 100644 --- a/applications/launchpad/docker_rig/docker-compose.yml +++ b/applications/launchpad/docker_rig/docker-compose.yml @@ -34,7 +34,7 @@ services: TARI_WALLET__WEATHERWAX__TOR_SOCKS_ADDRESS_OVERRIDE: "/dns4/tor/tcp/9050" TARI_WALLET__WEATHERWAX__TOR_FORWARD_ADDRESS: "/dns4/wallet/tcp/18188" TARI_WALLET__WEATHERWAX__TCP_LISTENER_ADDRESS: "/dns4/wallet/tcp/18188" - TARI_BASE_NODE__WEATHERWAX__GRPC_CONSOLE_WALLET_ADDRESS: "0.0.0.0:18143" + TARI_WALLET__GRPC_ADDRESS: "0.0.0.0:18143" command: ["--non-interactive"] ports: - 18188:18188 @@ -101,7 +101,7 @@ services: TARI_MINING_NODE__NUM_MINING_THREADS: 2 TARI_MINING_NODE__MINE_ON_TIP_ONLY: 1 TARI_BASE_NODE__WEATHERWAX__GRPC_BASE_NODE_ADDRESS: "/dns4/base_node/tcp/18142" - TARI_BASE_NODE__WEATHERWAX__GRPC_CONSOLE_WALLET_ADDRESS: "/dns4/wallet/tcp/18143" + TARI_WALLET__GRPC_ADDRESS: "/dns4/wallet/tcp/18143" command: [ ] depends_on: - base_node @@ -158,7 +158,7 @@ services: WAIT_FOR_TOR: 0 TARI_NETWORK: ${TARI_NETWORK} TARI_BASE_NODE__WEATHERWAX__GRPC_BASE_NODE_ADDRESS: "/dns4/base_node/tcp/18142" - TARI_BASE_NODE__WEATHERWAX__GRPC_CONSOLE_WALLET_ADDRESS: "/dns4/wallet/tcp/18143" + TARI_WALLET__GRPC_ADDRESS: "/dns4/wallet/tcp/18143" TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_URL: ${TARI_MONEROD_URL:-http://monero-stagenet.exan.tech:38081} TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_USERNAME: ${TARI_MONEROD_USERNAME} TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_PASSWORD: ${TARI_MONEROD_PASSWORD} diff --git a/applications/tari_app_utilities/src/initialization.rs b/applications/tari_app_utilities/src/initialization.rs index ead5b89f4b..8d84ef4240 100644 --- a/applications/tari_app_utilities/src/initialization.rs +++ b/applications/tari_app_utilities/src/initialization.rs @@ -9,6 +9,7 @@ use tari_common::{ DatabaseType, GlobalConfig, }; +use tari_comms::multiaddr::Multiaddr; pub const LOG_TARGET: &str = "tari::application"; @@ -61,6 +62,20 @@ pub fn init_configuration( }, } } + + if let Some(str) = bootstrap.wallet_grpc_address.clone() { + log::info!( + target: LOG_TARGET, + "{}", + format!("GRPC address specified in command parameters: {}", str) + ); + + let grpc_address = str + .parse::() + .map_err(|_| ExitCodes::InputError("GRPC address is not valid".to_string()))?; + global_config.grpc_console_wallet_address = grpc_address; + } + check_file_paths(&mut global_config, &bootstrap); Ok((bootstrap, global_config, cfg)) diff --git a/common/config/presets/base_node.toml b/common/config/presets/base_node.toml index aa359724c4..f1145ef36e 100644 --- a/common/config/presets/base_node.toml +++ b/common/config/presets/base_node.toml @@ -87,9 +87,6 @@ grpc_enabled = true # The socket to expose for the gRPC base node server. This value is ignored if grpc_enabled is false. # Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") grpc_base_node_address = "127.0.0.1:18142" -# The socket to expose for the gRPC wallet server. This value is ignored if grpc_enabled is false. -# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") -grpc_console_wallet_address = "127.0.0.1:18143" # A path to the file that stores your node identity and secret key base_node_identity_file = "config/base_node_id.json" @@ -265,9 +262,6 @@ grpc_enabled = true # The socket to expose for the gRPC base node server. This value is ignored if grpc_enabled is false. # Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") grpc_base_node_address = "127.0.0.1:18142" -# The socket to expose for the gRPC wallet server. This value is ignored if grpc_enabled is false. -# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") -grpc_console_wallet_address = "127.0.0.1:18143" # A path to the file that stores your node identity and secret key base_node_identity_file = "config/base_node_id.json" diff --git a/common/config/presets/console_wallet.toml b/common/config/presets/console_wallet.toml index ea7a7a4636..b071343dd0 100644 --- a/common/config/presets/console_wallet.toml +++ b/common/config/presets/console_wallet.toml @@ -14,6 +14,10 @@ wallet_db_file = "wallet/wallet.dat" console_wallet_db_file = "wallet/console-wallet.dat" +# The socket to expose for the gRPC wallet server. This value is ignored if grpc_enabled is false. +# Valid values here are IPv4 and IPv6 TCP sockets, local unix sockets (e.g. "ipc://base-node-gprc.sock.100") +grpc_address = "127.0.0.1:18143" + # Console wallet password # Should you wish to start your console wallet without typing in your password, the following options are available: # 1. Start the console wallet with the --password=secret argument, or @@ -197,4 +201,3 @@ tor_control_auth = "none" # or "password=xxxxxx" # tor_proxy_bypass_addresses = ["/dns4/my-foo-base-node/tcp/9998"] # When using the tor transport and set to true, outbound TCP connections bypass the tor proxy. Defaults to false for better privacy # tor_proxy_bypass_for_outbound_tcp = false; - diff --git a/common/config/presets/mining_node.toml b/common/config/presets/mining_node.toml index 87f3f1aa1f..3c5aa31bb1 100644 --- a/common/config/presets/mining_node.toml +++ b/common/config/presets/mining_node.toml @@ -14,7 +14,7 @@ #base_node_grpc_address = "127.0.0.1:18142" # GRPC address of console wallet -# Default: value from `base_node.grpc_console_wallet_address` +# Default: value from `wallet.grpc_address` #wallet_grpc_address = "127.0.0.1:18143" # Start mining only when base node is bootstrapped @@ -32,4 +32,3 @@ # mining_pool_address = "miningcore.tari.com:3052" # mining_wallet_address = "YOUR_WALLET_PUBLIC_KEY" # mining_worker_name = "worker1" - diff --git a/common/src/configuration/bootstrap.rs b/common/src/configuration/bootstrap.rs index 083b70a9bb..c659fae24f 100644 --- a/common/src/configuration/bootstrap.rs +++ b/common/src/configuration/bootstrap.rs @@ -154,6 +154,8 @@ pub struct ConfigBootstrap { /// Supply a network (overrides existing configuration) #[structopt(long, alias = "network")] pub network: Option, + #[structopt(long, alias = "grpc-address")] + pub wallet_grpc_address: Option, /// Metrics server bind address (prometheus pull) #[structopt(long, alias = "metrics-bind-addr")] pub metrics_server_bind_addr: Option, @@ -196,6 +198,7 @@ impl Default for ConfigBootstrap { miner_max_diff: None, tracing_enabled: false, network: None, + wallet_grpc_address: None, metrics_server_bind_addr: None, metrics_push_endpoint: None, } diff --git a/common/src/configuration/global.rs b/common/src/configuration/global.rs index 34ba629e92..0ee4c92999 100644 --- a/common/src/configuration/global.rs +++ b/common/src/configuration/global.rs @@ -345,11 +345,11 @@ fn convert_node_config( .get_str(&key) .map(|addr| socket_or_multi(&addr).map_err(|e| ConfigurationError::new(&key, &e.to_string())))??; - let key = config_string("base_node", net_str, "grpc_console_wallet_address"); + let key = "wallet.grpc_address"; let grpc_console_wallet_address = cfg - .get_str(&key) - .map_err(|e| ConfigurationError::new(&key, &e.to_string())) - .map(|addr| socket_or_multi(&addr).map_err(|e| ConfigurationError::new(&key, &e.to_string())))??; + .get_str(key) + .map_err(|e| ConfigurationError::new(key, &e.to_string())) + .map(|addr| socket_or_multi(&addr).map_err(|e| ConfigurationError::new(key, &e.to_string())))??; // Peer and DNS seeds let key = "common.peer_seeds"; diff --git a/common/src/configuration/utils.rs b/common/src/configuration/utils.rs index 78aaa5c73e..db72daa146 100644 --- a/common/src/configuration/utils.rs +++ b/common/src/configuration/utils.rs @@ -63,7 +63,6 @@ pub fn config_installer(app_type: ApplicationType, path: &Path) -> Result<(), st /// These will typically be overridden by userland settings in envars, the config file, or the command line. pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { let mut cfg = Config::new(); - let local_ip_addr = get_local_ip().unwrap_or_else(|| "/ip4/1.2.3.4".parse().unwrap()); // Common settings cfg.set_default("common.message_cache_size", 10).unwrap(); @@ -171,18 +170,12 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { default_subdir("config/console_wallet_tor.json", Some(&bootstrap.base_path)), ) .unwrap(); - cfg.set_default( - "base_node.mainnet.public_address", - format!("{}/tcp/18041", local_ip_addr), - ) - .unwrap(); cfg.set_default("base_node.mainnet.grpc_enabled", false).unwrap(); cfg.set_default("base_node.mainnet.allow_test_addresses", false) .unwrap(); cfg.set_default("base_node.mainnet.grpc_base_node_address", "127.0.0.1:18142") .unwrap(); - cfg.set_default("base_node.mainnet.grpc_console_wallet_address", "127.0.0.1:18143") - .unwrap(); + cfg.set_default("wallet.grpc_address", "127.0.0.1:18143").unwrap(); cfg.set_default("base_node.mainnet.flood_ban_max_msg_count", 10000) .unwrap(); @@ -231,8 +224,6 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("base_node.weatherwax.grpc_enabled", false).unwrap(); cfg.set_default("base_node.weatherwax.grpc_base_node_address", "127.0.0.1:18142") .unwrap(); - cfg.set_default("base_node.weatherwax.grpc_console_wallet_address", "127.0.0.1:18143") - .unwrap(); cfg.set_default( "base_node.weatherwax.dns_seeds_name_server", "1.1.1.1:853/cloudflare-dns.com", @@ -244,7 +235,6 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("wallet.base_node_service_peers", Vec::::new()) .unwrap(); - //---------------------------------- Igor Defaults --------------------------------------------// cfg.set_default("base_node.igor.db_type", "lmdb").unwrap(); @@ -255,13 +245,9 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("base_node.igor.pruned_mode_cleanup_interval", 50) .unwrap(); cfg.set_default("base_node.igor.flood_ban_max_msg_count", 1000).unwrap(); - cfg.set_default("base_node.igor.public_address", format!("{}/tcp/18141", local_ip_addr)) - .unwrap(); cfg.set_default("base_node.igor.grpc_enabled", false).unwrap(); cfg.set_default("base_node.igor.grpc_base_node_address", "127.0.0.1:18142") .unwrap(); - cfg.set_default("base_node.igor.grpc_console_wallet_address", "127.0.0.1:18143") - .unwrap(); cfg.set_default("base_node.igor.dns_seeds_name_server", "1.1.1.1:853/cloudflare-dns.com") .unwrap(); cfg.set_default("base_node.igor.dns_seeds_use_dnssec", true).unwrap(); diff --git a/integration_tests/features/support/steps.js b/integration_tests/features/support/steps.js index 1dc1374b04..5958886e28 100644 --- a/integration_tests/features/support/steps.js +++ b/integration_tests/features/support/steps.js @@ -2032,7 +2032,7 @@ When( When( /I send(.*) uT without waiting for broadcast from wallet (.*) to wallet (.*) at fee (.*)/, - { timeout: 20 * 1000 }, + { timeout: 120 * 1000 }, async function (tariAmount, source, dest, feePerGram) { const sourceWallet = this.getWallet(source); const sourceClient = await sourceWallet.connectClient(); diff --git a/integration_tests/helpers/config.js b/integration_tests/helpers/config.js index 23653699b8..1e44a0e464 100644 --- a/integration_tests/helpers/config.js +++ b/integration_tests/helpers/config.js @@ -136,7 +136,7 @@ function createEnv( const configEnvs = { [`TARI_BASE_NODE__${network}__GRPC_BASE_NODE_ADDRESS`]: `${baseNodeGrpcAddress}:${baseNodeGrpcPort}`, - [`TARI_BASE_NODE__${network}__GRPC_CONSOLE_WALLET_ADDRESS`]: `${walletGrpcAddress}:${walletGrpcPort}`, + [`TARI_WALLET__GRPC_ADDRESS`]: `${walletGrpcAddress}:${walletGrpcPort}`, [`TARI_BASE_NODE__${network}__BASE_NODE_IDENTITY_FILE`]: `${nodeFile}`, diff --git a/integration_tests/helpers/walletProcess.js b/integration_tests/helpers/walletProcess.js index aad8ed7048..986fec2ec7 100644 --- a/integration_tests/helpers/walletProcess.js +++ b/integration_tests/helpers/walletProcess.js @@ -89,7 +89,7 @@ class WalletProcess { this.peerSeeds ); } else if (this.options["grpc_console_wallet_address"]) { - envs[`TARI_BASE_NODE__${network}__GRPC_CONSOLE_WALLET_ADDRESS`] = + envs[`TARI_WALLET__GRPC_ADDRESS`] = this.options["grpc_console_wallet_address"]; this.grpcPort = this.options["grpc_console_wallet_address"].split(":")[1]; From 5790a9d776c954efd0dcc603839c91cba5907c69 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Fri, 26 Nov 2021 16:34:11 +0200 Subject: [PATCH 19/29] feat: add bulletproof rewind profiling (#3618) Description --- The following changes were made: - Added bulletproof rewind profiling measurements. - Improved wallet recovery resiliency with improved connection logic to the selected base node(s). - Improved recovering of seed words usability. - Improved some struct member names to be less confusing. - Improved recovery UTXO streaming performance by tuning the chunking parameter. **Results** The following tests were run using standard Tor connectivity on a laptop with an Intel(R) Core(TM) i7-7820HQ CPU and basic Samsung NVMe storage device. | Number of outputs | 52654 | 52284 | 52753 | 52762 | 52762 | | ------------------------------- | ------ | ------ | ------ | ----- | ------ | | Chunk size | 10 | 100 | 250 | 125 | 125 | | Stream form base node time (ms) | 197678 | 108757 | 236872 | 96274 | 115798 | | - per output (ms) | 3.754 | 2.080 | 4.490 | 1.825 | 2.195 | | - normalized | 0.836 | 0.463 | 1 | 0.406 | 0.489 | | Total processing time (ms) | 77157 | 72653 | 69005 | 67170 | 68563 | | - per output (ms) | 1.465 | 1.39 | 1.308 | 1.273 | 1.299 | | - normalized | 1 | 0.949 | 0.893 | 0.869 | 0.887 | | Bulletproofs rewind time (ms) | 10877 | 11040 | 10665 | 10854 | 10702 | | - per output (ms) | 0.207 | 0.211 | 0.202 | 0.206 | 0.203 | | - normalized | 0.981 | 1 | 0.957 | 0.976 | 0.962 | **Discussion** - As can be seen, Bulletproof rewind performance was consistently measured at ~0.21ms per output. - Total processing time, which includes all database processing and Bulletproof rewinding, averaged at about 1.35ms per output. - Optimum streaming performance was found to be ~2ms per output. - Output streaming performance was found to be closely related to the chunk size that are requested from the base node. Smaller (10) and larger (250) quantities were the worst performers, while a quantity in the range of 100 to 125 was approximately twice as fast. _For this PR a final chunk size value of 125 was chosen._ Motivation and Context --- - Further code improvements can be based on informed decisions using the profiling information as basis. How Has This Been Tested? --- - Unit testing. - Cucumber testing. - System-level testing doing wallet recoveries. --- .../tari_console_wallet/src/init/mod.rs | 15 +- .../tari_console_wallet/src/recovery.rs | 13 +- .../tari_console_wallet/src/wallet_modes.rs | 6 +- .../recovery/standard_outputs_recoverer.rs | 16 +- .../wallet/src/utxo_scanner_service/handle.rs | 4 +- .../utxo_scanner_service/utxo_scanner_task.rs | 72 +- base_layer/wallet_ffi/src/tasks.rs | 4 +- common/src/configuration/global.rs | 5 + integration_tests/package-lock.json | 4875 ++--------------- 9 files changed, 477 insertions(+), 4533 deletions(-) diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 74c9271c19..fb83907845 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -300,7 +300,10 @@ pub async fn init_wallet( ); let node_address = match wallet_db.get_node_address().await? { - None => config.public_address.clone().unwrap_or_else(Multiaddr::empty), + None => match config.public_address.clone() { + Some(val) => val, + None => Multiaddr::empty(), + }, Some(a) => a, }; @@ -464,12 +467,12 @@ pub async fn init_wallet( }, }; } - if let Some(file_name) = seed_words_file_name { - let seed_words = wallet.output_manager_service.get_seed_words().await?.join(" "); - let _ = fs::write(file_name, seed_words) - .map_err(|e| ExitCodes::WalletError(format!("Problem writing seed words to file: {}", e))); - }; } + if let Some(file_name) = seed_words_file_name { + let seed_words = wallet.output_manager_service.get_seed_words().await?.join(" "); + let _ = fs::write(file_name, seed_words) + .map_err(|e| ExitCodes::WalletError(format!("Problem writing seed words to file: {}", e))); + }; Ok(wallet) } diff --git a/applications/tari_console_wallet/src/recovery.rs b/applications/tari_console_wallet/src/recovery.rs index ab49446bc7..7af8cceb1b 100644 --- a/applications/tari_console_wallet/src/recovery.rs +++ b/applications/tari_console_wallet/src/recovery.rs @@ -79,7 +79,11 @@ pub fn get_seed_from_seed_words(seed_words: Vec) -> Result Result<(), ExitCodes> { +pub async fn wallet_recovery( + wallet: &WalletSqlite, + base_node_config: &PeerConfig, + retry_limit: usize, +) -> Result<(), ExitCodes> { println!("\nPress Ctrl-C to stop the recovery process\n"); // We dont care about the shutdown signal here, so we just create one let shutdown = Shutdown::new(); @@ -105,7 +109,8 @@ pub async fn wallet_recovery(wallet: &WalletSqlite, base_node_config: &PeerConfi let mut recovery_task = UtxoScannerService::::builder() .with_peers(peer_public_keys) - .with_retry_limit(3) + // Do not make this a small number as wallet recovery needs to be resilient + .with_retry_limit(retry_limit) .build_with_wallet(wallet, shutdown_signal); let mut event_stream = recovery_task.get_event_receiver(); @@ -122,8 +127,8 @@ pub async fn wallet_recovery(wallet: &WalletSqlite, base_node_config: &PeerConfi println!("OK (latency = {:.2?})", latency); }, Ok(UtxoScannerEvent::Progress { - current_block: current, - current_chain_height: total, + current_index: current, + total_index: total, }) => { let percentage_progress = ((current as f32) * 100f32 / (total as f32)).round() as u32; debug!( diff --git a/applications/tari_console_wallet/src/wallet_modes.rs b/applications/tari_console_wallet/src/wallet_modes.rs index a0fcaced5d..d90c650526 100644 --- a/applications/tari_console_wallet/src/wallet_modes.rs +++ b/applications/tari_console_wallet/src/wallet_modes.rs @@ -285,7 +285,11 @@ pub fn recovery_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<( println!("{}", CUCUMBER_TEST_MARKER_A); println!("Starting recovery..."); - match handle.block_on(wallet_recovery(&wallet, &base_node_config)) { + match handle.block_on(wallet_recovery( + &wallet, + &base_node_config, + config.global_config.wallet_recovery_retry_limit, + )) { Ok(_) => println!("Wallet recovered!"), Err(e) => { error!(target: LOG_TARGET, "Recovery failed: {}", e); 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 index 13877f7434..a4a477352e 100644 --- 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 @@ -20,7 +20,7 @@ // 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::sync::Arc; +use std::{sync::Arc, time::Instant}; use log::*; use rand::rngs::OsRng; @@ -73,6 +73,8 @@ where TBackend: OutputManagerBackend + 'static &mut self, outputs: Vec, ) -> Result, OutputManagerError> { + let start = Instant::now(); + let outputs_length = outputs.len(); let mut rewound_outputs: Vec = outputs .into_iter() .filter_map(|output| { @@ -114,6 +116,13 @@ where TBackend: OutputManagerBackend + 'static }, ) .collect(); + let rewind_time = start.elapsed(); + trace!( + target: LOG_TARGET, + "bulletproof rewind profile - rewound {} outputs in {} ms", + outputs_length, + rewind_time.as_millis(), + ); for output in rewound_outputs.iter_mut() { self.update_outputs_script_private_key_and_update_key_manager_index(output) @@ -136,10 +145,7 @@ where TBackend: OutputManagerBackend + 'static trace!( target: LOG_TARGET, "Output {} with value {} with {} recovered", - output - .as_transaction_input(&self.factories.commitment)? - .commitment - .to_hex(), + output_hex, output.value, output.features, ); diff --git a/base_layer/wallet/src/utxo_scanner_service/handle.rs b/base_layer/wallet/src/utxo_scanner_service/handle.rs index 845f45147c..a2164fdfbc 100644 --- a/base_layer/wallet/src/utxo_scanner_service/handle.rs +++ b/base_layer/wallet/src/utxo_scanner_service/handle.rs @@ -42,8 +42,8 @@ pub enum UtxoScannerEvent { }, /// Progress of the recovery process (current_block, current_chain_height) Progress { - current_block: u64, - current_chain_height: u64, + current_index: u64, + total_index: u64, }, /// Completed Recovery (Number scanned, Num of Recovered outputs, Value of recovered outputs, Time taken) Completed { diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index e6c1283641..9c0bac7cbe 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -48,7 +48,7 @@ use tari_core::{ }, }; use tari_shutdown::ShutdownSignal; -use tokio::sync::broadcast; +use tokio::{sync::broadcast, time}; use crate::{ error::WalletError, @@ -89,8 +89,8 @@ where TBackend: WalletBackend + 'static ) -> Result<(), UtxoScannerError> { let metadata = self.get_metadata().await?.unwrap_or_default(); self.publish_event(UtxoScannerEvent::Progress { - current_block: final_utxo_pos, - current_chain_height: final_utxo_pos, + current_index: final_utxo_pos, + total_index: final_utxo_pos, }); self.publish_event(UtxoScannerEvent::Completed { number_scanned: total_scanned, @@ -116,11 +116,20 @@ where TBackend: WalletBackend + 'static Ok(conn) => Ok(conn), Err(e) => { self.publish_event(UtxoScannerEvent::ConnectionFailedToBaseNode { - peer, + peer: peer.clone(), num_retries: self.num_retries, retry_limit: self.retry_limit, error: e.to_string(), }); + // No use re-dialing a peer that is not responsive for recovery mode + if self.mode == UtxoScannerMode::Recovery { + if let Ok(Some(connection)) = self.resources.comms_connectivity.get_connection(peer.clone()).await { + if connection.clone().disconnect().await.is_ok() { + debug!(target: LOG_TARGET, "Disconnected base node peer {}", peer); + } + }; + let _ = time::sleep(Duration::from_secs(30)); + } Err(e.into()) }, @@ -243,14 +252,14 @@ where TBackend: WalletBackend + 'static ); let end_header_hash = end_header.hash(); - let end_header_size = end_header.output_mmr_size; + let output_mmr_size = end_header.output_mmr_size; let mut num_recovered = 0u64; let mut total_amount = MicroTari::from(0); let mut total_scanned = 0; self.publish_event(UtxoScannerEvent::Progress { - current_block: start_mmr_leaf_index, - current_chain_height: (end_header_size - 1), + current_index: start_mmr_leaf_index, + total_index: (output_mmr_size - 1), }); let request = SyncUtxosRequest { start: start_mmr_leaf_index, @@ -259,13 +268,28 @@ where TBackend: WalletBackend + 'static include_deleted_bitmaps: false, }; + let start = Instant::now(); let utxo_stream = client.sync_utxos(request).await?; - // We download in chunks just because rewind_outputs works with multiple outputs (and could parallelized - // rewinding) - let mut utxo_stream = utxo_stream.chunks(10); + trace!( + target: LOG_TARGET, + "bulletproof rewind profile - UTXO stream request time {} ms", + start.elapsed().as_millis(), + ); + + // We download in chunks for improved streaming efficiency + const CHUNK_SIZE: usize = 125; + let mut utxo_stream = utxo_stream.chunks(CHUNK_SIZE); + const COMMIT_EVERY_N: u64 = (1000_i64 / CHUNK_SIZE as i64) as u64; let mut last_utxo_index = 0u64; let mut iteration_count = 0u64; - while let Some(response) = utxo_stream.next().await { + let mut utxo_next_await_profiling = Vec::new(); + let mut scan_for_outputs_profiling = Vec::new(); + while let Some(response) = { + let start = Instant::now(); + let utxo_stream_next = utxo_stream.next().await; + utxo_next_await_profiling.push(start.elapsed()); + utxo_stream_next + } { if self.shutdown_signal.is_triggered() { // if running is set to false, we know its been canceled upstream so lets exit the loop return Ok(total_scanned as u64); @@ -274,14 +298,16 @@ where TBackend: WalletBackend + 'static last_utxo_index = utxo_index; total_scanned += outputs.len(); iteration_count += 1; + + let start = Instant::now(); let found_outputs = self.scan_for_outputs(outputs).await?; + scan_for_outputs_profiling.push(start.elapsed()); // Reduce the number of db hits by only persisting progress every N iterations - const COMMIT_EVERY_N: u64 = 100; - if iteration_count % COMMIT_EVERY_N == 0 || last_utxo_index >= end_header_size - 1 { + if iteration_count % COMMIT_EVERY_N == 0 || last_utxo_index >= output_mmr_size - 1 { self.publish_event(UtxoScannerEvent::Progress { - current_block: last_utxo_index, - current_chain_height: (end_header_size - 1), + current_index: last_utxo_index, + total_index: (output_mmr_size - 1), }); self.update_scanning_progress_in_db( last_utxo_index, @@ -295,11 +321,23 @@ where TBackend: WalletBackend + 'static num_recovered = num_recovered.saturating_add(count); total_amount += amount; } + trace!( + target: LOG_TARGET, + "bulletproof rewind profile - streamed {} outputs in {} ms", + total_scanned, + utxo_next_await_profiling.iter().fold(0, |acc, &x| acc + x.as_millis()), + ); + trace!( + target: LOG_TARGET, + "bulletproof rewind profile - scanned {} outputs in {} ms", + total_scanned, + scan_for_outputs_profiling.iter().fold(0, |acc, &x| acc + x.as_millis()), + ); self.update_scanning_progress_in_db(last_utxo_index, total_amount, num_recovered, end_header_hash) .await?; self.publish_event(UtxoScannerEvent::Progress { - current_block: (end_header_size - 1), - current_chain_height: (end_header_size - 1), + current_index: (output_mmr_size - 1), + total_index: (output_mmr_size - 1), }); Ok(total_scanned as u64) } diff --git a/base_layer/wallet_ffi/src/tasks.rs b/base_layer/wallet_ffi/src/tasks.rs index 9e67eaa091..f538e92721 100644 --- a/base_layer/wallet_ffi/src/tasks.rs +++ b/base_layer/wallet_ffi/src/tasks.rs @@ -87,8 +87,8 @@ pub async fn recovery_event_monitoring( ); }, Ok(UtxoScannerEvent::Progress { - current_block: current, - current_chain_height: total, + current_index: current, + total_index: total, }) => { unsafe { (recovery_progress_callback)(RecoveryEvent::Progress as u8, current, total); diff --git a/common/src/configuration/global.rs b/common/src/configuration/global.rs index 0ee4c92999..51bee7b6c3 100644 --- a/common/src/configuration/global.rs +++ b/common/src/configuration/global.rs @@ -112,6 +112,7 @@ pub struct GlobalConfig { pub base_node_event_channel_size: usize, pub output_manager_event_channel_size: usize, pub wallet_connection_manager_pool_size: usize, + pub wallet_recovery_retry_limit: usize, pub console_wallet_password: Option, pub wallet_command_send_wait_stage: String, pub wallet_command_send_wait_timeout: u64, @@ -484,6 +485,9 @@ fn convert_node_config( let key = "wallet.connection_manager_pool_size"; let wallet_connection_manager_pool_size = optional(cfg.get_int(key))?.unwrap_or(16) as usize; + let key = "wallet.wallet_recovery_retry_limit"; + let wallet_recovery_retry_limit = optional(cfg.get_int(key))?.unwrap_or(3) as usize; + let key = "wallet.output_manager_event_channel_size"; let output_manager_event_channel_size = optional(cfg.get_int(key))?.unwrap_or(250) as usize; @@ -759,6 +763,7 @@ fn convert_node_config( transaction_event_channel_size, base_node_event_channel_size, wallet_connection_manager_pool_size, + wallet_recovery_retry_limit, output_manager_event_channel_size, console_wallet_password, wallet_command_send_wait_stage, diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index 90c7663a38..f06b53441d 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -1,4425 +1,8 @@ { "name": "integration_tests", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "name": "integration_tests", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@cucumber/pretty-formatter": "^1.0.0-alpha.1", - "archiver": "^5.3.0", - "axios": "^0.21.4", - "clone-deep": "^4.0.1", - "csv-parser": "^3.0.0", - "dateformat": "^3.0.3", - "glob": "^7.2.0", - "hex64": "^0.4.0", - "json5": "^2.2.0", - "jszip": "^3.7.1", - "nvm": "^0.0.4", - "sha3": "^2.1.3", - "synchronized-promise": "^0.3.1", - "tari_crypto": "^0.9.1", - "utf8": "^3.0.0", - "wallet-grpc-client": "file:../clients/wallet_grpc_client" - }, - "devDependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@babel/eslint-plugin": "^7.14.5", - "@cucumber/cucumber": "^8.0.0-rc.1", - "@cucumber/pretty-formatter": "^1.0.0-alpha.1", - "@grpc/grpc-js": "^1.4.4", - "@grpc/proto-loader": "^0.5.5", - "blakejs": "^1.1.0", - "chai": "^4.2.0", - "cucumber-html-reporter": "^5.5.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-config-standard": "^16.0.2", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^3.4.1", - "eslint-plugin-promise": "^4.3.1", - "ffi-napi": "^4.0.3", - "grpc-promise": "^1.4.0", - "husky": "^6.0.0", - "prettier": "^2.4.1", - "ref-napi": "^3.0.3" - } - }, - "../clients/wallet_grpc_client": { - "name": "@tari/wallet-grpc-client", - "version": "0.0.1", - "dependencies": { - "@grpc/grpc-js": "^1.3.6", - "@grpc/proto-loader": "^0.5.5", - "grpc-promise": "^1.4.0" - } - }, - "../clients/wallet_grpc_client/node_modules/@grpc/grpc-js": { - "version": "1.3.6", - "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", - "dependencies": { - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "../clients/wallet_grpc_client/node_modules/@grpc/proto-loader": { - "version": "0.5.6", - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - }, - "engines": { - "node": ">=6" - } - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/base64": { - "version": "1.1.2", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/float": { - "version": "1.0.2", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/path": { - "version": "1.1.2", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/pool": { - "version": "1.1.0", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "../clients/wallet_grpc_client/node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "../clients/wallet_grpc_client/node_modules/@types/long": { - "version": "4.0.1", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "../clients/wallet_grpc_client/node_modules/@types/node": { - "version": "16.3.2", - "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" - }, - "../clients/wallet_grpc_client/node_modules/grpc-promise": { - "version": "1.4.0", - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" - }, - "../clients/wallet_grpc_client/node_modules/lodash.camelcase": { - "version": "4.3.0", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "../clients/wallet_grpc_client/node_modules/long": { - "version": "4.0.0", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "../clients/wallet_grpc_client/node_modules/protobufjs": { - "version": "6.11.2", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", - "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.0.tgz", - "integrity": "sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.0.tgz", - "integrity": "sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.0", - "@babel/generator": "^7.16.0", - "@babel/helper-compilation-targets": "^7.16.0", - "@babel/helper-module-transforms": "^7.16.0", - "@babel/helpers": "^7.16.0", - "@babel/parser": "^7.16.0", - "@babel/template": "^7.16.0", - "@babel/traverse": "^7.16.0", - "@babel/types": "^7.16.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.3.tgz", - "integrity": "sha512-iB4ElZT0jAt7PKVaeVulOECdGe6UnmA/O0P9jlF5g5GBOwDVbna8AXhHRu4s27xQf6OkveyA8iTDv1jHdDejgQ==", - "dev": true, - "dependencies": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/@babel/eslint-plugin": { - "version": "7.14.5", - "integrity": "sha512-nzt/YMnOOIRikvSn2hk9+W2omgJBy6U8TN0R+WTTmqapA+HnZTuviZaketdTE9W7/k/+E/DfZlt1ey1NSE39pg==", - "dev": true, - "dependencies": { - "eslint-rule-composer": "^0.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/eslint-parser": ">=7.11.0", - "eslint": ">=7.5.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.0.tgz", - "integrity": "sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz", - "integrity": "sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.16.0", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz", - "integrity": "sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog==", - "dev": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.16.0", - "@babel/template": "^7.16.0", - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz", - "integrity": "sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz", - "integrity": "sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz", - "integrity": "sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz", - "integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz", - "integrity": "sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.16.0", - "@babel/helper-replace-supers": "^7.16.0", - "@babel/helper-simple-access": "^7.16.0", - "@babel/helper-split-export-declaration": "^7.16.0", - "@babel/helper-validator-identifier": "^7.15.7", - "@babel/template": "^7.16.0", - "@babel/traverse": "^7.16.0", - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz", - "integrity": "sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz", - "integrity": "sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA==", - "dev": true, - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.16.0", - "@babel/helper-optimise-call-expression": "^7.16.0", - "@babel/traverse": "^7.16.0", - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz", - "integrity": "sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz", - "integrity": "sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.3.tgz", - "integrity": "sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.16.0", - "@babel/traverse": "^7.16.3", - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.3.tgz", - "integrity": "sha512-dcNwU1O4sx57ClvLBVFbEgx0UZWfd0JQX5X6fxFRCLHelFBGXFfSz6Y0FAq2PEwUqlqLkdVjVr4VASEOuUnLJw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz", - "integrity": "sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.0", - "@babel/parser": "^7.16.0", - "@babel/types": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.3.tgz", - "integrity": "sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.0", - "@babel/generator": "^7.16.0", - "@babel/helper-function-name": "^7.16.0", - "@babel/helper-hoist-variables": "^7.16.0", - "@babel/helper-split-export-declaration": "^7.16.0", - "@babel/parser": "^7.16.3", - "@babel/types": "^7.16.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz", - "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cucumber/create-meta": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/create-meta/-/create-meta-6.0.1.tgz", - "integrity": "sha512-oaNFVBAfduO0GJ1xUtgfGZHvg6+CH56DYaGWPAraayLxvtsQwaOnBYMzzaccGHty/Q6sksQ+IIZK3SuGkTmdvg==", - "dev": true, - "dependencies": { - "@cucumber/messages": "^17.0.1" - } - }, - "node_modules/@cucumber/cucumber": { - "version": "8.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-8.0.0-rc.1.tgz", - "integrity": "sha512-NULLICYYNr0bSig2V/JiHzjONBONPeAo9/iBVoag4P7/GB4ZIeRGaBVzl7FaG8C3blx0AfTh3zfn9h+flFawQA==", - "dev": true, - "dependencies": { - "@cucumber/create-meta": "6.0.1", - "@cucumber/cucumber-expressions": "^14.0.0", - "@cucumber/gherkin": "^22.0.0", - "@cucumber/gherkin-streams": "^4.0.0", - "@cucumber/html-formatter": "^17.0.0", - "@cucumber/messages": "^17.1.1", - "@cucumber/tag-expressions": "^4.1.0", - "assertion-error-formatter": "^3.0.0", - "capital-case": "^1.0.4", - "cli-table3": "^0.6.0", - "colors": "^1.4.0", - "commander": "^8.0.0", - "duration": "^0.2.2", - "durations": "^3.4.2", - "figures": "^3.2.0", - "glob": "^7.1.6", - "indent-string": "^4.0.0", - "is-stream": "^2.0.0", - "knuth-shuffle-seeded": "^1.0.6", - "mz": "^2.7.0", - "progress": "^2.0.3", - "resolve": "^1.19.0", - "resolve-pkg": "^2.0.0", - "stack-chain": "^2.0.0", - "stacktrace-js": "^2.0.2", - "string-argv": "^0.3.1", - "tmp": "^0.2.1", - "util-arity": "^1.1.0", - "verror": "^1.10.0" - }, - "bin": { - "cucumber-js": "bin/cucumber-js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cucumber/cucumber-expressions": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-14.0.0.tgz", - "integrity": "sha512-QiuFBrj4dZRc1Igvp2/nOjUNFyDtO7uHTrzgY9DbwzebYAYOvM6CKGOSxSuPUzxowuc1nuRkzJfFUI1kHaZgPQ==", - "dev": true, - "dependencies": { - "regexp-match-indices": "1.0.2" - } - }, - "node_modules/@cucumber/gherkin": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-22.0.0.tgz", - "integrity": "sha512-D5OghXE8kkZm7pcwo8TvQMgrrXGMXEjERdKLU0T7dQIbc6k0BmMX8dTRh2cwAjH8c7vhwdd0qLU8FPQgGGj+bg==", - "dev": true, - "dependencies": { - "@cucumber/message-streams": "^3.0.0", - "@cucumber/messages": "^17.1.1" - } - }, - "node_modules/@cucumber/gherkin-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-4.0.0.tgz", - "integrity": "sha512-b/guGNeuxr3ghoJOK47QpLhwa2BOdRq+cs2hBYulMLPTiVfwvRBiZlq7P6xdjR9dIpUKBSpzYR6NwaLMgV5DTg==", - "dev": true, - "dependencies": { - "@cucumber/gherkin": "^21.0.0", - "@cucumber/message-streams": "^3.0.0", - "@cucumber/messages": "^17.1.0", - "commander": "8.1.0", - "source-map-support": "0.5.19" - }, - "bin": { - "gherkin-javascript": "bin/gherkin" - } - }, - "node_modules/@cucumber/gherkin-streams/node_modules/@cucumber/gherkin": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-21.0.0.tgz", - "integrity": "sha512-S6YFmTg56iEn563ReePL6Sygb77vwYrGHEr7NwuLIgg20Hi1pp7P80BAYVYNRgU7nK9vG2II9O6kaZbiOXF/5g==", - "dev": true, - "dependencies": { - "@cucumber/message-streams": "^3.0.0", - "@cucumber/messages": "^17.1.0" - } - }, - "node_modules/@cucumber/gherkin-streams/node_modules/commander": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", - "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@cucumber/html-formatter": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-17.0.0.tgz", - "integrity": "sha512-yegA8LY1HYUONyMtTvAYj+aG4zc/6WRtKQxqJahjcdmjgXWcL1BTe8y0lw4BFVqFjaZNI9onOM5KDnMHDm3J/w==", - "dev": true, - "dependencies": { - "@cucumber/messages": "^17.1.0", - "commander": "8.1.0", - "source-map-support": "0.5.19" - }, - "bin": { - "cucumber-html-formatter": "bin/cucumber-html-formatter.js" - } - }, - "node_modules/@cucumber/html-formatter/node_modules/commander": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", - "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/@cucumber/message-streams": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-3.0.0.tgz", - "integrity": "sha512-ABx91nKUebV8mLmpf7BsB3bmQ57CDAfj2EIZswThz+nJHYPAFlZ1JewI6ykFsR9RzJ7/QhgQs0KHeQh7nH/u1Q==", - "dev": true, - "dependencies": { - "@cucumber/messages": "^17.0.0" - } - }, - "node_modules/@cucumber/messages": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-17.1.1.tgz", - "integrity": "sha512-KQMn2Ag+1g1CXp/zKQ7LLqmuHjuQwuXw0N2u5SrDk8r72zPt36SxmDSJK7w6HiFTI+3p5ZuzwLi4S5jop3Tx4g==", - "dev": true, - "dependencies": { - "@types/uuid": "8.3.1", - "class-transformer": "0.4.0", - "reflect-metadata": "0.1.13", - "uuid": "8.3.2" - } - }, - "node_modules/@cucumber/messages/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@cucumber/pretty-formatter": { - "version": "1.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@cucumber/pretty-formatter/-/pretty-formatter-1.0.0-alpha.1.tgz", - "integrity": "sha512-emVFdRkEFAqksd3X9cMWn7cOE2fIPB0aTwZAFZPMO55ZRf+7IaZ7VlEY2Pd5qPxhTXNmyZvaBf6AOoZmx47pmA==", - "dev": true, - "dependencies": { - "ansi-styles": "^5.0.0", - "cli-table3": "^0.6.0", - "figures": "^3.2.0", - "ts-dedent": "^2.0.0" - }, - "peerDependencies": { - "@cucumber/cucumber": ">=7.0.0", - "@cucumber/messages": "*" - } - }, - "node_modules/@cucumber/pretty-formatter/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@cucumber/tag-expressions": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-4.1.0.tgz", - "integrity": "sha512-chTnjxV3vryL75N90wJIMdMafXmZoO2JgNJLYpsfcALL2/IQrRiny3vM9DgD5RDCSt1LNloMtb7rGey9YWxCsA==", - "dev": true - }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.11.0", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.4.4.tgz", - "integrity": "sha512-a6222b7Dl6fIlMgzVl7e+NiRoLiZFbpcwvBH2Oli56Bn7W4/3Ld+86hK4ffPn5rx2DlDidmIcvIJiOQXyhv9gA==", - "dev": true, - "dependencies": { - "@grpc/proto-loader": "^0.6.4", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.6.tgz", - "integrity": "sha512-cdMaPZ8AiFz6ua6PUbP+LKbhwJbFXnrQ/mlnKGUyzDUZ3wp7vPLksnmLCBX6SHgSmjX7CbNVNLFYD5GmmjO4GQ==", - "dev": true, - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.1.1" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.5.6", - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", - "dev": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.0", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", - "dev": true - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", - "dev": true - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dev": true - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", - "dev": true - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "dev": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", - "dev": true - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", - "dev": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", - "dev": true - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", - "dev": true - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", - "dev": true - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "node_modules/@types/long": { - "version": "4.0.1", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "16.10.3", - "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==", - "dev": true - }, - "node_modules/@types/uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", - "dev": true - }, - "node_modules/acorn": { - "version": "7.4.1", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - }, - "node_modules/archiver": { - "version": "5.3.0", - "integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==", - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.0", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.0.0", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.7", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-includes": { - "version": "3.1.4", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.2.5", - "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/assertion-error-formatter": { - "version": "3.0.0", - "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", - "dev": true, - "dependencies": { - "diff": "^4.0.1", - "pad-right": "^0.2.2", - "repeat-string": "^1.6.1" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.1", - "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" - }, - "node_modules/axios": { - "version": "0.21.4", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bindings": { - "version": "1.5.0", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/blakejs": { - "version": "1.1.1", - "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.0.tgz", - "integrity": "sha512-ER2M0g5iAR84fS/zjBDqEgU6iO5fS9JI2EkHr5zxDxYEFk3LjhU9Vpp/INb6RMQphxko7PDV1FH38H/qVP5yCA==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001280", - "electron-to-chromium": "^1.3.896", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/call-bind": { - "version": "1.0.2", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001280", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz", - "integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "node_modules/chai": { - "version": "4.3.4", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/check-error": { - "version": "1.0.2", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/class-transformer": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", - "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==", - "dev": true - }, - "node_modules/cli-table3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", - "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "colors": "^1.1.2" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/colors": { - "version": "1.4.0", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/compress-commons": { - "version": "4.1.1", - "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/convert-source-map": { - "version": "1.8.0", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "node_modules/crc-32": { - "version": "1.2.0", - "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", - "dependencies": { - "exit-on-epipe": "~1.0.1", - "printj": "~1.1.0" - }, - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.2", - "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csv-parser": { - "version": "3.0.0", - "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "csv-parser": "bin/csv-parser" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cucumber-html-reporter": { - "version": "5.5.0", - "integrity": "sha512-kF7vIwvTe7we7Wp/5uNZVZk+Ryozb688LpNvCNhou6N0RmLYPqaoV2aiN8GIB94JUBpribtlq6kDkEUHwxBVeQ==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2", - "find": "^0.3.0", - "fs-extra": "^8.1.0", - "js-base64": "^2.3.2", - "jsonfile": "^5.0.0", - "lodash": "^4.17.11", - "node-emoji": "^1.10.0", - "open": "^6.4.0", - "uuid": "^3.3.3" - } - }, - "node_modules/d": { - "version": "1.0.1", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "node_modules/dateformat": { - "version": "3.0.3", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "engines": { - "node": "*" - } - }, - "node_modules/deasync": { - "version": "0.1.23", - "integrity": "sha512-CGZSokFwidI50GOAmkz/7z3QdMzTQqAiUOzt95PuhKgi6VVztn9D03ZCzzi93uUWlp/v6A9osvNWpIvqHvKjTA==", - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^1.7.1" - }, - "engines": { - "node": ">=0.11.0" - } - }, - "node_modules/deasync/node_modules/node-addon-api": { - "version": "1.7.2", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" - }, - "node_modules/debug": { - "version": "4.3.2", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "3.0.1", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-properties": { - "version": "1.1.3", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/duration": { - "version": "0.2.2", - "integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.46" - } - }, - "node_modules/durations": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/durations/-/durations-3.4.2.tgz", - "integrity": "sha512-V/lf7y33dGaypZZetVI1eu7BmvkbC4dItq12OElLRpKuaU5JxQstV2zHwLv8P7cNbQ+KL1WD80zMCTx5dNC4dg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.3.896", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.896.tgz", - "integrity": "sha512-NcGkBVXePiuUrPLV8IxP43n1EOtdg+dudVjrfVEUd/bOqpQUFZ2diL5PPYzbgEhZFEltdXV3AcyKwGnEQ5lhMA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enquirer": { - "version": "2.3.6", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/error-stack-parser": { - "version": "2.0.6", - "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", - "dev": true, - "dependencies": { - "stackframe": "^1.1.1" - } - }, - "node_modules/es-abstract": { - "version": "1.19.1", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es5-ext": { - "version": "0.10.53", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, - "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint": { - "version": "7.32.0", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.3.0", - "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-config-standard": { - "version": "16.0.3", - "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "peerDependencies": { - "eslint": "^7.12.1", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.2.1 || ^5.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", - "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "find-up": "^2.1.0", - "pkg-dir": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.25.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", - "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.1", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.11.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, - "node_modules/eslint-plugin-node/node_modules/eslint-plugin-es": { - "version": "3.0.1", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-node/node_modules/ignore": { - "version": "5.1.8", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "3.4.1", - "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "eslint": ">=5.0.0", - "prettier": ">=1.13.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-promise": { - "version": "4.3.1", - "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-rule-composer": { - "version": "0.3.0", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.11.0", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/semver": { - "version": "7.3.5", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "7.3.1", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.4.0", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.2.0", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exit-on-epipe": { - "version": "1.0.1", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ext": { - "version": "1.6.0", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", - "dev": true, - "dependencies": { - "type": "^2.5.0" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.5.0", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", - "dev": true - }, - "node_modules/extsprintf": { - "version": "1.4.0", - "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=", - "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-diff": { - "version": "1.2.0", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "node_modules/ffi-napi": { - "version": "4.0.3", - "integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "debug": "^4.1.1", - "get-uv-event-loop-napi-h": "^1.0.5", - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.1", - "ref-napi": "^2.0.1 || ^3.0.2", - "ref-struct-di": "^1.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "node_modules/find": { - "version": "0.3.0", - "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", - "dev": true, - "dependencies": { - "traverse-chain": "~0.1.0" - } - }, - "node_modules/find-up": { - "version": "2.1.0", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.2", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.14.4", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.0", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-from-current-process-h": { - "version": "1.0.2", - "integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==", - "dev": true - }, - "node_modules/get-uv-event-loop-napi-h": { - "version": "1.0.6", - "integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==", - "dev": true, - "dependencies": { - "get-symbol-from-current-process-h": "^1.0.1" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.8", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" - }, - "node_modules/grpc-promise": { - "version": "1.4.0", - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hex64": { - "version": "0.4.0", - "integrity": "sha1-rRB4rIHVfXLeYjKxADvE9vsCh8A=", - "bin": { - "hex64": "bin/hex64" - } - }, - "node_modules/husky": { - "version": "6.0.0", - "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "4.0.6", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.4", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.1", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.6", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.1", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "1.1.0", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/isexe": { - "version": "2.0.0", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-base64": { - "version": "2.6.4", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "5.0.0", - "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", - "dev": true, - "dependencies": { - "universalify": "^0.1.2" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jszip": { - "version": "3.7.1", - "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.7", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/knuth-shuffle-seeded": { - "version": "1.0.6", - "integrity": "sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE=", - "dev": true, - "dependencies": { - "seed-random": "~2.2.0" - } - }, - "node_modules/lazystream": { - "version": "1.0.0", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.7", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/locate-path": { - "version": "2.0.0", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" - }, - "node_modules/long": { - "version": "4.0.0", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "dev": true - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node_modules/ms": { - "version": "2.1.2", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/next-tick": { - "version": "1.0.0", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true - }, - "node_modules/node-emoji": { - "version": "1.11.0", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - } - }, - "node_modules/node-gyp-build": { - "version": "4.3.0", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", - "dev": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nvm": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/nvm/-/nvm-0.0.4.tgz", - "integrity": "sha1-OKF46dMbKDUIyS0VydqGHRqSELw=", - "deprecated": "This is NOT the correct nvm. Visit https://nvm.sh and use the curl command to install it.", - "bin": { - "nvm": "bin/nvm" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.11.0", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.5", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/open": { - "version": "6.4.0", - "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "dev": true, - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pad-right": { - "version": "0.2.2", - "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=", - "dev": true, - "dependencies": { - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/pkg-dir": { - "version": "2.0.0", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "dependencies": { - "find-up": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.4.1", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/printj": { - "version": "1.1.2", - "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", - "bin": { - "printj": "bin/printj.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/progress": { - "version": "2.0.3", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/protobufjs": { - "version": "6.11.2", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.1", - "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", - "dependencies": { - "minimatch": "^3.0.4" - } - }, - "node_modules/ref-napi": { - "version": "3.0.3", - "integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "debug": "^4.1.1", - "get-symbol-from-current-process-h": "^1.0.2", - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.1" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/ref-struct-di": { - "version": "1.1.1", - "integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==", - "dev": true, - "dependencies": { - "debug": "^3.1.0" - } - }, - "node_modules/ref-struct-di/node_modules/debug": { - "version": "3.2.7", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "node_modules/regexp-match-indices": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", - "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", - "dev": true, - "dependencies": { - "regexp-tree": "^0.1.11" - } - }, - "node_modules/regexp-tree": { - "version": "0.1.24", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", - "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", - "dev": true, - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.20.0", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg/-/resolve-pkg-2.0.0.tgz", - "integrity": "sha512-+1lzwXehGCXSeryaISr6WujZzowloigEofRB+dj75y9RRa/obVcYgbHJd53tdYw8pvZj8GojXaaENws8Ktw/hQ==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-pkg/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/seed-random": { - "version": "2.2.0", - "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=", - "dev": true - }, - "node_modules/semver": { - "version": "6.3.0", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-immediate-shim": { - "version": "1.0.1", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sha3": { - "version": "2.1.4", - "integrity": "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==", - "dependencies": { - "buffer": "6.0.3" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/stack-chain": { - "version": "2.0.0", - "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", - "dev": true - }, - "node_modules/stack-generator": { - "version": "2.0.5", - "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", - "dev": true, - "dependencies": { - "stackframe": "^1.1.1" - } - }, - "node_modules/stackframe": { - "version": "1.2.0", - "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==", - "dev": true - }, - "node_modules/stacktrace-gps": { - "version": "3.0.4", - "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", - "dev": true, - "dependencies": { - "source-map": "0.5.6", - "stackframe": "^1.1.1" - } - }, - "node_modules/stacktrace-gps/node_modules/source-map": { - "version": "0.5.6", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stacktrace-js": { - "version": "2.0.2", - "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", - "dev": true, - "dependencies": { - "error-stack-parser": "^2.0.6", - "stack-generator": "^2.0.5", - "stacktrace-gps": "^3.0.4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/string-argv": { - "version": "0.3.1", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/synchronized-promise": { - "version": "0.3.1", - "integrity": "sha512-Iy+JzrERSUrwpOHUDku8HHIddk8V6iLG9bPIzboP2i5RYkn2eSmRB8waSaX7Rc/+DUUsnFsoOHrmniwOp9BOgw==", - "dependencies": { - "deasync": "^0.1.15" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/table": { - "version": "6.7.2", - "integrity": "sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.6.3", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tari_crypto": { - "version": "0.9.1", - "integrity": "sha512-K7LAtwQQKCeTH5CyyO8d/TiPDEePRaJ4e6+hrxpWv6jlkkAiS4m6csBuVqpSjyAlKeP8cQJpUQX2n22akOuZVg==" - }, - "node_modules/text-table": { - "version": "0.2.0", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/traverse-chain": { - "version": "0.1.0", - "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", - "dev": true - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true, - "engines": { - "node": ">=6.10" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.11.0", - "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "node_modules/type": { - "version": "1.2.0", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.4.0", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8": { - "version": "3.0.0", - "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" - }, - "node_modules/util-arity": { - "version": "1.1.0", - "integrity": "sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA=", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/uuid": { - "version": "3.4.0", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/verror": { - "version": "1.10.0", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/wallet-grpc-client": { - "resolved": "../clients/wallet_grpc_client", - "link": true - }, - "node_modules/which": { - "version": "2.0.2", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/zip-stream": { - "version": "4.1.0", - "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", - "dependencies": { - "archiver-utils": "^2.1.0", - "compress-commons": "^4.1.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - } - }, "dependencies": { "@babel/code-frame": { "version": "7.16.0", @@ -4472,6 +55,7 @@ }, "@babel/eslint-plugin": { "version": "7.14.5", + "resolved": false, "integrity": "sha512-nzt/YMnOOIRikvSn2hk9+W2omgJBy6U8TN0R+WTTmqapA+HnZTuviZaketdTE9W7/k/+E/DfZlt1ey1NSE39pg==", "dev": true, "requires": { @@ -4605,12 +189,13 @@ }, "@babel/helper-validator-identifier": { "version": "7.15.7", + "resolved": false, "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true }, "@babel/helper-validator-option": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "resolved": false, "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", "dev": true }, @@ -4828,7 +413,6 @@ "version": "1.0.0-alpha.1", "resolved": "https://registry.npmjs.org/@cucumber/pretty-formatter/-/pretty-formatter-1.0.0-alpha.1.tgz", "integrity": "sha512-emVFdRkEFAqksd3X9cMWn7cOE2fIPB0aTwZAFZPMO55ZRf+7IaZ7VlEY2Pd5qPxhTXNmyZvaBf6AOoZmx47pmA==", - "dev": true, "requires": { "ansi-styles": "^5.0.0", "cli-table3": "^0.6.0", @@ -4839,8 +423,7 @@ "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" } } }, @@ -4852,6 +435,7 @@ }, "@eslint/eslintrc": { "version": "0.4.3", + "resolved": false, "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dev": true, "requires": { @@ -4868,6 +452,7 @@ "dependencies": { "globals": { "version": "13.11.0", + "resolved": false, "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", "dev": true, "requires": { @@ -4876,6 +461,7 @@ }, "type-fest": { "version": "0.20.2", + "resolved": false, "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true } @@ -4908,6 +494,7 @@ }, "@grpc/proto-loader": { "version": "0.5.6", + "resolved": false, "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", "dev": true, "requires": { @@ -4917,6 +504,7 @@ }, "@humanwhocodes/config-array": { "version": "0.5.0", + "resolved": false, "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", "dev": true, "requires": { @@ -4927,31 +515,37 @@ }, "@humanwhocodes/object-schema": { "version": "1.2.0", + "resolved": false, "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, "@protobufjs/aspromise": { "version": "1.1.2", + "resolved": false, "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", "dev": true }, "@protobufjs/base64": { "version": "1.1.2", + "resolved": false, "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "dev": true }, "@protobufjs/codegen": { "version": "2.0.4", + "resolved": false, "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", "dev": true }, "@protobufjs/eventemitter": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", "dev": true }, "@protobufjs/fetch": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", "dev": true, "requires": { @@ -4961,41 +555,49 @@ }, "@protobufjs/float": { "version": "1.0.2", + "resolved": false, "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", "dev": true }, "@protobufjs/inquire": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", "dev": true }, "@protobufjs/path": { "version": "1.1.2", + "resolved": false, "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", "dev": true }, "@protobufjs/pool": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", "dev": true }, "@protobufjs/utf8": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", "dev": true }, "@types/json5": { "version": "0.0.29", + "resolved": false, "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, "@types/long": { "version": "4.0.1", + "resolved": false, "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", "dev": true }, "@types/node": { "version": "16.10.3", + "resolved": false, "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==", "dev": true }, @@ -5007,17 +609,19 @@ }, "acorn": { "version": "7.4.1", + "resolved": false, "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true }, "acorn-jsx": { "version": "5.3.2", + "resolved": false, "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv": { "version": "6.12.6", + "resolved": false, "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { @@ -5029,16 +633,18 @@ }, "ansi-colors": { "version": "4.1.1", + "resolved": false, "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, "ansi-regex": { "version": "5.0.1", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "resolved": false, + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "3.2.1", + "resolved": false, "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { @@ -5047,11 +653,13 @@ }, "any-promise": { "version": "1.3.0", + "resolved": false, "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", "dev": true }, "archiver": { "version": "5.3.0", + "resolved": false, "integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==", "requires": { "archiver-utils": "^2.1.0", @@ -5065,6 +673,7 @@ }, "archiver-utils": { "version": "2.1.0", + "resolved": false, "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "requires": { "glob": "^7.1.4", @@ -5081,6 +690,7 @@ "dependencies": { "readable-stream": { "version": "2.3.7", + "resolved": false, "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", @@ -5094,6 +704,7 @@ }, "string_decoder": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -5103,6 +714,7 @@ }, "argparse": { "version": "1.0.10", + "resolved": false, "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { @@ -5111,6 +723,7 @@ }, "array-includes": { "version": "3.1.4", + "resolved": false, "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", "dev": true, "requires": { @@ -5123,6 +736,7 @@ }, "array.prototype.flat": { "version": "1.2.5", + "resolved": false, "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", "dev": true, "requires": { @@ -5133,16 +747,19 @@ }, "assert-plus": { "version": "1.0.0", + "resolved": false, "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, "assertion-error": { "version": "1.1.0", + "resolved": false, "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, "assertion-error-formatter": { "version": "3.0.0", + "resolved": false, "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", "dev": true, "requires": { @@ -5153,15 +770,18 @@ }, "astral-regex": { "version": "2.0.0", + "resolved": false, "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { "version": "3.2.1", + "resolved": false, "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" }, "axios": { "version": "0.21.4", + "resolved": false, "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { "follow-redirects": "^1.14.0" @@ -5169,14 +789,17 @@ }, "balanced-match": { "version": "1.0.2", + "resolved": false, "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base64-js": { "version": "1.5.1", + "resolved": false, "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bindings": { "version": "1.5.0", + "resolved": false, "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "requires": { "file-uri-to-path": "1.0.0" @@ -5184,6 +807,7 @@ }, "bl": { "version": "4.1.0", + "resolved": false, "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "requires": { "buffer": "^5.5.0", @@ -5193,6 +817,7 @@ "dependencies": { "buffer": { "version": "5.7.1", + "resolved": false, "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "requires": { "base64-js": "^1.3.1", @@ -5203,11 +828,13 @@ }, "blakejs": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==", "dev": true }, "brace-expansion": { "version": "1.1.11", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", @@ -5229,6 +856,7 @@ }, "buffer": { "version": "6.0.3", + "resolved": false, "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "requires": { "base64-js": "^1.3.1", @@ -5237,6 +865,7 @@ }, "buffer-crc32": { "version": "0.2.13", + "resolved": false, "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, "buffer-from": { @@ -5247,6 +876,7 @@ }, "call-bind": { "version": "1.0.2", + "resolved": false, "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, "requires": { @@ -5256,6 +886,7 @@ }, "callsites": { "version": "3.1.0", + "resolved": false, "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, @@ -5278,6 +909,7 @@ }, "chai": { "version": "4.3.4", + "resolved": false, "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, "requires": { @@ -5291,6 +923,7 @@ }, "chalk": { "version": "2.4.2", + "resolved": false, "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { @@ -5301,6 +934,7 @@ }, "check-error": { "version": "1.0.2", + "resolved": false, "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, @@ -5314,7 +948,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", - "dev": true, "requires": { "colors": "^1.1.2", "object-assign": "^4.1.0", @@ -5334,6 +967,7 @@ }, "clone-deep": { "version": "4.0.1", + "resolved": false, "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "requires": { "is-plain-object": "^2.0.4", @@ -5343,6 +977,7 @@ }, "color-convert": { "version": "1.9.3", + "resolved": false, "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { @@ -5351,13 +986,14 @@ }, "color-name": { "version": "1.1.3", + "resolved": false, "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, "colors": { "version": "1.4.0", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true + "resolved": false, + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "commander": { "version": "8.3.0", @@ -5367,6 +1003,7 @@ }, "compress-commons": { "version": "4.1.1", + "resolved": false, "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", "requires": { "buffer-crc32": "^0.2.13", @@ -5377,10 +1014,12 @@ }, "concat-map": { "version": "0.0.1", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "convert-source-map": { "version": "1.8.0", + "resolved": false, "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, "requires": { @@ -5389,10 +1028,12 @@ }, "core-util-is": { "version": "1.0.2", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "crc-32": { "version": "1.2.0", + "resolved": false, "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", "requires": { "exit-on-epipe": "~1.0.1", @@ -5401,6 +1042,7 @@ }, "crc32-stream": { "version": "4.0.2", + "resolved": false, "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", "requires": { "crc-32": "^1.2.0", @@ -5409,6 +1051,7 @@ }, "cross-spawn": { "version": "7.0.3", + "resolved": false, "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { @@ -5419,6 +1062,7 @@ }, "csv-parser": { "version": "3.0.0", + "resolved": false, "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", "requires": { "minimist": "^1.2.0" @@ -5426,6 +1070,7 @@ }, "cucumber-html-reporter": { "version": "5.5.0", + "resolved": false, "integrity": "sha512-kF7vIwvTe7we7Wp/5uNZVZk+Ryozb688LpNvCNhou6N0RmLYPqaoV2aiN8GIB94JUBpribtlq6kDkEUHwxBVeQ==", "dev": true, "requires": { @@ -5442,6 +1087,7 @@ }, "d": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "dev": true, "requires": { @@ -5451,10 +1097,12 @@ }, "dateformat": { "version": "3.0.3", + "resolved": false, "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" }, "deasync": { "version": "0.1.23", + "resolved": false, "integrity": "sha512-CGZSokFwidI50GOAmkz/7z3QdMzTQqAiUOzt95PuhKgi6VVztn9D03ZCzzi93uUWlp/v6A9osvNWpIvqHvKjTA==", "requires": { "bindings": "^1.5.0", @@ -5463,12 +1111,14 @@ "dependencies": { "node-addon-api": { "version": "1.7.2", + "resolved": false, "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" } } }, "debug": { "version": "4.3.2", + "resolved": false, "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { @@ -5477,6 +1127,7 @@ }, "deep-eql": { "version": "3.0.1", + "resolved": false, "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { @@ -5485,11 +1136,13 @@ }, "deep-is": { "version": "0.1.4", + "resolved": false, "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "define-properties": { "version": "1.1.3", + "resolved": false, "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { @@ -5498,11 +1151,13 @@ }, "diff": { "version": "4.0.2", + "resolved": false, "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, "doctrine": { "version": "3.0.0", + "resolved": false, "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { @@ -5511,6 +1166,7 @@ }, "duration": { "version": "0.2.2", + "resolved": false, "integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==", "dev": true, "requires": { @@ -5532,11 +1188,12 @@ }, "emoji-regex": { "version": "8.0.0", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "resolved": false, + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "end-of-stream": { "version": "1.4.4", + "resolved": false, "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { "once": "^1.4.0" @@ -5544,6 +1201,7 @@ }, "enquirer": { "version": "2.3.6", + "resolved": false, "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, "requires": { @@ -5552,6 +1210,7 @@ }, "error-stack-parser": { "version": "2.0.6", + "resolved": false, "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", "dev": true, "requires": { @@ -5560,6 +1219,7 @@ }, "es-abstract": { "version": "1.19.1", + "resolved": false, "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, "requires": { @@ -5587,6 +1247,7 @@ }, "es-to-primitive": { "version": "1.2.1", + "resolved": false, "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { @@ -5597,6 +1258,7 @@ }, "es5-ext": { "version": "0.10.53", + "resolved": false, "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { @@ -5607,6 +1269,7 @@ }, "es6-iterator": { "version": "2.0.3", + "resolved": false, "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { @@ -5617,6 +1280,7 @@ }, "es6-symbol": { "version": "3.1.3", + "resolved": false, "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { @@ -5626,16 +1290,18 @@ }, "escalade": { "version": "3.1.1", + "resolved": false, "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, "escape-string-regexp": { "version": "1.0.5", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "resolved": false, + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "7.32.0", + "resolved": false, "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "requires": { @@ -5683,6 +1349,7 @@ "dependencies": { "@babel/code-frame": { "version": "7.12.11", + "resolved": false, "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { @@ -5691,6 +1358,7 @@ }, "ansi-styles": { "version": "4.3.0", + "resolved": false, "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { @@ -5699,6 +1367,7 @@ }, "chalk": { "version": "4.1.2", + "resolved": false, "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { @@ -5708,6 +1377,7 @@ }, "color-convert": { "version": "2.0.1", + "resolved": false, "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { @@ -5716,16 +1386,19 @@ }, "color-name": { "version": "1.1.4", + "resolved": false, "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "escape-string-regexp": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "globals": { "version": "13.11.0", + "resolved": false, "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", "dev": true, "requires": { @@ -5734,11 +1407,13 @@ }, "has-flag": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "semver": { "version": "7.3.5", + "resolved": false, "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { @@ -5747,6 +1422,7 @@ }, "supports-color": { "version": "7.2.0", + "resolved": false, "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { @@ -5755,6 +1431,7 @@ }, "type-fest": { "version": "0.20.2", + "resolved": false, "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true } @@ -5762,18 +1439,19 @@ }, "eslint-config-prettier": { "version": "8.3.0", + "resolved": false, "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-standard": { "version": "16.0.3", + "resolved": false, "integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", + "resolved": false, "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", "dev": true, "requires": { @@ -5783,6 +1461,7 @@ "dependencies": { "debug": { "version": "3.2.7", + "resolved": false, "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { @@ -5804,6 +1483,7 @@ "dependencies": { "debug": { "version": "3.2.7", + "resolved": false, "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { @@ -5835,6 +1515,7 @@ "dependencies": { "debug": { "version": "2.6.9", + "resolved": false, "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { @@ -5843,6 +1524,7 @@ }, "doctrine": { "version": "2.1.0", + "resolved": false, "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { @@ -5851,6 +1533,7 @@ }, "ms": { "version": "2.0.0", + "resolved": false, "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true } @@ -5858,6 +1541,7 @@ }, "eslint-plugin-node": { "version": "11.1.0", + "resolved": false, "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, "requires": { @@ -5871,6 +1555,7 @@ "dependencies": { "eslint-plugin-es": { "version": "3.0.1", + "resolved": false, "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", "dev": true, "requires": { @@ -5880,6 +1565,7 @@ }, "ignore": { "version": "5.1.8", + "resolved": false, "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true } @@ -5887,6 +1573,7 @@ }, "eslint-plugin-prettier": { "version": "3.4.1", + "resolved": false, "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", "dev": true, "requires": { @@ -5895,16 +1582,19 @@ }, "eslint-plugin-promise": { "version": "4.3.1", + "resolved": false, "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==", "dev": true }, "eslint-rule-composer": { "version": "0.3.0", + "resolved": false, "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", "dev": true }, "eslint-scope": { "version": "5.1.1", + "resolved": false, "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { @@ -5914,6 +1604,7 @@ }, "eslint-utils": { "version": "2.1.0", + "resolved": false, "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { @@ -5922,6 +1613,7 @@ "dependencies": { "eslint-visitor-keys": { "version": "1.3.0", + "resolved": false, "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true } @@ -5929,11 +1621,13 @@ }, "eslint-visitor-keys": { "version": "2.1.0", + "resolved": false, "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true }, "espree": { "version": "7.3.1", + "resolved": false, "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { @@ -5944,6 +1638,7 @@ "dependencies": { "eslint-visitor-keys": { "version": "1.3.0", + "resolved": false, "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true } @@ -5951,11 +1646,13 @@ }, "esprima": { "version": "4.0.1", + "resolved": false, "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, "esquery": { "version": "1.4.0", + "resolved": false, "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { @@ -5964,6 +1661,7 @@ "dependencies": { "estraverse": { "version": "5.2.0", + "resolved": false, "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true } @@ -5971,6 +1669,7 @@ }, "esrecurse": { "version": "4.3.0", + "resolved": false, "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { @@ -5979,6 +1678,7 @@ "dependencies": { "estraverse": { "version": "5.2.0", + "resolved": false, "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true } @@ -5986,20 +1686,24 @@ }, "estraverse": { "version": "4.3.0", + "resolved": false, "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "esutils": { "version": "2.0.3", + "resolved": false, "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, "exit-on-epipe": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" }, "ext": { "version": "1.6.0", + "resolved": false, "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "dev": true, "requires": { @@ -6008,6 +1712,7 @@ "dependencies": { "type": { "version": "2.5.0", + "resolved": false, "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", "dev": true } @@ -6015,31 +1720,37 @@ }, "extsprintf": { "version": "1.4.0", + "resolved": false, "integrity": "sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=", "dev": true }, "fast-deep-equal": { "version": "3.1.3", + "resolved": false, "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-diff": { "version": "1.2.0", + "resolved": false, "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, "fast-json-stable-stringify": { "version": "2.1.0", + "resolved": false, "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { "version": "2.0.6", + "resolved": false, "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, "ffi-napi": { "version": "4.0.3", + "resolved": false, "integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==", "dev": true, "requires": { @@ -6053,14 +1764,15 @@ }, "figures": { "version": "3.2.0", + "resolved": false, "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, "requires": { "escape-string-regexp": "^1.0.5" } }, "file-entry-cache": { "version": "6.0.1", + "resolved": false, "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { @@ -6069,10 +1781,12 @@ }, "file-uri-to-path": { "version": "1.0.0", + "resolved": false, "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "find": { "version": "0.3.0", + "resolved": false, "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", "dev": true, "requires": { @@ -6081,6 +1795,7 @@ }, "find-up": { "version": "2.1.0", + "resolved": false, "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { @@ -6089,6 +1804,7 @@ }, "flat-cache": { "version": "3.0.4", + "resolved": false, "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { @@ -6098,19 +1814,23 @@ }, "flatted": { "version": "3.2.2", + "resolved": false, "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", "dev": true }, "follow-redirects": { "version": "1.14.4", + "resolved": false, "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" }, "fs-constants": { "version": "1.0.0", + "resolved": false, "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { "version": "8.1.0", + "resolved": false, "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "requires": { @@ -6121,6 +1841,7 @@ "dependencies": { "jsonfile": { "version": "4.0.0", + "resolved": false, "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, "requires": { @@ -6131,20 +1852,24 @@ }, "fs.realpath": { "version": "1.0.0", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "function-bind": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, "functional-red-black-tree": { "version": "1.0.1", + "resolved": false, "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, "gensync": { "version": "1.0.0-beta.2", + "resolved": false, "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, @@ -6156,11 +1881,13 @@ }, "get-func-name": { "version": "2.0.0", + "resolved": false, "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, "get-intrinsic": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dev": true, "requires": { @@ -6171,6 +1898,7 @@ }, "get-symbol-description": { "version": "1.0.0", + "resolved": false, "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dev": true, "requires": { @@ -6180,11 +1908,13 @@ }, "get-symbol-from-current-process-h": { "version": "1.0.2", + "resolved": false, "integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==", "dev": true }, "get-uv-event-loop-napi-h": { "version": "1.0.6", + "resolved": false, "integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==", "dev": true, "requires": { @@ -6193,6 +1923,7 @@ }, "glob": { "version": "7.2.0", + "resolved": false, "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", @@ -6205,6 +1936,7 @@ }, "glob-parent": { "version": "5.1.2", + "resolved": false, "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { @@ -6213,21 +1945,24 @@ }, "globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "resolved": false, "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "graceful-fs": { "version": "4.2.8", + "resolved": false, "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "grpc-promise": { "version": "1.4.0", + "resolved": false, "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==", "dev": true }, "has": { "version": "1.0.3", + "resolved": false, "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { @@ -6236,21 +1971,25 @@ }, "has-bigints": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, "has-flag": { "version": "3.0.0", + "resolved": false, "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "has-symbols": { "version": "1.0.2", + "resolved": false, "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true }, "has-tostringtag": { "version": "1.0.0", + "resolved": false, "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, "requires": { @@ -6259,28 +1998,34 @@ }, "hex64": { "version": "0.4.0", + "resolved": false, "integrity": "sha1-rRB4rIHVfXLeYjKxADvE9vsCh8A=" }, "husky": { "version": "6.0.0", + "resolved": false, "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", "dev": true }, "ieee754": { "version": "1.2.1", + "resolved": false, "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "4.0.6", + "resolved": false, "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, "immediate": { "version": "3.0.6", + "resolved": false, "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, "import-fresh": { "version": "3.3.0", + "resolved": false, "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { @@ -6290,16 +2035,19 @@ }, "imurmurhash": { "version": "0.1.4", + "resolved": false, "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "indent-string": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, "inflight": { "version": "1.0.6", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", @@ -6308,10 +2056,12 @@ }, "inherits": { "version": "2.0.4", + "resolved": false, "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { "version": "1.0.3", + "resolved": false, "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, "requires": { @@ -6322,6 +2072,7 @@ }, "is-bigint": { "version": "1.0.4", + "resolved": false, "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "requires": { @@ -6330,6 +2081,7 @@ }, "is-boolean-object": { "version": "1.1.2", + "resolved": false, "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "requires": { @@ -6339,6 +2091,7 @@ }, "is-callable": { "version": "1.2.4", + "resolved": false, "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "dev": true }, @@ -6353,6 +2106,7 @@ }, "is-date-object": { "version": "1.0.5", + "resolved": false, "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "requires": { @@ -6361,17 +2115,18 @@ }, "is-extglob": { "version": "2.1.1", + "resolved": false, "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.3", + "resolved": false, "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { @@ -6380,11 +2135,13 @@ }, "is-negative-zero": { "version": "2.0.1", + "resolved": false, "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", "dev": true }, "is-number-object": { "version": "1.0.6", + "resolved": false, "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", "dev": true, "requires": { @@ -6393,6 +2150,7 @@ }, "is-plain-object": { "version": "2.0.4", + "resolved": false, "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "requires": { "isobject": "^3.0.1" @@ -6400,6 +2158,7 @@ }, "is-regex": { "version": "1.1.4", + "resolved": false, "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { @@ -6409,16 +2168,19 @@ }, "is-shared-array-buffer": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", "dev": true }, "is-stream": { "version": "2.0.1", + "resolved": false, "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, "is-string": { "version": "1.0.7", + "resolved": false, "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, "requires": { @@ -6427,6 +2189,7 @@ }, "is-symbol": { "version": "1.0.4", + "resolved": false, "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, "requires": { @@ -6435,6 +2198,7 @@ }, "is-weakref": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", "dev": true, "requires": { @@ -6443,34 +2207,41 @@ }, "is-wsl": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, "isarray": { "version": "1.0.0", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", + "resolved": false, "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "isobject": { "version": "3.0.1", + "resolved": false, "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "js-base64": { "version": "2.6.4", + "resolved": false, "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", "dev": true }, "js-tokens": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "js-yaml": { "version": "3.14.1", + "resolved": false, "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "requires": { @@ -6480,23 +2251,25 @@ }, "jsesc": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "resolved": false, "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, "json-schema-traverse": { "version": "0.4.1", + "resolved": false, "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": false, "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, "json5": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "resolved": false, "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "requires": { "minimist": "^1.2.5" @@ -6504,6 +2277,7 @@ }, "jsonfile": { "version": "5.0.0", + "resolved": false, "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", "dev": true, "requires": { @@ -6513,6 +2287,7 @@ }, "jszip": { "version": "3.7.1", + "resolved": false, "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "requires": { "lie": "~3.3.0", @@ -6523,6 +2298,7 @@ "dependencies": { "readable-stream": { "version": "2.3.7", + "resolved": false, "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", @@ -6536,6 +2312,7 @@ }, "string_decoder": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -6545,10 +2322,12 @@ }, "kind-of": { "version": "6.0.3", + "resolved": false, "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "knuth-shuffle-seeded": { "version": "1.0.6", + "resolved": false, "integrity": "sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE=", "dev": true, "requires": { @@ -6557,6 +2336,7 @@ }, "lazystream": { "version": "1.0.0", + "resolved": false, "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", "requires": { "readable-stream": "^2.0.5" @@ -6564,6 +2344,7 @@ "dependencies": { "readable-stream": { "version": "2.3.7", + "resolved": false, "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", @@ -6577,6 +2358,7 @@ }, "string_decoder": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -6586,6 +2368,7 @@ }, "levn": { "version": "0.4.1", + "resolved": false, "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { @@ -6595,6 +2378,7 @@ }, "lie": { "version": "3.3.0", + "resolved": false, "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "requires": { "immediate": "~3.0.5" @@ -6602,6 +2386,7 @@ }, "locate-path": { "version": "2.0.0", + "resolved": false, "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { @@ -6611,51 +2396,62 @@ }, "lodash": { "version": "4.17.21", + "resolved": false, "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.camelcase": { "version": "4.3.0", + "resolved": false, "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, "lodash.clonedeep": { "version": "4.5.0", + "resolved": false, "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, "lodash.defaults": { "version": "4.2.0", + "resolved": false, "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" }, "lodash.difference": { "version": "4.5.0", + "resolved": false, "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" }, "lodash.flatten": { "version": "4.4.0", + "resolved": false, "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, "lodash.isplainobject": { "version": "4.0.6", + "resolved": false, "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, "lodash.merge": { "version": "4.6.2", + "resolved": false, "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "lodash.truncate": { "version": "4.4.2", + "resolved": false, "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, "lodash.union": { "version": "4.6.0", + "resolved": false, "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" }, "long": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "dev": true }, @@ -6670,6 +2466,7 @@ }, "lru-cache": { "version": "6.0.0", + "resolved": false, "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { @@ -6678,6 +2475,7 @@ }, "minimatch": { "version": "3.0.4", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" @@ -6685,15 +2483,18 @@ }, "minimist": { "version": "1.2.5", + "resolved": false, "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "ms": { "version": "2.1.2", + "resolved": false, "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "mz": { "version": "2.7.0", + "resolved": false, "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "requires": { @@ -6704,11 +2505,13 @@ }, "natural-compare": { "version": "1.4.0", + "resolved": false, "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, "next-tick": { "version": "1.0.0", + "resolved": false, "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -6724,11 +2527,13 @@ }, "node-addon-api": { "version": "3.2.1", + "resolved": false, "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, "node-emoji": { "version": "1.11.0", + "resolved": false, "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", "dev": true, "requires": { @@ -6737,6 +2542,7 @@ }, "node-gyp-build": { "version": "4.3.0", + "resolved": false, "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", "dev": true }, @@ -6748,6 +2554,7 @@ }, "normalize-path": { "version": "3.0.0", + "resolved": false, "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "nvm": { @@ -6757,21 +2564,24 @@ }, "object-assign": { "version": "4.1.1", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "resolved": false, + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { "version": "1.11.0", + "resolved": false, "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", "dev": true }, "object-keys": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, "object.assign": { "version": "4.1.2", + "resolved": false, "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { @@ -6783,6 +2593,7 @@ }, "object.values": { "version": "1.1.5", + "resolved": false, "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", "dev": true, "requires": { @@ -6793,6 +2604,7 @@ }, "once": { "version": "1.4.0", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" @@ -6800,6 +2612,7 @@ }, "open": { "version": "6.4.0", + "resolved": false, "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", "dev": true, "requires": { @@ -6808,6 +2621,7 @@ }, "optionator": { "version": "0.9.1", + "resolved": false, "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { @@ -6821,6 +2635,7 @@ }, "p-limit": { "version": "1.3.0", + "resolved": false, "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { @@ -6829,6 +2644,7 @@ }, "p-locate": { "version": "2.0.0", + "resolved": false, "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { @@ -6837,11 +2653,13 @@ }, "p-try": { "version": "1.0.0", + "resolved": false, "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "pad-right": { "version": "0.2.2", + "resolved": false, "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=", "dev": true, "requires": { @@ -6850,10 +2668,12 @@ }, "pako": { "version": "1.0.11", + "resolved": false, "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parent-module": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "requires": { @@ -6862,25 +2682,30 @@ }, "path-exists": { "version": "3.0.0", + "resolved": false, "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true }, "path-is-absolute": { "version": "1.0.1", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", + "resolved": false, "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "path-parse": { "version": "1.0.7", + "resolved": false, "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "pathval": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, @@ -6892,6 +2717,7 @@ }, "pkg-dir": { "version": "2.0.0", + "resolved": false, "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", "dev": true, "requires": { @@ -6900,16 +2726,19 @@ }, "prelude-ls": { "version": "1.2.1", + "resolved": false, "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, "prettier": { "version": "2.4.1", + "resolved": false, "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", "dev": true }, "prettier-linter-helpers": { "version": "1.0.0", + "resolved": false, "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, "requires": { @@ -6918,19 +2747,23 @@ }, "printj": { "version": "1.1.2", + "resolved": false, "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" }, "process-nextick-args": { "version": "2.0.1", + "resolved": false, "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", + "resolved": false, "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, "protobufjs": { "version": "6.11.2", + "resolved": false, "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "dev": true, "requires": { @@ -6951,11 +2784,13 @@ }, "punycode": { "version": "2.1.1", + "resolved": false, "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, "readable-stream": { "version": "3.6.0", + "resolved": false, "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { "inherits": "^2.0.3", @@ -6965,6 +2800,7 @@ }, "readdir-glob": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", "requires": { "minimatch": "^3.0.4" @@ -6972,6 +2808,7 @@ }, "ref-napi": { "version": "3.0.3", + "resolved": false, "integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==", "dev": true, "requires": { @@ -6983,6 +2820,7 @@ }, "ref-struct-di": { "version": "1.1.1", + "resolved": false, "integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==", "dev": true, "requires": { @@ -6991,6 +2829,7 @@ "dependencies": { "debug": { "version": "3.2.7", + "resolved": false, "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { @@ -7022,11 +2861,13 @@ }, "regexpp": { "version": "3.2.0", + "resolved": false, "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "repeat-string": { "version": "1.6.1", + "resolved": false, "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, @@ -7038,11 +2879,13 @@ }, "require-from-string": { "version": "2.0.2", + "resolved": false, "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "resolve": { "version": "1.20.0", + "resolved": false, "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { @@ -7052,6 +2895,7 @@ }, "resolve-from": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, @@ -7074,6 +2918,7 @@ }, "rimraf": { "version": "3.0.2", + "resolved": false, "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { @@ -7082,24 +2927,29 @@ }, "safe-buffer": { "version": "5.1.2", + "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "seed-random": { "version": "2.2.0", + "resolved": false, "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=", "dev": true }, "semver": { "version": "6.3.0", + "resolved": false, "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, "set-immediate-shim": { "version": "1.0.1", + "resolved": false, "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" }, "sha3": { "version": "2.1.4", + "resolved": false, "integrity": "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==", "requires": { "buffer": "6.0.3" @@ -7107,6 +2957,7 @@ }, "shallow-clone": { "version": "3.0.1", + "resolved": false, "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "requires": { "kind-of": "^6.0.2" @@ -7114,6 +2965,7 @@ }, "shebang-command": { "version": "2.0.0", + "resolved": false, "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { @@ -7122,11 +2974,13 @@ }, "shebang-regex": { "version": "3.0.0", + "resolved": false, "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "side-channel": { "version": "1.0.4", + "resolved": false, "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "requires": { @@ -7137,6 +2991,7 @@ }, "slice-ansi": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { @@ -7147,6 +3002,7 @@ "dependencies": { "ansi-styles": { "version": "4.3.0", + "resolved": false, "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { @@ -7155,6 +3011,7 @@ }, "color-convert": { "version": "2.0.1", + "resolved": false, "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { @@ -7163,6 +3020,7 @@ }, "color-name": { "version": "1.1.4", + "resolved": false, "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true } @@ -7170,7 +3028,7 @@ }, "source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "resolved": false, "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, @@ -7194,16 +3052,19 @@ }, "sprintf-js": { "version": "1.0.3", + "resolved": false, "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, "stack-chain": { "version": "2.0.0", + "resolved": false, "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", "dev": true }, "stack-generator": { "version": "2.0.5", + "resolved": false, "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==", "dev": true, "requires": { @@ -7212,11 +3073,13 @@ }, "stackframe": { "version": "1.2.0", + "resolved": false, "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==", "dev": true }, "stacktrace-gps": { "version": "3.0.4", + "resolved": false, "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==", "dev": true, "requires": { @@ -7226,6 +3089,7 @@ "dependencies": { "source-map": { "version": "0.5.6", + "resolved": false, "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", "dev": true } @@ -7233,6 +3097,7 @@ }, "stacktrace-js": { "version": "2.0.2", + "resolved": false, "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", "dev": true, "requires": { @@ -7241,21 +3106,9 @@ "stacktrace-gps": "^3.0.4" } }, - "string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, "string-argv": { "version": "0.3.1", + "resolved": false, "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true }, @@ -7263,7 +3116,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7272,6 +3124,7 @@ }, "string.prototype.trimend": { "version": "1.0.4", + "resolved": false, "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "dev": true, "requires": { @@ -7281,6 +3134,7 @@ }, "string.prototype.trimstart": { "version": "1.0.4", + "resolved": false, "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", "dev": true, "requires": { @@ -7288,26 +3142,44 @@ "define-properties": "^1.1.3" } }, + "string_decoder": { + "version": "1.3.0", + "resolved": false, + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": false, + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "strip-ansi": { "version": "6.0.1", + "resolved": false, "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } }, "strip-bom": { "version": "3.0.0", + "resolved": false, "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, "strip-json-comments": { "version": "3.1.1", + "resolved": false, "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { "version": "5.5.0", + "resolved": false, "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { @@ -7316,6 +3188,7 @@ }, "synchronized-promise": { "version": "0.3.1", + "resolved": false, "integrity": "sha512-Iy+JzrERSUrwpOHUDku8HHIddk8V6iLG9bPIzboP2i5RYkn2eSmRB8waSaX7Rc/+DUUsnFsoOHrmniwOp9BOgw==", "requires": { "deasync": "^0.1.15" @@ -7323,6 +3196,7 @@ }, "table": { "version": "6.7.2", + "resolved": false, "integrity": "sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==", "dev": true, "requires": { @@ -7336,6 +3210,7 @@ "dependencies": { "ajv": { "version": "8.6.3", + "resolved": false, "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", "dev": true, "requires": { @@ -7347,6 +3222,7 @@ }, "json-schema-traverse": { "version": "1.0.0", + "resolved": false, "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true } @@ -7354,6 +3230,7 @@ }, "tar-stream": { "version": "2.2.0", + "resolved": false, "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "requires": { "bl": "^4.0.3", @@ -7365,15 +3242,18 @@ }, "tari_crypto": { "version": "0.9.1", + "resolved": false, "integrity": "sha512-K7LAtwQQKCeTH5CyyO8d/TiPDEePRaJ4e6+hrxpWv6jlkkAiS4m6csBuVqpSjyAlKeP8cQJpUQX2n22akOuZVg==" }, "text-table": { "version": "0.2.0", + "resolved": false, "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, "thenify": { "version": "3.3.1", + "resolved": false, "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { @@ -7382,6 +3262,7 @@ }, "thenify-all": { "version": "1.6.0", + "resolved": false, "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "dev": true, "requires": { @@ -7399,23 +3280,24 @@ }, "to-fast-properties": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "resolved": false, "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, "traverse-chain": { "version": "0.1.0", + "resolved": false, "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", "dev": true }, "ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==" }, "tsconfig-paths": { "version": "3.11.0", + "resolved": false, "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", "dev": true, "requires": { @@ -7427,6 +3309,7 @@ "dependencies": { "json5": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { @@ -7443,11 +3326,13 @@ }, "type": { "version": "1.2.0", + "resolved": false, "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, "type-check": { "version": "0.4.0", + "resolved": false, "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { @@ -7456,11 +3341,13 @@ }, "type-detect": { "version": "4.0.8", + "resolved": false, "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, "unbox-primitive": { "version": "1.0.1", + "resolved": false, "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", "dev": true, "requires": { @@ -7472,6 +3359,7 @@ }, "universalify": { "version": "0.1.2", + "resolved": false, "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, @@ -7486,6 +3374,7 @@ }, "uri-js": { "version": "4.4.1", + "resolved": false, "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "requires": { @@ -7494,29 +3383,35 @@ }, "utf8": { "version": "3.0.0", + "resolved": false, "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" }, "util-arity": { "version": "1.1.0", + "resolved": false, "integrity": "sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA=", "dev": true }, "util-deprecate": { "version": "1.0.2", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { "version": "3.4.0", + "resolved": false, "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, "v8-compile-cache": { "version": "2.3.0", + "resolved": false, "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "verror": { "version": "1.10.0", + "resolved": false, "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, "requires": { @@ -7535,86 +3430,68 @@ "dependencies": { "@grpc/grpc-js": { "version": "1.3.6", - "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", "requires": { "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { "version": "0.5.6", - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" } }, "@protobufjs/aspromise": { - "version": "1.1.2", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "version": "1.1.2" }, "@protobufjs/base64": { - "version": "1.1.2", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "version": "1.1.2" }, "@protobufjs/codegen": { - "version": "2.0.4", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "version": "2.0.4" }, "@protobufjs/eventemitter": { - "version": "1.1.0", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "version": "1.1.0" }, "@protobufjs/fetch": { "version": "1.1.0", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "@protobufjs/float": { - "version": "1.0.2", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "version": "1.0.2" }, "@protobufjs/inquire": { - "version": "1.1.0", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "version": "1.1.0" }, "@protobufjs/path": { - "version": "1.1.2", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "version": "1.1.2" }, "@protobufjs/pool": { - "version": "1.1.0", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "version": "1.1.0" }, "@protobufjs/utf8": { - "version": "1.1.0", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "version": "1.1.0" }, "@types/long": { - "version": "4.0.1", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.1" }, "@types/node": { - "version": "16.3.2", - "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" + "version": "16.3.2" }, "grpc-promise": { - "version": "1.4.0", - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" + "version": "1.4.0" }, "lodash.camelcase": { - "version": "4.3.0", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "version": "4.3.0" }, "long": { - "version": "4.0.0", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "4.0.0" }, "protobufjs": { "version": "6.11.2", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7635,6 +3512,7 @@ }, "which": { "version": "2.0.2", + "resolved": false, "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { @@ -7643,6 +3521,7 @@ }, "which-boxed-primitive": { "version": "1.0.2", + "resolved": false, "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, "requires": { @@ -7655,6 +3534,7 @@ }, "word-wrap": { "version": "1.2.3", + "resolved": false, "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, @@ -7697,6 +3577,7 @@ }, "wrappy": { "version": "1.0.2", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "y18n": { @@ -7707,6 +3588,7 @@ }, "yallist": { "version": "4.0.0", + "resolved": false, "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, @@ -7733,6 +3615,7 @@ }, "zip-stream": { "version": "4.1.0", + "resolved": false, "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", "requires": { "archiver-utils": "^2.1.0", From 73d862fb4380f606af27101374f94bad53c65dc5 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Mon, 29 Nov 2021 13:58:04 +0200 Subject: [PATCH 20/29] feat: improve wallet responsiveness (#3625) Description --- Improved wallet responsiveness by cutting down on unnecessary wallet database calls: - `TransactionEvent::TransactionValidationSuccess` was published regardless of any state change in the wallet database, which resulted in many unnecessary `fetch 'All Complete Transactions'` calls to the wallet database. This issue was optimized to only send the event when a state change was affected. - A missing event was added when state changed due to reorgs. - This should have a slight positive impact on mobile wallet battery usage. Motivation and Context --- Unnecessary wallet database calls to fetch all completed transactions were made each time the transaction validation protocol was run, especially when nothing changed in the database. This effect grew linearly with the amount of transactions in the database, How Has This Been Tested? --- - Unit tests - Cucumber tests (`npm test -- --tags "not @long-running and not @broken"`) - System level tests --- .../src/ui/state/wallet_event_monitor.rs | 2 +- base_layer/wallet/src/transaction_service/handle.rs | 2 +- .../protocols/transaction_validation_protocol.rs | 11 ++++++++++- base_layer/wallet_ffi/src/callback_handler.rs | 2 +- base_layer/wallet_ffi/src/callback_handler_tests.rs | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index b47b306e9c..1b2d2abce4 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -119,7 +119,7 @@ impl WalletEventMonitor { self.trigger_balance_refresh(); notifier.transaction_sent(tx_id); }, - TransactionEvent::TransactionValidationSuccess(_) => { + TransactionEvent::TransactionValidationStateChanged(_) => { self.trigger_full_tx_state_refresh().await; self.trigger_balance_refresh(); }, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 6e914aebde..8311773404 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -166,7 +166,7 @@ pub enum TransactionEvent { is_valid: bool, }, TransactionValidationTimedOut(u64), - TransactionValidationSuccess(u64), + TransactionValidationStateChanged(u64), TransactionValidationFailure(u64), TransactionValidationAborted(u64), TransactionValidationDelayed(u64), 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 bf34834081..c582c45b73 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 @@ -112,6 +112,7 @@ where .for_protocol(self.operation_id) .unwrap(); + let mut state_changed = false; for batch in unconfirmed_transactions.chunks(self.config.max_tx_query_batch_size) { let (mined, unmined, tip_info) = self .query_base_node_for_transactions(batch, &mut *base_node_wallet_client) @@ -133,6 +134,7 @@ where *num_confirmations, ) .await?; + state_changed = true; } if let Some((tip_height, tip_block)) = tip_info { for unmined_tx in &unmined { @@ -147,6 +149,7 @@ where tip_height.saturating_sub(unmined_tx.coinbase_block_height.unwrap_or_default()), ) .await?; + state_changed = true; } else { info!( target: LOG_TARGET, @@ -163,11 +166,14 @@ where ); self.update_transaction_as_unmined(unmined_tx.tx_id, &unmined_tx.status) .await?; + state_changed = true; } } } } - self.publish_event(TransactionEvent::TransactionValidationSuccess(self.operation_id)); + if state_changed { + self.publish_event(TransactionEvent::TransactionValidationStateChanged(self.operation_id)); + } Ok(self.operation_id) } @@ -234,6 +240,9 @@ where ); self.update_transaction_as_unmined(last_mined_transaction.tx_id, &last_mined_transaction.status) .await?; + self.publish_event(TransactionEvent::TransactionValidationStateChanged( + last_mined_transaction.tx_id, + )); } else { info!( target: LOG_TARGET, diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index 9418b38e1b..4f7b57eb64 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -258,7 +258,7 @@ where TBackend: TransactionBackend + 'static self.receive_transaction_mined_unconfirmed_event(tx_id, num_confirmations).await; self.trigger_balance_refresh().await; }, - TransactionEvent::TransactionValidationSuccess(tx_id) => { + TransactionEvent::TransactionValidationStateChanged(tx_id) => { self.transaction_validation_complete_event(tx_id, CallbackValidationResults::Success); self.trigger_balance_refresh().await; }, diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 4d90f2b3ee..75e233d158 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -476,7 +476,7 @@ mod test { assert_eq!(callback_balance_updated, 5); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionValidationSuccess(1u64))) + .send(Arc::new(TransactionEvent::TransactionValidationStateChanged(1u64))) .unwrap(); oms_event_sender From c3dbdc9726d647ebf1f8fe5a7e50743b12576093 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Tue, 30 Nov 2021 13:22:37 +0200 Subject: [PATCH 21/29] feat: removed transaction validation redundant events (#3630) Description --- - Removed transaction validation events from the code base. - Updated the `test_callback_handler` unit test in the `wallet_ffi` to take the changes into account. Motivation and Context --- Dead code has to be removed. How Has This Been Tested? --- Unit test --- .../wallet/src/transaction_service/handle.rs | 4 -- base_layer/wallet_ffi/src/callback_handler.rs | 37 +++---------------- .../wallet_ffi/src/callback_handler_tests.rs | 13 ++++--- 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 8311773404..720de8ce7f 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -165,11 +165,7 @@ pub enum TransactionEvent { num_confirmations: u64, is_valid: bool, }, - TransactionValidationTimedOut(u64), TransactionValidationStateChanged(u64), - TransactionValidationFailure(u64), - TransactionValidationAborted(u64), - TransactionValidationDelayed(u64), Error(String), } diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index 4f7b57eb64..3661cd23f5 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -258,19 +258,10 @@ where TBackend: TransactionBackend + 'static self.receive_transaction_mined_unconfirmed_event(tx_id, num_confirmations).await; self.trigger_balance_refresh().await; }, - TransactionEvent::TransactionValidationStateChanged(tx_id) => { - self.transaction_validation_complete_event(tx_id, CallbackValidationResults::Success); + TransactionEvent::TransactionValidationStateChanged(request_key) => { + self.transaction_validation_complete_event(request_key); self.trigger_balance_refresh().await; }, - TransactionEvent::TransactionValidationFailure(tx_id) => { - self.transaction_validation_complete_event(tx_id, CallbackValidationResults::Failure); - }, - TransactionEvent::TransactionValidationAborted(tx_id) => { - self.transaction_validation_complete_event(tx_id, CallbackValidationResults::Aborted); - }, - TransactionEvent::TransactionValidationDelayed(tx_id) => { - self.transaction_validation_complete_event(tx_id, CallbackValidationResults::BaseNodeNotInSync); - }, TransactionEvent::TransactionMinedRequestTimedOut(_tx_id) | TransactionEvent::TransactionImported(_tx_id) | TransactionEvent::TransactionCompletedImmediately(_tx_id) @@ -505,29 +496,13 @@ where TBackend: TransactionBackend + 'static } } - fn transaction_validation_complete_event(&mut self, request_key: u64, result: CallbackValidationResults) { + fn transaction_validation_complete_event(&mut self, request_key: u64) { debug!( target: LOG_TARGET, - "Calling Transaction Validation Complete callback function for Request Key: {} with result {:?}", - request_key, - result as u8, + "Calling Transaction Validation Complete callback function for Request Key: {}", request_key, ); - match result { - CallbackValidationResults::Success => unsafe { - (self.callback_transaction_validation_complete)(request_key, CallbackValidationResults::Success as u8); - }, - CallbackValidationResults::Aborted => unsafe { - (self.callback_transaction_validation_complete)(request_key, CallbackValidationResults::Aborted as u8); - }, - CallbackValidationResults::Failure => unsafe { - (self.callback_transaction_validation_complete)(request_key, CallbackValidationResults::Failure as u8); - }, - CallbackValidationResults::BaseNodeNotInSync => unsafe { - (self.callback_transaction_validation_complete)( - request_key, - CallbackValidationResults::BaseNodeNotInSync as u8, - ); - }, + unsafe { + (self.callback_transaction_validation_complete)(request_key, CallbackValidationResults::Success as u8); } } diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 75e233d158..9d3a140a88 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -65,6 +65,7 @@ mod test { use crate::{callback_handler::CallbackHandler, output_manager_service_mock::MockOutputManagerService}; use tari_wallet::transaction_service::protocols::TxRejection; + #[derive(Debug)] struct CallbackState { pub received_tx_callback_called: bool, pub received_tx_reply_callback_called: bool, @@ -194,9 +195,9 @@ mod test { Box::from_raw(balance); } - unsafe extern "C" fn transaction_validation_complete_callback(_tx_id: u64, result: u8) { + unsafe extern "C" fn transaction_validation_complete_callback(request_key: u64, _result: u8) { let mut lock = CALLBACK_STATE.lock().unwrap(); - lock.callback_transaction_validation_complete += result as u32; + lock.callback_transaction_validation_complete += request_key as u32; drop(lock); } @@ -492,7 +493,7 @@ mod test { .unwrap(); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionValidationFailure(1u64))) + .send(Arc::new(TransactionEvent::TransactionValidationStateChanged(2u64))) .unwrap(); oms_event_sender @@ -508,7 +509,7 @@ mod test { .unwrap(); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionValidationAborted(1u64))) + .send(Arc::new(TransactionEvent::TransactionValidationStateChanged(3u64))) .unwrap(); oms_event_sender @@ -524,7 +525,7 @@ mod test { .unwrap(); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionValidationDelayed(1u64))) + .send(Arc::new(TransactionEvent::TransactionValidationStateChanged(4u64))) .unwrap(); dht_event_sender @@ -548,7 +549,7 @@ mod test { assert!(lock.saf_messages_received); assert_eq!(lock.callback_txo_validation_complete, 18); assert_eq!(lock.callback_balance_updated, 5); - assert_eq!(lock.callback_transaction_validation_complete, 6); + assert_eq!(lock.callback_transaction_validation_complete, 10); drop(lock); } From 6e46957356213205cf014c3cced47bdda8409e18 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Tue, 30 Nov 2021 16:30:10 +0200 Subject: [PATCH 22/29] test: removed stress test log target (#3631) Description --- Removed the temporary stress test log target that is not in use any more. Motivation and Context --- Dead code needs to be removed. How Has This Been Tested? --- Compile test --- applications/launchpad/docker_rig/log4rs.yml | 5 --- .../protocols/transaction_receive_protocol.rs | 7 --- .../protocols/transaction_send_protocol.rs | 43 ------------------- .../tasks/send_finalized_transaction.rs | 20 --------- .../transaction_service/tasks/wait_on_dial.rs | 24 ----------- common/logging/log4rs_debug_sample.yml | 23 ---------- common/logging/log4rs_sample_base_node.yml | 5 --- integration_tests/log4rs/base_node.yml | 22 ---------- 8 files changed, 149 deletions(-) diff --git a/applications/launchpad/docker_rig/log4rs.yml b/applications/launchpad/docker_rig/log4rs.yml index 8546b05f01..6549a22a57 100644 --- a/applications/launchpad/docker_rig/log4rs.yml +++ b/applications/launchpad/docker_rig/log4rs.yml @@ -155,8 +155,3 @@ loggers: appenders: - other additive: false - stress_test: - level: info - appenders: - - other - additive: false diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index 1ce1e4ba9c..88244ed5be 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -49,7 +49,6 @@ use tari_crypto::tari_utilities::Hashable; use tokio::time::sleep; const LOG_TARGET: &str = "wallet::transaction_service::protocols::receive_protocol"; -const LOG_TARGET_STRESS: &str = "stress_test::receive_protocol"; #[derive(Debug, PartialEq)] pub enum TransactionReceiveProtocolStage { @@ -360,12 +359,6 @@ where self.id, self.source_pubkey.clone() ); - debug!( - target: LOG_TARGET_STRESS, - "Finalized Transaction with TX_ID = {} received from {}", - self.id, - self.source_pubkey.clone() - ); finalized_transaction .validate_internal_consistency( diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 5812f2ac58..1feab3720e 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -67,7 +67,6 @@ use tokio::{ }; const LOG_TARGET: &str = "wallet::transaction_service::protocols::send_protocol"; -const LOG_TARGET_STRESS: &str = "stress_test::send_protocol"; #[derive(Debug, PartialEq)] pub enum TransactionSendProtocolStage { @@ -473,10 +472,6 @@ where target: LOG_TARGET, "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", self.id, e, ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", self.id, e, - ); TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)) })?; @@ -508,10 +503,6 @@ where target: LOG_TARGET, "Transaction Recipient Reply for TX_ID = {} received", tx_id, ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction Recipient Reply for TX_ID = {} received", tx_id, - ); send_finalized_transaction_message( tx_id, @@ -628,10 +619,6 @@ where target: LOG_TARGET, "Transaction Send Direct for TxID {} failed: {}", self.id, err ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction Send Direct for TxID {} failed: {}", self.id, err - ); store_and_forward_send_result = self.send_transaction_store_and_forward(msg.clone()).await?; }, SendMessageResponse::PendingDiscovery(rx) => { @@ -671,7 +658,6 @@ where }, Err(e) => { warn!(target: LOG_TARGET, "Direct Transaction Send failed: {:?}", e); - debug!(target: LOG_TARGET_STRESS, "Direct Transaction Send failed: {:?}", e); }, } @@ -716,13 +702,6 @@ where self.id, successful_sends[0], ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction (TxId: {}) Send to Neighbours for Store and Forward successful with Message \ - Tags: {:?}", - self.id, - successful_sends[0], - ); Ok(true) } else if !failed_sends.is_empty() { warn!( @@ -731,12 +710,6 @@ where messages were sent", self.id ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction Send to Neighbours for Store and Forward for TX_ID: {} was unsuccessful and no \ - messages were sent", - self.id - ); Ok(false) } else { warn!( @@ -745,12 +718,6 @@ where unsuccessful. Some message might still be sent.", self.id ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction Send to Neighbours for Store and Forward for TX_ID: {} timed out and was \ - unsuccessful. Some message might still be sent.", - self.id - ); Ok(false) } }, @@ -761,12 +728,6 @@ where messages were sent", self.id ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction Send to Neighbours for Store and Forward for TX_ID: {} was unsuccessful and no \ - messages were sent", - self.id - ); Ok(false) }, Err(e) => { @@ -774,10 +735,6 @@ where target: LOG_TARGET, "Transaction Send (TxId: {}) to neighbours for Store and Forward failed: {:?}", self.id, e ); - debug!( - target: LOG_TARGET_STRESS, - "Transaction Send (TxId: {}) to neighbours for Store and Forward failed: {:?}", self.id, e - ); Ok(false) }, } diff --git a/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs b/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs index 8c504d460b..29f5cdd758 100644 --- a/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs +++ b/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs @@ -39,7 +39,6 @@ use crate::transaction_service::{ }; const LOG_TARGET: &str = "wallet::transaction_service::tasks::send_finalized_transaction"; -const LOG_TARGET_STRESS: &str = "stress_test::send_finalized_transaction"; pub async fn send_finalized_transaction_message( tx_id: TxId, @@ -142,10 +141,6 @@ pub async fn send_finalized_transaction_message_direct( target: LOG_TARGET, "Finalized Transaction Send Direct for TxID {} failed: {}", tx_id, err ); - debug!( - target: LOG_TARGET_STRESS, - "Finalized Transaction Send Direct for TxID {} failed: {}", tx_id, err - ); if transaction_routing_mechanism == TransactionRoutingMechanism::DirectAndStoreAndForward { store_and_forward_send_result = send_transaction_finalized_message_store_and_forward( tx_id, @@ -197,10 +192,6 @@ pub async fn send_finalized_transaction_message_direct( }, Err(e) => { warn!(target: LOG_TARGET, "Direct Finalized Transaction Send failed: {:?}", e); - debug!( - target: LOG_TARGET_STRESS, - "Direct Finalized Transaction Send failed: {:?}", e - ); }, } if !direct_send_result && !store_and_forward_send_result { @@ -232,23 +223,12 @@ async fn send_transaction_finalized_message_store_and_forward( tx_id, send_states.to_tags(), ); - debug!( - target: LOG_TARGET_STRESS, - "Sending Finalized Transaction (TxId: {}) to Neighbours for Store and Forward successful with Message \ - Tags: {:?}", - tx_id, - send_states.to_tags(), - ); }, Err(e) => { warn!( target: LOG_TARGET, "Sending Finalized Transaction (TxId: {}) to neighbours for Store and Forward failed: {:?}", tx_id, e ); - debug!( - target: LOG_TARGET_STRESS, - "Sending Finalized Transaction (TxId: {}) to neighbours for Store and Forward failed: {:?}", tx_id, e - ); return Ok(false); }, }; diff --git a/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs b/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs index 9d881701e4..fd75ce2023 100644 --- a/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs +++ b/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs @@ -27,7 +27,6 @@ use tari_comms::types::CommsPublicKey; use tari_comms_dht::outbound::MessageSendStates; const LOG_TARGET: &str = "wallet::transaction_service::tasks"; -const LOG_TARGET_STRESS: &str = "stress_test::transaction_service::tasks"; /// This function contains the logic to wait on a dial and send of a queued message pub async fn wait_on_dial( @@ -46,24 +45,12 @@ pub async fn wait_on_dial( destination_pubkey, send_states[0].tag, ); - debug!( - target: LOG_TARGET_STRESS, - "{} (TxId: {}) Direct Send to {} queued with Message {}", - message, - tx_id, - destination_pubkey, - send_states[0].tag, - ); let (sent, failed) = send_states.wait_n_timeout(direct_send_timeout, 1).await; if !sent.is_empty() { info!( target: LOG_TARGET, "Direct Send process for {} TX_ID: {} was successful with Message: {}", message, tx_id, sent[0] ); - debug!( - target: LOG_TARGET_STRESS, - "Direct Send process for {} TX_ID: {} was successful with Message: {}", message, tx_id, sent[0] - ); true } else { if failed.is_empty() { @@ -71,10 +58,6 @@ pub async fn wait_on_dial( target: LOG_TARGET, "Direct Send process for {} TX_ID: {} timed out", message, tx_id ); - debug!( - target: LOG_TARGET_STRESS, - "Direct Send process for {} TX_ID: {} timed out", message, tx_id - ); } else { warn!( target: LOG_TARGET, @@ -83,13 +66,6 @@ pub async fn wait_on_dial( tx_id, failed[0] ); - debug!( - target: LOG_TARGET_STRESS, - "Direct Send process for {} TX_ID: {} and Message {} was unsuccessful and no message was sent", - message, - tx_id, - failed[0] - ); } false } diff --git a/common/logging/log4rs_debug_sample.yml b/common/logging/log4rs_debug_sample.yml index b80b070b4a..7141029d52 100644 --- a/common/logging/log4rs_debug_sample.yml +++ b/common/logging/log4rs_debug_sample.yml @@ -85,23 +85,6 @@ appenders: encoder: pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}" - # An appender named "stress_test" that writes to a file with a custom pattern encoder - stress_test: - kind: rolling_file - path: "log/base-node/stress_test.log" - policy: - kind: compound - trigger: - kind: size - limit: 10mb - roller: - kind: fixed_window - base: 1 - count: 10 - pattern: "log/base-node/stress_test.{}.log" - encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}" - # Set the default logging level to "warn" and attach the "stdout" appender to the root root: level: warn @@ -163,9 +146,3 @@ loggers: appenders: - mm_proxy additive: false - # Route log events sent to the "stress_test" logger to the "stress_test" appender - stress_test: - level: debug - appenders: - - stress_test - additive: false diff --git a/common/logging/log4rs_sample_base_node.yml b/common/logging/log4rs_sample_base_node.yml index dee1bc500a..6d5ebfc929 100644 --- a/common/logging/log4rs_sample_base_node.yml +++ b/common/logging/log4rs_sample_base_node.yml @@ -146,8 +146,3 @@ loggers: level: info appenders: - base_layer - # Route log events sent to the "stress_test" logger to the "base_layer" appender - stress_test: - level: info - appenders: - - base_layer diff --git a/integration_tests/log4rs/base_node.yml b/integration_tests/log4rs/base_node.yml index 4d44f9aeb3..8238ce18bf 100644 --- a/integration_tests/log4rs/base_node.yml +++ b/integration_tests/log4rs/base_node.yml @@ -73,23 +73,6 @@ appenders: encoder: pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}" - # An appender named "stress_test" that writes to a file with a custom pattern encoder - stress_test: - kind: rolling_file - path: "log/base-node/stress_test.log" - policy: - kind: compound - trigger: - kind: size - limit: 10mb - roller: - kind: fixed_window - base: 1 - count: 10 - pattern: "log/base-node/stress_test.{}.log" - encoder: - pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}" - # Set the default logging level to "warn" and attach the "stdout" appender to the root root: level: warn @@ -164,8 +147,3 @@ loggers: level: info appenders: - base_layer - # Route log events sent to the "stress_test" logger to the "base_layer" appender - stress_test: - level: info - appenders: - - base_layer From 326579bf1d933b93f421a5876221a281b0f6e178 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 1 Dec 2021 23:29:28 +0200 Subject: [PATCH 23/29] fix!: separate peer seeds to common.network (#3635) Description --- Separate some [common] configs to [common.network] - peer seeds - dns seeds - auto update settings Tested by starting base node sync on both weatherwax and igor and checking peer seeds are specific to network Update the clippy github action since the other version fails to run on forks. Fix Linux build issue on github actions. --- .github/workflows/ci.yml | 23 ++++- .../tari_app_utilities/src/initialization.rs | 11 +-- .../tari_stratum_transcoder/src/main.rs | 2 +- common/config/presets/common.toml | 87 +++++++++++-------- common/config/presets/merge_mining_proxy.toml | 1 - common/src/configuration/global.rs | 80 +++++++++-------- common/src/configuration/utils.rs | 81 ++++++++++------- common/src/exit_codes.rs | 6 ++ common/src/lib.rs | 2 +- integration_tests/Makefile | 14 +++ integration_tests/helpers/config.js | 20 +++-- 11 files changed, 200 insertions(+), 127 deletions(-) create mode 100644 integration_tests/Makefile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cb04fed20..9d9961d7e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ name: CI env: CARGO_HTTP_MULTIPLEXING: false + PROTOC: protoc toolchain: nightly-2021-09-18 jobs: @@ -18,6 +19,21 @@ jobs: name: clippy runs-on: ubuntu-18.04 steps: + - name: ubuntu dependencies + run: | + sudo apt-get update && \ + sudo apt-get -y install \ + libssl-dev \ + openssl \ + libsqlite3-dev \ + pkg-config \ + git \ + cmake \ + zip \ + libc++-dev \ + libc++abi-dev \ + libprotobuf-dev \ + protobuf-compiler - name: checkout uses: actions/checkout@v2 - name: toolchain @@ -40,10 +56,10 @@ jobs: command: fmt args: --all -- --check - name: Clippy check - uses: actions-rs/clippy-check@v1 + uses: actions-rs/cargo@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + command: clippy + args: --all-targets -- -D warnings test: name: test runs-on: ubuntu-18.04 @@ -59,7 +75,6 @@ jobs: target key: ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-${{ hashFiles('**/Cargo.lock') }} - name: ubuntu dependencies - if: startsWith(matrix.os,'ubuntu') run: | sudo apt-get update && \ sudo apt-get -y install \ diff --git a/applications/tari_app_utilities/src/initialization.rs b/applications/tari_app_utilities/src/initialization.rs index 8d84ef4240..49e3b03ddd 100644 --- a/applications/tari_app_utilities/src/initialization.rs +++ b/applications/tari_app_utilities/src/initialization.rs @@ -31,8 +31,7 @@ pub fn init_configuration( log::info!(target: LOG_TARGET, "{} ({})", application_type, consts::APP_VERSION); // Populate the configuration struct - let mut global_config = GlobalConfig::convert_from(application_type, cfg.clone()) - .map_err(|err| ExitCodes::ConfigError(err.to_string()))?; + let mut global_config = GlobalConfig::convert_from(application_type, cfg.clone(), bootstrap.network.clone())?; if let Some(str) = bootstrap.network.clone() { log::info!(target: LOG_TARGET, "Network selection requested"); @@ -54,11 +53,9 @@ pub fn init_configuration( global_config.wallet_peer_db_path = global_config.data_dir.join("wallet_peer_db"); global_config.console_wallet_peer_db_path = global_config.data_dir.join("console_wallet_peer_db"); }, - Err(_) => { - log::warn!( - target: LOG_TARGET, - "Network selection was invalid, continuing with default network." - ); + Err(e) => { + log::error!(target: LOG_TARGET, "Network selection was invalid, exiting."); + return Err(e.into()); }, } } diff --git a/applications/tari_stratum_transcoder/src/main.rs b/applications/tari_stratum_transcoder/src/main.rs index 61e0da2d88..5c8b52c1fd 100644 --- a/applications/tari_stratum_transcoder/src/main.rs +++ b/applications/tari_stratum_transcoder/src/main.rs @@ -95,6 +95,6 @@ fn initialize() -> Result { #[cfg(not(feature = "envlog"))] bootstrap.initialize_logging()?; - let cfg = GlobalConfig::convert_from(application_type, cfg)?; + let cfg = GlobalConfig::convert_from(application_type, cfg, bootstrap.network)?; Ok(cfg) } diff --git a/common/config/presets/common.toml b/common/config/presets/common.toml index c114713bf7..e046132364 100644 --- a/common/config/presets/common.toml +++ b/common/config/presets/common.toml @@ -10,40 +10,6 @@ # weatherwax - the Tari testnet network = "weatherwax" -# When first logging onto the Tari network, you need to find a few peers to bootstrap the process. In the absence of -# any servers, this is a little more challenging than usual. Our best strategy is just to try and connect to the peers -# you knew about last time you ran the software. But what about when you run the software for the first time? That's -# where this allowlist comes in. It's a list of known Tari nodes that are likely to be around for a long time and that -# new nodes can use to introduce themselves to the network. -# peer_seeds = ["public_key1::address1", "public_key2::address2",... ] -peer_seeds = [ - # weatherwax - "98bc76afc1c35ad4651bdc9ef57bbe0655a2ea3cd86c0e19b5fd5890546eb040::/onion3/33izgtjkrlxhxybj6luqowkpiy2wvte43osejnbqyieqtdfhovzghxad:18141", #jozi - "9a26e910288213d649b26f9a7a7ee51fe2b2a67ff7d42334523463bf4be94312::/onion3/56kq54ylttnbl5ikotqex3oqvtzlxdpn7zlx4v56rvzf4kq7eezlclid:18141", #london - "6afd5b3c7772ad7d4bb26e0c19668fe04f2d68f99de9e132bee50a6c1846946d::/onion3/may4ajbmcn4dlnzf6fanvqlklxzqiw6qwu6ywqwkjc3bb354rc2i5wid:18141", #ncal - "8e7beec9becdc44fe6015a00d97a77fa3dbafe65127dcc988df6326bd9fd040d::/onion3/3pise36l4imoopsbjic5rtw67adx7rms6w5pgjmccpdwiqx66j7oqcqd:18141", #nvir - "80bb590d943a46e63ae79af5dc2c7d35a3dcd7922c182b28f619dc4cfc366f44::/onion3/oaxwahri7r3h5qjlcdbveyjmg4jsttausik66bicmhixft73nmvecdad:18141", #oregon - "981cc8cd1e4fe2f99ea1bd3e0ab1e7821ca0bfab336a4967cfec053fee86254c::/onion3/7hxpnxrxycdfevirddau7ybofwedaamjrg2ijm57k2kevh5q46ixamid:18141", #seoul - "f2ce179fb733725961a5f7e1e45dacdd443dd43ba6237438d6abe344fb717058::/onion3/nvgdmjf4wucgatz7vemzvi2u4sw5o4gyzwuikagpepoj4w7mkii47zid:18141", #stockholm - "909c0160f4d8e815aba5c2bbccfcceb448877e7b38759fb160f3e9494484d515::/onion3/qw5uxv533sqdn2qoncfyqo35dgecy4rt4x27rexi2her6q6pcpxbm4qd:18141", #sydney - # igor - "8e7eb81e512f3d6347bf9b1ca9cd67d2c8e29f2836fc5bd608206505cc72af34::/onion3/l4wouomx42nezhzexjdzfh7pcou5l7df24ggmwgekuih7tkv2rsaokqd:18141", - "00b35047a341401bcd336b2a3d564280a72f6dc72ec4c739d30c502acce4e803::/onion3/ojhxd7z6ga7qrvjlr3px66u7eiwasmffnuklscbh5o7g6wrbysj45vid:18141", - "40a9d8573745072534bce7d0ecafe882b1c79570375a69841c08a98dee9ecb5f::/onion3/io37fylc2pupg4cte4siqlsmuszkeythgjsxs2i3prm6jyz2dtophaad:18141", - "126c7ee64f71aca36398b977dd31fbbe9f9dad615df96473fb655bef5709c540::/onion3/6ilmgndocop7ybgmcvivbdsetzr5ggj4hhsivievoa2dx2b43wqlrlid:18141", -] - -# DNS seeds -# The DNS records in these hostnames should provide TXT records as per https://github.com/tari-project/tari/pull/2319 -# Enter a domain name for the TXT records: seeds.tari.com -dns_seeds =["seeds.weatherwax.tari.com"] -# The name server used to resolve DNS seeds format: {socket address}/{tls sni dns name} (Default: cloudflare) -# dns_seeds_name_server = "1.1.1.1:853/cloudfare-dns.com" -# Servers addresses, majority of them have to agree. -# autoupdate_dns_hosts = [#server1, #server2, ...] -# Set to true to only accept DNS records that pass DNSSEC validation (Default: true) -dns_seeds_use_dnssec = false - # Tari is a 100% peer-to-peer network, so there are no servers to hold messages for you while you're offline. # Instead, we rely on our peers to hold messages for us while we're offline. This settings sets maximum size of the # message cache that for holding our peers' messages, in MB. @@ -92,13 +58,60 @@ dedup_cache_capacity = 25000 # sessions. rpc_max_simultaneous_sessions = 10000 +[common.weatherwax] +# When first logging onto the Tari network, you need to find a few peers to bootstrap the process. In the absence of +# any servers, this is a little more challenging than usual. Our best strategy is just to try and connect to the peers +# you knew about last time you ran the software. But what about when you run the software for the first time? That's +# where this allowlist comes in. It's a list of known Tari nodes that are likely to be around for a long time and that +# new nodes can use to introduce themselves to the network. +# peer_seeds = ["public_key1::address1", "public_key2::address2",... ] +peer_seeds = [ + # weatherwax + "98bc76afc1c35ad4651bdc9ef57bbe0655a2ea3cd86c0e19b5fd5890546eb040::/onion3/33izgtjkrlxhxybj6luqowkpiy2wvte43osejnbqyieqtdfhovzghxad:18141", #jozi + "9a26e910288213d649b26f9a7a7ee51fe2b2a67ff7d42334523463bf4be94312::/onion3/56kq54ylttnbl5ikotqex3oqvtzlxdpn7zlx4v56rvzf4kq7eezlclid:18141", #london + "6afd5b3c7772ad7d4bb26e0c19668fe04f2d68f99de9e132bee50a6c1846946d::/onion3/may4ajbmcn4dlnzf6fanvqlklxzqiw6qwu6ywqwkjc3bb354rc2i5wid:18141", #ncal + "8e7beec9becdc44fe6015a00d97a77fa3dbafe65127dcc988df6326bd9fd040d::/onion3/3pise36l4imoopsbjic5rtw67adx7rms6w5pgjmccpdwiqx66j7oqcqd:18141", #nvir + "80bb590d943a46e63ae79af5dc2c7d35a3dcd7922c182b28f619dc4cfc366f44::/onion3/oaxwahri7r3h5qjlcdbveyjmg4jsttausik66bicmhixft73nmvecdad:18141", #oregon + "981cc8cd1e4fe2f99ea1bd3e0ab1e7821ca0bfab336a4967cfec053fee86254c::/onion3/7hxpnxrxycdfevirddau7ybofwedaamjrg2ijm57k2kevh5q46ixamid:18141", #seoul + "f2ce179fb733725961a5f7e1e45dacdd443dd43ba6237438d6abe344fb717058::/onion3/nvgdmjf4wucgatz7vemzvi2u4sw5o4gyzwuikagpepoj4w7mkii47zid:18141", #stockholm + "909c0160f4d8e815aba5c2bbccfcceb448877e7b38759fb160f3e9494484d515::/onion3/qw5uxv533sqdn2qoncfyqo35dgecy4rt4x27rexi2her6q6pcpxbm4qd:18141", #sydney +] + +# DNS seeds +# The DNS records in these hostnames should provide TXT records as per https://github.com/tari-project/tari/pull/2319 +# Enter a domain name for the TXT records: +dns_seeds =["seeds.weatherwax.tari.com"] +# The name server used to resolve DNS seeds format: {socket address}/{tls sni dns name} (Default: cloudflare) +# dns_seeds_name_server = "1.1.1.1:853/cloudfare-dns.com" +# Servers addresses, majority of them have to agree. +# autoupdate_dns_hosts = [#server1, #server2, ...] +# Set to true to only accept DNS records that pass DNSSEC validation (Default: true) +dns_seeds_use_dnssec = false + # Auto Update # # This interval in seconds to check for software updates. Setting this to 0 disables checking. # auto_update.check_interval = 300 # Customize the hosts that are used to check for updates. These hosts must contain update information in DNS TXT records. -# auto_update.dns_hosts = ["updates.taripulse.com"] +# "auto_update.dns_hosts" = ["updates.weatherwax.taripulse.com"] # Customize the location of the update SHA hashes and maintainer-signed signature. -# auto_update.hashes_url = "https://
/hashes.txt" -# auto_update.hashes_sig_url = "https://
/hashes.txt.sig" +# "auto_update.hashes_url" = "https://
/hashes.txt" +# "auto_update.hashes_sig_url" = "https://
/hashes.txt.sig" + +[common.igor] +peer_seeds = [ + # igor + "8e7eb81e512f3d6347bf9b1ca9cd67d2c8e29f2836fc5bd608206505cc72af34::/onion3/l4wouomx42nezhzexjdzfh7pcou5l7df24ggmwgekuih7tkv2rsaokqd:18141", + "00b35047a341401bcd336b2a3d564280a72f6dc72ec4c739d30c502acce4e803::/onion3/ojhxd7z6ga7qrvjlr3px66u7eiwasmffnuklscbh5o7g6wrbysj45vid:18141", + "40a9d8573745072534bce7d0ecafe882b1c79570375a69841c08a98dee9ecb5f::/onion3/io37fylc2pupg4cte4siqlsmuszkeythgjsxs2i3prm6jyz2dtophaad:18141", + "126c7ee64f71aca36398b977dd31fbbe9f9dad615df96473fb655bef5709c540::/onion3/6ilmgndocop7ybgmcvivbdsetzr5ggj4hhsivievoa2dx2b43wqlrlid:18141", +] +dns_seeds =["seeds.igor.tari.com"] +# dns_seeds_name_server = "1.1.1.1:853/cloudfare-dns.com" +dns_seeds_use_dnssec = false + +# auto_update.check_interval = 300 +# "auto_update.dns_hosts" = ["updates.igor.taripulse.com"] +# "auto_update.hashes_url" = "https://
/hashes.txt" +# "auto_update.hashes_sig_url" = "https://
/hashes.txt.sig" \ No newline at end of file diff --git a/common/config/presets/merge_mining_proxy.toml b/common/config/presets/merge_mining_proxy.toml index 2a9d93a11c..ffc10048b8 100644 --- a/common/config/presets/merge_mining_proxy.toml +++ b/common/config/presets/merge_mining_proxy.toml @@ -34,4 +34,3 @@ monerod_password = "" # or not. If merge mining starts before the base node has achieved initial sync, those Tari mined blocks will not be # accepted. (Default value = true; will wait for base node initial sync). #wait_for_initial_sync_at_startup = true - diff --git a/common/src/configuration/global.rs b/common/src/configuration/global.rs index 51bee7b6c3..c497dd05a8 100644 --- a/common/src/configuration/global.rs +++ b/common/src/configuration/global.rs @@ -145,27 +145,37 @@ pub struct GlobalConfig { } impl GlobalConfig { - pub fn convert_from(application: ApplicationType, mut cfg: Config) -> Result { + pub fn convert_from( + application: ApplicationType, + mut cfg: Config, + network: Option, + ) -> Result { // Add in settings from the environment (with a prefix of TARI_NODE) // Eg.. `TARI_NODE_DEBUG=1 ./target/app` would set the `debug` key let env = Environment::with_prefix("tari").separator("__"); cfg.merge(env) .map_err(|e| ConfigurationError::new("environment variable", &e.to_string()))?; - let network = one_of::(&cfg, &[ - &format!("{}.network", application.as_config_str()), - "common.network", - // TODO: Remove this once some time has passed and folks have upgraded their configs - "base_node.network", - ])?; - - convert_node_config(application, network, cfg) + + // network override from command line + let network = match network { + Some(s) => Network::from_str(&s)?, + None => { + // otherwise specified from config + one_of::(&cfg, &[ + "common.network", + &format!("{}.network", application.as_config_str()), + ])? + }, + }; + + convert_node_config(application, cfg, network) } } fn convert_node_config( application: ApplicationType, - network: Network, cfg: Config, + network: Network, ) -> Result { let net_str = network.as_str(); @@ -353,38 +363,35 @@ fn convert_node_config( .map(|addr| socket_or_multi(&addr).map_err(|e| ConfigurationError::new(key, &e.to_string())))??; // Peer and DNS seeds - let key = "common.peer_seeds"; + let key = config_string("common", net_str, "peer_seeds"); // Peer seeds can be an array or a comma separated list (e.g. in an ENVVAR) - let peer_seeds = match cfg.get_array(key) { + let peer_seeds = match cfg.get_array(&key) { Ok(seeds) => seeds.into_iter().map(|v| v.into_str().unwrap()).collect(), - Err(..) => optional(cfg.get_str(key))? + Err(..) => optional(cfg.get_str(&key))? .map(|s| s.split(',').map(|v| v.trim().to_string()).collect()) .unwrap_or_default(), }; // TODO: dns resolver presets e.g. "cloudflare", "quad9", "custom" (maybe just in toml) and // add support for multiple addresses - let key = "common.dns_seeds_name_server"; + let key = config_string("common", net_str, "dns_seeds_name_server"); let dns_seeds_name_server = cfg - .get_str(key) - .map_err(|e| ConfigurationError::new(key, &e.to_string())) + .get_str(&key) + .map_err(|e| ConfigurationError::new(&key, &e.to_string())) .and_then(|s| { s.parse::() - .map_err(|e| ConfigurationError::new(key, &e.to_string())) + .map_err(|e| ConfigurationError::new(&key, &e.to_string())) })?; - let key = config_string("base_node", net_str, "bypass_range_proof_verification"); - let base_node_bypass_range_proof_verification = cfg.get_bool(&key).unwrap_or(false); - - let key = "common.dns_seeds_use_dnssec"; + let key = config_string("common", net_str, "dns_seeds_use_dnssec"); let dns_seeds_use_dnssec = cfg - .get_bool(key) - .map_err(|e| ConfigurationError::new(key, &e.to_string()))?; + .get_bool(&key) + .map_err(|e| ConfigurationError::new(&key, &e.to_string()))?; - let key = "common.dns_seeds"; - let dns_seeds = match cfg.get_array(key) { + let key = config_string("common", net_str, "dns_seeds"); + let dns_seeds = match cfg.get_array(&key) { Ok(seeds) => seeds.into_iter().map(|v| v.into_str().unwrap()).collect(), - Err(..) => optional(cfg.get_str(key))? + Err(..) => optional(cfg.get_str(&key))? .map(|s| { s.split(',') .map(|v| v.trim().to_string()) @@ -394,6 +401,9 @@ fn convert_node_config( .unwrap_or_default(), }; + let key = config_string("base_node", net_str, "bypass_range_proof_verification"); + let base_node_bypass_range_proof_verification = cfg.get_bool(&key).unwrap_or(false); + // Peer DB path let peer_db_path = data_dir.join("peer_db"); let wallet_peer_db_path = data_dir.join("wallet_peer_db"); @@ -668,8 +678,8 @@ fn convert_node_config( let validate_tip_timeout_sec = optional(cfg.get_int(key))?.unwrap_or(0) as u64; // Auto update - let key = "common.auto_update.check_interval"; - let autoupdate_check_interval = optional(cfg.get_int(key))?.and_then(|secs| { + let key = config_string("common", net_str, "auto_update.check_interval"); + let autoupdate_check_interval = optional(cfg.get_int(&key))?.and_then(|secs| { if secs > 0 { Some(Duration::from_secs(secs as u64)) } else { @@ -677,20 +687,20 @@ fn convert_node_config( } }); - let key = "common.auto_update.dns_hosts"; + let key = config_string("common", net_str, "auto_update.dns_hosts"); let autoupdate_dns_hosts = cfg - .get_array(key) + .get_array(&key) .and_then(|arr| arr.into_iter().map(|s| s.into_str()).collect::, _>>()) .or_else(|_| { - cfg.get_str(key) + cfg.get_str(&key) .map(|s| s.split(',').map(ToString::to_string).collect()) })?; - let key = "common.auto_update.hashes_url"; - let autoupdate_hashes_url = cfg.get_str(key)?; + let key = config_string("common", net_str, "auto_update.hashes_url"); + let autoupdate_hashes_url = cfg.get_str(&key)?; - let key = "common.auto_update.hashes_sig_url"; - let autoupdate_hashes_sig_url = cfg.get_str(key)?; + let key = config_string("common", net_str, "auto_update.hashes_sig_url"); + let autoupdate_hashes_sig_url = cfg.get_str(&key)?; let key = "mining_node.mining_pool_address"; let mining_pool_address = cfg.get_str(key).unwrap_or_else(|_| "".to_string()); diff --git a/common/src/configuration/utils.rs b/common/src/configuration/utils.rs index db72daa146..3edcd4b4bf 100644 --- a/common/src/configuration/utils.rs +++ b/common/src/configuration/utils.rs @@ -81,25 +81,6 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("common.fetch_utxos_timeout", 600).unwrap(); cfg.set_default("common.service_request_timeout", 180).unwrap(); - cfg.set_default("common.auto_update.dns_hosts", vec!["versions.tari.com"]) - .unwrap(); - // TODO: Change to a more permanent link - cfg.set_default( - "common.auto_update.hashes_url", - "https://raw.githubusercontent.com/tari-project/tari/development/meta/hashes.txt", - ) - .unwrap(); - cfg.set_default( - "common.auto_update.hashes_sig_url", - "https://raw.githubusercontent.com/tari-project/tari/development/meta/hashes.txt.sig", - ) - .unwrap(); - cfg.set_default("common.peer_seeds", Vec::::new()).unwrap(); - cfg.set_default("common.dns_seeds", Vec::::new()).unwrap(); - cfg.set_default("common.dns_seeds_name_server", "1.1.1.1:853/cloudflare-dns.com") - .unwrap(); - cfg.set_default("common.dns_seeds_use_dnssec", true).unwrap(); - // Wallet settings cfg.set_default("wallet.grpc_enabled", false).unwrap(); cfg.set_default("wallet.grpc_address", "127.0.0.1:18040").unwrap(); @@ -224,17 +205,7 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("base_node.weatherwax.grpc_enabled", false).unwrap(); cfg.set_default("base_node.weatherwax.grpc_base_node_address", "127.0.0.1:18142") .unwrap(); - cfg.set_default( - "base_node.weatherwax.dns_seeds_name_server", - "1.1.1.1:853/cloudflare-dns.com", - ) - .unwrap(); - cfg.set_default("base_node.weatherwax.dns_seeds_use_dnssec", true) - .unwrap(); - cfg.set_default("base_node.weatherwax.auto_ping_interval", 30).unwrap(); - cfg.set_default("wallet.base_node_service_peers", Vec::::new()) - .unwrap(); //---------------------------------- Igor Defaults --------------------------------------------// cfg.set_default("base_node.igor.db_type", "lmdb").unwrap(); @@ -248,11 +219,8 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg.set_default("base_node.igor.grpc_enabled", false).unwrap(); cfg.set_default("base_node.igor.grpc_base_node_address", "127.0.0.1:18142") .unwrap(); - cfg.set_default("base_node.igor.dns_seeds_name_server", "1.1.1.1:853/cloudflare-dns.com") - .unwrap(); - cfg.set_default("base_node.igor.dns_seeds_use_dnssec", true).unwrap(); - cfg.set_default("base_node.igor.auto_ping_interval", 30).unwrap(); + set_common_network_defaults(&mut cfg); set_transport_defaults(&mut cfg).unwrap(); set_merge_mining_defaults(&mut cfg); set_mining_node_defaults(&mut cfg); @@ -261,6 +229,48 @@ pub fn default_config(bootstrap: &ConfigBootstrap) -> Config { cfg } +fn set_common_network_defaults(cfg: &mut Config) { + for network in ["mainnet", "weatherwax", "igor", "localnet"] { + let key = format!("base_node.{}.dns_seeds_name_server", network); + cfg.set_default(&key, "1.1.1.1:853/cloudflare-dns.com").unwrap(); + + let key = format!("base_node.{}.dns_seeds_use_dnssec", network); + cfg.set_default(&key, true).unwrap(); + + let key = format!("base_node.{}.auto_ping_interval", network); + cfg.set_default(&key, 30).unwrap(); + + let key = format!("common.{}.peer_seeds", network); + cfg.set_default(&key, Vec::::new()).unwrap(); + + let key = format!("common.{}.dns_seeds", network); + cfg.set_default(&key, Vec::::new()).unwrap(); + + let key = format!("common.{}.dns_seeds_name_server", network); + cfg.set_default(&key, "1.1.1.1:853/cloudflare-dns.com").unwrap(); + + let key = format!("common.{}.dns_seeds_use_dnssec", network); + cfg.set_default(&key, true).unwrap(); + + let key = format!("common.{}.auto_update.dns_hosts", network); + cfg.set_default(&key, vec!["versions.tari.com"]).unwrap(); + + let key = format!("common.{}.auto_update.hashes_url", network); + cfg.set_default( + &key, + "https://raw.githubusercontent.com/tari-project/tari/development/meta/hashes.txt", + ) + .unwrap(); + + let key = format!("common.{}.auto_update.hashes_sig_url", network); + cfg.set_default( + &key, + "https://raw.githubusercontent.com/tari-project/tari/development/meta/hashes.txt.sig", + ) + .unwrap(); + } +} + fn set_stratum_transcoder_defaults(cfg: &mut Config) { cfg.set_default("stratum_transcoder.mainnet.transcoder_host_address", "127.0.0.1:7879") .unwrap(); @@ -306,6 +316,11 @@ fn set_merge_mining_defaults(cfg: &mut Config) { .unwrap(); cfg.set_default("merge_mining_proxy.weatherwax.wait_for_initial_sync_at_startup", true) .unwrap(); + cfg.set_default( + "merge_mining_proxy.igor.monerod_url", + "http://monero-stagenet.exan.tech:38081", + ) + .unwrap(); cfg.set_default("merge_mining_proxy.igor.proxy_host_address", "127.0.0.1:7878") .unwrap(); cfg.set_default("merge_mining_proxy.igor.proxy_submit_to_origin", true) diff --git a/common/src/exit_codes.rs b/common/src/exit_codes.rs index ffd4790406..7ab4a9ca9d 100644 --- a/common/src/exit_codes.rs +++ b/common/src/exit_codes.rs @@ -86,6 +86,12 @@ impl From for ExitCodes { } } +impl From for ExitCodes { + fn from(err: crate::ConfigurationError) -> Self { + Self::ConfigError(err.to_string()) + } +} + impl ExitCodes { pub fn grpc(err: M) -> Self { ExitCodes::GrpcError(format!("GRPC connection error: {}", err)) diff --git a/common/src/lib.rs b/common/src/lib.rs index 46e5560a57..6dd37b8fe5 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -70,7 +70,7 @@ //! # args.init = true; //! args.init_dirs(ApplicationType::BaseNode); //! let config = args.load_configuration().unwrap(); -//! let global = GlobalConfig::convert_from(ApplicationType::BaseNode, config).unwrap(); +//! let global = GlobalConfig::convert_from(ApplicationType::BaseNode, config, Some("weatherwax".into())).unwrap(); //! assert_eq!(global.network, Network::Weatherwax); //! assert!(global.core_threads.is_none()); //! # std::fs::remove_dir_all(temp_dir).unwrap(); diff --git a/integration_tests/Makefile b/integration_tests/Makefile new file mode 100644 index 0000000000..1401d46349 --- /dev/null +++ b/integration_tests/Makefile @@ -0,0 +1,14 @@ +.DEFAULT_GOAL := test + +.phony: build +build: + cargo build --release --bin tari_base_node && \ + cargo build --release --bin tari_console_wallet && \ + cargo build --release --bin tari_mining_node && \ + cargo build --release --bin tari_merge_mining_proxy && \ + cargo build --release --bin tari_stratum_transcoder && \ + cargo build --release --package tari_wallet_ffi + +.phony: test +test: build + npm test -- --profile "ci" --tags "@critical and not @long-running and not @broken and not @wallet-ffi" \ No newline at end of file diff --git a/integration_tests/helpers/config.js b/integration_tests/helpers/config.js index 1e44a0e464..49bf822dbd 100644 --- a/integration_tests/helpers/config.js +++ b/integration_tests/helpers/config.js @@ -40,21 +40,25 @@ function mapEnvs(options) { if (options.common && options.common.auto_update) { let { auto_update } = options.common; if (auto_update.enabled) { - res.TARI_COMMON__AUTO_UPDATE__ENABLED = auto_update.enabled + res.TARI_COMMON__LOCALNET__AUTO_UPDATE__ENABLED = auto_update.enabled ? "true" : "false"; } if (auto_update.check_interval) { - res.TARI_COMMON__AUTO_UPDATE__CHECK_INTERVAL = auto_update.check_interval; + res.TARI_COMMON__LOCALNET__AUTO_UPDATE__CHECK_INTERVAL = + auto_update.check_interval; } if (auto_update.dns_hosts) { - res.TARI_COMMON__AUTO_UPDATE__DNS_HOSTS = auto_update.dns_hosts.join(","); + res.TARI_COMMON__LOCALNET__AUTO_UPDATE__DNS_HOSTS = + auto_update.dns_hosts.join(","); } if (auto_update.hashes_url) { - res.TARI_COMMON__AUTO_UPDATE__HASHES_URL = auto_update.hashes_url; + res.TARI_COMMON__LOCALNET__AUTO_UPDATE__HASHES_URL = + auto_update.hashes_url; } if (auto_update.hashes_sig_url) { - res.TARI_COMMON__AUTO_UPDATE__HASHES_SIG_URL = auto_update.hashes_sig_url; + res.TARI_COMMON__LOCALNET__AUTO_UPDATE__HASHES_SIG_URL = + auto_update.hashes_sig_url; } } return res; @@ -83,8 +87,8 @@ function baseEnvs(peerSeeds = [], forceSyncPeers = []) { TARI_BASE_NODE__LOCALNET__ALLOW_TEST_ADDRESSES: true, TARI_BASE_NODE__LOCALNET__GRPC_ENABLED: true, TARI_BASE_NODE__LOCALNET__ENABLE_WALLET: false, - TARI_COMMON__DNS_SEEDS_USE_DNSSEC: "false", - TARI_COMMON__DNS_SEEDS: "", + TARI_COMMON__LOCALNET__DNS_SEEDS_USE_DNSSEC: "false", + TARI_COMMON__LOCALNET__DNS_SEEDS: "", TARI_BASE_NODE__LOCALNET__BLOCK_SYNC_STRATEGY: "ViaBestChainMetadata", TARI_BASE_NODE__LOCALNET__ORPHAN_DB_CLEAN_OUT_THRESHOLD: "0", TARI_BASE_NODE__LOCALNET__MAX_RANDOMX_VMS: "1", @@ -107,7 +111,7 @@ function baseEnvs(peerSeeds = [], forceSyncPeers = []) { envs.TARI_BASE_NODE__LOCALNET__FORCE_SYNC_PEERS = forceSyncPeers.join(","); } if (peerSeeds.length > 0) { - envs.TARI_COMMON__PEER_SEEDS = peerSeeds.join(","); + envs.TARI_COMMON__LOCALNET__PEER_SEEDS = peerSeeds.join(","); } return envs; From 2e1500b857ab9cc5d08b8d394de09c2400686f5f Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 2 Dec 2021 08:33:47 +0200 Subject: [PATCH 24/29] fix: get-peer command works with public key again (#3636) Description --- Fix regression on get-peer, arg takes (partial) node id and a public key/emojiid Motivation and Context --- get-peer should work with full pk How Has This Been Tested? --- Manually --- .../tari_base_node/src/command_handler.rs | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/applications/tari_base_node/src/command_handler.rs b/applications/tari_base_node/src/command_handler.rs index 82c4d42e84..37fd713143 100644 --- a/applications/tari_base_node/src/command_handler.rs +++ b/applications/tari_base_node/src/command_handler.rs @@ -32,7 +32,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use tari_app_utilities::consts; +use tari_app_utilities::{consts, utilities::parse_emoji_id_or_public_key}; use tari_common::GlobalConfig; use tari_common_types::{ emoji::EmojiId, @@ -417,36 +417,46 @@ impl CommandHandler { let peer_manager = self.peer_manager.clone(); self.executor.spawn(async move { - match peer_manager.find_all_starts_with(&partial).await { + let peer = match peer_manager.find_all_starts_with(&partial).await { Ok(peers) if peers.is_empty() => { - println!("No peer matching '{}'", original_str); - }, - Ok(peers) => { - let peer = peers.first().unwrap(); - let eid = EmojiId::from_pubkey(&peer.public_key); - println!("Emoji ID: {}", eid); - println!("Public Key: {}", peer.public_key); - println!("NodeId: {}", peer.node_id); - println!("Addresses:"); - peer.addresses.iter().for_each(|a| { - println!("- {}", a); - }); - println!("User agent: {}", peer.user_agent); - println!("Features: {:?}", peer.features); - println!("Supported protocols:"); - peer.supported_protocols.iter().for_each(|p| { - println!("- {}", String::from_utf8_lossy(p)); - }); - if let Some(dt) = peer.banned_until() { - println!("Banned until {}, reason: {}", dt, peer.banned_reason); - } - if let Some(dt) = peer.last_seen() { - println!("Last seen: {}", dt); + if let Some(pk) = parse_emoji_id_or_public_key(&original_str) { + if let Ok(peer) = peer_manager.find_by_public_key(&pk).await { + peer + } else { + println!("No peer matching '{}'", original_str); + return; + } + } else { + println!("No peer matching '{}'", original_str); + return; } }, + Ok(mut peers) => peers.remove(0), Err(err) => { println!("{}", err); + return; }, + }; + + let eid = EmojiId::from_pubkey(&peer.public_key); + println!("Emoji ID: {}", eid); + println!("Public Key: {}", peer.public_key); + println!("NodeId: {}", peer.node_id); + println!("Addresses:"); + peer.addresses.iter().for_each(|a| { + println!("- {}", a); + }); + println!("User agent: {}", peer.user_agent); + println!("Features: {:?}", peer.features); + println!("Supported protocols:"); + peer.supported_protocols.iter().for_each(|p| { + println!("- {}", String::from_utf8_lossy(p)); + }); + if let Some(dt) = peer.banned_until() { + println!("Banned until {}, reason: {}", dt, peer.banned_reason); + } + if let Some(dt) = peer.last_seen() { + println!("Last seen: {}", dt); } }); } From ddb926872e97070cf49314720509d6f46c2b260c Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Thu, 2 Dec 2021 09:52:06 +0200 Subject: [PATCH 25/29] fix!: multiple monerod addresses in tari merge mining proxy (#3628) Description --- This PR allows multiple monero daemons to be specified in the configuration of tari_merge_mining_proxy, this will help ensure that the proxy is still able to service requests in the event a monero daemon in the list were to become unreachable whether it be temporarily or permanently. Motivation and Context --- Reliability How Has This Been Tested? --- cargo test --all Manually nvm use 12.22.6 && node_modules/.bin/cucumber-js --profile "ci" --tags "not @long-running and not @broken and @merge-mining" --- README.md | 49 ++++++-- .../launchpad/docker_rig/docker-compose.yml | 5 +- .../tari_merge_mining_proxy/src/error.rs | 2 + .../tari_merge_mining_proxy/src/proxy.rs | 109 ++++++++++++------ common/config/presets/merge_mining_proxy.toml | 17 ++- common/src/configuration/global.rs | 24 +++- integration_tests/helpers/config.js | 7 +- .../helpers/mergeMiningProxyProcess.js | 76 +----------- 8 files changed, 158 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index b8d3a63517..613b76e6c6 100644 --- a/README.md +++ b/README.md @@ -614,7 +614,7 @@ they are not enabled already: ``` ``` [base_node.weatherwax] - transport = "tor" + transpo*_r_*t = "tor" allow_test_addresses = false grpc_enabled = true grpc_base_node_address = "127.0.0.1:18142" @@ -627,7 +627,14 @@ And then depending on if you are using solo mining or self-select mining you wil - For the Tari Merge Mining Proxy, under section **`merge_mining_proxy.weatherwax`** ``` [merge_mining_proxy.weatherwax] - monerod_url = "http://monero-stagenet.exan.tech:38081" + monerod_url = [ # stagenet + "http://stagenet.xmr-tw.org:38081", + "http://stagenet.community.xmr.to:38081", + "http://monero-stagenet.exan.tech:38081", + "http://xmr-lux.boldsuck.org:38081", + "http://singapore.node.xmr.pm:38081", + ] + proxy_host_address = "127.0.0.1:7878" proxy_submit_to_origin = true monerod_use_auth = false @@ -640,7 +647,14 @@ And then depending on if you are using solo mining or self-select mining you wil - For the Tari Merge Mining Proxy, under section **`merge_mining_proxy.weatherwax`** ``` [merge_mining_proxy.weatherwax] - monerod_url = "http://18.132.124.81:18081" + monerod_url = [ # stagenet + "http://stagenet.xmr-tw.org:38081", + "http://stagenet.community.xmr.to:38081", + "http://monero-stagenet.exan.tech:38081", + "http://xmr-lux.boldsuck.org:38081", + "http://singapore.node.xmr.pm:38081", + ] + proxy_host_address = "127.0.0.1:7878" proxy_submit_to_origin = false monerod_use_auth = false @@ -651,8 +665,8 @@ And then depending on if you are using solo mining or self-select mining you wil **Note:** The ports `7878`, `18142` and `18143` shown in the example above should not be in use by other processes. If they are, choose different ports. You will need to update the ports in the steps below as well. -The `monerod_url` must be set to a valid address (`host:port`) for `monerod` that is running Monero mainnet (e.g. -`http://18.132.124.81:18081`) or stagenet (e.g. `http://monero-stagenet.exan.tech:38081`), which can be a +The `monerod_url` set must contain valid addresses (`host:port`) for `monerod` that is running Monero mainnet (e.g. +`["http://18.132.124.81:18081"]`) or stagenet (e.g. `["http://monero-stagenet.exan.tech:38081"]`), which can be a [public node hosted by XMR.to](https://community.xmr.to/nodes.html), or to a local instance. To test if the `monerod_url` address is working properly, try to paste `host:port/get_height` in an internet browser, for example: @@ -688,7 +702,7 @@ in via the command line upon runtime. being a subaddress. It is possible to do with the self-select configuration since the template is requested by the miner with the wallet address of the pool. -###### Solo mining +###### Solo-mining The [XMRig configuration wizard](https://xmrig.com/wizard) can be used to create a solo mining configuration file in JSON format: @@ -832,8 +846,19 @@ Monero wallet address: ``` # URL to monerod -#monerod_url = "http://18.132.124.81:18081" # mainnet -monerod_url = "http://monero-stagenet.exan.tech:38081" # stagenet + monerod_url = [ # mainnet + "http://18.132.124.81:18081", + "http://xmr.support:18081", + "http://node1.xmr-tw.org:18081", + "http://xmr.nthrow.nyc:18081", + ] + monerod_url = [ # stagenet + "http://stagenet.xmr-tw.org:38081", + "http://stagenet.community.xmr.to:38081", + "http://monero-stagenet.exan.tech:38081", + "http://xmr-lux.boldsuck.org:38081", + "http://singapore.node.xmr.pm:38081", + ] ``` ###### Runtime @@ -891,8 +916,12 @@ The `monerod_url` field in the `config.toml` should be enabled for the mainnet v ``` # URL to monerod -monerod_url = "http://18.132.124.81:18081" # mainnet -#monerod_url = "http://monero-stagenet.exan.tech:38081" # stagenet + monerod_url = [ # mainnet + "http://18.132.124.81:18081", + "http://xmr.support:18081", + "http://node1.xmr-tw.org:18081", + "http://xmr.nthrow.nyc:18081", + ] ``` ###### Runtime diff --git a/applications/launchpad/docker_rig/docker-compose.yml b/applications/launchpad/docker_rig/docker-compose.yml index 7dde2f30fa..f7c0924d27 100644 --- a/applications/launchpad/docker_rig/docker-compose.yml +++ b/applications/launchpad/docker_rig/docker-compose.yml @@ -159,7 +159,7 @@ services: TARI_NETWORK: ${TARI_NETWORK} TARI_BASE_NODE__WEATHERWAX__GRPC_BASE_NODE_ADDRESS: "/dns4/base_node/tcp/18142" TARI_WALLET__GRPC_ADDRESS: "/dns4/wallet/tcp/18143" - TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_URL: ${TARI_MONEROD_URL:-http://monero-stagenet.exan.tech:38081} + TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_URL: ${TARI_MONEROD_URL:-["http://stagenet.community.xmr.to:38081","http://monero-stagenet.exan.tech:38081","http://stagenet.xmr-tw.org:38081","http://xmr-lux.boldsuck.org:38081","http://singapore.node.xmr.pm:38081"]} TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_USERNAME: ${TARI_MONEROD_USERNAME} TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_PASSWORD: ${TARI_MONEROD_PASSWORD} TARI_MERGE_MINING_PROXY__WEATHERWAX__MONEROD_USE_AUTH: ${TARI_MONEROD_USE_AUTH:-0} @@ -179,6 +179,3 @@ volumes: # `docker run --rm -v $(pwd):/backup -v blockchain:/blockchain ubuntu tar czvf /backup/backup.tar.gz /blockchain` blockchain: monero-blockchain: - - - diff --git a/applications/tari_merge_mining_proxy/src/error.rs b/applications/tari_merge_mining_proxy/src/error.rs index 7affa3a6e3..53a6ad1527 100644 --- a/applications/tari_merge_mining_proxy/src/error.rs +++ b/applications/tari_merge_mining_proxy/src/error.rs @@ -79,6 +79,8 @@ pub enum MmProxyError { InvalidHeaderValue(#[from] InvalidHeaderValue), #[error("Block was lost due to a failed precondition, and should be retried")] FailedPreconditionBlockLostRetry, + #[error("No reachable servers in configuration")] + ServersUnavailable, } impl From for MmProxyError { diff --git a/applications/tari_merge_mining_proxy/src/proxy.rs b/applications/tari_merge_mining_proxy/src/proxy.rs index f3a9ad5a96..0100602469 100644 --- a/applications/tari_merge_mining_proxy/src/proxy.rs +++ b/applications/tari_merge_mining_proxy/src/proxy.rs @@ -41,6 +41,7 @@ use std::{ sync::{ atomic::{AtomicBool, Ordering}, Arc, + RwLock, }, task::{Context, Poll}, time::Instant, @@ -61,7 +62,7 @@ const TARI_CHAIN_ID: &str = "xtr"; #[derive(Debug, Clone)] pub struct MergeMiningProxyConfig { pub network: Network, - pub monerod_url: String, + pub monerod_url: Vec, pub monerod_username: String, pub monerod_password: String, pub monerod_use_auth: bool, @@ -114,6 +115,7 @@ impl MergeMiningProxyService { base_node_client, wallet_client, initial_sync_achieved: Arc::new(AtomicBool::new(false)), + last_available_server: Arc::new(RwLock::new(None)), }, } } @@ -135,7 +137,7 @@ impl Service> for MergeMiningProxyService { let bytes = match proxy::read_body_until_end(request.body_mut()).await { Ok(b) => b, Err(err) => { - eprintln!("Method: Unknown, Failed to read request: {}", err); + eprintln!("Method: Unknown, Failed to read request: {:?}", err); let resp = proxy::json_response( StatusCode::BAD_REQUEST, &json_rpc::standard_error_response( @@ -153,8 +155,8 @@ impl Service> for MergeMiningProxyService { match inner.handle(&method_name, request).await { Ok(resp) => Ok(resp), Err(err) => { - error!(target: LOG_TARGET, "Error handling request: {}", err); - eprintln!("Method: {}, Failed to handle request: {}", method_name, err); + error!(target: LOG_TARGET, "Error handling request: {:?}", err); + eprintln!("Method: {}, Failed to handle request: {:?}", method_name, err); Ok(proxy::json_response( StatusCode::INTERNAL_SERVER_ERROR, &json_rpc::standard_error_response( @@ -180,6 +182,7 @@ struct InnerService { base_node_client: grpc::base_node_client::BaseNodeClient, wallet_client: grpc::wallet_client::WalletClient, initial_sync_achieved: Arc, + last_available_server: Arc>>, } impl InnerService { @@ -582,9 +585,36 @@ impl InnerService { Ok(proxy::into_response(parts, &resp)) } - fn get_fully_qualified_monerod_url(&self, uri: &Uri) -> Result { - let uri = format!("{}{}", self.config.monerod_url, uri.path()).parse::()?; - Ok(uri) + async fn get_fully_qualified_monerod_url(&self, uri: &Uri) -> Result { + { + let lock = self + .last_available_server + .read() + .expect("Read lock should not fail") + .clone(); + if let Some(server) = lock { + let uri = format!("{}{}", server, uri.path()).parse::()?; + return Ok(uri); + } + } + + for monerod_url in self.config.monerod_url.iter() { + let uri = format!("{}{}", monerod_url, uri.path()).parse::()?; + match reqwest::get(uri.clone()).await { + Ok(_) => { + let mut lock = self.last_available_server.write().expect("Write lock should not fail"); + *lock = Some(monerod_url.to_string()); + info!(target: LOG_TARGET, "Monerod server available: {:?}", uri.clone()); + return Ok(uri); + }, + Err(_) => { + warn!(target: LOG_TARGET, "Monerod server unavailable: {:?}", uri); + continue; + }, + } + } + + Err(MmProxyError::ServersUnavailable) } /// Proxy a request received by this server to Monerod @@ -592,7 +622,7 @@ impl InnerService { &self, request: Request, ) -> Result<(Request, Response), MmProxyError> { - let monerod_uri = self.get_fully_qualified_monerod_url(request.uri())?; + let monerod_uri = self.get_fully_qualified_monerod_url(request.uri()).await?; let mut headers = request.headers().clone(); // Some public monerod setups (e.g. those that are reverse proxied by nginx) require the Host header. @@ -744,34 +774,43 @@ impl InnerService { .join(","), ); - let (request, monerod_resp) = self.proxy_request_to_monerod(request).await?; - // Any failed (!= 200 OK) responses from Monero are immediately returned to the requester - let monerod_status = monerod_resp.status(); - if !monerod_status.is_success() { - // we dont break on xmrig returned error. - warn!( - target: LOG_TARGET, - "Monerod returned an error: {}", - monerod_resp.status() - ); - println!( - "Method: {}, MoneroD Status: {}, Proxy Status: N/A, Response Time: {}ms", - method_name, - monerod_status, - start.elapsed().as_millis() - ); - return Ok(monerod_resp.map(|json| json.to_string().into())); - } + match self.proxy_request_to_monerod(request).await { + Ok((request, monerod_resp)) => { + // Any failed (!= 200 OK) responses from Monero are immediately returned to the requester + let monerod_status = monerod_resp.status(); + if !monerod_status.is_success() { + // we dont break on monerod returning an error code. + warn!( + target: LOG_TARGET, + "Monerod returned an error: {}", + monerod_resp.status() + ); + println!( + "Method: {}, MoneroD Status: {}, Proxy Status: N/A, Response Time: {}ms", + method_name, + monerod_status, + start.elapsed().as_millis() + ); + return Ok(monerod_resp.map(|json| json.to_string().into())); + } - let response = self.get_proxy_response(request, monerod_resp).await?; - println!( - "Method: {}, MoneroD Status: {}, Proxy Status: {}, Response Time: {}ms", - method_name, - monerod_status, - response.status(), - start.elapsed().as_millis() - ); - Ok(response) + let response = self.get_proxy_response(request, monerod_resp).await?; + println!( + "Method: {}, MoneroD Status: {}, Proxy Status: {}, Response Time: {}ms", + method_name, + monerod_status, + response.status(), + start.elapsed().as_millis() + ); + Ok(response) + }, + Err(e) => { + // Monero Server encountered a problem processing the request, reset the last_available_server + let mut lock = self.last_available_server.write().expect("Write lock should not fail"); + *lock = None; + Err(e) + }, + } } } diff --git a/common/config/presets/merge_mining_proxy.toml b/common/config/presets/merge_mining_proxy.toml index ffc10048b8..6c3ef13ed7 100644 --- a/common/config/presets/merge_mining_proxy.toml +++ b/common/config/presets/merge_mining_proxy.toml @@ -7,10 +7,19 @@ [merge_mining_proxy.weatherwax] # URL to monerod -monerod_url = "http://monero-stagenet.exan.tech:38081" # stagenet -#monerod_url = "http://18.133.59.45:28081" # testnet -#monerod_url = "http://18.132.124.81:18081" # mainnet -#monerod_url = "http://monero.exan.tech:18081" # mainnet alternative +monerod_url = [ # stagenet + "http://stagenet.xmr-tw.org:38081", + "http://stagenet.community.xmr.to:38081", + "http://monero-stagenet.exan.tech:38081", + "http://xmr-lux.boldsuck.org:38081", + "http://singapore.node.xmr.pm:38081", +] +#monerod_url = [ # mainnet +# "http://18.132.124.81:18081", +# "http://xmr.support:18081", +# "http://node1.xmr-tw.org:18081", +# "http://xmr.nthrow.nyc:18081", +#] # Address of the tari_merge_mining_proxy application proxy_host_address = "127.0.0.1:7878" diff --git a/common/src/configuration/global.rs b/common/src/configuration/global.rs index c497dd05a8..90bab9ca46 100644 --- a/common/src/configuration/global.rs +++ b/common/src/configuration/global.rs @@ -121,7 +121,7 @@ pub struct GlobalConfig { pub wallet_base_node_service_request_max_age: u64, pub wallet_balance_enquiry_cooldown_period: u64, pub prevent_fee_gt_amount: bool, - pub monerod_url: String, + pub monerod_url: Vec, pub monerod_username: String, pub monerod_password: String, pub monerod_use_auth: bool, @@ -623,9 +623,25 @@ fn convert_node_config( ); let key = config_string("merge_mining_proxy", net_str, "monerod_url"); - let monerod_url = cfg - .get_str(&key) - .map_err(|e| ConfigurationError::new(&key, &e.to_string()))?; + let mut monerod_url: Vec = cfg + .get_array(&key) + .unwrap_or_default() + .into_iter() + .map(|v| { + v.into_str() + .map_err(|err| ConfigurationError::new(&key, &err.to_string())) + }) + .collect::>()?; + + // default to stagenet on empty + if monerod_url.is_empty() { + monerod_url = vec![ + "http://stagenet.xmr-tw.org:38081".to_string(), + "http://singapore.node.xmr.pm:38081".to_string(), + "http://xmr-lux.boldsuck.org:38081".to_string(), + "http://monero-stagenet.exan.tech:38081".to_string(), + ]; + } let key = config_string("merge_mining_proxy", net_str, "monerod_use_auth"); let monerod_use_auth = cfg diff --git a/integration_tests/helpers/config.js b/integration_tests/helpers/config.js index 49bf822dbd..70698c4540 100644 --- a/integration_tests/helpers/config.js +++ b/integration_tests/helpers/config.js @@ -94,8 +94,13 @@ function baseEnvs(peerSeeds = [], forceSyncPeers = []) { TARI_BASE_NODE__LOCALNET__MAX_RANDOMX_VMS: "1", TARI_BASE_NODE__LOCALNET__AUTO_PING_INTERVAL: "15", TARI_BASE_NODE__LOCALNET__FLOOD_BAN_MAX_MSG_COUNT: "100000", - TARI_MERGE_MINING_PROXY__LOCALNET__MONEROD_URL: + TARI_MERGE_MINING_PROXY__LOCALNET__MONEROD_URL: [ + "http://stagenet.xmr-tw.org:38081", + "http://stagenet.community.xmr.to:38081", "http://monero-stagenet.exan.tech:38081", + "http://xmr-lux.boldsuck.org:38081", + "http://singapore.node.xmr.pm:38081", + ], TARI_MERGE_MINING_PROXY__LOCALNET__MONEROD_USE_AUTH: false, TARI_MERGE_MINING_PROXY__LOCALNET__MONEROD_USERNAME: '""', TARI_MERGE_MINING_PROXY__LOCALNET__MONEROD_PASSWORD: '""', diff --git a/integration_tests/helpers/mergeMiningProxyProcess.js b/integration_tests/helpers/mergeMiningProxyProcess.js index fe0b87159a..f31d85dde5 100644 --- a/integration_tests/helpers/mergeMiningProxyProcess.js +++ b/integration_tests/helpers/mergeMiningProxyProcess.js @@ -40,7 +40,7 @@ class MergeMiningProxyProcess { // console.log("MergeMiningProxyProcess init - assign server GRPC:", this.grpcPort); } - run(cmd, args, monerodUrl) { + async run(cmd, args) { return new Promise((resolve, reject) => { if (!fs.existsSync(this.baseDir)) { fs.mkdirSync(this.baseDir, { recursive: true }); @@ -67,9 +67,9 @@ class MergeMiningProxyProcess { const extraEnvs = { TARI_MERGE_MINING_PROXY__LOCALNET__PROXY_SUBMIT_TO_ORIGIN: this.submitOrigin, - TARI_MERGE_MINING_PROXY__LOCALNET__monerod_url: monerodUrl, }; const completeEnvs = { ...envs, ...extraEnvs }; + console.log(completeEnvs); const ps = spawn(cmd, args, { cwd: this.baseDir, // shell: true, @@ -105,75 +105,6 @@ class MergeMiningProxyProcess { }); } - async testWebsite(protocol, address, port, path) { - const url = protocol + "://" + address + ":" + port; - const webRequest = require(protocol); - - let request; - let thePromise; - const displayData = false; - try { - thePromise = await new Promise((resolve, reject) => { - request = webRequest - .get(url + path, (resp) => { - let data = ""; - // Read all data chunks until the end - resp.on("data", (chunk) => { - data += chunk; - }); - // Finish when complete response has been received - resp.on("end", () => { - if (displayData) { - console.log(data); // `data` is 'used' here to keep eslint happy - } - return resolve(true); - }); - }) - .on("error", () => { - return reject(false); - }); - }); - console.log( - " >> Info: `monerod` at", - url, - "is responsive and available" - ); - } catch { - console.log(" >> Warn: `monerod` at", url, "is not available!"); - } - request.end(); - - return thePromise; - } - - async getMoneroStagenetUrl() { - // See: https://monero.fail/?nettype=stagenet - const monerodUrl = [ - ["http", "singapore.node.xmr.pm", "38081"], - ["http", "stagenet.xmr-tw.org", "38081"], - ["http", "xmr-lux.boldsuck.org", "38081"], - ["http", "monero-stagenet.exan.tech", "38081"], - ["http", "3.104.4.129", "18081"], // flaky - ["http", "stagenet.community.xmr.to", "38081"], // flaky - ["http", "super.fast.node.xmr.pm", "38089"], // flaky - ]; - let url; - for (let i = 0; i < monerodUrl.length; i++) { - let availble = await this.testWebsite( - monerodUrl[i][0], - monerodUrl[i][1], - monerodUrl[i][2], - "/get_height" - ); - if (availble) { - url = - monerodUrl[i][0] + "://" + monerodUrl[i][1] + ":" + monerodUrl[i][2]; - break; - } - } - return url; - } - async startNew() { await this.init(); const args = ["--base-path", ".", "--init"]; @@ -181,8 +112,7 @@ class MergeMiningProxyProcess { args.push("--log-config", this.logFilePath); } - let url = await this.getMoneroStagenetUrl(); - return await this.run(await this.compile(), args, url); + return await this.run(await this.compile(), args); } async compile() { From c53be9ba3fe463090bf6e0db60ea26d6bb527176 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Thu, 2 Dec 2021 10:39:47 +0200 Subject: [PATCH 26/29] test: add operation_id to log messages (#3633) Description --- Added operation_id to log messages to enable more efficient tracing in log files. Motivation and Context --- See above How Has This Been Tested? --- `cargo clippy --all-targets -- -D warnings` --- .../tasks/txo_validation_task.rs | 57 +++++++++------ .../transaction_validation_protocol.rs | 71 ++++++++++++++----- 2 files changed, 89 insertions(+), 39 deletions(-) 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 363530f6fd..7c253118e9 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 @@ -94,6 +94,10 @@ where self.update_spent_outputs(&mut base_node_client, last_mined_header) .await?; self.publish_event(OutputManagerEvent::TxoValidationSuccess(self.operation_id)); + info!( + target: LOG_TARGET, + "Finished TXO validation protocol (Id: {})", self.operation_id, + ); Ok(self.operation_id) } @@ -115,8 +119,9 @@ where for batch in mined_outputs.chunks(self.config.tx_validator_batch_size) { debug!( target: LOG_TARGET, - "Asking base node for status of {} mmr_positions", - batch.len() + "Asking base node for status of {} mmr_positions (Operation ID: {})", + batch.len(), + self.operation_id ); // We have to send positions to the base node because if the base node cannot find the hash of the output @@ -174,10 +179,11 @@ where .for_protocol(self.operation_id)?; info!( target: LOG_TARGET, - "Updating output comm:{}: hash {} as spent at tip height {}", + "Updating output comm:{}: hash {} as spent at tip height {} (Operation ID: {})", output.commitment.to_hex(), output.hash.to_hex(), - deleted_bitmap_response.height_of_longest_chain + deleted_bitmap_response.height_of_longest_chain, + self.operation_id ); } @@ -196,10 +202,11 @@ where .for_protocol(self.operation_id)?; info!( target: LOG_TARGET, - "Updating output comm:{}: hash {} as unspent at tip height {}", + "Updating output comm:{}: hash {} as unspent at tip height {} (Operation ID: {})", output.commitment.to_hex(), output.hash.to_hex(), - deleted_bitmap_response.height_of_longest_chain + deleted_bitmap_response.height_of_longest_chain, + self.operation_id ); } } @@ -220,8 +227,9 @@ where for batch in unconfirmed_outputs.chunks(self.config.tx_validator_batch_size) { info!( target: LOG_TARGET, - "Asking base node for location of {} unconfirmed outputs by hash", - batch.len() + "Asking base node for location of {} unconfirmed outputs by hash (Operation ID: {})", + batch.len(), + self.operation_id ); let (mined, unmined, tip_height) = self .query_base_node_for_outputs(batch, wallet_client) @@ -229,18 +237,20 @@ where .for_protocol(self.operation_id)?; debug!( target: LOG_TARGET, - "Base node returned {} outputs as mined and {} outputs as unmined", + "Base node returned {} outputs as mined and {} outputs as unmined (Operation ID: {})", mined.len(), - unmined.len() + unmined.len(), + self.operation_id ); 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 {}", + "Updating output comm:{}: hash {} as mined at height {} with current tip at {} (Operation ID: {})", output.commitment.to_hex(), output.hash.to_hex(), mined_height, - tip_height + tip_height, + self.operation_id ); self.update_output_as_mined(output, mined_in_block, *mined_height, *mmr_position, tip_height) .await?; @@ -258,7 +268,7 @@ where let mut last_mined_header_hash = None; info!( target: LOG_TARGET, - "Checking last mined TXO to see if the base node has re-orged" + "Checking last mined TXO to see if the base node has re-orged (Operation ID: {})", self.operation_id ); while let Some(last_spent_output) = self.db.get_last_spent_output().await.for_protocol(self.operation_id)? { @@ -285,8 +295,9 @@ where warn!( target: LOG_TARGET, "The block that output ({}) was spent in has been reorged out, will try to find this output \ - again, but these funds have potentially been re-orged out of the chain", - last_spent_output.commitment.to_hex() + again, but these funds have potentially been re-orged out of the chain (Operation ID: {})", + last_spent_output.commitment.to_hex(), + self.operation_id ); self.db .mark_output_as_unspent(last_spent_output.hash.clone()) @@ -295,7 +306,8 @@ where } else { info!( target: LOG_TARGET, - "Last mined transaction is still in the block chain according to base node." + "Last mined transaction is still in the block chain according to base node. (Operation ID: {})", + self.operation_id ); break; } @@ -321,8 +333,9 @@ where warn!( target: LOG_TARGET, "The block that output ({}) was in has been reorged out, will try to find this output again, but \ - these funds have potentially been re-orged out of the chain", - last_mined_output.commitment.to_hex() + these funds have potentially been re-orged out of the chain (Operation ID: {})", + last_mined_output.commitment.to_hex(), + self.operation_id ); self.db .set_output_to_unmined(last_mined_output.hash.clone()) @@ -331,7 +344,8 @@ where } else { info!( target: LOG_TARGET, - "Last mined transaction is still in the block chain according to base node." + "Last mined transaction is still in the block chain according to base node (Operation ID: {}).", + self.operation_id ); last_mined_header_hash = Some(mined_in_block_hash); break; @@ -350,7 +364,10 @@ where let result = match client.get_header_by_height(height).await { Ok(r) => r, Err(rpc_error) => { - info!(target: LOG_TARGET, "Error asking base node for header:{}", rpc_error); + info!( + target: LOG_TARGET, + "Error asking base node for header:{} (Operation ID: {})", rpc_error, self.operation_id + ); match &rpc_error { RequestFailed(status) => { if status.as_status_code().is_not_found() { 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 c582c45b73..869da39798 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 @@ -102,7 +102,7 @@ where self.check_for_reorgs(&mut *base_node_wallet_client).await?; info!( target: LOG_TARGET, - "Checking if transactions have been mined since last we checked" + "Checking if transactions have been mined since last we checked (Operation ID: {})", self.operation_id ); // Fetch completed but unconfirmed transactions that were not imported let unconfirmed_transactions = self @@ -120,12 +120,19 @@ where .for_protocol(self.operation_id)?; info!( target: LOG_TARGET, - "Base node returned {} as mined and {} as unmined", + "Base node returned {} as mined and {} as unmined (Operation ID: {})", mined.len(), - unmined.len() + unmined.len(), + self.operation_id ); for (mined_tx, mined_height, mined_in_block, num_confirmations) in &mined { - info!(target: LOG_TARGET, "Updating transaction {} as mined", mined_tx.tx_id); + info!( + target: LOG_TARGET, + "Updating transaction {} as mined and confirmed '{}' (Operation ID: {})", + mined_tx.tx_id, + *num_confirmations >= self.config.num_confirmations_required, + self.operation_id + ); self.update_transaction_as_mined( mined_tx.tx_id, &mined_tx.status, @@ -141,7 +148,12 @@ where // Treat coinbases separately if unmined_tx.is_coinbase() { if unmined_tx.coinbase_block_height.unwrap_or_default() <= tip_height { - info!(target: LOG_TARGET, "Updated coinbase {} as abandoned", unmined_tx.tx_id); + info!( + target: LOG_TARGET, + "Updated coinbase {} as abandoned (Operation ID: {})", + unmined_tx.tx_id, + self.operation_id + ); self.update_coinbase_as_abandoned( unmined_tx.tx_id, &tip_block, @@ -154,15 +166,16 @@ where info!( target: LOG_TARGET, "Coinbase not found, but it is for a block that is not yet in the chain. Coinbase \ - height: {}, tip height:{}", + height: {}, tip height:{} (Operation ID: {})", unmined_tx.coinbase_block_height.unwrap_or_default(), - tip_height + tip_height, + self.operation_id ); } } else { info!( target: LOG_TARGET, - "Updated transaction {} as unmined", unmined_tx.tx_id + "Updated transaction {} as unmined (Operation ID: {})", unmined_tx.tx_id, self.operation_id ); self.update_transaction_as_unmined(unmined_tx.tx_id, &unmined_tx.status) .await?; @@ -192,7 +205,8 @@ where ) -> Result<(), TransactionServiceProtocolError> { info!( target: LOG_TARGET, - "Checking last mined transactions to see if the base node has re-orged" + "Checking last mined transactions to see if the base node has re-orged (Operation ID: {})", + self.operation_id ); while let Some(last_mined_transaction) = self .db @@ -229,14 +243,16 @@ where warn!( target: LOG_TARGET, "The block that transaction (excess:{}) was in has been reorged out, will try to find this \ - transaction again, but these funds have potentially been re-orged out of the chain", + transaction again, but these funds have potentially been re-orged out of the chain (Operation \ + ID: {})", last_mined_transaction .transaction .body .kernels() .first() .map(|k| k.excess.to_hex()) - .unwrap() + .unwrap(), + self.operation_id ); self.update_transaction_as_unmined(last_mined_transaction.tx_id, &last_mined_transaction.status) .await?; @@ -246,7 +262,8 @@ where } else { info!( target: LOG_TARGET, - "Last mined transaction is still in the block chain according to base node." + "Last mined transaction is still in the block chain according to base node (Operation ID: {}).", + self.operation_id ); break; } @@ -278,14 +295,18 @@ where } if batch_signatures.is_empty() { - info!(target: LOG_TARGET, "No transactions needed to query with the base node"); + info!( + target: LOG_TARGET, + "No transactions needed to query with the base node (Operation ID: {})", self.operation_id + ); return Ok((mined, unmined, None)); } info!( target: LOG_TARGET, - "Asking base node for location of {} transactions by excess signature", - batch_signatures.len() + "Asking base node for location of {} transactions by excess signature (Operation ID: {})", + batch_signatures.len(), + self.operation_id ); let batch_response = base_node_client @@ -334,7 +355,10 @@ where let result = match client.get_header_by_height(height).await { Ok(r) => r, Err(rpc_error) => { - warn!(target: LOG_TARGET, "Error asking base node for header:{}", rpc_error); + warn!( + target: LOG_TARGET, + "Error asking base node for header:{} (Operation ID: {})", rpc_error, self.operation_id + ); match &rpc_error { RequestFailed(status) => { if status.as_status_code() == NotFound { @@ -391,7 +415,10 @@ where if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx_id, false).await { warn!( target: LOG_TARGET, - "Could not mark coinbase output for TxId: {} as not abandoned: {}", tx_id, e + "Could not mark coinbase output for TxId: {} as not abandoned: {} (Operation ID: {})", + tx_id, + e, + self.operation_id ); }; } @@ -422,7 +449,10 @@ where if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx_id, true).await { warn!( target: LOG_TARGET, - "Could not mark coinbase output for TxId: {} as abandoned: {}", tx_id, e + "Could not mark coinbase output for TxId: {} as abandoned: {} (Operation ID: {})", + tx_id, + e, + self.operation_id ); }; @@ -445,7 +475,10 @@ where if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx_id, false).await { warn!( target: LOG_TARGET, - "Could not mark coinbase output for TxId: {} as not abandoned: {}", tx_id, e + "Could not mark coinbase output for TxId: {} as not abandoned: {} (Operation ID: {})", + tx_id, + e, + self.operation_id ); }; } From 7d49fa4c092f5d7e0a373f3f0c91d9007534e575 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 2 Dec 2021 13:26:26 +0200 Subject: [PATCH 27/29] fix: use json 5 for tor identity (regression) (#3624) Description --- Use json 5 for save/load identity functions Motivation and Context --- Save and load identity were using older version of the json parser which coul not parse the json comments. This causes a new tor identity and onion address to be created on each base node startup. How Has This Been Tested? --- Onion address does not change on base node restart --- Cargo.lock | 2 +- applications/tari_app_utilities/Cargo.toml | 2 +- .../tari_app_utilities/src/identity_management.rs | 14 ++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 730b9ef7f5..caa7719ce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4539,7 +4539,7 @@ dependencies = [ "log", "qrcode", "rand 0.8.4", - "serde_json", + "serde 1.0.130", "structopt", "strum 0.19.5", "strum_macros 0.19.4", diff --git a/applications/tari_app_utilities/Cargo.toml b/applications/tari_app_utilities/Cargo.toml index ae6d9c9d1f..d4f64bc4c7 100644 --- a/applications/tari_app_utilities/Cargo.toml +++ b/applications/tari_app_utilities/Cargo.toml @@ -15,7 +15,7 @@ config = { version = "0.9.3" } futures = { version = "^0.3.16", default-features = false, features = ["alloc"] } qrcode = { version = "0.12" } dirs-next = "1.0.2" -serde_json = "1.0" +serde = "1.0.126" json5 = "0.2.2" log = { version = "0.4.8", features = ["std"] } rand = "0.8" diff --git a/applications/tari_app_utilities/src/identity_management.rs b/applications/tari_app_utilities/src/identity_management.rs index 7dc6458b5f..086d35c672 100644 --- a/applications/tari_app_utilities/src/identity_management.rs +++ b/applications/tari_app_utilities/src/identity_management.rs @@ -22,6 +22,7 @@ use log::*; use rand::rngs::OsRng; +use serde::{de::DeserializeOwned, Serialize}; use std::{clone::Clone, fs, path::Path, str::FromStr, string::ToString, sync::Arc}; use tari_common::{ configuration::{bootstrap::prompt, utils::get_local_ip}, @@ -29,10 +30,7 @@ use tari_common::{ }; use tari_common_types::types::PrivateKey; use tari_comms::{multiaddr::Multiaddr, peer_manager::PeerFeatures, NodeIdentity}; -use tari_crypto::{ - keys::SecretKey, - tari_utilities::{hex::Hex, message_format::MessageFormat}, -}; +use tari_crypto::{keys::SecretKey, tari_utilities::hex::Hex}; pub const LOG_TARGET: &str = "tari_application"; @@ -194,7 +192,7 @@ pub fn recover_node_identity>( /// /// ## Returns /// Result containing an object on success, string will indicate reason on error -pub fn load_from_json, T: MessageFormat>(path: P) -> Result { +pub fn load_from_json, T: DeserializeOwned>(path: P) -> Result { if !path.as_ref().exists() { return Err(format!( "Identity file, {}, does not exist.", @@ -203,7 +201,7 @@ pub fn load_from_json, T: MessageFormat>(path: P) -> Result, T: MessageFormat>(path: P) -> Result, T: MessageFormat>(path: P, object: &T) -> Result<(), String> { - let json = object.to_json().unwrap(); +pub fn save_as_json, T: Serialize>(path: P, object: &T) -> Result<(), String> { + let json = json5::to_string(object).unwrap(); if let Some(p) = path.as_ref().parent() { if !p.exists() { fs::create_dir_all(p).map_err(|e| format!("Could not save json to data folder. {}", e.to_string()))?; From e501aa09baf21cba6dbed940fbdf4432399cf2cc Mon Sep 17 00:00:00 2001 From: David Main <51991544+StriderDM@users.noreply.github.com> Date: Thu, 2 Dec 2021 22:05:24 +0200 Subject: [PATCH 28/29] feat!: sending one-sided transactions in wallet_ffi (#3634) Description --- This PR allows one-sided transactions to be sent via the wallet_ffi library. Motivation and Context --- Feature How Has This Been Tested? --- nvm use 12.22.6 && node_modules/.bin/cucumber-js --profile "ci" --tags "not @long-running and not @broken and @wallet-ffi" cargo test --all --- base_layer/wallet_ffi/src/lib.rs | 48 ++++++++++++++----- base_layer/wallet_ffi/wallet.h | 2 +- integration_tests/features/WalletFFI.feature | 24 ++++++++++ .../features/support/ffi_steps.js | 18 ++++++- integration_tests/helpers/ffi/ffiInterface.js | 5 +- integration_tests/helpers/ffi/wallet.js | 5 +- integration_tests/helpers/walletFFIClient.js | 5 +- 7 files changed, 87 insertions(+), 20 deletions(-) diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 26ec4fdbc4..a4623d4841 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -3801,6 +3801,7 @@ pub unsafe extern "C" fn wallet_send_transaction( amount: c_ulonglong, fee_per_gram: c_ulonglong, message: *const c_char, + one_sided: bool, error_out: *mut c_int, ) -> c_ulonglong { let mut error = 0; @@ -3843,19 +3844,40 @@ pub unsafe extern "C" fn wallet_send_transaction( .to_owned(); }; - match (*wallet) - .runtime - .block_on((*wallet).wallet.transaction_service.send_transaction( - (*dest_public_key).clone(), - MicroTari::from(amount), - MicroTari::from(fee_per_gram), - message_string, - )) { - Ok(tx_id) => tx_id, - Err(e) => { - error = LibWalletError::from(WalletError::TransactionServiceError(e)).code; - ptr::swap(error_out, &mut error as *mut c_int); - 0 + match one_sided { + true => { + match (*wallet) + .runtime + .block_on((*wallet).wallet.transaction_service.send_one_sided_transaction( + (*dest_public_key).clone(), + MicroTari::from(amount), + MicroTari::from(fee_per_gram), + message_string, + )) { + Ok(tx_id) => tx_id, + Err(e) => { + error = LibWalletError::from(WalletError::TransactionServiceError(e)).code; + ptr::swap(error_out, &mut error as *mut c_int); + 0 + }, + } + }, + false => { + match (*wallet) + .runtime + .block_on((*wallet).wallet.transaction_service.send_transaction( + (*dest_public_key).clone(), + MicroTari::from(amount), + MicroTari::from(fee_per_gram), + message_string, + )) { + Ok(tx_id) => tx_id, + Err(e) => { + error = LibWalletError::from(WalletError::TransactionServiceError(e)).code; + ptr::swap(error_out, &mut error as *mut c_int); + 0 + }, + } }, } } diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index 661d861103..c6ed604541 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -531,7 +531,7 @@ unsigned long long wallet_get_num_confirmations_required(struct TariWallet *wall void wallet_set_num_confirmations_required(struct TariWallet *wallet, unsigned long long num, int *error_out); // Sends a TariPendingOutboundTransaction -unsigned long long wallet_send_transaction(struct TariWallet *wallet, struct TariPublicKey *destination, unsigned long long amount, unsigned long long fee_per_gram, const char *message, int *error_out); +unsigned long long wallet_send_transaction(struct TariWallet *wallet, struct TariPublicKey *destination, unsigned long long amount, unsigned long long fee_per_gram, const char *message, bool one_sided, int *error_out); // Get the TariContacts from a TariWallet struct TariContacts *wallet_get_contacts(struct TariWallet *wallet, int *error_out); diff --git a/integration_tests/features/WalletFFI.feature b/integration_tests/features/WalletFFI.feature index 79b23f241d..df04647db7 100644 --- a/integration_tests/features/WalletFFI.feature +++ b/integration_tests/features/WalletFFI.feature @@ -135,6 +135,30 @@ Feature: Wallet FFI Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT And I stop ffi wallet FFI_WALLET + Scenario: As a client I want to send a one-sided transaction + Given I have a seed node SEED + And I have a base node BASE1 connected to all seed nodes + And I have a base node BASE2 connected to all seed nodes + And I have wallet SENDER connected to base node BASE1 + And I have a ffi wallet FFI_WALLET connected to base node BASE2 + And I have wallet RECEIVER connected to base node BASE2 + And I have mining node MINER connected to base node BASE1 and wallet SENDER + And mining node MINER mines 10 blocks + Then I wait for wallet SENDER to have at least 1000000 uT + And I send 2000000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 + Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + And mining node MINER mines 10 blocks + Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT + And I send 1000000 uT from ffi wallet FFI_WALLET to wallet RECEIVER at fee 20 via one-sided transactions + And mining node MINER mines 2 blocks + Then all nodes are at height 22 + And mining node MINER mines 2 blocks + Then all nodes are at height 24 + And mining node MINER mines 6 blocks + Then I wait for wallet RECEIVER to have at least 1000000 uT + Then I wait for ffi wallet FFI_WALLET to receive 2 mined + And I stop ffi wallet FFI_WALLET + # Scenario: As a client I want to get my balance # It's a subtest of "As a client I want to retrieve a list of transactions I have made and received" diff --git a/integration_tests/features/support/ffi_steps.js b/integration_tests/features/support/ffi_steps.js index 6c083b01d1..1e41a71cb3 100644 --- a/integration_tests/features/support/ffi_steps.js +++ b/integration_tests/features/support/ffi_steps.js @@ -31,7 +31,23 @@ When( this.getWalletPubkey(receiver), amount, feePerGram, - `Send from ffi ${sender} to ${receiver} at fee ${feePerGram}` + `Send from ffi ${sender} to ${receiver} at fee ${feePerGram}`, + false + ); + console.log(result); + } +); + +When( + "I send {int} uT from ffi wallet {word} to wallet {word} at fee {int} via one-sided transactions", + function (amount, sender, receiver, feePerGram) { + let ffiWallet = this.getWallet(sender); + let result = ffiWallet.sendTransaction( + this.getWalletPubkey(receiver), + amount, + feePerGram, + `Send from ffi ${sender} to ${receiver} at fee ${feePerGram}`, + true ); console.log(result); } diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index 9507ad18f9..8eb3b95ade 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -341,6 +341,7 @@ class InterfaceFFI { this.ulonglong, this.ulonglong, this.string, + this.bool, this.intPtr, ], ], @@ -1331,7 +1332,8 @@ class InterfaceFFI { destination, amount, fee_per_gram, - message + message, + one_sided ) { let error = this.initError(); let result = this.fn.wallet_send_transaction( @@ -1340,6 +1342,7 @@ class InterfaceFFI { amount, fee_per_gram, message, + one_sided, error ); this.checkErrorResult(error, `walletSendTransaction`); diff --git a/integration_tests/helpers/ffi/wallet.js b/integration_tests/helpers/ffi/wallet.js index ff1b783ca1..feae5535b8 100644 --- a/integration_tests/helpers/ffi/wallet.js +++ b/integration_tests/helpers/ffi/wallet.js @@ -373,14 +373,15 @@ class Wallet { return result; } - sendTransaction(destination, amount, fee_per_gram, message) { + sendTransaction(destination, amount, fee_per_gram, message, one_sided) { let dest_public_key = PublicKey.fromHexString(utf8.encode(destination)); let result = InterfaceFFI.walletSendTransaction( this.ptr, dest_public_key.getPtr(), amount, fee_per_gram, - utf8.encode(message) + utf8.encode(message), + one_sided ); dest_public_key.destroy(); return result; diff --git a/integration_tests/helpers/walletFFIClient.js b/integration_tests/helpers/walletFFIClient.js index 592ddaff6c..2a84de67cf 100644 --- a/integration_tests/helpers/walletFFIClient.js +++ b/integration_tests/helpers/walletFFIClient.js @@ -139,12 +139,13 @@ class WalletFFIClient { } } - sendTransaction(destination, amount, fee_per_gram, message) { + sendTransaction(destination, amount, fee_per_gram, message, one_sided) { return this.wallet.sendTransaction( destination, amount, fee_per_gram, - message + message, + one_sided ); } From a4341a03afedd9df9a29cd09219b2ed9b5cf7a5a Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 3 Dec 2021 10:40:50 +0200 Subject: [PATCH 29/29] fix(pruned mode)!: prune inputs, allow horizon sync resume and other fixes (#3521) Description --- - delete inputs behind pruning horizon during pruned mode cleanup - keep cumulative utxo_sum and kernel_sum in `BlockAccumulatedData` - prune spent outputs that may have not been pruned in previous horizon sync - bugfix: check rangeproof validation result in horizon sync (failed validation was ignored) - parallelize rangeproof verification for horizon sync - prune blocks between new and old horizon height if necessary before syncing - fix off-by-one in pruning horizon (pruning horizon is consistently defined as the last pruned block inclusive) - minor optimisations in horizon sync - new blockchain db unit tests Motivation and Context --- Inputs behind the pruning horizon were not pruned as propagated blocks arrive. Breaking change: Blockchain database will need to be resynced Based on #3520 How Has This Been Tested? --- Cucumber tests pass Manually, horizon sync to tip and wallet recovery --- applications/tari_base_node/src/bootstrap.rs | 2 +- .../state_machine_service/initializer.rs | 2 +- .../state_machine_service/state_machine.rs | 12 +- .../states/horizon_state_sync.rs | 53 +-- .../states/horizon_state_sync/error.rs | 21 +- .../horizon_state_synchronization.rs | 410 +++++++++++------- .../states/sync_decide.rs | 35 +- .../base_node/sync/block_sync/synchronizer.rs | 3 +- .../sync/header_sync/synchronizer.rs | 2 +- .../core/src/base_node/sync/rpc/service.rs | 104 +++-- .../src/base_node/sync/rpc/sync_utxos_task.rs | 196 +++++---- .../core/src/blocks/accumulated_data.rs | 26 +- base_layer/core/src/blocks/mod.rs | 1 + base_layer/core/src/chain_storage/async_db.rs | 45 +- .../src/chain_storage/blockchain_backend.rs | 11 +- .../src/chain_storage/blockchain_database.rs | 127 ++++-- .../core/src/chain_storage/db_transaction.rs | 125 +++--- base_layer/core/src/chain_storage/error.rs | 8 +- .../core/src/chain_storage/horizon_data.rs | 8 +- .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 363 +++++----------- .../tests/blockchain_database.rs | 122 +++++- .../core/src/test_helpers/blockchain.rs | 16 +- .../core/src/transactions/aggregated_body.rs | 6 +- .../core/src/transactions/coinbase_builder.rs | 2 +- .../transactions/transaction_entities/mod.rs | 4 +- .../transaction_output.rs | 10 +- .../transaction_protocol/recipient.rs | 2 +- .../transaction_protocol/sender.rs | 12 +- .../transaction_protocol/single_receiver.rs | 5 +- base_layer/core/src/validation/error.rs | 2 +- .../chain_storage_tests/chain_storage.rs | 2 +- common/src/build/protobuf.rs | 9 +- comms/Cargo.toml | 2 +- comms/dht/src/config.rs | 1 + .../features/Propagation.feature | 2 +- 35 files changed, 973 insertions(+), 778 deletions(-) diff --git a/applications/tari_base_node/src/bootstrap.rs b/applications/tari_base_node/src/bootstrap.rs index 516e93d2f1..e584827cd2 100644 --- a/applications/tari_base_node/src/bootstrap.rs +++ b/applications/tari_base_node/src/bootstrap.rs @@ -170,7 +170,7 @@ where B: BlockchainBackend + 'static orphan_db_clean_out_threshold: config.orphan_db_clean_out_threshold, max_randomx_vms: config.max_randomx_vms, blocks_behind_before_considered_lagging: self.config.blocks_behind_before_considered_lagging, - block_sync_validation_concurrency: num_cpus::get(), + sync_validation_concurrency: num_cpus::get(), ..Default::default() }, self.rules, diff --git a/base_layer/core/src/base_node/state_machine_service/initializer.rs b/base_layer/core/src/base_node/state_machine_service/initializer.rs index 04a66f073c..582be35742 100644 --- a/base_layer/core/src/base_node/state_machine_service/initializer.rs +++ b/base_layer/core/src/base_node/state_machine_service/initializer.rs @@ -107,7 +107,7 @@ where B: BlockchainBackend + 'static rules.clone(), factories, config.bypass_range_proof_verification, - config.block_sync_validation_concurrency, + config.sync_validation_concurrency, ); let max_randomx_vms = config.max_randomx_vms; diff --git a/base_layer/core/src/base_node/state_machine_service/state_machine.rs b/base_layer/core/src/base_node/state_machine_service/state_machine.rs index 4bb5aa7ccb..fb364fc797 100644 --- a/base_layer/core/src/base_node/state_machine_service/state_machine.rs +++ b/base_layer/core/src/base_node/state_machine_service/state_machine.rs @@ -54,7 +54,7 @@ pub struct BaseNodeStateMachineConfig { pub max_randomx_vms: usize, pub blocks_behind_before_considered_lagging: u64, pub bypass_range_proof_verification: bool, - pub block_sync_validation_concurrency: usize, + pub sync_validation_concurrency: usize, } impl Default for BaseNodeStateMachineConfig { @@ -68,7 +68,7 @@ impl Default for BaseNodeStateMachineConfig { max_randomx_vms: 0, blocks_behind_before_considered_lagging: 0, bypass_range_proof_verification: false, - block_sync_validation_concurrency: 8, + sync_validation_concurrency: 8, } } } @@ -259,9 +259,13 @@ impl BaseNodeStateMachine { /// Polls both the interrupt signal and the given future. If the given future `state_fut` is ready first it's value is /// returned, otherwise if the interrupt signal is triggered, `StateEvent::UserQuit` is returned. -async fn select_next_state_event(interrupt_signal: ShutdownSignal, state_fut: F) -> StateEvent -where F: Future { +async fn select_next_state_event(interrupt_signal: I, state_fut: F) -> StateEvent +where + F: Future, + I: Future, +{ futures::pin_mut!(state_fut); + futures::pin_mut!(interrupt_signal); // If future A and B are both ready `future::select` will prefer A match future::select(interrupt_signal, state_fut).await { Either::Left(_) => StateEvent::UserQuit, diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync.rs index ca2161e033..e4a163f003 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync.rs @@ -25,30 +25,26 @@ // TODO: Move the horizon synchronizer to the `sync` module -use log::*; +mod config; +pub use self::config::HorizonSyncConfig; +mod error; pub use error::HorizonSyncError; -use horizon_state_synchronization::HorizonStateSynchronization; -use crate::{ - base_node::{sync::SyncPeer, BaseNodeStateMachine}, - chain_storage::BlockchainBackend, - transactions::CryptoFactories, -}; +mod horizon_state_synchronization; +use horizon_state_synchronization::HorizonStateSynchronization; use super::{ events_and_states::{HorizonSyncInfo, HorizonSyncStatus}, StateEvent, StateInfo, }; - -pub use self::config::HorizonSyncConfig; - -mod config; - -mod error; - -mod horizon_state_synchronization; +use crate::{ + base_node::{sync::SyncPeer, BaseNodeStateMachine}, + chain_storage::BlockchainBackend, + transactions::CryptoFactories, +}; +use log::*; const LOG_TARGET: &str = "c::bn::state_machine_service::states::horizon_state_sync"; @@ -72,21 +68,26 @@ impl HorizonStateSync { ) -> StateEvent { let local_metadata = match shared.db.get_chain_metadata().await { Ok(metadata) => metadata, - Err(err) => return StateEvent::FatalError(err.to_string()), + Err(err) => return err.into(), }; - if local_metadata.height_of_longest_chain() > 0 && - local_metadata.height_of_longest_chain() >= local_metadata.pruned_height() - { + let last_header = match shared.db.fetch_last_header().await { + Ok(h) => h, + Err(err) => return err.into(), + }; + + let horizon_sync_height = local_metadata.horizon_block(last_header.height); + if local_metadata.pruned_height() >= horizon_sync_height { + info!(target: LOG_TARGET, "Horizon state was already synchronized."); return StateEvent::HorizonStateSynchronized; } - let horizon_sync_height = match shared.db.fetch_last_header().await { - Ok(header) => header.height.saturating_sub(local_metadata.pruning_horizon()), - Err(err) => return StateEvent::FatalError(err.to_string()), - }; - - if local_metadata.height_of_longest_chain() > horizon_sync_height { + // We're already synced because we have full blocks higher than our target pruned height + if local_metadata.height_of_longest_chain() >= horizon_sync_height { + info!( + target: LOG_TARGET, + "Tip height is higher than our pruned height. Horizon state is already synchronized." + ); return StateEvent::HorizonStateSynchronized; } @@ -94,7 +95,7 @@ impl HorizonStateSync { shared.set_state_info(StateInfo::HorizonSync(info)); let prover = CryptoFactories::default().range_proof; - let mut horizon_state = HorizonStateSynchronization::new(shared, &self.sync_peer, horizon_sync_height, &prover); + let mut horizon_state = HorizonStateSynchronization::new(shared, &self.sync_peer, horizon_sync_height, prover); match horizon_state.synchronize().await { Ok(()) => { diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs index 8dd46d70f7..df62e5495a 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs @@ -20,23 +20,20 @@ // 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::num::TryFromIntError; - -use thiserror::Error; -use tokio::task; - -use tari_comms::{ - connectivity::ConnectivityError, - protocol::rpc::{RpcError, RpcStatus}, -}; -use tari_mmr::error::MerkleMountainRangeError; - use crate::{ base_node::{comms_interface::CommsInterfaceError, state_machine_service::states::helpers::BaseNodeRequestError}, chain_storage::{ChainStorageError, MmrTree}, transactions::transaction_entities::error::TransactionError, validation::ValidationError, }; +use std::num::TryFromIntError; +use tari_comms::{ + connectivity::ConnectivityError, + protocol::rpc::{RpcError, RpcStatus}, +}; +use tari_mmr::error::MerkleMountainRangeError; +use thiserror::Error; +use tokio::task; #[derive(Debug, Error)] pub enum HorizonSyncError { @@ -71,7 +68,7 @@ pub enum HorizonSyncError { ConversionError(String), #[error("MerkleMountainRangeError: {0}")] MerkleMountainRangeError(#[from] MerkleMountainRangeError), - #[error("Connectivity Error: {0}")] + #[error("Connectivity error: {0}")] ConnectivityError(#[from] ConnectivityError), } diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs index 05385f876a..0a3e208e29 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs @@ -20,22 +20,7 @@ // 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}, - sync::Arc, -}; - -use croaring::Bitmap; -use futures::StreamExt; -use log::*; -use tari_crypto::{ - commitment::HomomorphicCommitment, - tari_utilities::{hex::Hex, Hashable}, -}; - -use tari_common_types::types::{HashDigest, RangeProofService}; -use tari_mmr::{MerkleMountainRange, MutableMmr}; - +use super::error::HorizonSyncError; use crate::{ base_node::{ state_machine_service::{ @@ -44,7 +29,7 @@ use crate::{ }, sync::{rpc, SyncPeer}, }, - blocks::BlockHeader, + blocks::{BlockHeader, ChainHeader, UpdateBlockAccumulatedData}, chain_storage::{async_db::AsyncBlockchainDb, BlockchainBackend, ChainStorageError, MmrTree, PrunedOutput}, proto::base_node::{ sync_utxo as proto_sync_utxo, @@ -59,8 +44,23 @@ use crate::{ transaction_output::TransactionOutput, }, }; - -use super::error::HorizonSyncError; +use croaring::Bitmap; +use futures::{stream::FuturesUnordered, StreamExt}; +use log::*; +use std::{ + cmp, + convert::{TryFrom, TryInto}, + mem, + sync::Arc, + time::Instant, +}; +use tari_common_types::types::{Commitment, HashDigest, RangeProofService}; +use tari_crypto::{ + commitment::HomomorphicCommitment, + tari_utilities::{hex::Hex, Hashable}, +}; +use tari_mmr::{MerkleMountainRange, MutableMmr}; +use tokio::task; const LOG_TARGET: &str = "c::bn::state_machine_service::states::horizon_state_sync"; @@ -68,9 +68,10 @@ pub struct HorizonStateSynchronization<'a, B: BlockchainBackend> { shared: &'a mut BaseNodeStateMachine, sync_peer: &'a SyncPeer, horizon_sync_height: u64, - prover: &'a RangeProofService, + prover: Arc, num_kernels: u64, num_outputs: u64, + full_bitmap: Option, } impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { @@ -78,7 +79,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { shared: &'a mut BaseNodeStateMachine, sync_peer: &'a SyncPeer, horizon_sync_height: u64, - prover: &'a RangeProofService, + prover: Arc, ) -> Self { Self { shared, @@ -87,14 +88,16 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { prover, num_kernels: 0, num_outputs: 0, + full_bitmap: None, } } pub async fn synchronize(&mut self) -> Result<(), HorizonSyncError> { debug!( target: LOG_TARGET, - "Preparing database for horizon sync to height (#{})", self.horizon_sync_height + "Preparing database for horizon sync to height #{}", self.horizon_sync_height ); + let header = self.db().fetch_header(self.horizon_sync_height).await?.ok_or_else(|| { ChainStorageError::ValueNotFound { entity: "Header", @@ -130,6 +133,8 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { client: &mut rpc::BaseNodeSyncRpcClient, to_header: &BlockHeader, ) -> Result<(), HorizonSyncError> { + debug!(target: LOG_TARGET, "Initializing"); + self.initialize().await?; debug!(target: LOG_TARGET, "Synchronizing kernels"); self.synchronize_kernels(client, to_header).await?; debug!(target: LOG_TARGET, "Synchronizing outputs"); @@ -137,6 +142,21 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { Ok(()) } + async fn initialize(&mut self) -> Result<(), HorizonSyncError> { + let db = self.db(); + let local_metadata = db.get_chain_metadata().await?; + + let new_prune_height = cmp::min(local_metadata.height_of_longest_chain(), self.horizon_sync_height); + if local_metadata.pruned_height() < new_prune_height { + debug!(target: LOG_TARGET, "Pruning block chain to height {}", new_prune_height); + db.prune_to_height(new_prune_height).await?; + } + + self.full_bitmap = Some(db.fetch_deleted_bitmap_at_tip().await?.into_bitmap()); + + Ok(()) + } + async fn synchronize_kernels( &mut self, client: &mut rpc::BaseNodeSyncRpcClient, @@ -174,41 +194,43 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { latency.unwrap_or_default().as_millis() ); - let start = local_num_kernels; - let end = remote_num_kernels; - let end_hash = to_header.hash(); - + let mut current_header = self + .db() + .fetch_header_containing_kernel_mmr(local_num_kernels + 1) + .await?; let req = SyncKernelsRequest { - start, - end_header_hash: end_hash, + start: local_num_kernels, + end_header_hash: to_header.hash(), }; let mut kernel_stream = client.sync_kernels(req).await?; - let mut current_header = self.db().fetch_header_containing_kernel_mmr(start + 1).await?; debug!( target: LOG_TARGET, "Found header for kernels at mmr pos: {} height: {}", - start, + local_num_kernels, current_header.height() ); - let mut kernels = vec![]; + let mut kernel_hashes = vec![]; let db = self.db().clone(); let mut txn = db.write_transaction(); - let mut mmr_position = start; + let mut mmr_position = local_num_kernels; + let end = remote_num_kernels; while let Some(kernel) = kernel_stream.next().await { let kernel: TransactionKernel = kernel?.try_into().map_err(HorizonSyncError::ConversionError)?; kernel .verify_signature() .map_err(HorizonSyncError::InvalidKernelSignature)?; - kernels.push(kernel.clone()); + kernel_hashes.push(kernel.hash()); + txn.insert_kernel_via_horizon_sync(kernel, current_header.hash().clone(), mmr_position as u32); if mmr_position == current_header.header().kernel_mmr_size - 1 { + let num_kernels = kernel_hashes.len(); debug!( target: LOG_TARGET, "Header #{} ({} kernels)", current_header.height(), - kernels.len() + num_kernels, ); // Validate root let block_data = db @@ -217,8 +239,8 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { let kernel_pruned_set = block_data.dissolve().0; let mut kernel_mmr = MerkleMountainRange::::new(kernel_pruned_set); - for kernel in kernels.drain(..) { - kernel_mmr.push(kernel.hash())?; + for hash in kernel_hashes.drain(..) { + kernel_mmr.push(hash)?; } let mmr_root = kernel_mmr.get_merkle_root()?; @@ -231,13 +253,29 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { }); } - txn.update_pruned_hash_set( - MmrTree::Kernel, + let kernel_hash_set = kernel_mmr.get_pruned_hash_set()?; + debug!( + target: LOG_TARGET, + "Updating block data at height {}", + current_header.height() + ); + txn.update_block_accumulated_data_via_horizon_sync( current_header.hash().clone(), - kernel_mmr.get_pruned_hash_set()?, + UpdateBlockAccumulatedData { + kernel_hash_set: Some(kernel_hash_set), + ..Default::default() + }, ); txn.commit().await?; + debug!( + target: LOG_TARGET, + "Committed {} kernel(s), ({}/{}) {} remaining", + num_kernels, + mmr_position + 1, + end, + end - (mmr_position + 1) + ); if mmr_position < end - 1 { current_header = db.fetch_chain_header(current_header.height() + 1).await?; } @@ -308,39 +346,39 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { include_deleted_bitmaps: true, include_pruned_utxos: true, }; - let mut output_stream = client.sync_utxos(req).await?; let mut current_header = self.db().fetch_header_containing_utxo_mmr(start + 1).await?; + let mut output_stream = client.sync_utxos(req).await?; + debug!( target: LOG_TARGET, "Found header for utxos at mmr pos: {} - {} height: {}", - start + 1, + start, current_header.header().output_mmr_size, current_header.height() ); let db = self.db().clone(); - let mut output_hashes = vec![]; - let mut witness_hashes = vec![]; let mut txn = db.write_transaction(); let mut unpruned_outputs = vec![]; let mut mmr_position = start; let mut height_utxo_counter = 0u64; let mut height_txo_counter = 0u64; + let mut timer = Instant::now(); let block_data = db .fetch_block_accumulated_data(current_header.header().prev_hash.clone()) .await?; - let (_, output_pruned_set, rp_pruned_set, mut full_bitmap) = block_data.dissolve(); + let (_, output_pruned_set, witness_pruned_set, _) = block_data.dissolve(); let mut output_mmr = MerkleMountainRange::::new(output_pruned_set); - let mut witness_mmr = MerkleMountainRange::::new(rp_pruned_set); + let mut witness_mmr = MerkleMountainRange::::new(witness_pruned_set); while let Some(response) = output_stream.next().await { let res: SyncUtxosResponse = response?; - if res.mmr_index > 0 && res.mmr_index != mmr_position { + if res.mmr_index != 0 && res.mmr_index != mmr_position { return Err(HorizonSyncError::IncorrectResponse(format!( "Expected MMR position of {} but got {}", mmr_position, res.mmr_index, @@ -363,9 +401,11 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { ); height_utxo_counter += 1; let output = TransactionOutput::try_from(output).map_err(HorizonSyncError::ConversionError)?; - output_hashes.push(output.hash()); - witness_hashes.push(output.witness_hash()); unpruned_outputs.push(output.clone()); + + output_mmr.push(output.hash())?; + witness_mmr.push(output.witness_hash())?; + txn.insert_output_via_horizon_sync( output, current_header.hash().clone(), @@ -384,8 +424,9 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { current_header.height() ); height_txo_counter += 1; - output_hashes.push(utxo.hash.clone()); - witness_hashes.push(utxo.witness_hash.clone()); + output_mmr.push(utxo.hash.clone())?; + witness_mmr.push(utxo.witness_hash.clone())?; + txn.insert_pruned_output_via_horizon_sync( utxo.hash, utxo.witness_hash, @@ -404,29 +445,8 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { ))); } - debug!( - target: LOG_TARGET, - "UTXO: {} (Header #{}), added {} utxos, added {} txos", - mmr_position, - current_header.height(), - height_utxo_counter, - height_txo_counter - ); - - height_txo_counter = 0; - height_utxo_counter = 0; - - // Validate root - for hash in output_hashes.drain(..) { - output_mmr.push(hash)?; - } - - for hash in witness_hashes.drain(..) { - witness_mmr.push(hash)?; - } - - // Check that the difference bitmap is excessively large. Bitmap::deserialize panics if greater than - // isize::MAX, however isize::MAX is still an inordinate amount of data. An + // Check that the difference bitmap isn't excessively large. Bitmap::deserialize panics if greater + // than isize::MAX, however isize::MAX is still an inordinate amount of data. An // arbitrary 4 MiB limit is used. const MAX_DIFF_BITMAP_BYTE_LEN: usize = 4 * 1024 * 1024; if diff_bitmap.len() > MAX_DIFF_BITMAP_BYTE_LEN { @@ -448,11 +468,12 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { // Merge the differences into the final bitmap so that we can commit to the entire spend state // in the output MMR - full_bitmap.or_inplace(&diff_bitmap); - full_bitmap.run_optimize(); + let bitmap = self.full_bitmap_mut(); + bitmap.or_inplace(&diff_bitmap); + bitmap.run_optimize(); let pruned_output_set = output_mmr.get_pruned_hash_set()?; - let output_mmr = MutableMmr::::new(pruned_output_set.clone(), full_bitmap.clone())?; + let output_mmr = MutableMmr::::new(pruned_output_set.clone(), bitmap.clone())?; let mmr_root = output_mmr.get_merkle_root()?; if mmr_root != current_header.header().output_mr { @@ -474,29 +495,54 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { }); } - // Validate rangeproofs if the MMR matches - for o in unpruned_outputs.drain(..) { - o.verify_range_proof(self.prover) - .map_err(|err| HorizonSyncError::InvalidRangeProof(o.hash().to_hex(), err.to_string()))?; - } + self.validate_rangeproofs(mem::take(&mut unpruned_outputs)).await?; txn.update_deleted_bitmap(diff_bitmap.clone()); - txn.update_pruned_hash_set(MmrTree::Utxo, current_header.hash().clone(), pruned_output_set); - txn.update_pruned_hash_set( - MmrTree::Witness, + + let witness_hash_set = witness_mmr.get_pruned_hash_set()?; + txn.update_block_accumulated_data_via_horizon_sync( current_header.hash().clone(), - witness_mmr.get_pruned_hash_set()?, + UpdateBlockAccumulatedData { + utxo_hash_set: Some(pruned_output_set), + witness_hash_set: Some(witness_hash_set), + deleted_diff: Some(diff_bitmap.into()), + ..Default::default() + }, ); - txn.update_block_accumulated_data_with_deleted_diff(current_header.hash().clone(), diff_bitmap); - txn.commit().await?; - current_header = db.fetch_chain_header(current_header.height() + 1).await?; debug!( target: LOG_TARGET, - "Expecting to receive the next UTXO set for header #{}", - current_header.height() + "UTXO: {}/{}, Header #{}, added {} utxos, added {} txos in {:.2?}", + mmr_position, + end, + current_header.height(), + height_utxo_counter, + height_txo_counter, + timer.elapsed() ); + height_txo_counter = 0; + height_utxo_counter = 0; + timer = Instant::now(); + + if mmr_position == end { + debug!( + target: LOG_TARGET, + "Sync complete at mmr position {}, height #{}", + mmr_position, + current_header.height() + ); + break; + } else { + current_header = db.fetch_chain_header(current_header.height() + 1).await?; + debug!( + target: LOG_TARGET, + "Expecting to receive the next UTXO set {}-{} for header #{}", + mmr_position, + current_header.header().output_mmr_size, + current_header.height() + ); + } }, v => { error!(target: LOG_TARGET, "Remote node returned an invalid response {:?}", v); @@ -520,6 +566,37 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { "Sync node did not send all utxos requested".to_string(), )); } + + Ok(()) + } + + async fn validate_rangeproofs(&self, mut unpruned_outputs: Vec) -> Result<(), HorizonSyncError> { + let concurrency = self.shared.config.sync_validation_concurrency; + let mut chunk_size = unpruned_outputs.len() / concurrency; + if unpruned_outputs.len() % concurrency > 0 { + chunk_size += 1; + } + // Validate rangeproofs in parallel + let mut tasks = (0..concurrency) + .map(|_| { + let end = cmp::min(unpruned_outputs.len(), chunk_size); + unpruned_outputs.drain(..end).collect::>() + }) + .map(|chunk| { + let prover = self.prover.clone(); + task::spawn_blocking(move || -> Result<(), HorizonSyncError> { + for o in chunk { + o.verify_range_proof(&prover) + .map_err(|err| HorizonSyncError::InvalidRangeProof(o.hash().to_hex(), err.to_string()))?; + } + Ok(()) + }) + }) + .collect::>(); + + while let Some(result) = tasks.next().await { + result??; + } Ok(()) } @@ -528,22 +605,75 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { async fn finalize_horizon_sync(&mut self) -> Result<(), HorizonSyncError> { debug!(target: LOG_TARGET, "Validating horizon state"); - let info = HorizonSyncInfo::new(vec![self.sync_peer.node_id().clone()], HorizonSyncStatus::Finalizing); - self.shared.set_state_info(StateInfo::HorizonSync(info)); + self.shared.set_state_info(StateInfo::HorizonSync(HorizonSyncInfo::new( + vec![self.sync_peer.node_id().clone()], + HorizonSyncStatus::Finalizing, + ))); let header = self.db().fetch_chain_header(self.horizon_sync_height).await?; + let (calc_utxo_sum, calc_kernel_sum) = self.calculate_commitment_sums(&header).await?; + + self.shared + .sync_validators + .final_horizon_state + .validate( + &*self.db().inner().db_read_access()?, + header.height(), + &calc_utxo_sum, + &calc_kernel_sum, + ) + .map_err(HorizonSyncError::FinalStateValidationFailed)?; + + let metadata = self.db().get_chain_metadata().await?; + info!( + target: LOG_TARGET, + "Horizon state validation succeeded! Committing horizon state." + ); + self.db() + .write_transaction() + .set_best_block( + header.height(), + header.hash().clone(), + header.accumulated_data().total_accumulated_difficulty, + metadata.best_block().clone(), + ) + .set_pruned_height(header.height()) + .set_horizon_data(calc_kernel_sum, calc_utxo_sum) + .commit() + .await?; + + Ok(()) + } + + fn take_final_bitmap(&mut self) -> Arc { + self.full_bitmap + .take() + .map(Arc::new) + .expect("take_full_bitmap called before initialize") + } + + fn full_bitmap_mut(&mut self) -> &mut Bitmap { + self.full_bitmap + .as_mut() + .expect("full_bitmap_mut called before initialize") + } + + /// (UTXO sum, Kernel sum) + async fn calculate_commitment_sums( + &mut self, + header: &ChainHeader, + ) -> Result<(Commitment, Commitment), HorizonSyncError> { let mut pruned_utxo_sum = HomomorphicCommitment::default(); let mut pruned_kernel_sum = HomomorphicCommitment::default(); let mut prev_mmr = 0; let mut prev_kernel_mmr = 0; - let bitmap = Arc::new( - self.db() - .fetch_complete_deleted_bitmap_at(header.hash().clone()) - .await? - .into_bitmap(), - ); - let expected_prev_best_block = self.shared.db.get_chain_metadata().await?.best_block().clone(); + + let bitmap = self.take_final_bitmap(); + let mut txn = self.db().write_transaction(); + let mut utxo_mmr_position = 0; + let mut prune_positions = vec![]; + for h in 0..=header.height() { let curr_header = self.db().fetch_chain_header(h).await?; @@ -555,10 +685,7 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { prev_mmr, curr_header.header().output_mmr_size - 1 ); - let (utxos, _) = self - .db() - .fetch_utxos_by_mmr_position(prev_mmr, curr_header.header().output_mmr_size - 1, bitmap.clone()) - .await?; + let (utxos, _) = self.db().fetch_utxos_in_block(curr_header.hash().clone(), None).await?; trace!( target: LOG_TARGET, "Fetching kernels from db: height:{}, header.kernel_mmr:{}, prev_mmr:{}, end:{}", @@ -567,74 +694,59 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> { prev_kernel_mmr, curr_header.header().kernel_mmr_size - 1 ); - let kernels = self - .db() - .fetch_kernels_by_mmr_position(prev_kernel_mmr, curr_header.header().kernel_mmr_size - 1) - .await?; - - let mut utxo_sum = HomomorphicCommitment::default(); - debug!(target: LOG_TARGET, "Number of kernels returned: {}", kernels.len()); - debug!(target: LOG_TARGET, "Number of utxos returned: {}", utxos.len()); + + trace!(target: LOG_TARGET, "Number of utxos returned: {}", utxos.len()); let mut prune_counter = 0; for u in utxos { match u { PrunedOutput::NotPruned { output } => { - utxo_sum = &output.commitment + &utxo_sum; + if bitmap.contains(utxo_mmr_position) { + debug!( + target: LOG_TARGET, + "Found output that needs pruning at height: {} position: {}", h, utxo_mmr_position + ); + prune_positions.push(utxo_mmr_position); + prune_counter += 1; + } else { + pruned_utxo_sum = &output.commitment + &pruned_utxo_sum; + } }, _ => { prune_counter += 1; }, } + utxo_mmr_position += 1; } if prune_counter > 0 { - debug!(target: LOG_TARGET, "Pruned {} outputs", prune_counter); + trace!(target: LOG_TARGET, "Pruned {} outputs", prune_counter); } prev_mmr = curr_header.header().output_mmr_size; - pruned_utxo_sum = &utxo_sum + &pruned_utxo_sum; - + let kernels = self.db().fetch_kernels_in_block(curr_header.hash().clone()).await?; + trace!(target: LOG_TARGET, "Number of kernels returned: {}", kernels.len()); for k in kernels { pruned_kernel_sum = &k.excess + &pruned_kernel_sum; } prev_kernel_mmr = curr_header.header().kernel_mmr_size; - trace!( - target: LOG_TARGET, - "Height: {} Kernel sum:{:?} Pruned UTXO sum: {:?}", - h, - pruned_kernel_sum, - pruned_utxo_sum - ); + if h % 1000 == 0 { + debug!( + target: LOG_TARGET, + "Final Validation: {:.2}% complete. Height: {}, mmr_position: {} ", + (h as f32 / header.height() as f32) * 100.0, + h, + utxo_mmr_position, + ); + } } - self.shared - .sync_validators - .final_horizon_state - .validate( - &*self.db().clone().into_inner().db_read_access()?, - header.height(), - &pruned_utxo_sum, - &pruned_kernel_sum, - ) - .map_err(HorizonSyncError::FinalStateValidationFailed)?; - - info!( - target: LOG_TARGET, - "Horizon state validation succeeded! Committing horizon state." - ); - self.db() - .write_transaction() - .set_best_block( - header.height(), - header.hash().clone(), - header.accumulated_data().total_accumulated_difficulty, - expected_prev_best_block, - ) - .set_pruned_height(header.height(), pruned_kernel_sum, pruned_utxo_sum) - .commit() - .await?; + if !prune_positions.is_empty() { + debug!(target: LOG_TARGET, "Pruning {} spent outputs", prune_positions.len()); + txn.prune_output_at_positions(prune_positions); + txn.commit().await?; + } - Ok(()) + Ok((pruned_utxo_sum, pruned_kernel_sum)) } #[inline] diff --git a/base_layer/core/src/base_node/state_machine_service/states/sync_decide.rs b/base_layer/core/src/base_node/state_machine_service/states/sync_decide.rs index fb7c85673b..6f50f748a9 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/sync_decide.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/sync_decide.rs @@ -56,17 +56,28 @@ impl DecideNextSync { ); if shared.config.pruning_horizon > 0 { - // Filter sync peers that claim to be able to provide full blocks up until our pruned height + let last_header = match shared.db.fetch_last_header().await { + Ok(h) => h, + Err(err) => return err.into(), + }; + + let horizon_sync_height = local_metadata.horizon_block(last_header.height); + // Filter sync peers that claim to be able to provide blocks up until our pruned height let sync_peers_iter = self.sync_peers.iter().filter(|sync_peer| { - let chain_metadata = sync_peer.claimed_chain_metadata(); - let our_pruned_height_from_peer = - local_metadata.horizon_block(chain_metadata.height_of_longest_chain()); - let their_pruned_height = chain_metadata.pruned_height(); - our_pruned_height_from_peer >= their_pruned_height + let remote_metadata = sync_peer.claimed_chain_metadata(); + remote_metadata.height_of_longest_chain() >= horizon_sync_height }); match find_best_latency(sync_peers_iter) { - Some(sync_peer) => ProceedToHorizonSync(sync_peer), + Some(sync_peer) => { + debug!( + target: LOG_TARGET, + "Proceeding to horizon sync with sync peer {} with a latency of {:.2?}", + sync_peer.node_id(), + sync_peer.latency() + ); + ProceedToHorizonSync(sync_peer) + }, None => Continue, } } else { @@ -76,7 +87,15 @@ impl DecideNextSync { }); match find_best_latency(sync_peers_iter) { - Some(sync_peer) => ProceedToBlockSync(sync_peer), + Some(sync_peer) => { + debug!( + target: LOG_TARGET, + "Proceeding to block sync with sync peer {} with a latency of {:.2?}", + sync_peer.node_id(), + sync_peer.latency() + ); + ProceedToBlockSync(sync_peer) + }, None => Continue, } } diff --git a/base_layer/core/src/base_node/sync/block_sync/synchronizer.rs b/base_layer/core/src/base_node/sync/block_sync/synchronizer.rs index b17da13bc3..b476f4078f 100644 --- a/base_layer/core/src/base_node/sync/block_sync/synchronizer.rs +++ b/base_layer/core/src/base_node/sync/block_sync/synchronizer.rs @@ -31,7 +31,7 @@ use crate::{ proto::base_node::SyncBlocksRequest, tari_utilities::{hex::Hex, Hashable}, transactions::aggregated_body::AggregateBody, - validation::BlockSyncBodyValidation, + validation::{BlockSyncBodyValidation, ValidationError}, }; use futures::StreamExt; use log::*; @@ -96,6 +96,7 @@ impl BlockSynchronizer { self.db.cleanup_orphans().await?; Ok(()) }, + Err(err @ BlockSyncError::ValidationError(ValidationError::AsyncTaskFailed(_))) => Err(err), Err(err @ BlockSyncError::ValidationError(_)) | Err(err @ BlockSyncError::ReceivedInvalidBlockBody(_)) => { self.ban_peer(node_id, &err).await?; Err(err) diff --git a/base_layer/core/src/base_node/sync/header_sync/synchronizer.rs b/base_layer/core/src/base_node/sync/header_sync/synchronizer.rs index 3748a557f5..f0fcd4a148 100644 --- a/base_layer/core/src/base_node/sync/header_sync/synchronizer.rs +++ b/base_layer/core/src/base_node/sync/header_sync/synchronizer.rs @@ -114,7 +114,7 @@ impl<'a, B: BlockchainBackend + 'static> HeaderSynchronizer<'a, B> { target: LOG_TARGET, "Attempting to synchronize headers with `{}`", node_id ); - match self.attempt_sync(sync_peer, peer_conn).await { + match self.attempt_sync(sync_peer, peer_conn.clone()).await { Ok(()) => return Ok(sync_peer.clone()), // Try another peer Err(err @ BlockHeaderSyncError::NotInSync) => { diff --git a/base_layer/core/src/base_node/sync/rpc/service.rs b/base_layer/core/src/base_node/sync/rpc/service.rs index 8a48cb3ce1..cc62c819d0 100644 --- a/base_layer/core/src/base_node/sync/rpc/service.rs +++ b/base_layer/core/src/base_node/sync/rpc/service.rs @@ -22,7 +22,7 @@ use crate::{ base_node::sync::rpc::{sync_utxos_task::SyncUtxosTask, BaseNodeSyncService}, - chain_storage::{async_db::AsyncBlockchainDb, BlockchainBackend, OrNotFound}, + chain_storage::{async_db::AsyncBlockchainDb, BlockchainBackend}, iterators::NonOverlappingIntegerPairIter, proto, proto::base_node::{ @@ -34,6 +34,7 @@ use crate::{ SyncUtxosRequest, SyncUtxosResponse, }, + tari_utilities::Hashable, }; use log::*; use std::{ @@ -387,47 +388,57 @@ impl BaseNodeSyncService for BaseNodeSyncRpcServ request: Request, ) -> Result, RpcStatus> { let req = request.into_message(); - const BATCH_SIZE: usize = 1000; - let (tx, rx) = mpsc::channel(BATCH_SIZE); + let (tx, rx) = mpsc::channel(100); let db = self.db(); - task::spawn(async move { - let end = match db - .fetch_chain_header_by_block_hash(req.end_header_hash.clone()) - .await - .or_not_found("BlockHeader", "hash", req.end_header_hash.to_hex()) - .map_err(RpcStatus::log_internal_error(LOG_TARGET)) - { - Ok(header) => { - if header.header().kernel_mmr_size < req.start { - let _ = tx - .send(Err(RpcStatus::bad_request("Start mmr position after requested header"))) - .await; - return; - } + let start_header = db + .fetch_header_containing_kernel_mmr(req.start + 1) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .into_header(); + + let end_header = db + .fetch_header_by_block_hash(req.end_header_hash.clone()) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| RpcStatus::not_found("Unknown end header"))?; + + let mut current_height = start_header.height; + let end_height = end_header.height; + let mut current_mmr_position = start_header.kernel_mmr_size; + let mut current_header_hash = start_header.hash(); + + if current_height > end_height { + return Err(RpcStatus::bad_request("start header height is after end header")); + } - header.header().kernel_mmr_size - }, - Err(err) => { - let _ = tx.send(Err(err)).await; - return; - }, - }; - let iter = NonOverlappingIntegerPairIter::new(req.start, end, BATCH_SIZE); - for (start, end) in iter { + task::spawn(async move { + while current_height <= end_height { if tx.is_closed() { break; } - debug!(target: LOG_TARGET, "Streaming kernels {} to {}", start, end); let res = db - .fetch_kernels_by_mmr_position(start, end) + .fetch_kernels_in_block(current_header_hash.clone()) .await .map_err(RpcStatus::log_internal_error(LOG_TARGET)); match res { Ok(kernels) if kernels.is_empty() => { + let _ = tx + .send(Err(RpcStatus::general(format!( + "No kernels in block {}", + current_header_hash.to_hex() + )))) + .await; break; }, Ok(kernels) => { + debug!( + target: LOG_TARGET, + "Streaming kernels {} to {}", + current_mmr_position, + current_mmr_position + kernels.len() as u64 + ); + current_mmr_position += kernels.len() as u64; let kernels = kernels.into_iter().map(proto::types::TransactionKernel::from).map(Ok); // Ensure task stops if the peer prematurely stops their RPC session if utils::mpsc::send_all(&tx, kernels).await.is_err() { @@ -439,6 +450,36 @@ impl BaseNodeSyncService for BaseNodeSyncRpcServ break; }, } + + current_height += 1; + + if current_height <= end_height { + let res = db + .fetch_header(current_height) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET)); + match res { + Ok(Some(header)) => { + current_header_hash = header.hash(); + }, + Ok(None) => { + let _ = tx + .send(Err(RpcStatus::not_found(format!( + "Could not find header #{} while streaming UTXOs after position {}", + current_height, current_mmr_position + )))) + .await; + break; + }, + Err(err) => { + error!(target: LOG_TARGET, "DB error while streaming kernels: {}", err); + let _ = tx + .send(Err(RpcStatus::general("DB error while streaming kernels"))) + .await; + break; + }, + } + } } }); Ok(Streaming::new(rx)) @@ -450,15 +491,18 @@ impl BaseNodeSyncService for BaseNodeSyncRpcServ let peer = request.context().peer_node_id(); debug!( target: LOG_TARGET, - "Received sync_utxos request from {} (start = {}, include_pruned_utxos = {}, include_deleted_bitmaps = {})", + "Received sync_utxos request from header {} to {} (start = {}, include_pruned_utxos = {}, \ + include_deleted_bitmaps = {})", peer, req.start, + req.end_header_hash.to_hex(), req.include_pruned_utxos, req.include_deleted_bitmaps ); let (tx, rx) = mpsc::channel(200); - task::spawn(SyncUtxosTask::new(self.db(), request.into_message()).run(tx)); + let task = SyncUtxosTask::new(self.db()); + task.run(request.into_message(), tx).await?; Ok(Streaming::new(rx)) } diff --git a/base_layer/core/src/base_node/sync/rpc/sync_utxos_task.rs b/base_layer/core/src/base_node/sync/rpc/sync_utxos_task.rs index 343be2425b..48da40edc3 100644 --- a/base_layer/core/src/base_node/sync/rpc/sync_utxos_task.rs +++ b/base_layer/core/src/base_node/sync/rpc/sync_utxos_task.rs @@ -21,137 +21,169 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{ + blocks::BlockHeader, chain_storage::{async_db::AsyncBlockchainDb, BlockchainBackend}, proto, proto::base_node::{SyncUtxo, SyncUtxosRequest, SyncUtxosResponse}, }; use log::*; -use std::{cmp, sync::Arc, time::Instant}; +use std::{sync::Arc, time::Instant}; use tari_comms::{protocol::rpc::RpcStatus, utils}; use tari_crypto::tari_utilities::{hex::Hex, Hashable}; -use tokio::sync::mpsc; +use tokio::{sync::mpsc, task}; const LOG_TARGET: &str = "c::base_node::sync_rpc::sync_utxo_task"; pub(crate) struct SyncUtxosTask { db: AsyncBlockchainDb, - request: SyncUtxosRequest, } impl SyncUtxosTask where B: BlockchainBackend + 'static { - pub(crate) fn new(db: AsyncBlockchainDb, request: SyncUtxosRequest) -> Self { - Self { db, request } + pub(crate) fn new(db: AsyncBlockchainDb) -> Self { + Self { db } } - pub(crate) async fn run(self, mut tx: mpsc::Sender>) { - if let Err(err) = self.start_streaming(&mut tx).await { - let _ = tx.send(Err(err)).await; - } - } - - async fn start_streaming( - &self, - tx: &mut mpsc::Sender>, + pub(crate) async fn run( + self, + request: SyncUtxosRequest, + mut tx: mpsc::Sender>, ) -> Result<(), RpcStatus> { + let start_header = self + .db + .fetch_header_containing_utxo_mmr(request.start + 1) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))?; + let end_header = self .db - .fetch_header_by_block_hash(self.request.end_header_hash.clone()) + .fetch_header_by_block_hash(request.end_header_hash.clone()) .await .map_err(RpcStatus::log_internal_error(LOG_TARGET))? - .ok_or_else(|| { - RpcStatus::not_found(format!( - "End header hash {} is was not found", - self.request.end_header_hash.to_hex() - )) - })?; + .ok_or_else(|| RpcStatus::not_found("End header hash is was not found"))?; - if self.request.start > end_header.output_mmr_size - 1 { + if start_header.height() > end_header.height { return Err(RpcStatus::bad_request(format!( - "start index {} cannot be greater than the end header's output MMR size ({})", - self.request.start, end_header.output_mmr_size + "start header height {} cannot be greater than the end header height ({})", + start_header.height(), + end_header.height ))); } - let prev_header = self - .db - .fetch_header_containing_utxo_mmr(self.request.start) - .await - .map_err(RpcStatus::log_internal_error(LOG_TARGET))?; - let (mut prev_header, _) = prev_header.into_parts(); + let (skip_outputs, prev_utxo_mmr_size) = if start_header.height() == 0 { + (request.start, 0) + } else { + let prev_header = self + .db + .fetch_header_by_block_hash(start_header.header().prev_hash.clone()) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| RpcStatus::not_found("Previous start header hash is was not found"))?; + + let skip = request.start.checked_sub(prev_header.output_mmr_size) + // This is a data inconsistency because fetch_header_containing_utxo_mmr returned the header we are basing this on + .ok_or_else(|| RpcStatus::general(format!("Data inconsistency: output mmr size of header at {} was more than the start index {}", prev_header.height, request.start)))?; + (skip, prev_header.output_mmr_size) + }; + + let include_pruned_utxos = request.include_pruned_utxos; + let include_deleted_bitmaps = request.include_deleted_bitmaps; + task::spawn(async move { + if let Err(err) = self + .start_streaming( + &mut tx, + start_header.into_header(), + skip_outputs, + prev_utxo_mmr_size, + end_header, + include_pruned_utxos, + include_deleted_bitmaps, + ) + .await + { + let _ = tx.send(Err(err)).await; + } + }); - if prev_header.height > end_header.height { - return Err(RpcStatus::bad_request("start index is greater than end index")); - } - // we need to construct a temp bitmap for the height the client requested + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + async fn start_streaming( + &self, + tx: &mut mpsc::Sender>, + mut current_header: BlockHeader, + mut skip_outputs: u64, + mut prev_utxo_mmr_size: u64, + end_header: BlockHeader, + include_pruned_utxos: bool, + include_deleted_bitmaps: bool, + ) -> Result<(), RpcStatus> { + // we need to fetch the spent bitmap for the height the client requested let bitmap = self .db .fetch_complete_deleted_bitmap_at(end_header.hash()) .await - .map_err(|_| RpcStatus::not_found("Could not get tip deleted bitmap"))? + .map_err(|err| { + error!(target: LOG_TARGET, "Failed to get deleted bitmap: {}", err); + RpcStatus::general(format!( + "Could not get deleted bitmap at hash {}", + end_header.hash().to_hex() + )) + })? .into_bitmap(); - let bitmap = Arc::new(bitmap); + debug!( + target: LOG_TARGET, + "Starting stream task with current_header: {}, skip_outputs: {}, prev_utxo_mmr_size: {}, end_header: {}, \ + include_pruned_utxos: {:?}, include_deleted_bitmaps: {:?}", + current_header.hash().to_hex(), + skip_outputs, + prev_utxo_mmr_size, + end_header.hash().to_hex(), + include_pruned_utxos, + include_deleted_bitmaps + ); loop { let timer = Instant::now(); - if prev_header.height == end_header.height { - break; - } - - let current_header = self - .db - .fetch_header(prev_header.height + 1) - .await - .map_err(RpcStatus::log_internal_error(LOG_TARGET))? - .ok_or_else(|| { - RpcStatus::general(format!( - "Potential data consistency issue: header {} not found", - prev_header.height + 1 - )) - })?; + let current_header_hash = current_header.hash(); debug!( target: LOG_TARGET, - "previous header = {} ({}) current header = {} ({})", - prev_header.height, - prev_header.hash().to_hex(), + "current header = {} ({})", current_header.height, - current_header.hash().to_hex() + current_header_hash.to_hex() ); - let start = cmp::max(self.request.start, prev_header.output_mmr_size); - let end = current_header.output_mmr_size - 1; + let start = prev_utxo_mmr_size + skip_outputs; + let end = current_header.output_mmr_size; if tx.is_closed() { debug!(target: LOG_TARGET, "Exiting sync_utxos early because client has gone",); break; } - debug!( - target: LOG_TARGET, - "Streaming UTXOs {}-{} ({}) for block #{}", - start, - end, - end.saturating_sub(start).saturating_add(1), - current_header.height - ); let (utxos, deleted_diff) = self .db - .fetch_utxos_by_mmr_position(start, end, bitmap.clone()) + .fetch_utxos_in_block(current_header.hash(), Some(bitmap.clone())) .await .map_err(RpcStatus::log_internal_error(LOG_TARGET))?; - trace!( + debug!( target: LOG_TARGET, - "Loaded {} UTXO(s) and |deleted_diff| = {}", + "Streaming UTXO(s) {}-{} ({}) for block #{}. Deleted diff len = {}", + start, + end, utxos.len(), + current_header.height, deleted_diff.cardinality(), ); let utxos = utxos .into_iter() .enumerate() + .skip(skip_outputs as usize) // Only include pruned UTXOs if include_pruned_utxos is true - .filter(|(_, utxo)| self.request.include_pruned_utxos || !utxo.is_pruned()) + .filter(|(_, utxo)| include_pruned_utxos || !utxo.is_pruned()) .map(|(i, utxo)| { SyncUtxosResponse { utxo_or_deleted: Some(proto::base_node::sync_utxos_response::UtxoOrDeleted::Utxo( @@ -167,7 +199,10 @@ where B: BlockchainBackend + 'static break; } - if self.request.include_deleted_bitmaps { + // We only want to skip the first block UTXOs + skip_outputs = 0; + + if include_deleted_bitmaps { let bitmaps = SyncUtxosResponse { utxo_or_deleted: Some(proto::base_node::sync_utxos_response::UtxoOrDeleted::DeletedDiff( deleted_diff.serialize(), @@ -187,14 +222,29 @@ where B: BlockchainBackend + 'static timer.elapsed() ); - prev_header = current_header; + prev_utxo_mmr_size = current_header.output_mmr_size; + if current_header.height + 1 > end_header.height { + break; + } + + current_header = self + .db + .fetch_header(current_header.height + 1) + .await + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| { + RpcStatus::general(format!( + "Potential data consistency issue: header {} not found", + current_header.height + 1 + )) + })?; } debug!( target: LOG_TARGET, "UTXO sync completed to UTXO {} (Header hash = {})", - prev_header.output_mmr_size, - prev_header.hash().to_hex() + current_header.output_mmr_size, + current_header.hash().to_hex() ); Ok(()) diff --git a/base_layer/core/src/blocks/accumulated_data.rs b/base_layer/core/src/blocks/accumulated_data.rs index c14f494c2e..88c45f9504 100644 --- a/base_layer/core/src/blocks/accumulated_data.rs +++ b/base_layer/core/src/blocks/accumulated_data.rs @@ -53,8 +53,8 @@ const LOG_TARGET: &str = "c::bn::acc_data"; pub struct BlockAccumulatedData { pub(crate) kernels: PrunedHashSet, pub(crate) outputs: PrunedHashSet, + pub(crate) witness: PrunedHashSet, pub(crate) deleted: DeletedBitmap, - pub(crate) range_proofs: PrunedHashSet, pub(crate) kernel_sum: Commitment, } @@ -62,14 +62,14 @@ impl BlockAccumulatedData { pub fn new( kernels: PrunedHashSet, outputs: PrunedHashSet, - range_proofs: PrunedHashSet, + witness: PrunedHashSet, deleted: Bitmap, total_kernel_sum: Commitment, ) -> Self { Self { kernels, outputs, - range_proofs, + witness, deleted: DeletedBitmap { deleted }, kernel_sum: total_kernel_sum, } @@ -79,8 +79,13 @@ impl BlockAccumulatedData { &self.deleted.deleted } + pub fn set_deleted(&mut self, deleted: DeletedBitmap) -> &mut Self { + self.deleted = deleted; + self + } + pub fn dissolve(self) -> (PrunedHashSet, PrunedHashSet, PrunedHashSet, Bitmap) { - (self.kernels, self.outputs, self.range_proofs, self.deleted.deleted) + (self.kernels, self.outputs, self.witness, self.deleted.deleted) } pub fn kernel_sum(&self) -> &Commitment { @@ -96,7 +101,7 @@ impl Default for BlockAccumulatedData { deleted: DeletedBitmap { deleted: Bitmap::create(), }, - range_proofs: Default::default(), + witness: Default::default(), kernel_sum: Default::default(), } } @@ -110,11 +115,20 @@ impl Display for BlockAccumulatedData { self.outputs.len().unwrap_or(0), self.deleted.deleted.cardinality(), self.kernels.len().unwrap_or(0), - self.range_proofs.len().unwrap_or(0) + self.witness.len().unwrap_or(0) ) } } +#[derive(Debug, Clone, Default)] +pub struct UpdateBlockAccumulatedData { + pub kernel_hash_set: Option, + pub utxo_hash_set: Option, + pub witness_hash_set: Option, + pub deleted_diff: Option, + pub kernel_sum: Option, +} + /// Wrapper struct to serialize and deserialize Bitmap #[derive(Debug, Clone)] pub struct DeletedBitmap { diff --git a/base_layer/core/src/blocks/mod.rs b/base_layer/core/src/blocks/mod.rs index 19a49c3071..3b7eb851f1 100644 --- a/base_layer/core/src/blocks/mod.rs +++ b/base_layer/core/src/blocks/mod.rs @@ -30,6 +30,7 @@ pub use accumulated_data::{ ChainHeader, CompleteDeletedBitmap, DeletedBitmap, + UpdateBlockAccumulatedData, }; mod error; diff --git a/base_layer/core/src/chain_storage/async_db.rs b/base_layer/core/src/chain_storage/async_db.rs index 99e2bcbd4d..116e87e8dc 100644 --- a/base_layer/core/src/chain_storage/async_db.rs +++ b/base_layer/core/src/chain_storage/async_db.rs @@ -30,7 +30,6 @@ use tari_common_types::{ chain_metadata::ChainMetadata, types::{BlockHash, Commitment, HashOutput, Signature}, }; -use tari_mmr::pruned_hashset::PrunedHashSet; use crate::{ blocks::{ @@ -44,6 +43,7 @@ use crate::{ DeletedBitmap, HistoricalBlock, NewBlockTemplate, + UpdateBlockAccumulatedData, }, chain_storage::{ blockchain_database::MmrRoots, @@ -157,7 +157,7 @@ impl AsyncBlockchainDb { //---------------------------------- Metadata --------------------------------------------// make_async_fn!(get_chain_metadata() -> ChainMetadata, "get_chain_metadata"); - make_async_fn!(fetch_horizon_data() -> Option, "fetch_horizon_data"); + make_async_fn!(fetch_horizon_data() -> HorizonData, "fetch_horizon_data"); //---------------------------------- TXO --------------------------------------------// make_async_fn!(fetch_utxo(hash: HashOutput) -> Option, "fetch_utxo"); @@ -166,12 +166,12 @@ impl AsyncBlockchainDb { make_async_fn!(fetch_utxos_and_mined_info(hashes: Vec) -> Vec>, "fetch_utxos_and_mined_info"); - make_async_fn!(fetch_utxos_by_mmr_position(start: u64, end: u64, deleted: Arc) -> (Vec, Bitmap), "fetch_utxos_by_mmr_position"); + make_async_fn!(fetch_utxos_in_block(hash: HashOutput, deleted: Option>) -> (Vec, Bitmap), "fetch_utxos_in_block"); //---------------------------------- Kernel --------------------------------------------// make_async_fn!(fetch_kernel_by_excess_sig(excess_sig: Signature) -> Option<(TransactionKernel, HashOutput)>, "fetch_kernel_by_excess_sig"); - make_async_fn!(fetch_kernels_by_mmr_position(start: u64, end: u64) -> Vec, "fetch_kernels_by_mmr_position"); + make_async_fn!(fetch_kernels_in_block(hash: HashOutput) -> Vec, "fetch_kernels_in_block"); //---------------------------------- MMR --------------------------------------------// make_async_fn!(prepare_new_block(template: NewBlockTemplate) -> Block, "prepare_new_block"); @@ -240,6 +240,8 @@ impl AsyncBlockchainDb { //---------------------------------- Misc. --------------------------------------------// + make_async_fn!(prune_to_height(height: u64) -> (), "prune_to_height"); + make_async_fn!(rewind_to_height(height: u64) -> Vec>, "rewind_to_height"); make_async_fn!(rewind_to_hash(hash: BlockHash) -> Vec>, "rewind_to_hash"); @@ -292,16 +294,21 @@ impl<'a, B: BlockchainBackend + 'static> AsyncDbTransaction<'a, B> { &mut self, height: u64, hash: HashOutput, - accumulated_data: u128, + accumulated_difficulty: u128, expected_prev_best_block: HashOutput, ) -> &mut Self { self.transaction - .set_best_block(height, hash, accumulated_data, expected_prev_best_block); + .set_best_block(height, hash, accumulated_difficulty, expected_prev_best_block); + self + } + + pub fn set_pruned_height(&mut self, height: u64) -> &mut Self { + self.transaction.set_pruned_height(height); self } - pub fn set_pruned_height(&mut self, height: u64, kernel_sum: Commitment, utxo_sum: Commitment) -> &mut Self { - self.transaction.set_pruned_height(height, kernel_sum, utxo_sum); + pub fn set_horizon_data(&mut self, kernel_sum: Commitment, utxo_sum: Commitment) -> &mut Self { + self.transaction.set_horizon_data(kernel_sum, utxo_sum); self } @@ -340,23 +347,12 @@ impl<'a, B: BlockchainBackend + 'static> AsyncDbTransaction<'a, B> { self } - pub fn update_pruned_hash_set( - &mut self, - mmr_tree: MmrTree, - header_hash: HashOutput, - pruned_hash_set: PrunedHashSet, - ) -> &mut Self { - self.transaction - .update_pruned_hash_set(mmr_tree, header_hash, pruned_hash_set); - self - } - - pub fn update_block_accumulated_data_with_deleted_diff( + pub fn update_block_accumulated_data_via_horizon_sync( &mut self, header_hash: HashOutput, - deleted: Bitmap, + values: UpdateBlockAccumulatedData, ) -> &mut Self { - self.transaction.update_deleted_with_diff(header_hash, deleted); + self.transaction.update_block_accumulated_data(header_hash, values); self } @@ -376,6 +372,11 @@ impl<'a, B: BlockchainBackend + 'static> AsyncDbTransaction<'a, B> { self } + pub fn prune_output_at_positions(&mut self, positions: Vec) -> &mut Self { + self.transaction.prune_outputs_at_positions(positions); + self + } + pub async fn commit(&mut self) -> Result<(), ChainStorageError> { let transaction = mem::take(&mut self.transaction); self.db.write(transaction).await diff --git a/base_layer/core/src/chain_storage/blockchain_backend.rs b/base_layer/core/src/chain_storage/blockchain_backend.rs index 17595aae6c..ede1ca87c2 100644 --- a/base_layer/core/src/chain_storage/blockchain_backend.rs +++ b/base_layer/core/src/chain_storage/blockchain_backend.rs @@ -100,14 +100,11 @@ pub trait BlockchainBackend: Send + Sync { excess_sig: &Signature, ) -> Result, ChainStorageError>; - /// Fetch kernels by MMR position - fn fetch_kernels_by_mmr_position(&self, start: u64, end: u64) -> Result, ChainStorageError>; - - fn fetch_utxos_by_mmr_position( + /// Fetch all UTXOs and spends in the block + fn fetch_utxos_in_block( &self, - start: u64, - end: u64, - deleted: &Bitmap, + header_hash: &HashOutput, + deleted: Option<&Bitmap>, ) -> Result<(Vec, Bitmap), ChainStorageError>; /// Fetch a specific output. Returns the output and the leaf index in the output MMR diff --git a/base_layer/core/src/chain_storage/blockchain_database.rs b/base_layer/core/src/chain_storage/blockchain_database.rs index e2a189b54a..07cd9c20b3 100644 --- a/base_layer/core/src/chain_storage/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/blockchain_database.rs @@ -52,6 +52,7 @@ use crate::{ DeletedBitmap, HistoricalBlock, NewBlockTemplate, + UpdateBlockAccumulatedData, }, chain_storage::{ consts::{ @@ -211,8 +212,19 @@ where B: BlockchainBackend }; if is_empty { info!(target: LOG_TARGET, "Blockchain db is empty. Adding genesis block."); - let genesis_block = blockchain_db.consensus_manager.get_genesis_block(); - blockchain_db.insert_block(Arc::new(genesis_block))?; + let genesis_block = Arc::new(blockchain_db.consensus_manager.get_genesis_block()); + blockchain_db.insert_block(genesis_block.clone())?; + let mut txn = DbTransaction::new(); + let body = &genesis_block.block().body; + let utxo_sum = body.outputs().iter().map(|k| &k.commitment).sum::(); + let kernel_sum = body.kernels().iter().map(|k| &k.excess).sum::(); + txn.update_block_accumulated_data(genesis_block.hash().clone(), UpdateBlockAccumulatedData { + kernel_sum: Some(kernel_sum.clone()), + ..Default::default() + }); + txn.set_pruned_height(0); + txn.set_horizon_data(kernel_sum, utxo_sum); + blockchain_db.write(txn)?; blockchain_db.store_pruning_horizon(config.pruning_horizon)?; } if cleanup_orphans_at_startup { @@ -361,23 +373,18 @@ where B: BlockchainBackend db.fetch_kernel_by_excess_sig(&excess_sig) } - pub fn fetch_kernels_by_mmr_position( - &self, - start: u64, - end: u64, - ) -> Result, ChainStorageError> { + pub fn fetch_kernels_in_block(&self, hash: HashOutput) -> Result, ChainStorageError> { let db = self.db_read_access()?; - db.fetch_kernels_by_mmr_position(start, end) + db.fetch_kernels_in_block(&hash) } - pub fn fetch_utxos_by_mmr_position( + pub fn fetch_utxos_in_block( &self, - start: u64, - end: u64, - deleted: Arc, + hash: HashOutput, + deleted: Option>, ) -> Result<(Vec, Bitmap), ChainStorageError> { let db = self.db_read_access()?; - db.fetch_utxos_by_mmr_position(start, end, deleted.as_ref()) + db.fetch_utxos_in_block(&hash, deleted.as_deref()) } /// Returns the block header at the given block height. @@ -576,7 +583,7 @@ where B: BlockchainBackend /// Returns the sum of all kernels pub fn fetch_kernel_commitment_sum(&self, at_hash: &HashOutput) -> Result { - Ok(self.fetch_block_accumulated_data(at_hash.clone())?.kernel_sum) + Ok(self.fetch_block_accumulated_data(at_hash.clone())?.kernel_sum().clone()) } /// Returns `n` hashes from height _h - offset_ where _h_ is the tip header height back to `h - n - offset`. @@ -872,6 +879,12 @@ where B: BlockchainBackend store_pruning_horizon(&mut *db, pruning_horizon) } + /// Prunes the blockchain up to and including the given height + pub fn prune_to_height(&self, height: u64) -> Result<(), ChainStorageError> { + let mut db = self.db_write_access()?; + prune_to_height(&mut *db, height) + } + /// Fetch a block from the blockchain database. /// /// # Returns @@ -975,9 +988,9 @@ where B: BlockchainBackend rewind_to_hash(&mut *db, hash) } - pub fn fetch_horizon_data(&self) -> Result, ChainStorageError> { + pub fn fetch_horizon_data(&self) -> Result { let db = self.db_read_access()?; - db.fetch_horizon_data() + Ok(db.fetch_horizon_data()?.unwrap_or_default()) } pub fn fetch_complete_deleted_bitmap_at( @@ -1074,7 +1087,7 @@ pub fn calculate_mmr_roots(db: &T, block: &Block) -> Resul let BlockAccumulatedData { kernels, outputs, - range_proofs, + witness: range_proofs, .. } = db .fetch_block_accumulated_data(&header.prev_hash)? @@ -2037,6 +2050,7 @@ fn cleanup_orphans(db: &mut T, orphan_storage_capacity: us db.delete_oldest_orphans(horizon_height, orphan_storage_capacity) } + fn prune_database_if_needed( db: &mut T, pruning_horizon: u64, @@ -2058,34 +2072,75 @@ fn prune_database_if_needed( pruning_interval, ); if metadata.pruned_height() < abs_pruning_horizon.saturating_sub(pruning_interval) { - let last_pruned = metadata.pruned_height(); + prune_to_height(db, abs_pruning_horizon)?; + } + + Ok(()) +} + +fn prune_to_height(db: &mut T, target_horizon_height: u64) -> Result<(), ChainStorageError> { + let metadata = db.fetch_chain_metadata()?; + let last_pruned = metadata.pruned_height(); + if target_horizon_height < last_pruned { + return Err(ChainStorageError::InvalidArguments { + func: "prune_to_height", + arg: "target_horizon_height", + message: format!( + "Target pruning horizon {} is less than current pruning horizon {}", + target_horizon_height, last_pruned + ), + }); + } + + if target_horizon_height == last_pruned { info!( target: LOG_TARGET, - "Pruning blockchain database at height {} (was={})", abs_pruning_horizon, last_pruned, + "Blockchain already pruned to height {}", target_horizon_height ); - let mut last_block = db.fetch_block_accumulated_data_by_height(last_pruned).or_not_found( + return Ok(()); + } + + if target_horizon_height > metadata.height_of_longest_chain() { + return Err(ChainStorageError::InvalidArguments { + func: "prune_to_height", + arg: "target_horizon_height", + message: format!( + "Target pruning horizon {} is greater than current block height {}", + target_horizon_height, + metadata.height_of_longest_chain() + ), + }); + } + + info!( + target: LOG_TARGET, + "Pruning blockchain database at height {} (was={})", target_horizon_height, last_pruned, + ); + let mut last_block = db.fetch_block_accumulated_data_by_height(last_pruned).or_not_found( + "BlockAccumulatedData", + "height", + last_pruned.to_string(), + )?; + let mut txn = DbTransaction::new(); + for block_to_prune in (last_pruned + 1)..=target_horizon_height { + let header = db.fetch_chain_header_by_height(block_to_prune)?; + let curr_block = db.fetch_block_accumulated_data_by_height(block_to_prune).or_not_found( "BlockAccumulatedData", "height", - last_pruned.to_string(), + block_to_prune.to_string(), )?; - let mut txn = DbTransaction::new(); - for block_to_prune in (last_pruned + 1)..abs_pruning_horizon { - let curr_block = db.fetch_block_accumulated_data_by_height(block_to_prune).or_not_found( - "BlockAccumulatedData", - "height", - block_to_prune.to_string(), - )?; - // Note, this could actually be done in one step instead of each block, since deleted is - // accumulated - let inputs_to_prune = curr_block.deleted.bitmap().clone() - last_block.deleted.bitmap(); - last_block = curr_block; - - txn.prune_outputs_and_update_horizon(inputs_to_prune.to_vec(), block_to_prune); - } + // Note, this could actually be done in one step instead of each block, since deleted is + // accumulated + let output_mmr_positions = curr_block.deleted() - last_block.deleted(); + last_block = curr_block; - db.write(txn)?; + txn.prune_outputs_at_positions(output_mmr_positions.to_vec()); + txn.delete_all_inputs_in_block(header.hash().clone()); } + txn.set_pruned_height(target_horizon_height); + + db.write(txn)?; Ok(()) } diff --git a/base_layer/core/src/chain_storage/db_transaction.rs b/base_layer/core/src/chain_storage/db_transaction.rs index 066528f73b..bfdd480033 100644 --- a/base_layer/core/src/chain_storage/db_transaction.rs +++ b/base_layer/core/src/chain_storage/db_transaction.rs @@ -1,18 +1,3 @@ -use std::{ - fmt, - fmt::{Display, Error, Formatter}, - sync::Arc, -}; - -use croaring::Bitmap; -use tari_crypto::tari_utilities::{ - hex::{to_hex, Hex}, - Hashable, -}; - -use tari_common_types::types::{BlockHash, Commitment, HashOutput}; -use tari_mmr::pruned_hashset::PrunedHashSet; - // Copyright 2019. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the @@ -34,14 +19,28 @@ use tari_mmr::pruned_hashset::PrunedHashSet; // 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::{ - blocks::{Block, BlockHeader, BlockHeaderAccumulatedData, ChainBlock, ChainHeader}, - chain_storage::{error::ChainStorageError, MmrTree}, + blocks::{Block, BlockHeader, BlockHeaderAccumulatedData, ChainBlock, ChainHeader, UpdateBlockAccumulatedData}, + chain_storage::error::ChainStorageError, transactions::transaction_entities::{ transaction_kernel::TransactionKernel, transaction_output::TransactionOutput, }, }; +use std::{ + fmt, + fmt::{Display, Error, Formatter}, + sync::Arc, +}; + +use crate::chain_storage::HorizonData; +use croaring::Bitmap; +use tari_common_types::types::{BlockHash, Commitment, HashOutput}; +use tari_crypto::tari_utilities::{ + hex::{to_hex, Hex}, + Hashable, +}; #[derive(Debug)] pub struct DbTransaction { @@ -148,31 +147,26 @@ impl DbTransaction { self } - pub fn update_pruned_hash_set( - &mut self, - mmr_tree: MmrTree, - header_hash: HashOutput, - pruned_hash_set: PrunedHashSet, - ) -> &mut Self { - self.operations.push(WriteOperation::UpdatePrunedHashSet { - mmr_tree, - header_hash, - pruned_hash_set: Box::new(pruned_hash_set), + pub fn prune_outputs_at_positions(&mut self, output_mmr_positions: Vec) -> &mut Self { + self.operations.push(WriteOperation::PruneOutputsAtMmrPositions { + output_positions: output_mmr_positions, }); self } - pub fn prune_outputs_and_update_horizon(&mut self, output_mmr_positions: Vec, horizon: u64) -> &mut Self { - self.operations.push(WriteOperation::PruneOutputsAndUpdateHorizon { - output_positions: output_mmr_positions, - horizon, - }); + pub fn delete_all_inputs_in_block(&mut self, block_hash: BlockHash) -> &mut Self { + self.operations + .push(WriteOperation::DeleteAllInputsInBlock { block_hash }); self } - pub fn update_deleted_with_diff(&mut self, header_hash: HashOutput, deleted: Bitmap) -> &mut Self { + pub fn update_block_accumulated_data( + &mut self, + header_hash: HashOutput, + values: UpdateBlockAccumulatedData, + ) -> &mut Self { self.operations - .push(WriteOperation::UpdateDeletedBlockAccumulatedDataWithDiff { header_hash, deleted }); + .push(WriteOperation::UpdateBlockAccumulatedData { header_hash, values }); self } @@ -248,11 +242,14 @@ impl DbTransaction { self } - pub fn set_pruned_height(&mut self, height: u64, kernel_sum: Commitment, utxo_sum: Commitment) -> &mut Self { - self.operations.push(WriteOperation::SetPrunedHeight { - height, - kernel_sum, - utxo_sum, + pub fn set_pruned_height(&mut self, height: u64) -> &mut Self { + self.operations.push(WriteOperation::SetPrunedHeight { height }); + self + } + + pub fn set_horizon_data(&mut self, kernel_sum: Commitment, utxo_sum: Commitment) -> &mut Self { + self.operations.push(WriteOperation::SetHorizonData { + horizon_data: HorizonData::new(kernel_sum, utxo_sum), }); self } @@ -304,25 +301,18 @@ pub enum WriteOperation { DeleteOrphanChainTip(HashOutput), InsertOrphanChainTip(HashOutput), InsertMoneroSeedHeight(Vec, u64), - UpdatePrunedHashSet { - mmr_tree: MmrTree, + UpdateBlockAccumulatedData { header_hash: HashOutput, - pruned_hash_set: Box, - }, - UpdateDeletedBlockAccumulatedDataWithDiff { - header_hash: HashOutput, - deleted: Bitmap, + values: UpdateBlockAccumulatedData, }, UpdateDeletedBitmap { deleted: Bitmap, }, - PruneOutputsAndUpdateHorizon { + PruneOutputsAtMmrPositions { output_positions: Vec, - horizon: u64, }, - UpdateKernelSum { - header_hash: HashOutput, - kernel_sum: Commitment, + DeleteAllInputsInBlock { + block_hash: BlockHash, }, SetAccumulatedDataForOrphan(BlockHeaderAccumulatedData), SetBestBlock { @@ -334,8 +324,9 @@ pub enum WriteOperation { SetPruningHorizonConfig(u64), SetPrunedHeight { height: u64, - kernel_sum: Commitment, - utxo_sum: Commitment, + }, + SetHorizonData { + horizon_data: HorizonData, }, } @@ -389,14 +380,6 @@ impl fmt::Display for WriteOperation { write!(f, "Insert Monero seed string {} for height: {}", data.to_hex(), height) }, InsertChainOrphanBlock(block) => write!(f, "InsertChainOrphanBlock({})", block.hash().to_hex()), - UpdatePrunedHashSet { - mmr_tree, header_hash, .. - } => write!( - f, - "Update pruned hash set: {} header: {}", - mmr_tree, - header_hash.to_hex() - ), InsertPrunedOutput { header_hash: _, header_height: _, @@ -404,23 +387,14 @@ impl fmt::Display for WriteOperation { witness_hash: _, mmr_position: _, } => write!(f, "Insert pruned output"), - UpdateDeletedBlockAccumulatedDataWithDiff { - header_hash: _, - deleted: _, - } => write!(f, "Add deleted data for block"), + UpdateBlockAccumulatedData { header_hash, .. } => { + write!(f, "Update Block data for block {}", header_hash.to_hex()) + }, UpdateDeletedBitmap { deleted } => { write!(f, "Merge deleted bitmap at tip ({} new indexes)", deleted.cardinality()) }, - PruneOutputsAndUpdateHorizon { - output_positions, - horizon, - } => write!( - f, - "Prune {} outputs and set horizon to {}", - output_positions.len(), - horizon - ), - UpdateKernelSum { header_hash, .. } => write!(f, "Update kernel sum for block: {}", header_hash.to_hex()), + PruneOutputsAtMmrPositions { output_positions } => write!(f, "Prune {} output(s)", output_positions.len()), + DeleteAllInputsInBlock { block_hash } => write!(f, "Delete outputs in block {}", block_hash.to_hex()), SetAccumulatedDataForOrphan(accumulated_data) => { write!(f, "Set accumulated data for orphan {}", accumulated_data) }, @@ -440,6 +414,7 @@ impl fmt::Display for WriteOperation { SetPrunedHeight { height, .. } => write!(f, "Set pruned height to {}", height), DeleteHeader(height) => write!(f, "Delete header at height: {}", height), DeleteOrphan(hash) => write!(f, "Delete orphan with hash: {}", hash.to_hex()), + SetHorizonData { .. } => write!(f, "Set horizon data"), } } } diff --git a/base_layer/core/src/chain_storage/error.rs b/base_layer/core/src/chain_storage/error.rs index 14cbd7a53f..c776ec2222 100644 --- a/base_layer/core/src/chain_storage/error.rs +++ b/base_layer/core/src/chain_storage/error.rs @@ -169,13 +169,7 @@ pub trait OrNotFound { impl OrNotFound for Result, ChainStorageError> { fn or_not_found(self, entity: &'static str, field: &'static str, value: String) -> Result { - match self { - Ok(inner) => match inner { - None => Err(ChainStorageError::ValueNotFound { entity, field, value }), - Some(v) => Ok(v), - }, - Err(err) => Err(err), - } + self.and_then(|inner| inner.ok_or(ChainStorageError::ValueNotFound { entity, field, value })) } } diff --git a/base_layer/core/src/chain_storage/horizon_data.rs b/base_layer/core/src/chain_storage/horizon_data.rs index 6213d490f3..ae6a120bb4 100644 --- a/base_layer/core/src/chain_storage/horizon_data.rs +++ b/base_layer/core/src/chain_storage/horizon_data.rs @@ -21,9 +21,8 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use serde::{Deserialize, Serialize}; use tari_common_types::types::Commitment; -use tari_crypto::tari_utilities::ByteArray; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct HorizonData { kernel_sum: Commitment, utxo_sum: Commitment, @@ -35,10 +34,7 @@ impl HorizonData { } pub fn zero() -> Self { - HorizonData { - kernel_sum: Commitment::from_bytes(&[0u8; 32]).expect("Could not create commitment"), - utxo_sum: Commitment::from_bytes(&[0u8; 32]).expect("Could not create commitment"), - } + Default::default() } pub fn kernel_sum(&self) -> &Commitment { diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index 0d3e18a6a5..6a8908ad75 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -24,22 +24,14 @@ // let's ignore this clippy error in this module #![allow(clippy::ptr_arg)] -use std::{convert::TryFrom, fmt, fs, fs::File, ops::Deref, path::Path, sync::Arc, time::Instant}; - use croaring::Bitmap; use fs2::FileExt; use lmdb_zero::{ConstTransaction, Database, Environment, ReadTransaction, WriteTransaction}; use log::*; use serde::{Deserialize, Serialize}; +use std::{fmt, fs, fs::File, ops::Deref, path::Path, sync::Arc, time::Instant}; use tari_crypto::tari_utilities::{hash::Hashable, hex::Hex, ByteArray}; -use tari_common_types::{ - chain_metadata::ChainMetadata, - types::{BlockHash, Commitment, HashDigest, HashOutput, Signature, BLOCK_HASH_LENGTH}, -}; -use tari_mmr::{pruned_hashset::PrunedHashSet, Hash, MerkleMountainRange, MutableMmr}; -use tari_storage::lmdb_store::{db, LMDBBuilder, LMDBConfig, LMDBStore}; - use crate::{ blocks::{ Block, @@ -49,6 +41,7 @@ use crate::{ ChainBlock, ChainHeader, DeletedBitmap, + UpdateBlockAccumulatedData, }, chain_storage::{ db_transaction::{DbKey, DbTransaction, DbValue, WriteOperation}, @@ -94,6 +87,12 @@ use crate::{ }, }, }; +use tari_common_types::{ + chain_metadata::ChainMetadata, + types::{BlockHash, Commitment, HashDigest, HashOutput, Signature, BLOCK_HASH_LENGTH}, +}; +use tari_mmr::{Hash, MerkleMountainRange, MutableMmr}; +use tari_storage::lmdb_store::{db, LMDBBuilder, LMDBConfig, LMDBStore}; type DatabaseRef = Arc>; @@ -320,32 +319,19 @@ impl LMDBDatabase { self.insert_orphan_block(&write_txn, chain_block.block())?; self.set_accumulated_data_for_orphan(&write_txn, chain_block.accumulated_data())?; }, - UpdatePrunedHashSet { - mmr_tree, - header_hash, - pruned_hash_set, - } => { - self.update_pruned_hash_set(&write_txn, *mmr_tree, header_hash, (**pruned_hash_set).clone())?; - }, - UpdateDeletedBlockAccumulatedDataWithDiff { header_hash, deleted } => { - self.update_deleted_block_accumulated_data_with_diff(&write_txn, header_hash, deleted.clone())?; + UpdateBlockAccumulatedData { header_hash, values } => { + self.update_block_accumulated_data(&write_txn, header_hash, values.clone())?; }, UpdateDeletedBitmap { deleted } => { let mut bitmap = self.load_deleted_bitmap_model(&write_txn)?; bitmap.merge(deleted)?; bitmap.finish()?; }, - PruneOutputsAndUpdateHorizon { - output_positions, - horizon, - } => { - self.prune_outputs_and_update_horizon(&write_txn, output_positions, *horizon)?; + PruneOutputsAtMmrPositions { output_positions } => { + self.prune_outputs_at_positions(&write_txn, output_positions)?; }, - UpdateKernelSum { - header_hash, - kernel_sum, - } => { - self.update_block_accumulated_data_kernel_sum(&write_txn, header_hash, kernel_sum.clone())?; + DeleteAllInputsInBlock { block_hash } => { + self.delete_all_inputs_in_block(&write_txn, block_hash)?; }, SetBestBlock { height, @@ -397,20 +383,18 @@ impl LMDBDatabase { MetadataValue::PruningHorizon(*pruning_horizon), )?; }, - SetPrunedHeight { - height, - kernel_sum, - utxo_sum, - } => { + SetPrunedHeight { height } => { self.set_metadata( &write_txn, MetadataKey::PrunedHeight, MetadataValue::PrunedHeight(*height), )?; + }, + SetHorizonData { horizon_data } => { self.set_metadata( &write_txn, MetadataKey::HorizonData, - MetadataValue::HorizonData(HorizonData::new(kernel_sum.clone(), utxo_sum.clone())), + MetadataValue::HorizonData(horizon_data.clone()), )?; }, } @@ -1049,17 +1033,16 @@ impl LMDBDatabase { self.fetch_block_accumulated_data(&*txn, header.height - 1)? .ok_or_else(|| ChainStorageError::ValueNotFound { entity: "BlockAccumulatedData", - field: "prev_hash", - value: header.prev_hash.to_hex(), + field: "height", + value: (header.height - 1).to_string(), })? }; let mut total_kernel_sum = Commitment::default(); - let mut total_utxo_sum = Commitment::default(); let BlockAccumulatedData { kernels: pruned_kernel_set, outputs: pruned_output_set, - range_proofs: pruned_proof_set, + witness: pruned_proof_set, .. } = data; @@ -1079,7 +1062,6 @@ impl LMDBDatabase { let mut output_mmr = MutableMmr::::new(pruned_output_set, Bitmap::create())?; let mut witness_mmr = MerkleMountainRange::::new(pruned_proof_set); for output in outputs { - total_utxo_sum = &total_utxo_sum + &output.commitment; output_mmr.push(output.hash())?; witness_mmr.push(output.witness_hash())?; debug!(target: LOG_TARGET, "Inserting output `{}`", output.commitment.to_hex()); @@ -1093,7 +1075,6 @@ impl LMDBDatabase { } for input in inputs { - total_utxo_sum = &total_utxo_sum - &input.commitment; let index = self .fetch_mmr_leaf_index(&**txn, MmrTree::Utxo, &input.output_hash())? .ok_or(ChainStorageError::UnspendableInput)?; @@ -1151,31 +1132,11 @@ impl LMDBDatabase { ) } - fn update_block_accumulated_data_kernel_sum( + fn update_block_accumulated_data( &self, write_txn: &WriteTransaction<'_>, header_hash: &HashOutput, - kernel_sum: Commitment, - ) -> Result<(), ChainStorageError> { - let height = self.fetch_height_from_hash(write_txn, header_hash).or_not_found( - "BlockHash", - "hash", - header_hash.to_hex(), - )?; - let mut block_accum_data = self - .fetch_block_accumulated_data(write_txn, height)? - .unwrap_or_default(); - - block_accum_data.kernel_sum = kernel_sum; - lmdb_replace(write_txn, &self.block_accumulated_data_db, &height, &block_accum_data)?; - Ok(()) - } - - fn update_deleted_block_accumulated_data_with_diff( - &self, - write_txn: &WriteTransaction<'_>, - header_hash: &HashOutput, - deleted: Bitmap, + values: UpdateBlockAccumulatedData, ) -> Result<(), ChainStorageError> { let height = self.fetch_height_from_hash(write_txn, header_hash).or_not_found( "BlockHash", @@ -1184,10 +1145,25 @@ impl LMDBDatabase { )?; let mut block_accum_data = self - .fetch_block_accumulated_data(write_txn, height)? + .fetch_block_accumulated_data(&*write_txn, height)? .unwrap_or_default(); - block_accum_data.deleted = deleted.into(); + if let Some(deleted_diff) = values.deleted_diff { + block_accum_data.deleted = deleted_diff; + } + if let Some(kernel_sum) = values.kernel_sum { + block_accum_data.kernel_sum = kernel_sum; + } + if let Some(kernel_hash_set) = values.kernel_hash_set { + block_accum_data.kernels = kernel_hash_set; + } + if let Some(utxo_hash_set) = values.utxo_hash_set { + block_accum_data.outputs = utxo_hash_set; + } + if let Some(witness_hash_set) = values.witness_hash_set { + block_accum_data.witness = witness_hash_set; + } + lmdb_replace(write_txn, &self.block_accumulated_data_db, &height, &block_accum_data)?; Ok(()) } @@ -1215,36 +1191,21 @@ impl LMDBDatabase { Ok(()) } - fn update_pruned_hash_set( + fn delete_all_inputs_in_block( &self, - write_txn: &WriteTransaction<'_>, - mmr_tree: MmrTree, - header_hash: &HashOutput, - pruned_hash_set: PrunedHashSet, + txn: &WriteTransaction<'_>, + block_hash: &BlockHash, ) -> Result<(), ChainStorageError> { - let height = self.fetch_height_from_hash(write_txn, header_hash).or_not_found( - "BlockHash", - "hash", - header_hash.to_hex(), - )?; - let mut block_accum_data = self - .fetch_block_accumulated_data(write_txn, height)? - .unwrap_or_default(); - match mmr_tree { - MmrTree::Kernel => block_accum_data.kernels = pruned_hash_set, - MmrTree::Utxo => block_accum_data.outputs = pruned_hash_set, - MmrTree::Witness => block_accum_data.range_proofs = pruned_hash_set, - } - - lmdb_replace(write_txn, &self.block_accumulated_data_db, &height, &block_accum_data)?; + let inputs = + lmdb_delete_keys_starting_with::(txn, &self.inputs_db, block_hash.to_hex().as_str())?; + debug!(target: LOG_TARGET, "Deleted {} input(s)", inputs.len()); Ok(()) } - fn prune_outputs_and_update_horizon( + fn prune_outputs_at_positions( &self, write_txn: &WriteTransaction<'_>, output_positions: &[u32], - horizon: u64, ) -> Result<(), ChainStorageError> { for pos in output_positions { let (_height, hash) = lmdb_first_after::<_, (u64, Vec)>( @@ -1258,12 +1219,6 @@ impl LMDBDatabase { self.prune_output(write_txn, &key)?; } - self.set_metadata( - write_txn, - MetadataKey::PrunedHeight, - MetadataValue::PrunedHeight(horizon), - )?; - Ok(()) } @@ -1628,12 +1583,11 @@ impl BlockchainBackend for LMDBDatabase { fn fetch_kernels_in_block(&self, header_hash: &HashOutput) -> Result, ChainStorageError> { let txn = self.read_transaction()?; - Ok( - lmdb_fetch_keys_starting_with(header_hash.to_hex().as_str(), &txn, &self.kernels_db)? - .into_iter() - .map(|f: TransactionKernelRowData| f.kernel) - .collect(), - ) + let kernels = lmdb_fetch_keys_starting_with(header_hash.to_hex().as_str(), &txn, &self.kernels_db)? + .into_iter() + .map(|f: TransactionKernelRowData| f.kernel) + .collect(); + Ok(kernels) } fn fetch_kernel_by_excess( @@ -1671,163 +1625,58 @@ impl BlockchainBackend for LMDBDatabase { } } - fn fetch_kernels_by_mmr_position(&self, start: u64, end: u64) -> Result, ChainStorageError> { + fn fetch_utxos_in_block( + &self, + header_hash: &HashOutput, + deleted: Option<&Bitmap>, + ) -> Result<(Vec, Bitmap), ChainStorageError> { let txn = self.read_transaction()?; - if let Some(start_height) = lmdb_first_after(&txn, &self.kernel_mmr_size_index, &(start + 1).to_be_bytes())? { - let end_height: u64 = - lmdb_first_after(&txn, &self.kernel_mmr_size_index, &(end + 1).to_be_bytes())?.unwrap_or(start_height); - let previous_mmr_count = if start_height == 0 { - 0 + let utxos = lmdb_fetch_keys_starting_with::( + header_hash.to_hex().as_str(), + &txn, + &self.utxos_db, + )? + .into_iter() + .map(|row| { + if deleted.map(|b| b.contains(row.mmr_position)).unwrap_or(false) { + return PrunedOutput::Pruned { + output_hash: row.hash, + witness_hash: row.witness_hash, + }; + } + if let Some(output) = row.output { + PrunedOutput::NotPruned { output } } else { - let header: BlockHeader = - lmdb_get(&txn, &self.headers_db, &(start_height - 1))?.expect("Header should exist"); - debug!(target: LOG_TARGET, "Previous header:{}", header); - header.kernel_mmr_size - }; - - let total_size = (end - start) as usize + 1; - let mut result = Vec::with_capacity(total_size); - - let mut skip_amount = (start - previous_mmr_count) as usize; - debug!( - target: LOG_TARGET, - "Fetching kernels by MMR position. Start {}, end {}, in headers at height {}-{}, prev mmr count: {}, \ - skipping the first:{}", - start, - end, - start_height, - end_height, - previous_mmr_count, - skip_amount - ); - - for height in start_height..=end_height { - let hash = lmdb_get::<_, BlockHeaderAccumulatedData>(&txn, &self.header_accumulated_data_db, &height)? - .ok_or_else(|| ChainStorageError::ValueNotFound { - entity: "BlockHeader", - field: "height", - value: height.to_string(), - })? - .hash; - - result.extend( - lmdb_fetch_keys_starting_with::( - hash.to_hex().as_str(), - &txn, - &self.kernels_db, - )? - .into_iter() - .skip(skip_amount) - .take(total_size - result.len()) - .map(|f| f.kernel), - ); - - skip_amount = 0; + PrunedOutput::Pruned { + output_hash: row.hash, + witness_hash: row.witness_hash, + } } - Ok(result) - } else { - Ok(vec![]) - } - } - - fn fetch_utxos_by_mmr_position( - &self, - start: u64, - end: u64, - deleted: &Bitmap, - ) -> Result<(Vec, Bitmap), ChainStorageError> { - let txn = self.read_transaction()?; - let start_height = lmdb_first_after(&txn, &self.output_mmr_size_index, &(start + 1).to_be_bytes())? - .ok_or_else(|| { - ChainStorageError::InvalidQuery(format!( - "Unable to find block height from start output MMR index {}", - start - )) - })?; - let end_height: u64 = - lmdb_first_after(&txn, &self.output_mmr_size_index, &(end + 1).to_be_bytes())?.unwrap_or(start_height); + }) + .collect(); - let previous_mmr_count = if start_height == 0 { - 0 - } else { - let header: BlockHeader = - lmdb_get(&txn, &self.headers_db, &(start_height - 1))?.expect("Header should exist"); - debug!(target: LOG_TARGET, "Previous header:{}", header); - header.output_mmr_size - }; + let height = + self.fetch_height_from_hash(&txn, header_hash)? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "BlockHeader", + field: "hash", + value: header_hash.to_hex(), + })?; - let total_size = end - .checked_sub(start) - .and_then(|v| v.checked_add(1)) - .and_then(|v| usize::try_from(v).ok()) - .ok_or_else(|| { - ChainStorageError::InvalidQuery("fetch_utxos_by_mmr_position: end is less than start".to_string()) - })?; - let mut result = Vec::with_capacity(total_size); + // Builds a BitMap of the deleted UTXO MMR indexes that occurred at the current height + let acc_data = + self.fetch_block_accumulated_data(&txn, height)? + .ok_or_else(|| ChainStorageError::ValueNotFound { + entity: "BlockAccumulatedData", + field: "height", + value: height.to_string(), + })?; - let mut skip_amount = (start - previous_mmr_count) as usize; - debug!( - target: LOG_TARGET, - "Fetching outputs by MMR position. Start {}, end {}, starting in header at height {}, prev mmr count: \ - {}, skipping the first:{}", - start, - end, - start_height, - previous_mmr_count, - skip_amount - ); let mut difference_bitmap = Bitmap::create(); + difference_bitmap.or_inplace(acc_data.deleted()); - for height in start_height..=end_height { - let accum_data = - lmdb_get::<_, BlockHeaderAccumulatedData>(&txn, &self.header_accumulated_data_db, &height)? - .ok_or_else(|| ChainStorageError::ValueNotFound { - entity: "BlockHeader", - field: "height", - value: height.to_string(), - })?; - - result.extend( - lmdb_fetch_keys_starting_with::( - accum_data.hash.to_hex().as_str(), - &txn, - &self.utxos_db, - )? - .into_iter() - .skip(skip_amount) - .take(total_size - result.len()) - .map(|row| { - if deleted.contains(row.mmr_position) { - return PrunedOutput::Pruned { - output_hash: row.hash, - witness_hash: row.witness_hash, - }; - } - if let Some(output) = row.output { - PrunedOutput::NotPruned { output } - } else { - PrunedOutput::Pruned { - output_hash: row.hash, - witness_hash: row.witness_hash, - } - } - }), - ); - - // Builds a BitMap of the deleted UTXO MMR indexes that occurred at the current height - let diff_bitmap = self - .fetch_block_accumulated_data(&txn, height) - .or_not_found("BlockAccumulatedData", "height", height.to_string())? - .deleted() - .clone(); - difference_bitmap.or_inplace(&diff_bitmap); - - skip_amount = 0; - } - - difference_bitmap.run_optimize(); - Ok((result, difference_bitmap)) + Ok((utxos, difference_bitmap)) } fn fetch_output(&self, output_hash: &HashOutput) -> Result, ChainStorageError> { @@ -2171,7 +2020,7 @@ impl BlockchainBackend for LMDBDatabase { fn fetch_horizon_data(&self) -> Result, ChainStorageError> { let txn = self.read_transaction()?; - fetch_horizon_data(&txn, &self.metadata_db) + Ok(Some(fetch_horizon_data(&txn, &self.metadata_db)?)) } fn get_stats(&self) -> Result { @@ -2228,7 +2077,7 @@ fn fetch_chain_height(txn: &ConstTransaction<'_>, db: &Database) -> Result, db: &Database) -> Result { let k = MetadataKey::PrunedHeight; let val: Option = lmdb_get(txn, db, &k.as_u32())?; @@ -2237,18 +2086,22 @@ fn fetch_pruned_height(txn: &ConstTransaction<'_>, db: &Database) -> Result Ok(0), } } -// Fetches the best block hash from the provided metadata db. -fn fetch_horizon_data(txn: &ConstTransaction<'_>, db: &Database) -> Result, ChainStorageError> { + +/// Fetches the horizon data from the provided metadata db. +fn fetch_horizon_data(txn: &ConstTransaction<'_>, db: &Database) -> Result { let k = MetadataKey::HorizonData; let val: Option = lmdb_get(txn, db, &k.as_u32())?; match val { - Some(MetadataValue::HorizonData(data)) => Ok(Some(data)), - None => Ok(None), - _ => Err(ChainStorageError::ValueNotFound { - entity: "ChainMetadata", - field: "HorizonData", + Some(MetadataValue::HorizonData(data)) => Ok(data), + None => Err(ChainStorageError::ValueNotFound { + entity: "HorizonData", + field: "metadata", value: "".to_string(), }), + Some(k) => Err(ChainStorageError::DataInconsistencyDetected { + function: "fetch_horizon_data", + details: format!("Received incorrect value {:?} for key horizon data", k), + }), } } // Fetches the best block hash from the provided metadata db. diff --git a/base_layer/core/src/chain_storage/tests/blockchain_database.rs b/base_layer/core/src/chain_storage/tests/blockchain_database.rs index 4bcd6099ec..0d7365229d 100644 --- a/base_layer/core/src/chain_storage/tests/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/tests/blockchain_database.rs @@ -20,15 +20,11 @@ // 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::sync::Arc; - -use tari_common::configuration::Network; -use tari_test_utils::unpack_enum; - use crate::{ blocks::{Block, BlockHeader, NewBlockTemplate}, chain_storage::{BlockchainDatabase, ChainStorageError}, consensus::ConsensusManager, + crypto::tari_utilities::hex::Hex, proof_of_work::Difficulty, tari_utilities::Hashable, test_helpers::{ @@ -38,9 +34,18 @@ use crate::{ }, transactions::{ tari_amount::T, - transaction_entities::{transaction::Transaction, unblinded_output::UnblindedOutput}, + test_helpers::{schema_to_transaction, TransactionSchema}, + transaction_entities::{ + output_features::OutputFeatures, + transaction::Transaction, + unblinded_output::UnblindedOutput, + }, }, + txn_schema, }; +use std::sync::Arc; +use tari_common::configuration::Network; +use tari_test_utils::unpack_enum; fn setup() -> BlockchainDatabase { create_new_blockchain() @@ -62,7 +67,13 @@ fn add_many_chained_blocks( size: usize, db: &BlockchainDatabase, ) -> (Vec>, Vec) { - let mut prev_block = Arc::new(db.fetch_block(0).unwrap().try_into_block().unwrap()); + let last_header = db.fetch_last_header().unwrap(); + let mut prev_block = db + .fetch_block(last_header.height) + .unwrap() + .try_into_block() + .map(Arc::new) + .unwrap(); let mut blocks = Vec::with_capacity(size); let mut outputs = Vec::with_capacity(size); for _ in 1..=size as u64 { @@ -353,16 +364,6 @@ mod fetch_block_hashes_from_header_tip { } mod add_block { - use crate::{ - chain_storage::ChainStorageError, - crypto::tari_utilities::hex::Hex, - transactions::{ - tari_amount::T, - test_helpers::{schema_to_transaction, TransactionSchema}, - transaction_entities::output_features::OutputFeatures, - }, - txn_schema, - }; use super::*; @@ -484,3 +485,90 @@ mod prepare_new_block { assert_eq!(block.header.height, 1); } } + +mod fetch_header_containing_utxo_mmr { + use super::*; + + #[test] + fn it_returns_genesis() { + let db = setup(); + let genesis = db.fetch_block(0).unwrap(); + assert_eq!(genesis.block().body.outputs().len(), 4001); + let mut mmr_position = 0; + genesis.block().body.outputs().iter().for_each(|_| { + let header = db.fetch_header_containing_utxo_mmr(mmr_position).unwrap(); + assert_eq!(header.height(), 0); + mmr_position += 1; + }); + let err = db.fetch_header_containing_utxo_mmr(4002).unwrap_err(); + matches!(err, ChainStorageError::ValueNotFound { .. }); + } + + #[test] + fn it_returns_corresponding_header() { + let db = setup(); + let genesis = db.fetch_block(0).unwrap(); + let _ = add_many_chained_blocks(5, &db); + let num_genesis_outputs = genesis.block().body.outputs().len() as u64; + + for i in 1..=5 { + let header = db.fetch_header_containing_utxo_mmr(num_genesis_outputs + i).unwrap(); + assert_eq!(header.height(), i); + } + let err = db + .fetch_header_containing_utxo_mmr(num_genesis_outputs + 5 + 1) + .unwrap_err(); + matches!(err, ChainStorageError::ValueNotFound { .. }); + } +} + +mod fetch_header_containing_kernel_mmr { + use super::*; + + #[test] + fn it_returns_genesis() { + let db = setup(); + let genesis = db.fetch_block(0).unwrap(); + assert_eq!(genesis.block().body.kernels().len(), 2); + let mut mmr_position = 0; + genesis.block().body.kernels().iter().for_each(|_| { + let header = db.fetch_header_containing_kernel_mmr(mmr_position).unwrap(); + assert_eq!(header.height(), 0); + mmr_position += 1; + }); + let err = db.fetch_header_containing_kernel_mmr(3).unwrap_err(); + matches!(err, ChainStorageError::ValueNotFound { .. }); + } + + #[test] + fn it_returns_corresponding_header() { + let db = setup(); + let genesis = db.fetch_block(0).unwrap(); + let (blocks, outputs) = add_many_chained_blocks(1, &db); + let num_genesis_kernels = genesis.block().body.kernels().len() as u64; + let (txns, _) = schema_to_transaction(&[txn_schema!(from: vec![outputs[0].clone()], to: vec![50 * T])]); + + let (block, _) = create_next_block(&blocks[0], txns); + db.add_block(block).unwrap(); + let _ = add_many_chained_blocks(3, &db); + + let header = db.fetch_header_containing_kernel_mmr(num_genesis_kernels).unwrap(); + assert_eq!(header.height(), 0); + let header = db.fetch_header_containing_kernel_mmr(num_genesis_kernels + 1).unwrap(); + assert_eq!(header.height(), 1); + + for i in 2..=3 { + let header = db.fetch_header_containing_kernel_mmr(num_genesis_kernels + i).unwrap(); + assert_eq!(header.height(), 2); + } + for i in 4..=6 { + let header = db.fetch_header_containing_kernel_mmr(num_genesis_kernels + i).unwrap(); + assert_eq!(header.height(), i - 1); + } + + let err = db + .fetch_header_containing_kernel_mmr(num_genesis_kernels + 6 + 1) + .unwrap_err(); + matches!(err, ChainStorageError::ValueNotFound { .. }); + } +} diff --git a/base_layer/core/src/test_helpers/blockchain.rs b/base_layer/core/src/test_helpers/blockchain.rs index 462c972621..f18b55ee65 100644 --- a/base_layer/core/src/test_helpers/blockchain.rs +++ b/base_layer/core/src/test_helpers/blockchain.rs @@ -268,20 +268,12 @@ impl BlockchainBackend for TempDatabase { self.db.as_ref().unwrap().fetch_kernel_by_excess_sig(excess_sig) } - fn fetch_kernels_by_mmr_position(&self, start: u64, end: u64) -> Result, ChainStorageError> { - self.db.as_ref().unwrap().fetch_kernels_by_mmr_position(start, end) - } - - fn fetch_utxos_by_mmr_position( + fn fetch_utxos_in_block( &self, - start: u64, - end: u64, - deleted: &Bitmap, + header_hash: &HashOutput, + deleted: Option<&Bitmap>, ) -> Result<(Vec, Bitmap), ChainStorageError> { - self.db - .as_ref() - .unwrap() - .fetch_utxos_by_mmr_position(start, end, deleted) + self.db.as_ref().unwrap().fetch_utxos_in_block(header_hash, deleted) } fn fetch_output(&self, output_hash: &HashOutput) -> Result, ChainStorageError> { diff --git a/base_layer/core/src/transactions/aggregated_body.rs b/base_layer/core/src/transactions/aggregated_body.rs index 1592115d7b..0cda236641 100644 --- a/base_layer/core/src/transactions/aggregated_body.rs +++ b/base_layer/core/src/transactions/aggregated_body.rs @@ -462,11 +462,7 @@ impl AggregateBody { fn validate_range_proofs(&self, range_proof_service: &RangeProofService) -> Result<(), TransactionError> { trace!(target: LOG_TARGET, "Checking range proofs"); for o in &self.outputs { - if !o.verify_range_proof(range_proof_service)? { - return Err(TransactionError::ValidationError( - "Range proof could not be verified".into(), - )); - } + o.verify_range_proof(range_proof_service)?; } Ok(()) } diff --git a/base_layer/core/src/transactions/coinbase_builder.rs b/base_layer/core/src/transactions/coinbase_builder.rs index e6e3450465..b74751feb8 100644 --- a/base_layer/core/src/transactions/coinbase_builder.rs +++ b/base_layer/core/src/transactions/coinbase_builder.rs @@ -324,7 +324,7 @@ mod test { assert!(factories .commitment .open_value(&p.spend_key, block_reward.into(), utxo.commitment())); - assert!(utxo.verify_range_proof(&factories.range_proof).unwrap()); + utxo.verify_range_proof(&factories.range_proof).unwrap(); assert!(utxo.features.flags.contains(OutputFlags::COINBASE_OUTPUT)); assert_eq!( tx.body.check_coinbase_output( diff --git a/base_layer/core/src/transactions/transaction_entities/mod.rs b/base_layer/core/src/transactions/transaction_entities/mod.rs index 34a1d76771..396f9cb1c7 100644 --- a/base_layer/core/src/transactions/transaction_entities/mod.rs +++ b/base_layer/core/src/transactions/transaction_entities/mod.rs @@ -156,7 +156,7 @@ mod test { }); let script = unblinded_output1.script.clone(); let tx_output1 = unblinded_output1.as_transaction_output(&factories).unwrap(); - assert!(tx_output1.verify_range_proof(&factories.range_proof).unwrap()); + tx_output1.verify_range_proof(&factories.range_proof).unwrap(); let unblinded_output2 = test_params_2.create_unblinded_output(UtxoTestParams { value: (2u64.pow(32) + 1u64).into(), @@ -196,7 +196,7 @@ mod test { ) .unwrap(), ); - assert!(!tx_output3.verify_range_proof(&factories.range_proof).unwrap()); + tx_output3.verify_range_proof(&factories.range_proof).unwrap_err(); } #[test] diff --git a/base_layer/core/src/transactions/transaction_entities/transaction_output.rs b/base_layer/core/src/transactions/transaction_entities/transaction_output.rs index 204278b27a..a9f5a290eb 100644 --- a/base_layer/core/src/transactions/transaction_entities/transaction_output.rs +++ b/base_layer/core/src/transactions/transaction_entities/transaction_output.rs @@ -117,8 +117,14 @@ impl TransactionOutput { } /// Verify that range proof is valid - pub fn verify_range_proof(&self, prover: &RangeProofService) -> Result { - Ok(prover.verify(&self.proof.0, &self.commitment)) + pub fn verify_range_proof(&self, prover: &RangeProofService) -> Result<(), TransactionError> { + if prover.verify(&self.proof.0, &self.commitment) { + Ok(()) + } else { + Err(TransactionError::ValidationError( + "Recipient output range proof failed to verify".to_string(), + )) + } } /// Verify that the metadata signature is valid diff --git a/base_layer/core/src/transactions/transaction_protocol/recipient.rs b/base_layer/core/src/transactions/transaction_protocol/recipient.rs index 2ddf1c5e02..af92a1b2ee 100644 --- a/base_layer/core/src/transactions/transaction_protocol/recipient.rs +++ b/base_layer/core/src/transactions/transaction_protocol/recipient.rs @@ -266,7 +266,7 @@ mod test { assert!(factories .commitment .open_value(&p.spend_key, 500, &data.output.commitment)); - assert!(data.output.verify_range_proof(&factories.range_proof).unwrap()); + data.output.verify_range_proof(&factories.range_proof).unwrap(); let r_sum = &msg.public_nonce + &p.public_nonce; let e = build_challenge(&r_sum, &m); let s = Signature::sign(p.spend_key.clone(), p.nonce, &e).unwrap(); diff --git a/base_layer/core/src/transactions/transaction_protocol/sender.rs b/base_layer/core/src/transactions/transaction_protocol/sender.rs index dc90972098..3961ba05cc 100644 --- a/base_layer/core/src/transactions/transaction_protocol/sender.rs +++ b/base_layer/core/src/transactions/transaction_protocol/sender.rs @@ -395,11 +395,7 @@ impl SenderTransactionProtocol { ) -> Result<(), TPE> { match &mut self.state { SenderState::CollectingSingleSignature(info) => { - if !rec.output.verify_range_proof(prover)? { - return Err(TPE::ValidationError( - "Recipient output range proof failed to verify".into(), - )); - } + rec.output.verify_range_proof(prover)?; // Consolidate transaction info info.outputs.push(rec.output.clone()); @@ -756,7 +752,7 @@ mod test { crypto_factories::CryptoFactories, tari_amount::*, test_helpers::{create_test_input, create_unblinded_output, TestParams}, - transaction_entities::{KernelFeatures, OutputFeatures, TransactionOutput}, + transaction_entities::{KernelFeatures, OutputFeatures, TransactionError, TransactionOutput}, transaction_protocol::{ sender::SenderTransactionProtocol, single_receiver::SingleReceiverTransactionProtocol, @@ -1049,7 +1045,9 @@ mod test { Ok(_) => panic!("Range proof should have failed to verify"), Err(e) => assert_eq!( e, - TransactionProtocolError::ValidationError("Recipient output range proof failed to verify".into()) + TransactionProtocolError::TransactionBuildError(TransactionError::ValidationError( + "Recipient output range proof failed to verify".into() + )) ), } } diff --git a/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs b/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs index 93356a4a5f..9fd710d4ee 100644 --- a/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs +++ b/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs @@ -221,10 +221,7 @@ mod test { factories.commitment.open_value(&k, info.amount.into(), &out.commitment), "Output commitment is invalid" ); - assert!( - out.verify_range_proof(&factories.range_proof).unwrap(), - "Range proof is invalid" - ); + out.verify_range_proof(&factories.range_proof).unwrap(); assert!(out.features.flags.is_empty(), "Output features flags have changed"); } } diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index 369cf1831a..bc2f3bb0d5 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -57,7 +57,7 @@ pub enum ValidationError { InvalidAccountingBalance, #[error("Transaction contains already spent inputs")] ContainsSTxO, - #[error("Transaction contains already outputs that already exist")] + #[error("Transaction contains outputs that already exist")] ContainsTxO, #[error("Transaction contains an output commitment that already exists")] ContainsDuplicateUtxoCommitment, diff --git a/base_layer/core/tests/chain_storage_tests/chain_storage.rs b/base_layer/core/tests/chain_storage_tests/chain_storage.rs index ef6a257488..b58bba6766 100644 --- a/base_layer/core/tests/chain_storage_tests/chain_storage.rs +++ b/base_layer/core/tests/chain_storage_tests/chain_storage.rs @@ -1718,7 +1718,7 @@ fn pruned_mode_cleanup_and_fetch_block() { let _block5 = append_block(&store, &block4, vec![], &consensus_manager, 1.into()).unwrap(); let metadata = store.get_chain_metadata().unwrap(); - assert_eq!(metadata.pruned_height(), 1); + assert_eq!(metadata.pruned_height(), 2); assert_eq!(metadata.height_of_longest_chain(), 5); assert_eq!(metadata.pruning_horizon(), 3); } diff --git a/common/src/build/protobuf.rs b/common/src/build/protobuf.rs index 6898052e94..875320a468 100644 --- a/common/src/build/protobuf.rs +++ b/common/src/build/protobuf.rs @@ -24,9 +24,12 @@ where P: AsRef + Display { .output() .unwrap(); - if !out.status.success() { - panic!("status: {} - {}", out.status, String::from_utf8_lossy(&out.stderr)); - } + assert!( + out.status.success(), + "status: {} - {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); } } diff --git a/comms/Cargo.toml b/comms/Cargo.toml index 861ab55179..76b6ce3fa0 100644 --- a/comms/Cargo.toml +++ b/comms/Cargo.toml @@ -69,4 +69,4 @@ tari_common = { version = "^0.21", path = "../common", features = ["build"] } c_integration = [] avx2 = ["tari_crypto/avx2"] metrics = [] -rpc = ["tower-make"] +rpc = ["tower-make", "tower/util"] diff --git a/comms/dht/src/config.rs b/comms/dht/src/config.rs index ccfd260c9b..978f3581d9 100644 --- a/comms/dht/src/config.rs +++ b/comms/dht/src/config.rs @@ -82,6 +82,7 @@ pub struct DhtConfig { /// Default: 6 hrs pub ban_duration: Duration, /// This allows the use of test addresses in the network. + /// Default: false pub allow_test_addresses: bool, /// The maximum number of messages over `flood_ban_timespan` to allow before banning the peer (for `ban_duration`) /// Default: 1000 messages diff --git a/integration_tests/features/Propagation.feature b/integration_tests/features/Propagation.feature index 3c1f5ef8fb..54f7cfaab5 100644 --- a/integration_tests/features/Propagation.feature +++ b/integration_tests/features/Propagation.feature @@ -101,4 +101,4 @@ Feature: Block Propagation Then TX1 is in the MINED of all nodes When I mine 17 blocks on SENDER Then all nodes are on the same chain at height 21 - Then node PNODE1 has a pruned height of 15 + Then node PNODE1 has a pruned height of 16