From 3c37073cfd21a189ac2fc496662557f2dc4bce49 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Mon, 14 Apr 2025 11:45:54 +0800 Subject: [PATCH 01/14] add log of bdk wallet full sync --- mutiny-core/src/onchain.rs | 11 +++++++++++ mutiny-core/src/storage.rs | 3 --- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 9e6fe9aeb..b0bfbb17f 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -39,6 +39,11 @@ use crate::utils; use crate::utils::{now, sleep}; use crate::TransactionDetails; +#[cfg(not(target_arch = "wasm32"))] +use std::time::Instant; +#[cfg(target_arch = "wasm32")] +use web_time::Instant; + pub(crate) const FULL_SYNC_STOP_GAP: usize = 150; pub(crate) const RESTORE_SYNC_STOP_GAP: usize = 50; const PARALLEL_REQUESTS: usize = 10; @@ -253,7 +258,13 @@ impl OnChainWallet { pub async fn sync(&self) -> Result<(), MutinyError> { // if we need a full sync from a restore if self.storage.get(NEED_FULL_SYNC_KEY)?.unwrap_or_default() { + let start = Instant::now(); self.full_sync(RESTORE_SYNC_STOP_GAP).await?; + log_info!( + self.logger, + "Full sync took {} seconds", + start.elapsed().as_secs() + ); self.storage.delete(&[NEED_FULL_SYNC_KEY])?; } // get first wallet lock that only needs to read diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index cf80f998f..78df80cf1 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -1209,9 +1209,6 @@ pub(crate) fn list_payment_info( .collect()) } -#[derive(Clone)] -pub struct OnChainStorage(pub(crate) S); - pub(crate) fn get_payment_hash_from_key<'a>(key: &'a str, prefix: &str) -> &'a str { key.trim_start_matches(prefix) .splitn(2, '_') // To support the old format that had `_{node_id}` at the end From c7d4881596fa4d8fb8cb124e7e7e888bf0860552 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 10:27:53 +0800 Subject: [PATCH 02/14] add tr_descriptors for OnChainWallet --- mutiny-core/src/onchain.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index b0bfbb17f..5fd262b9c 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -58,6 +58,7 @@ pub struct OnChainWallet { pub(crate) stop: Arc, logger: Arc, ln_event_callback: Option, + tr_descriptors: (DescriptorTemplateOut, DescriptorTemplateOut), } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] @@ -117,8 +118,11 @@ impl OnChainWallet { None | Some(Ok(None)) => { // we don't have a bdk wallet, create one Wallet::create_with_params( - CreateParams::new(receive_descriptor_template, change_descriptor_template) - .network(network), + CreateParams::new( + receive_descriptor_template.clone(), + change_descriptor_template.clone(), + ) + .network(network), )? } Some(Err(bdk_wallet::LoadError::Mismatch(_))) => { @@ -126,8 +130,11 @@ impl OnChainWallet { db.delete(&[KEYCHAIN_STORE_KEY])?; db.write_data(NEED_FULL_SYNC_KEY.to_string(), true, None)?; Wallet::create_with_params( - CreateParams::new(receive_descriptor_template, change_descriptor_template) - .network(network), + CreateParams::new( + receive_descriptor_template.clone(), + change_descriptor_template.clone(), + ) + .network(network), )? } Some(Err(e)) => { @@ -145,6 +152,7 @@ impl OnChainWallet { stop, logger, ln_event_callback, + tr_descriptors: (receive_descriptor_template, change_descriptor_template), }) } From ba5f10beb4d9b9e138fb6cda0dd00bc599d17033 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 12:21:53 +0800 Subject: [PATCH 03/14] add method full_scan and new_wallet --- mutiny-core/src/onchain.rs | 80 +++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 5fd262b9c..b7da29548 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -336,34 +336,13 @@ impl OnChainWallet { } pub async fn full_sync(&self, gap: usize) -> Result<(), MutinyError> { - // get first wallet lock that only needs to read - let spks = { - if let Ok(wallet) = self.wallet.try_read() { - wallet.all_unbounded_spk_iters() - } else { - log_error!(self.logger, "Could not get wallet lock to sync"); - return Err(MutinyError::WalletOperationFailed); - } - }; - - let mut request_builder = FullScanRequestBuilder::default(); - for (kind, pks) in spks.into_iter() { - request_builder = request_builder.spks_for_keychain(kind, pks) - } - - let FullScanResult { - tx_update, - last_active_indices, - chain_update, - } = self - .blockchain - .full_scan(request_builder, gap, PARALLEL_REQUESTS) - .await?; - let update = Update { - last_active_indices, - tx_update, - chain: chain_update, - }; + let update = Self::full_scan( + self.wallet.clone(), + gap, + self.blockchain.clone(), + self.logger.clone(), + ) + .await?; // get new wallet lock for writing and apply the update for _ in 0..10 { @@ -854,6 +833,51 @@ impl OnChainWallet { log_debug!(self.logger, "Fee bump Transaction broadcast! TXID: {txid}"); Ok(txid) } + + pub fn new_wallet(&self) -> Result { + let wallet = Wallet::create_with_params( + CreateParams::new(self.tr_descriptors.0.clone(), self.tr_descriptors.1.clone()) + .network(self.network), + )?; + Ok(wallet) + } + + pub async fn full_scan( + wallet: Arc>, + gap: usize, + blockchain: Arc, + logger: Arc, + ) -> Result { + // get first wallet lock that only needs to read + let spks = { + if let Ok(wallet) = wallet.try_read() { + wallet.all_unbounded_spk_iters() + } else { + log_error!(logger, "Could not get wallet lock to sync"); + return Err(MutinyError::WalletOperationFailed); + } + }; + + let mut request_builder = FullScanRequestBuilder::default(); + for (kind, pks) in spks.into_iter() { + request_builder = request_builder.spks_for_keychain(kind, pks) + } + + let FullScanResult { + tx_update, + last_active_indices, + chain_update, + } = blockchain + .full_scan(request_builder, gap, PARALLEL_REQUESTS) + .await?; + let update = Update { + last_active_indices, + tx_update, + chain: chain_update, + }; + + Ok(update) + } } fn get_tr_descriptors_for_extended_key( From fdbcb720ea0bfb12651054527f88320f5c6759d2 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 16:39:51 +0800 Subject: [PATCH 04/14] add check keychain size in node_manager start_sync --- mutiny-core/src/nodemanager.rs | 52 +++++++++++++++++++++++++++++++++- mutiny-core/src/storage.rs | 8 ++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index ca608dc27..75e7e997a 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -3,7 +3,9 @@ use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX; use crate::logging::LOGGING_KEY; use crate::lsp::voltage; use crate::messagehandler::{CommonLnEvent, CommonLnEventCallback}; +use crate::onchain::RESTORE_SYNC_STOP_GAP; use crate::peermanager::PeerManager; +use crate::utils::now; use crate::utils::sleep; use crate::MutinyInvoice; use crate::MutinyWalletConfig; @@ -59,11 +61,17 @@ use std::cmp::max; use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; -use std::{collections::HashMap, ops::Deref, sync::Arc}; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, RwLock as StdRwLock}, +}; use url::Url; #[cfg(target_arch = "wasm32")] use web_time::Instant; +const KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES: usize = 128 * 1024; // 128KB + // This is the NodeStorage object saved to the DB #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub struct NodeStorage { @@ -696,6 +704,48 @@ impl NodeManager { } sleep(1_000).await; } + + // check keychain size + if let Ok(Some(changes)) = nm.storage.read_changes() { + let value = serde_json::to_vec(&changes).unwrap_or_default(); + let size = value.len(); + if size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { + log_info!( + nm.logger, + "Keychain Size threshold exceeded, spawning simplified compaction task." + ); + if let Ok(new_wallet) = nm.wallet.new_wallet() { + let new_wallet = Arc::new(StdRwLock::new(new_wallet)); + if let Ok(update) = OnChainWallet::::full_scan( + new_wallet.clone(), + RESTORE_SYNC_STOP_GAP, + nm.esplora.clone(), + nm.logger.clone(), + ) + .await + { + if let Ok(mut new_wallet) = new_wallet.try_write() { + if new_wallet + .apply_update_at(update, Some(now().as_secs())) + .is_ok() + { + if let Ok(mut wallet) = nm.wallet.wallet.try_write() { + wallet = new_wallet; + if let Some(changeset) = wallet.take_staged() { + if nm.storage.restore_changes(&changeset).is_ok() { + log_info!( + nm.logger, + "Keychain compaction completed successfully." + ); + } + } + } + } + } + } + } + } + } } }); } diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 78df80cf1..daf31a34a 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -674,6 +674,14 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { } } + /// Restore changeset to the storage + fn restore_changes(&self, changeset: &ChangeSet) -> Result<(), MutinyError> { + let version = now().as_secs() as u32; + let value = serde_json::to_value(changeset)?; + let value = VersionedValue { value, version }; + self.write_data(KEYCHAIN_STORE_KEY.to_string(), value, Some(version)) + } + /// Spawn background task to run db tasks fn spawn(&self, _fut: Fut); } From 7096ceb8a7414de400ea54739829793e72baf904 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 17:36:49 +0800 Subject: [PATCH 05/14] add did_keychain_compact_this_round flag --- mutiny-core/src/nodemanager.rs | 102 ++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 75e7e997a..27a785861 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -31,7 +31,7 @@ use crate::{ use anyhow::anyhow; use async_lock::RwLock; use bdk_chain::{BlockId, ConfirmationTime}; -use bdk_wallet::{KeychainKind, LocalOutput}; +use bdk_wallet::{ChangeSet, KeychainKind, LocalOutput}; use bitcoin::address::NetworkUnchecked; use bitcoin::bip32::Xpriv; use bitcoin::blockdata::script; @@ -656,6 +656,8 @@ impl NodeManager { utils::spawn(async move { let mut synced = false; loop { + let mut did_keychain_compact_this_round = false; + // If we are stopped, don't sync if nm.stop.load(Ordering::Relaxed) { return; @@ -697,55 +699,73 @@ impl NodeManager { } } - // wait for next sync round, checking graceful shutdown check each second. - for _ in 0..sync_interval_secs { - if nm.stop.load(Ordering::Relaxed) { - return; - } - sleep(1_000).await; - } - // check keychain size - if let Ok(Some(changes)) = nm.storage.read_changes() { - let value = serde_json::to_vec(&changes).unwrap_or_default(); - let size = value.len(); - if size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { - log_info!( - nm.logger, - "Keychain Size threshold exceeded, spawning simplified compaction task." - ); - if let Ok(new_wallet) = nm.wallet.new_wallet() { - let new_wallet = Arc::new(StdRwLock::new(new_wallet)); - if let Ok(update) = OnChainWallet::::full_scan( - new_wallet.clone(), - RESTORE_SYNC_STOP_GAP, - nm.esplora.clone(), - nm.logger.clone(), - ) - .await - { - if let Ok(mut new_wallet) = new_wallet.try_write() { - if new_wallet - .apply_update_at(update, Some(now().as_secs())) - .is_ok() - { - if let Ok(mut wallet) = nm.wallet.wallet.try_write() { - wallet = new_wallet; - if let Some(changeset) = wallet.take_staged() { - if nm.storage.restore_changes(&changeset).is_ok() { - log_info!( - nm.logger, - "Keychain compaction completed successfully." - ); - } + let changes = match nm.storage.read_changes() { + Ok(Some(c)) => c, + Ok(None) => ChangeSet::default(), + Err(e) => { + log_error!( + nm.logger, + "Compaction check: Failed to read changes: {:?}", + e + ); + ChangeSet::default() + } + }; + let value = serde_json::to_vec(&changes).unwrap_or_default(); + let size = value.len(); + if size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { + log_info!( + nm.logger, + "Keychain Size threshold exceeded, spawning simplified compaction task." + ); + if let Ok(new_wallet) = nm.wallet.new_wallet() { + let new_wallet = Arc::new(StdRwLock::new(new_wallet)); + if let Ok(update) = OnChainWallet::::full_scan( + new_wallet.clone(), + RESTORE_SYNC_STOP_GAP, + nm.esplora.clone(), + nm.logger.clone(), + ) + .await + { + did_keychain_compact_this_round = true; + if let Ok(mut new_wallet) = new_wallet.try_write() { + if new_wallet + .apply_update_at(update, Some(now().as_secs())) + .is_ok() + { + if let Ok(mut wallet) = nm.wallet.wallet.try_write() { + wallet = new_wallet; + if let Some(changeset) = wallet.take_staged() { + if nm.storage.restore_changes(&changeset).is_ok() { + log_info!( + nm.logger, + "Keychain compaction completed successfully." + ); } } + } else { + log_warn!( + nm.logger, + "Failed to get wallet lock to apply update" + ); } } } } } } + + // wait for next sync round, checking graceful shutdown check each second. + if !did_keychain_compact_this_round { + for _ in 0..sync_interval_secs { + if nm.stop.load(Ordering::Relaxed) { + return; + } + sleep(1_000).await; + } + } } }); } From 41a5b36a800510da5cb7ef0938bd4515580a0706 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 18:38:44 +0800 Subject: [PATCH 06/14] backup old keychain when restore --- mutiny-core/src/nodemanager.rs | 2 +- mutiny-core/src/storage.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 27a785861..b141537cd 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -736,9 +736,9 @@ impl NodeManager { .is_ok() { if let Ok(mut wallet) = nm.wallet.wallet.try_write() { - wallet = new_wallet; if let Some(changeset) = wallet.take_staged() { if nm.storage.restore_changes(&changeset).is_ok() { + wallet = new_wallet; log_info!( nm.logger, "Keychain compaction completed successfully." diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index daf31a34a..6b3054f54 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -676,7 +676,18 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { /// Restore changeset to the storage fn restore_changes(&self, changeset: &ChangeSet) -> Result<(), MutinyError> { - let version = now().as_secs() as u32; + let current_timestamp_secs = now().as_secs(); + let backup_key = format!("{}_backup_{}", KEYCHAIN_STORE_KEY, current_timestamp_secs); + let _ = match self.get_data::(KEYCHAIN_STORE_KEY) { + Ok(Some(versioned)) => self.write_data(backup_key, versioned, None), + Ok(None) => Ok(()), + Err(e) => { + log_error!(self.logger(), "Error writing backup: {:?}", e); + Err(e) + } + }; + + let version = current_timestamp_secs as u32; let value = serde_json::to_value(changeset)?; let value = VersionedValue { value, version }; self.write_data(KEYCHAIN_STORE_KEY.to_string(), value, Some(version)) From 2e2d1a6589517ed3ad55a730c7f494043fc52935 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 19:53:57 +0800 Subject: [PATCH 07/14] update full_scan method --- mutiny-core/src/nodemanager.rs | 50 ++++++++++++++-------------------- mutiny-core/src/onchain.rs | 47 ++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index b141537cd..4aa9e28b1 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -61,11 +61,7 @@ use std::cmp::max; use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; -use std::{ - collections::HashMap, - ops::Deref, - sync::{Arc, RwLock as StdRwLock}, -}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use url::Url; #[cfg(target_arch = "wasm32")] use web_time::Instant; @@ -717,40 +713,36 @@ impl NodeManager { if size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { log_info!( nm.logger, - "Keychain Size threshold exceeded, spawning simplified compaction task." + "Keychain size threshold exceeded, spawning simplified compaction task." ); - if let Ok(new_wallet) = nm.wallet.new_wallet() { - let new_wallet = Arc::new(StdRwLock::new(new_wallet)); + if let Ok(mut new_wallet) = nm.wallet.new_wallet() { if let Ok(update) = OnChainWallet::::full_scan( - new_wallet.clone(), + &new_wallet, RESTORE_SYNC_STOP_GAP, nm.esplora.clone(), - nm.logger.clone(), ) .await { did_keychain_compact_this_round = true; - if let Ok(mut new_wallet) = new_wallet.try_write() { - if new_wallet - .apply_update_at(update, Some(now().as_secs())) - .is_ok() - { - if let Ok(mut wallet) = nm.wallet.wallet.try_write() { - if let Some(changeset) = wallet.take_staged() { - if nm.storage.restore_changes(&changeset).is_ok() { - wallet = new_wallet; - log_info!( - nm.logger, - "Keychain compaction completed successfully." - ); - } + if new_wallet + .apply_update_at(update, Some(now().as_secs())) + .is_ok() + { + if let Ok(mut wallet) = nm.wallet.wallet.try_write() { + if let Some(changeset) = wallet.take_staged() { + if nm.storage.restore_changes(&changeset).is_ok() { + *wallet = new_wallet; + log_info!( + nm.logger, + "Keychain compaction completed successfully." + ); } - } else { - log_warn!( - nm.logger, - "Failed to get wallet lock to apply update" - ); } + } else { + log_warn!( + nm.logger, + "Failed to get wallet lock to apply update" + ); } } } diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index b7da29548..70f449a30 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -336,13 +336,34 @@ impl OnChainWallet { } pub async fn full_sync(&self, gap: usize) -> Result<(), MutinyError> { - let update = Self::full_scan( - self.wallet.clone(), - gap, - self.blockchain.clone(), - self.logger.clone(), - ) - .await?; + // get first wallet lock that only needs to read + let spks = { + if let Ok(wallet) = self.wallet.try_read() { + wallet.all_unbounded_spk_iters() + } else { + log_error!(self.logger, "Could not get wallet lock to sync"); + return Err(MutinyError::WalletOperationFailed); + } + }; + + let mut request_builder = FullScanRequestBuilder::default(); + for (kind, pks) in spks.into_iter() { + request_builder = request_builder.spks_for_keychain(kind, pks) + } + + let FullScanResult { + tx_update, + last_active_indices, + chain_update, + } = self + .blockchain + .full_scan(request_builder, gap, PARALLEL_REQUESTS) + .await?; + let update = Update { + last_active_indices, + tx_update, + chain: chain_update, + }; // get new wallet lock for writing and apply the update for _ in 0..10 { @@ -843,20 +864,12 @@ impl OnChainWallet { } pub async fn full_scan( - wallet: Arc>, + wallet: &Wallet, gap: usize, blockchain: Arc, - logger: Arc, ) -> Result { // get first wallet lock that only needs to read - let spks = { - if let Ok(wallet) = wallet.try_read() { - wallet.all_unbounded_spk_iters() - } else { - log_error!(logger, "Could not get wallet lock to sync"); - return Err(MutinyError::WalletOperationFailed); - } - }; + let spks = wallet.all_unbounded_spk_iters(); let mut request_builder = FullScanRequestBuilder::default(); for (kind, pks) in spks.into_iter() { From e99c765b73effcf47f27f598a3afb3b569cda6a7 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 20:24:42 +0800 Subject: [PATCH 08/14] add log for keychain compaction time cost --- mutiny-core/src/nodemanager.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 4aa9e28b1..2a1a4a844 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -696,6 +696,7 @@ impl NodeManager { } // check keychain size + let start = Instant::now(); let changes = match nm.storage.read_changes() { Ok(Some(c)) => c, Ok(None) => ChangeSet::default(), @@ -729,7 +730,7 @@ impl NodeManager { .is_ok() { if let Ok(mut wallet) = nm.wallet.wallet.try_write() { - if let Some(changeset) = wallet.take_staged() { + if let Some(changeset) = new_wallet.take_staged() { if nm.storage.restore_changes(&changeset).is_ok() { *wallet = new_wallet; log_info!( @@ -748,6 +749,11 @@ impl NodeManager { } } } + log_info!( + nm.logger, + "Keychain compaction took {} seconds", + start.elapsed().as_secs() + ); // wait for next sync round, checking graceful shutdown check each second. if !did_keychain_compact_this_round { From da5e1a71e646138427726c642779bf0a3b8bd935 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Tue, 15 Apr 2025 20:55:04 +0800 Subject: [PATCH 09/14] bump version to v1.16.0 --- Cargo.lock | 2 +- mutiny-wasm/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d3a0e1aa..b6a15be3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,7 @@ dependencies = [ [[package]] name = "mutiny-wasm" -version = "1.15.0" +version = "1.16.0" dependencies = [ "anyhow", "async-trait", diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index 94513c381..b301aabb9 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["per-package-target"] [package] name = "mutiny-wasm" -version = "1.15.0" +version = "1.16.0" edition = "2021" authors = ["utxostack"] forced-target = "wasm32-unknown-unknown" From acbc165d8465504ab7926a4281dceedb26877ee7 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Wed, 16 Apr 2025 23:20:26 +0800 Subject: [PATCH 10/14] add log when compact keychain --- mutiny-core/src/nodemanager.rs | 52 ++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 2a1a4a844..24b313500 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -709,13 +709,33 @@ impl NodeManager { ChangeSet::default() } }; - let value = serde_json::to_vec(&changes).unwrap_or_default(); - let size = value.len(); - if size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { + let total_size = serde_json::to_vec(&changes).unwrap_or_default().len(); + log_info!(nm.logger, "Keychain size: {} bytes", total_size); + if total_size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { log_info!( nm.logger, - "Keychain size threshold exceeded, spawning simplified compaction task." + "Keychain size threshold exceeded {} Bytes, spawning simplified compaction task.", + KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES ); + + let local_chain_size = serde_json::to_vec(&changes.local_chain) + .map(|v| v.len()) + .unwrap_or(0); + let tx_graph_size = serde_json::to_vec(&changes.tx_graph) + .map(|v| v.len()) + .unwrap_or(0); + let indexer_size = serde_json::to_vec(&changes.indexer) + .map(|v| v.len()) + .unwrap_or(0); + log_debug!( + nm.logger, + "PRE-COMPACTION size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", + total_size, + local_chain_size, + tx_graph_size, + indexer_size + ); + if let Ok(mut new_wallet) = nm.wallet.new_wallet() { if let Ok(update) = OnChainWallet::::full_scan( &new_wallet, @@ -724,11 +744,33 @@ impl NodeManager { ) .await { + let total_size = serde_json::to_vec(&changes).unwrap_or_default().len(); + let local_chain_size = serde_json::to_vec(&changes.local_chain) + .map(|v| v.len()) + .unwrap_or(0); + let tx_graph_size = serde_json::to_vec(&changes.tx_graph) + .map(|v| v.len()) + .unwrap_or(0); + let indexer_size = serde_json::to_vec(&changes.indexer) + .map(|v| v.len()) + .unwrap_or(0); + log_debug!(nm.logger, + "POST-COMPACTION size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", + total_size, + local_chain_size, + tx_graph_size, + indexer_size + ); + did_keychain_compact_this_round = true; if new_wallet .apply_update_at(update, Some(now().as_secs())) .is_ok() { + // Strategy: Try acquiring main lock once. + // - Failure indicates contention. Abort compaction this cycle to ensure we don't overwrite + // changes from the contending operation (unlike a retry-until-success approach which *would* overwrite). + // - Success indicates no contention detected now; proceed with replace/overwrite. if let Ok(mut wallet) = nm.wallet.wallet.try_write() { if let Some(changeset) = new_wallet.take_staged() { if nm.storage.restore_changes(&changeset).is_ok() { @@ -742,7 +784,7 @@ impl NodeManager { } else { log_warn!( nm.logger, - "Failed to get wallet lock to apply update" + "Compaction: Failed to acquire main wallet lock due to contention. Aborting compaction attempt for this cycle." ); } } From d513f18444e0d5e629246083aad994466bec792d Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Thu, 17 Apr 2025 00:07:31 +0800 Subject: [PATCH 11/14] update the activity index in compact task --- mutiny-core/src/nodemanager.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 24b313500..1c35d15a0 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -26,7 +26,10 @@ use crate::{ use crate::{gossip::*, scorer::HubPreferentialScorer}; use crate::{ node::NodeBuilder, - storage::{MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY}, + storage::{ + IndexItem, MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY, + ONCHAIN_PREFIX, + }, }; use anyhow::anyhow; use async_lock::RwLock; @@ -781,12 +784,41 @@ impl NodeManager { ); } } + drop(wallet); // drop so we can read from wallet + + // update the activity index, just get the list of transactions + // and insert them into the index, this is done in background so shouldn't + // block the wallet update + if let Ok(txs) = nm.wallet.list_transactions(false) { + let index_items = txs + .into_iter() + .map(|t| IndexItem { + timestamp: match t.confirmation_time { + ConfirmationTime::Confirmed { + time, .. + } => Some(time), + ConfirmationTime::Unconfirmed { .. } => None, + }, + key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), + }) + .collect::>(); + + if let Ok(mut index) = + nm.storage.activity_index().try_write() + { + // remove old-onchain txs + index.retain(|i| !i.key.starts_with(ONCHAIN_PREFIX)); + index.extend(index_items); + } + } } else { log_warn!( nm.logger, "Compaction: Failed to acquire main wallet lock due to contention. Aborting compaction attempt for this cycle." ); } + } else { + log_error!(nm.logger, "Keychain compaction failed to apply update"); } } } From 4597b31c4353cde671e215d3237d4be04ad6fd3c Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Thu, 17 Apr 2025 15:00:28 +0800 Subject: [PATCH 12/14] refactoring --- mutiny-core/src/nodemanager.rs | 143 ++------------------------------- mutiny-core/src/onchain.rs | 138 +++++++++++++++++++++++++------ 2 files changed, 120 insertions(+), 161 deletions(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 1c35d15a0..46c414fff 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -3,9 +3,7 @@ use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX; use crate::logging::LOGGING_KEY; use crate::lsp::voltage; use crate::messagehandler::{CommonLnEvent, CommonLnEventCallback}; -use crate::onchain::RESTORE_SYNC_STOP_GAP; use crate::peermanager::PeerManager; -use crate::utils::now; use crate::utils::sleep; use crate::MutinyInvoice; use crate::MutinyWalletConfig; @@ -26,15 +24,12 @@ use crate::{ use crate::{gossip::*, scorer::HubPreferentialScorer}; use crate::{ node::NodeBuilder, - storage::{ - IndexItem, MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY, - ONCHAIN_PREFIX, - }, + storage::{MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY}, }; use anyhow::anyhow; use async_lock::RwLock; use bdk_chain::{BlockId, ConfirmationTime}; -use bdk_wallet::{ChangeSet, KeychainKind, LocalOutput}; +use bdk_wallet::{KeychainKind, LocalOutput}; use bitcoin::address::NetworkUnchecked; use bitcoin::bip32::Xpriv; use bitcoin::blockdata::script; @@ -69,8 +64,6 @@ use url::Url; #[cfg(target_arch = "wasm32")] use web_time::Instant; -const KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES: usize = 128 * 1024; // 128KB - // This is the NodeStorage object saved to the DB #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub struct NodeStorage { @@ -655,8 +648,6 @@ impl NodeManager { utils::spawn(async move { let mut synced = false; loop { - let mut did_keychain_compact_this_round = false; - // If we are stopped, don't sync if nm.stop.load(Ordering::Relaxed) { return; @@ -699,135 +690,13 @@ impl NodeManager { } // check keychain size - let start = Instant::now(); - let changes = match nm.storage.read_changes() { - Ok(Some(c)) => c, - Ok(None) => ChangeSet::default(), + let did_keychain_compact_this_round = match nm.wallet.try_compact_keychain().await { + Ok(did_keychain_compact_this_round) => did_keychain_compact_this_round, Err(e) => { - log_error!( - nm.logger, - "Compaction check: Failed to read changes: {:?}", - e - ); - ChangeSet::default() + log_error!(nm.logger, "Failed to compact keychain: {e}"); + false } }; - let total_size = serde_json::to_vec(&changes).unwrap_or_default().len(); - log_info!(nm.logger, "Keychain size: {} bytes", total_size); - if total_size > KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { - log_info!( - nm.logger, - "Keychain size threshold exceeded {} Bytes, spawning simplified compaction task.", - KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES - ); - - let local_chain_size = serde_json::to_vec(&changes.local_chain) - .map(|v| v.len()) - .unwrap_or(0); - let tx_graph_size = serde_json::to_vec(&changes.tx_graph) - .map(|v| v.len()) - .unwrap_or(0); - let indexer_size = serde_json::to_vec(&changes.indexer) - .map(|v| v.len()) - .unwrap_or(0); - log_debug!( - nm.logger, - "PRE-COMPACTION size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", - total_size, - local_chain_size, - tx_graph_size, - indexer_size - ); - - if let Ok(mut new_wallet) = nm.wallet.new_wallet() { - if let Ok(update) = OnChainWallet::::full_scan( - &new_wallet, - RESTORE_SYNC_STOP_GAP, - nm.esplora.clone(), - ) - .await - { - let total_size = serde_json::to_vec(&changes).unwrap_or_default().len(); - let local_chain_size = serde_json::to_vec(&changes.local_chain) - .map(|v| v.len()) - .unwrap_or(0); - let tx_graph_size = serde_json::to_vec(&changes.tx_graph) - .map(|v| v.len()) - .unwrap_or(0); - let indexer_size = serde_json::to_vec(&changes.indexer) - .map(|v| v.len()) - .unwrap_or(0); - log_debug!(nm.logger, - "POST-COMPACTION size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", - total_size, - local_chain_size, - tx_graph_size, - indexer_size - ); - - did_keychain_compact_this_round = true; - if new_wallet - .apply_update_at(update, Some(now().as_secs())) - .is_ok() - { - // Strategy: Try acquiring main lock once. - // - Failure indicates contention. Abort compaction this cycle to ensure we don't overwrite - // changes from the contending operation (unlike a retry-until-success approach which *would* overwrite). - // - Success indicates no contention detected now; proceed with replace/overwrite. - if let Ok(mut wallet) = nm.wallet.wallet.try_write() { - if let Some(changeset) = new_wallet.take_staged() { - if nm.storage.restore_changes(&changeset).is_ok() { - *wallet = new_wallet; - log_info!( - nm.logger, - "Keychain compaction completed successfully." - ); - } - } - drop(wallet); // drop so we can read from wallet - - // update the activity index, just get the list of transactions - // and insert them into the index, this is done in background so shouldn't - // block the wallet update - if let Ok(txs) = nm.wallet.list_transactions(false) { - let index_items = txs - .into_iter() - .map(|t| IndexItem { - timestamp: match t.confirmation_time { - ConfirmationTime::Confirmed { - time, .. - } => Some(time), - ConfirmationTime::Unconfirmed { .. } => None, - }, - key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), - }) - .collect::>(); - - if let Ok(mut index) = - nm.storage.activity_index().try_write() - { - // remove old-onchain txs - index.retain(|i| !i.key.starts_with(ONCHAIN_PREFIX)); - index.extend(index_items); - } - } - } else { - log_warn!( - nm.logger, - "Compaction: Failed to acquire main wallet lock due to contention. Aborting compaction attempt for this cycle." - ); - } - } else { - log_error!(nm.logger, "Keychain compaction failed to apply update"); - } - } - } - } - log_info!( - nm.logger, - "Keychain compaction took {} seconds", - start.elapsed().as_secs() - ); // wait for next sync round, checking graceful shutdown check each second. if !did_keychain_compact_this_round { diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 70f449a30..b42ad495d 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -13,7 +13,7 @@ use bdk_wallet::bitcoin::FeeRate; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::template::DescriptorTemplateOut; use bdk_wallet::{ - CreateParams, KeychainKind, LoadParams, LocalOutput, SignOptions, Update, Wallet, + ChangeSet, CreateParams, KeychainKind, LoadParams, LocalOutput, SignOptions, Update, Wallet, }; use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; use bitcoin::consensus::serialize; @@ -47,6 +47,7 @@ use web_time::Instant; pub(crate) const FULL_SYNC_STOP_GAP: usize = 150; pub(crate) const RESTORE_SYNC_STOP_GAP: usize = 50; const PARALLEL_REQUESTS: usize = 10; +const KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES: usize = 128 * 1024; // 128KB #[derive(Clone)] pub struct OnChainWallet { @@ -863,36 +864,125 @@ impl OnChainWallet { Ok(wallet) } - pub async fn full_scan( - wallet: &Wallet, - gap: usize, - blockchain: Arc, - ) -> Result { - // get first wallet lock that only needs to read - let spks = wallet.all_unbounded_spk_iters(); + pub async fn try_compact_keychain(&self) -> Result { + let start = Instant::now(); - let mut request_builder = FullScanRequestBuilder::default(); - for (kind, pks) in spks.into_iter() { - request_builder = request_builder.spks_for_keychain(kind, pks) + let changes = self.storage.read_changes()?.unwrap_or_default(); + let total_size = serde_json::to_vec(&changes).unwrap_or_default().len(); + if total_size < KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { + log_info!( + self.logger, + "Keychain size {}is below threshold {}, not compacting", + total_size, + KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES + ); + return Ok(false); } + log_info!( + self.logger, + "Keychain size threshold exceeded {} Bytes, spawning simplified compaction task.", + KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES + ); + self.log_keychain_size(&changes); - let FullScanResult { - tx_update, - last_active_indices, - chain_update, - } = blockchain - .full_scan(request_builder, gap, PARALLEL_REQUESTS) - .await?; - let update = Update { - last_active_indices, - tx_update, - chain: chain_update, - }; + let mut new_wallet = self.new_wallet()?; + let update = full_scan(&new_wallet, RESTORE_SYNC_STOP_GAP, self.blockchain.clone()).await?; - Ok(update) + new_wallet + .apply_update_at(update, Some(now().as_secs())) + .map_err(|e| { + log_error!(self.logger, "Could not apply wallet update: {e}"); + MutinyError::Other(anyhow!("Could not apply update: {e}")) + })?; + let mut wallet = self.wallet.try_write()?; + let index = self.storage.activity_index(); + let mut index = index.try_write()?; + let new_changeset = new_wallet.take_staged().ok_or(MutinyError::Other(anyhow!( + "Failed to take staged changeset from new wallet" + )))?; + self.log_keychain_size(&new_changeset); + self.storage.restore_changes(&new_changeset)?; + *wallet = new_wallet; + drop(wallet); // drop so we can read from wallet + + // update the activity index, just get the list of transactions + // and insert them into the index + let index_items = self + .list_transactions(false)? + .into_iter() + .map(|t| IndexItem { + timestamp: match t.confirmation_time { + ConfirmationTime::Confirmed { time, .. } => Some(time), + ConfirmationTime::Unconfirmed { .. } => None, + }, + key: format!("{ONCHAIN_PREFIX}{}", t.internal_id), + }) + .collect::>(); + + // remove old-onchain txs + index.retain(|i| !i.key.starts_with(ONCHAIN_PREFIX)); + index.extend(index_items); + + log_info!(self.logger, "Keychain compaction completed successfully."); + log_info!( + self.logger, + "Keychain compaction took {} seconds", + start.elapsed().as_secs() + ); + + Ok(true) + } + + fn log_keychain_size(&self, keychain: &ChangeSet) { + let total_size = serde_json::to_vec(&keychain).unwrap_or_default().len(); + let local_chain_size = serde_json::to_vec(&keychain.local_chain) + .map(|v| v.len()) + .unwrap_or(0); + let tx_graph_size = serde_json::to_vec(&keychain.tx_graph) + .map(|v| v.len()) + .unwrap_or(0); + let indexer_size = serde_json::to_vec(&keychain.indexer) + .map(|v| v.len()) + .unwrap_or(0); + log_debug!(self.logger, + "PRE-COMPACTION size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", + total_size, + local_chain_size, + tx_graph_size, + indexer_size + ); } } +async fn full_scan( + wallet: &Wallet, + gap: usize, + blockchain: Arc, +) -> Result { + // get first wallet lock that only needs to read + let spks = wallet.all_unbounded_spk_iters(); + + let mut request_builder = FullScanRequestBuilder::default(); + for (kind, pks) in spks.into_iter() { + request_builder = request_builder.spks_for_keychain(kind, pks) + } + + let FullScanResult { + tx_update, + last_active_indices, + chain_update, + } = blockchain + .full_scan(request_builder, gap, PARALLEL_REQUESTS) + .await?; + let update = Update { + last_active_indices, + tx_update, + chain: chain_update, + }; + + Ok(update) +} + fn get_tr_descriptors_for_extended_key( master_xprv: Xpriv, network: Network, From a6c34c80a7c5335166ddba049a30e87dc9bada78 Mon Sep 17 00:00:00 2001 From: EthanYuan Date: Fri, 18 Apr 2025 09:49:59 +0800 Subject: [PATCH 13/14] set KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES 256KB --- mutiny-core/src/onchain.rs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index b42ad495d..4e0be2d75 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -47,7 +47,7 @@ use web_time::Instant; pub(crate) const FULL_SYNC_STOP_GAP: usize = 150; pub(crate) const RESTORE_SYNC_STOP_GAP: usize = 50; const PARALLEL_REQUESTS: usize = 10; -const KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES: usize = 128 * 1024; // 128KB +const KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES: usize = 256 * 1024; // 256KB #[derive(Clone)] pub struct OnChainWallet { @@ -872,7 +872,7 @@ impl OnChainWallet { if total_size < KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES { log_info!( self.logger, - "Keychain size {}is below threshold {}, not compacting", + "Keychain size {} bytes is below threshold {} bytes, not compacting", total_size, KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES ); @@ -883,7 +883,7 @@ impl OnChainWallet { "Keychain size threshold exceeded {} Bytes, spawning simplified compaction task.", KEYCHAIN_COMPACTION_SIZE_THRESHOLD_BYTES ); - self.log_keychain_size(&changes); + self.log_keychain_size(&changes, false); let mut new_wallet = self.new_wallet()?; let update = full_scan(&new_wallet, RESTORE_SYNC_STOP_GAP, self.blockchain.clone()).await?; @@ -900,7 +900,7 @@ impl OnChainWallet { let new_changeset = new_wallet.take_staged().ok_or(MutinyError::Other(anyhow!( "Failed to take staged changeset from new wallet" )))?; - self.log_keychain_size(&new_changeset); + self.log_keychain_size(&new_changeset, true); self.storage.restore_changes(&new_changeset)?; *wallet = new_wallet; drop(wallet); // drop so we can read from wallet @@ -933,8 +933,8 @@ impl OnChainWallet { Ok(true) } - fn log_keychain_size(&self, keychain: &ChangeSet) { - let total_size = serde_json::to_vec(&keychain).unwrap_or_default().len(); + fn log_keychain_size(&self, keychain: &ChangeSet, is_post_compaction: bool) { + let total_size = serde_json::to_vec(keychain).unwrap_or_default().len(); let local_chain_size = serde_json::to_vec(&keychain.local_chain) .map(|v| v.len()) .unwrap_or(0); @@ -944,13 +944,22 @@ impl OnChainWallet { let indexer_size = serde_json::to_vec(&keychain.indexer) .map(|v| v.len()) .unwrap_or(0); - log_debug!(self.logger, - "PRE-COMPACTION size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", - total_size, - local_chain_size, - tx_graph_size, - indexer_size - ); + + let prefix = if is_post_compaction { + "POST-COMPACTION" + } else { + "PRE-COMPACTION" + }; + + log_debug!( + self.logger, + "{} size: {} bytes. Approx component sizes (bytes): LocalChain={}, TxGraph={}, Indexer={}", + prefix, + total_size, + local_chain_size, + tx_graph_size, + indexer_size + ); } } From c7fbfe906d1265cba9703e37edac4299f8bd5d02 Mon Sep 17 00:00:00 2001 From: Chengxing Yuan Date: Mon, 21 Apr 2025 16:53:24 +0800 Subject: [PATCH 14/14] Add comment for OnChainWallet.tr_descriptors Co-authored-by: Flouse <1297478+Flouse@users.noreply.github.com> --- mutiny-core/src/onchain.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 4e0be2d75..d96396092 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -59,6 +59,9 @@ pub struct OnChainWallet { pub(crate) stop: Arc, logger: Arc, ln_event_callback: Option, + /// The Bitcoin output descriptors for the wallet’s keychains: + /// 0: receive_descriptor + /// 1: change_descriptor tr_descriptors: (DescriptorTemplateOut, DescriptorTemplateOut), }