Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -232,16 +232,16 @@ harness = false
#vss-client-ng = { path = "../vss-client" }
#vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" }
#
#[patch."https://github.com/lightningdevkit/rust-lightning"]
#lightning = { path = "../rust-lightning/lightning" }
#lightning-types = { path = "../rust-lightning/lightning-types" }
#lightning-invoice = { path = "../rust-lightning/lightning-invoice" }
#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" }
#lightning-persister = { path = "../rust-lightning/lightning-persister" }
#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" }
#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" }
#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" }
#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" }
#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" }
#lightning-macros = { path = "../rust-lightning/lightning-macros" }
#lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolver" }
[patch."https://github.com/lightningdevkit/rust-lightning"]
lightning = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-types = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-invoice = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-net-tokio = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-persister = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-background-processor = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-rapid-gossip-sync = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-block-sync = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-transaction-sync = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-liquidity = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-macros = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
lightning-dns-resolver = { git = "https://git.rust-bitcoin.org/tnull/rust-lightning", branch = "2026-09-recovery-node-compat-review" }
28 changes: 28 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ interface ProbingConfigBuilder {
interface Builder {
};

typedef dictionary RecoveryPeer;

typedef dictionary RecoveryStatus;

interface RecoveryBuilder {
constructor();
[Name=from_config]
constructor(Config config);
void add_peer(RecoveryPeer peer);
void set_storage_dir_path(string storage_dir_path);
void set_filesystem_logger(string? log_file_path, LogLevel? max_log_level);
void set_log_facade_logger();
void set_custom_logger(LogWriter log_writer);
void set_network(Network network);
[Throws=BuildError]
void set_tor_config(TorConfig tor_config);
};

interface RecoveryNode {
[Throws=NodeError]
void start();
[Throws=NodeError]
void stop();
RecoveryStatus status();
PublicKey node_id();
BalanceDetails list_balances();
};

interface Node {
[Throws=NodeError]
void start();
Expand Down
1 change: 1 addition & 0 deletions src/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,5 +404,6 @@ fn value_from_descriptor(descriptor: &SpendableOutputDescriptor) -> Amount {
SpendableOutputDescriptor::StaticOutput { output, .. } => output.value,
SpendableOutputDescriptor::DelayedPaymentOutput(output) => output.output.value,
SpendableOutputDescriptor::StaticPaymentOutput(output) => output.output.value,
SpendableOutputDescriptor::RecoveredStaticPaymentOutput(output) => output.output.value,
}
}
50 changes: 48 additions & 2 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ impl std::error::Error for BuildError {}
#[derive(Debug)]
pub struct NodeBuilder {
config: Config,
recovery_store_scope: bool,
chain_data_source_config: Option<ChainDataSourceConfig>,
gossip_source_config: Option<GossipSourceConfig>,
liquidity_source_config: Option<LiquiditySourceConfig>,
Expand Down Expand Up @@ -357,6 +358,7 @@ impl NodeBuilder {
let probing_config = None;
Self {
config,
recovery_store_scope: false,
chain_data_source_config,
gossip_source_config,
liquidity_source_config,
Expand All @@ -368,6 +370,12 @@ impl NodeBuilder {
}
}

pub(crate) fn for_recovery(config: Config) -> Self {
let mut builder = Self::from_config(config);
builder.recovery_store_scope = true;
builder
}

/// Configures the [`Node`] instance to (re-)use a specific `tokio` runtime.
///
/// If not provided, the node will spawn its own runtime or reuse any outer runtime context it
Expand Down Expand Up @@ -956,8 +964,15 @@ impl NodeBuilder {
let seed_bytes = node_entropy.to_seed_bytes();
let config = Arc::new(self.config.clone());

let kv_store: Arc<DynStore> = if self.recovery_store_scope {
Arc::new(DynStoreWrapper(crate::recovery::RecoveryStore::new(kv_store)))
} else {
Arc::new(DynStoreWrapper(kv_store))
};

build_with_store_internal(
config,
self.recovery_store_scope,
self.chain_data_source_config.as_ref(),
self.gossip_source_config.as_ref(),
self.liquidity_source_config.as_ref(),
Expand All @@ -967,7 +982,7 @@ impl NodeBuilder {
seed_bytes,
runtime,
logger,
Arc::new(DynStoreWrapper(kv_store)),
kv_store,
)
}
}
Expand Down Expand Up @@ -1507,7 +1522,8 @@ impl ArcedNodeBuilder {

/// Builds a [`Node`] instance according to the options previously configured.
fn build_with_store_internal(
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
config: Arc<Config>, recovery_build: bool,
chain_data_source_config: Option<&ChainDataSourceConfig>,
gossip_source_config: Option<&GossipSourceConfig>,
liquidity_source_config: Option<&LiquiditySourceConfig>,
pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>,
Expand Down Expand Up @@ -1553,6 +1569,7 @@ fn build_with_store_internal(
node_metris_res,
pending_payment_store_res,
address_pool_res,
recovery_state_res,
) = runtime.block_on(async move {
tokio::join!(
read_n_objects(
Expand All @@ -1576,9 +1593,34 @@ fn build_with_store_internal(
Arc::clone(&logger_ref),
),
read_address_pool(&*kv_store_ref, &*logger_ref),
async {
if recovery_build {
KVStore::read(
&*kv_store_ref,
crate::recovery::RECOVERY_STATE_PRIMARY_NAMESPACE,
crate::recovery::RECOVERY_STATE_SECONDARY_NAMESPACE,
crate::recovery::RECOVERY_STATE_KEY,
)
.await
} else {
Err(bitcoin::io::Error::new(
bitcoin::io::ErrorKind::NotFound,
"not a recovery build",
))
}
}
)
});

let pending_recovery_state = match recovery_state_res {
Ok(bytes) => Some(bytes),
Err(e) if e.kind() == bitcoin::io::ErrorKind::NotFound => None,
Err(e) => {
log_error!(logger, "Failed to read recovery state from store: {}", e);
return Err(BuildError::ReadFailed);
},
};

// Initialize the status fields.
let node_metrics = match node_metris_res {
Ok(metrics) => Arc::new(PersistedNodeMetrics::new(metrics)),
Expand Down Expand Up @@ -2095,6 +2137,9 @@ fn build_with_store_internal(
));

let mut user_config = default_user_config(&config);
if recovery_build {
user_config.accept_inbound_channels = false;
}

if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() {
// If we act as an LSPS2 service, we need to be able to intercept HTLCs and forward the
Expand Down Expand Up @@ -2558,6 +2603,7 @@ fn build_with_store_internal(
#[cfg(feature = "unified-payments")]
hrn_resolver,
prober,
pending_recovery_state,
#[cfg(cycle_tests)]
_leak_checker,
})
Expand Down
12 changes: 12 additions & 0 deletions src/chain/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ pub(super) struct ElectrumChainSource {
}

impl ElectrumChainSource {
pub(super) fn recovery_client(&self) -> Option<(Arc<ElectrumClient>, Arc<Runtime>)> {
self.electrum_runtime_status
.read()
.expect("lock")
.client()
.map(|client| (Arc::clone(&client.electrum_client), Arc::clone(&client.runtime)))
}

pub(super) fn force_wallet_full_scan(&self) {
self.force_wallet_full_scan.store(true, Ordering::Release);
}

pub(super) fn new(
server_url: String, sync_config: ElectrumSyncConfig,
fee_estimator: Arc<OnchainFeeEstimator>, kv_store: Arc<DynStore>, config: Arc<Config>,
Expand Down
8 changes: 8 additions & 0 deletions src/chain/esplora.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ pub(super) struct EsploraChainSource {
}

impl EsploraChainSource {
pub(super) fn recovery_client(&self) -> EsploraAsyncClient {
self.esplora_client.clone()
}

pub(super) fn force_wallet_full_scan(&self) {
self.force_wallet_full_scan.store(true, Ordering::Release);
}

pub(crate) fn new(
server_url: String, headers: HashMap<String, String>, sync_config: EsploraSyncConfig,
fee_estimator: Arc<OnchainFeeEstimator>, kv_store: Arc<DynStore>, config: Arc<Config>,
Expand Down
38 changes: 38 additions & 0 deletions src/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;

use bitcoin::{Script, Txid};
#[cfg(feature = "chain-electrum")]
use electrum_client::Client as ElectrumClient;
#[cfg(feature = "chain-esplora")]
use esplora_client::AsyncClient as EsploraAsyncClient;
use lightning::chain::{BlockLocator, Filter};

#[cfg(feature = "chain-bitcoind")]
Expand Down Expand Up @@ -159,6 +163,15 @@ enum ChainSourceKind {
Bitcoind(BitcoindChainSource),
}

pub(crate) enum RecoveryChainSource {
#[cfg(feature = "chain-esplora")]
Esplora(EsploraAsyncClient),
#[cfg(feature = "chain-electrum")]
Electrum { client: Arc<ElectrumClient>, runtime: Arc<Runtime> },
#[cfg(feature = "chain-bitcoind")]
Bitcoind(UtxoSourceClient),
}

impl ChainSource {
#[cfg(feature = "chain-esplora")]
pub(crate) fn new_esplora(
Expand Down Expand Up @@ -294,6 +307,31 @@ impl ChainSource {
}
}

pub(crate) fn recovery_source(&self) -> Result<RecoveryChainSource, Error> {
match &self.kind {
#[cfg(feature = "chain-esplora")]
ChainSourceKind::Esplora(source) => Ok(RecoveryChainSource::Esplora(source.recovery_client())),
#[cfg(feature = "chain-electrum")]
ChainSourceKind::Electrum(source) => source
.recovery_client()
.map(|(client, runtime)| RecoveryChainSource::Electrum { client, runtime })
.ok_or(Error::ConnectionFailed),
#[cfg(feature = "chain-bitcoind")]
ChainSourceKind::Bitcoind(source) => Ok(RecoveryChainSource::Bitcoind(source.as_utxo_source())),
}
}

pub(crate) fn force_recovery_wallet_full_scan(&self) {
match &self.kind {
#[cfg(feature = "chain-esplora")]
ChainSourceKind::Esplora(source) => source.force_wallet_full_scan(),
#[cfg(feature = "chain-electrum")]
ChainSourceKind::Electrum(source) => source.force_wallet_full_scan(),
#[cfg(feature = "chain-bitcoind")]
ChainSourceKind::Bitcoind(_) => {},
}
}

pub(crate) fn registered_txids(&self) -> HashSet<Txid> {
self.registered_txids.lock().expect("lock").clone()
}
Expand Down
2 changes: 1 addition & 1 deletion src/ffi/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl VssClientHeaderProvider for VssHeaderProviderAdapter {
}

use crate::builder::sanitize_alias;
pub use crate::config::default_config;
pub use crate::config::{default_config, TorConfig};
use crate::error::Error;
pub use crate::liquidity::LSPS1OrderStatus;
pub use crate::logger::{LogLevel, LogRecord, LogWriter};
Expand Down
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ mod message_handler;
pub mod payment;
mod peer_store;
pub mod probing;
pub mod recovery;
mod runtime;
mod scoring;
mod tx_broadcaster;
Expand Down Expand Up @@ -187,6 +188,11 @@ use peer_store::{PeerInfo, PeerStore};
#[cfg(feature = "uniffi")]
pub use probing::ArcedProbingConfigBuilder as ProbingConfigBuilder;
use probing::{run_prober, Prober};
#[cfg(feature = "uniffi")]
pub use recovery::ArcedRecoveryNodeBuilder as RecoveryBuilder;
#[cfg(not(feature = "uniffi"))]
pub use recovery::RecoveryNodeBuilder as RecoveryBuilder;
pub use recovery::{RecoveryNode, RecoveryPeer, RecoveryStatus};
use runtime::Runtime;
pub use tokio;
use types::{
Expand Down Expand Up @@ -283,6 +289,7 @@ pub struct Node {
#[cfg(feature = "unified-payments")]
hrn_resolver: HRNResolver,
prober: Option<Arc<Prober>>,
pending_recovery_state: Option<Vec<u8>>,
#[cfg(cycle_tests)]
_leak_checker: LeakChecker,
}
Expand Down
Loading
Loading