diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c1a926f2fd..4d4abbd241 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -247,6 +247,7 @@ enum NodeError { "LnurlAuthTimeout", "InvalidLnurl", "ChainSourceNotSupported", + "ChannelMonitorNotFound", }; typedef dictionary NodeStatus; diff --git a/src/error.rs b/src/error.rs index 8546af0dd2..1ed402b727 100644 --- a/src/error.rs +++ b/src/error.rs @@ -139,6 +139,8 @@ pub enum Error { InvalidLnurl, /// The configured chain source is not supported. ChainSourceNotSupported, + /// No channel monitor could be found for the given channel ID. + ChannelMonitorNotFound, } impl fmt::Display for Error { @@ -227,6 +229,9 @@ impl fmt::Display for Error { Self::ChainSourceNotSupported => { write!(f, "The configured chain source is not supported.") }, + Self::ChannelMonitorNotFound => { + write!(f, "No channel monitor could be found for the given channel ID.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 14ee734a3d..92934dc98d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,7 @@ pub use bitcoin; use bitcoin::secp256k1::PublicKey; #[cfg(feature = "uniffi")] pub use bitcoin::FeeRate; -use bitcoin::{Address, Amount, BlockHash, Network}; +use bitcoin::{Address, Amount, BlockHash, Network, Transaction}; #[cfg(feature = "uniffi")] pub use builder::ArcedNodeBuilder as Builder; pub use builder::BuildError; @@ -147,17 +147,20 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::update_and_persist_node_metrics; pub use lightning; +use lightning::chain::channelmonitor::ChannelMonitorUpdate; use lightning::chain::BlockLocator; use lightning::impl_writeable_tlv_based; -use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; +use lightning::ln::chan_utils::{CommitmentTransaction, FUNDING_TRANSACTION_WITNESS_WEIGHT}; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; pub use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::{BaseMessageHandler, SocketAddress}; use lightning::ln::peer_handler::CustomMessageHandler; +use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeAlias; use lightning::sign::EntropySource; use lightning::util::persist::KVStore; +use lightning::util::ser::Readable; use lightning::util::wallet_utils::{Input, Wallet as LdkWallet}; use lightning_background_processor::process_events_async; pub use lightning_invoice; @@ -2318,6 +2321,183 @@ impl Node { }) } + /// Returns the counterparty's commitment transactions provided by the given + /// [`ChannelMonitorUpdate`] for the channel with the given `channel_id`. + /// + /// This may be empty if the update doesn't include any new counterparty commitments. + /// Returned commitment transactions are unsigned. + /// + /// This is provided so that watchtower clients (e.g., an [Eye of Satoshi](https://github.com/talaia-labs/rust-teos) + /// tower) are able to build justice transactions for each counterparty commitment. It is + /// expected that a watchtower client may use this method to retrieve the latest counterparty + /// commitment transaction(s), and then hold the necessary data until the commitment has been + /// revoked, at which point a justice transaction spending the `to_local` output can be signed + /// via [`Node::sign_to_local_justice_tx`]. + /// + /// This will only return a non-empty list for monitor updates that have been created after + /// upgrading to LDK 0.0.117+. + /// + /// Returns [`Error::ChannelMonitorNotFound`] if no channel monitor exists for the given + /// `channel_id`. + pub fn counterparty_commitment_txs_from_update( + &self, channel_id: ChannelId, update: ChannelMonitorUpdate, + ) -> Result, Error> { + let monitor = self.chain_monitor.get_monitor(channel_id).map_err(|()| { + log_error!(self.logger, "No channel monitor found for channel ID {}", channel_id); + Error::ChannelMonitorNotFound + })?; + Ok(monitor.counterparty_commitment_txs_from_update(&update)) + } + + /// Returns the counterparty's initial commitment transaction for the channel with the given + /// `channel_id`. The returned commitment transaction is unsigned. + /// + /// This is similar to [`Node::counterparty_commitment_txs_from_update`], except that for the + /// initial commitment transaction, we don't have a corresponding [`ChannelMonitorUpdate`]. + /// + /// This will only return `Some` for channel monitors that have been created after upgrading + /// to LDK 0.0.117+. + /// + /// Returns [`Error::ChannelMonitorNotFound`] if no channel monitor exists for the given + /// `channel_id`. + pub fn initial_counterparty_commitment_tx( + &self, channel_id: ChannelId, + ) -> Result, Error> { + let monitor = self.chain_monitor.get_monitor(channel_id).map_err(|()| { + log_error!(self.logger, "No channel monitor found for channel ID {}", channel_id); + Error::ChannelMonitorNotFound + })?; + Ok(monitor.initial_counterparty_commitment_tx()) + } + + /// Signs the input at `input_idx` of the given `justice_tx`, claiming the `to_local` output + /// of a revoked counterparty commitment transaction of the channel with the given + /// `channel_id`. + /// + /// This is a wrapper around + /// [`ChannelMonitor::sign_to_local_justice_tx`] intended to + /// allow watchtower clients to finalize justice transactions they built from counterparty + /// commitment transactions retrieved via [`Node::counterparty_commitment_txs_from_update`] + /// or [`Node::initial_counterparty_commitment_tx`]. + /// + /// Note that this method will only produce a valid signature for a transaction spending the + /// `to_local` output of a commitment transaction, i.e., this cannot be used for revoked HTLC + /// outputs. + /// + /// `value_sat` is the value, in satoshis, of the output being spent by the input at + /// `input_idx`, committed in the BIP 143 signature. + /// + /// This method will only succeed if the channel monitor has received the revocation secret + /// for the given `commitment_number`. + /// + /// Returns [`Error::ChannelMonitorNotFound`] if no channel monitor exists for the given + /// `channel_id`, and [`Error::OnchainTxSigningFailed`] if signing the justice transaction + /// failed. + /// + /// [`ChannelMonitor::sign_to_local_justice_tx`]: lightning::chain::channelmonitor::ChannelMonitor::sign_to_local_justice_tx + pub fn sign_to_local_justice_tx( + &self, channel_id: ChannelId, justice_tx: Transaction, input_idx: usize, value_sat: u64, + commitment_number: u64, + ) -> Result { + let monitor = self.chain_monitor.get_monitor(channel_id).map_err(|()| { + log_error!(self.logger, "No channel monitor found for channel ID {}", channel_id); + Error::ChannelMonitorNotFound + })?; + monitor + .sign_to_local_justice_tx(justice_tx, input_idx, value_sat, commitment_number) + .map_err(|()| { + log_error!( + self.logger, + "Failed to sign justice transaction for channel ID {}", + channel_id + ); + Error::OnchainTxSigningFailed + }) + } + + /// Returns all [`ChannelMonitorUpdate`]s persisted for the channel with the given + /// `channel_id`, ordered by their update IDs. + /// + /// The returned updates can be passed to [`Node::counterparty_commitment_txs_from_update`] + /// to retrieve the corresponding counterparty commitment transactions, e.g., by watchtower + /// clients that need to (re-)build justice transaction data for past channel states. + /// + /// Note that the returned list may be empty if no updates have been persisted for the given + /// channel, or if the given `channel_id` is unknown. + pub fn channel_monitor_updates( + &self, channel_id: ChannelId, + ) -> Result, Error> { + use lightning::util::persist::CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE; + + // Channel monitor updates are persisted under a secondary namespace derived from the + // funding outpoint for v1 channels, and from the channel ID for v2 channels. As the + // channel might have been closed since (and hence might not be returned by + // `list_channels` anymore), we fall back to trying the channel ID namespace. + let mut namespaces = Vec::with_capacity(2); + if let Some(channel) = + self.channel_manager.list_channels().into_iter().find(|c| c.channel_id == channel_id) + { + if let Some(funding_txo) = channel.funding_txo { + // Note that this matches `MonitorName`'s encoding, not `OutPoint`'s `Display`. + namespaces.push(format!("{}_{}", funding_txo.txid, funding_txo.index)); + } + } + namespaces.push(channel_id.to_string()); + + let mut updates = Vec::new(); + for namespace in namespaces { + let mut keys = self + .runtime + .block_on(KVStore::list( + &*self.kv_store, + CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE, + &namespace, + )) + .map_err(|e| { + log_error!( + self.logger, + "Failed to access store while reading channel monitor updates: {}", + e + ); + Error::PersistenceFailed + })?; + + // Update keys are the string-encoded update IDs. + keys.sort_by_key(|k| k.parse::().unwrap_or(u64::MAX)); + + for key in keys { + let buf = self + .runtime + .block_on(KVStore::read( + &*self.kv_store, + CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE, + &namespace, + &key, + )) + .map_err(|e| { + log_error!( + self.logger, + "Failed to access store while reading channel monitor update: {}", + e + ); + Error::PersistenceFailed + })?; + + let update = ChannelMonitorUpdate::read(&mut &buf[..]).map_err(|e| { + log_error!(self.logger, "Failed to deserialize channel monitor update: {}", e); + Error::PersistenceFailed + })?; + updates.push(update); + } + + if !updates.is_empty() { + break; + } + } + + Ok(updates) + } + /// Return the features used in node announcement. fn node_features(&self) -> LdkNodeFeatures { let gossip_features = match self.gossip_source.as_gossip_sync() { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index df477588f1..9f9be97249 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -41,6 +41,7 @@ use ldk_node::payment::{ }; use ldk_node::{BuildError, Builder, Event, Node, NodeError, ReserveType}; use lightning::ln::channelmanager::PaymentId; +use lightning::ln::types::ChannelId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; @@ -410,6 +411,105 @@ async fn channel_open_fails_when_funds_insufficient() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn watchtower_commitment_and_justice_apis() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 500_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + + open_channel(&node_a, &node_b, 200_000, false, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let channel_id = node_a.list_channels().first().unwrap().channel_id; + + // The initial counterparty commitment transaction is available for monitors created with + // LDK 0.0.117+. + assert!(node_a.initial_counterparty_commitment_tx(channel_id).unwrap().is_some()); + assert!(node_b.initial_counterparty_commitment_tx(channel_id).unwrap().is_some()); + + // An unknown channel ID yields `ChannelMonitorNotFound`. + let unknown_channel_id = ChannelId::from_bytes([42u8; 32]); + assert_eq!( + Err(NodeError::ChannelMonitorNotFound), + node_a.initial_counterparty_commitment_tx(unknown_channel_id) + ); + assert!(node_a.channel_monitor_updates(unknown_channel_id).unwrap().is_empty()); + + // Send a payment to generate channel monitor updates. + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("watchtower")).unwrap()); + let invoice = + node_b.bolt11_payment().receive(10_000_000, &invoice_description.into(), 3600).unwrap(); + let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); + expect_payment_received_event!(&node_b, 10_000_000); + expect_payment_successful_event!(node_a, Some(payment_id), None); + + // The persisted monitor updates are readable and ordered by update ID. Monitor updates are + // persisted asynchronously, so we retry a few times to avoid racing the persister. + let updates = { + let mut updates = Vec::new(); + for _ in 0..10 { + updates = node_a.channel_monitor_updates(channel_id).unwrap(); + if !updates.is_empty() { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + updates + }; + assert!(!updates.is_empty()); + let update_ids: Vec = updates.iter().map(|u| u.update_id).collect(); + let mut sorted_update_ids = update_ids.clone(); + sorted_update_ids.sort_unstable(); + assert_eq!(update_ids, sorted_update_ids); + + // We can build counterparty commitment transactions from the persisted updates. + let commitment_tx_count: usize = updates + .iter() + .map(|u| { + node_a.counterparty_commitment_txs_from_update(channel_id, u.clone()).unwrap().len() + }) + .sum(); + assert!(commitment_tx_count > 0); + + assert_eq!( + Err(NodeError::ChannelMonitorNotFound), + node_a.counterparty_commitment_txs_from_update( + unknown_channel_id, + updates.first().unwrap().clone() + ) + ); + + // Signing a justice transaction fails if the monitor has not received the revocation + // secret for the given commitment number. + let justice_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn::default()], + output: vec![], + }; + assert_eq!( + Err(NodeError::OnchainTxSigningFailed), + node_a.sign_to_local_justice_tx(channel_id, justice_tx, 0, 1000, u64::MAX) + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn multi_hop_sending() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();