From 543ad4fd23c1f52e2afa68bbd09d3213d9e2761d Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Wed, 14 Jun 2023 15:14:14 -0500 Subject: [PATCH 1/6] Add feerate and balances to `LatestCounterpartyCommitmentTXInfo` This adds the feerate and local and remote output values to this channel monitor update step so that a monitor can reconstruct the counterparty's commitment transaction from an update. These commitment transactions will be exposed to users in the following commits to support third-party watchtowers in the persistence pipeline. With only the HTLC outputs currently available in the monitor update, we can tell how much of the channel balance is in-flight and towards which side, however it doesn't tell us the amount that resides on either side. Because of dust, we can't reliably derive the remote value from the local value and visa versa. Thus, it seems these are the minimum fields that need to be added. --- lightning/src/chain/channelmonitor.rs | 8 +++++++- lightning/src/ln/channel.rs | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 4d3b4d53a..56228906e 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -502,6 +502,9 @@ pub(crate) enum ChannelMonitorUpdateStep { htlc_outputs: Vec<(HTLCOutputInCommitment, Option>)>, commitment_number: u64, their_per_commitment_point: PublicKey, + feerate_per_kw: Option, + to_broadcaster_value_sat: Option, + to_countersignatory_value_sat: Option, }, PaymentPreimage { payment_preimage: PaymentPreimage, @@ -544,8 +547,11 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep, }, (1, LatestCounterpartyCommitmentTXInfo) => { (0, commitment_txid, required), + (1, feerate_per_kw, option), (2, commitment_number, required), + (3, to_broadcaster_value_sat, option), (4, their_per_commitment_point, required), + (5, to_countersignatory_value_sat, option), (6, htlc_outputs, required_vec), }, (2, PaymentPreimage) => { @@ -2471,7 +2477,7 @@ impl ChannelMonitorImpl { ret = Err(()); } } - ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { commitment_txid, htlc_outputs, commitment_number, their_per_commitment_point } => { + ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { commitment_txid, htlc_outputs, commitment_number, their_per_commitment_point, .. } => { log_trace!(logger, "Updating ChannelMonitor with latest counterparty commitment transaction info"); self.provide_latest_counterparty_commitment_tx(*commitment_txid, htlc_outputs.clone(), *commitment_number, *their_per_commitment_point, logger) }, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index f66f48c50..0dcb56ff6 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5289,7 +5289,9 @@ impl Channel where } self.context.resend_order = RAACommitmentOrder::RevokeAndACKFirst; - let (counterparty_commitment_txid, mut htlcs_ref) = self.build_commitment_no_state_update(logger); + let (mut htlcs_ref, counterparty_commitment_tx) = + self.build_commitment_no_state_update(logger); + let counterparty_commitment_txid = counterparty_commitment_tx.trust().txid(); let htlcs: Vec<(HTLCOutputInCommitment, Option>)> = htlcs_ref.drain(..).map(|(htlc, htlc_source)| (htlc, htlc_source.map(|source_ref| Box::new(source_ref.clone())))).collect(); @@ -5304,17 +5306,23 @@ impl Channel where commitment_txid: counterparty_commitment_txid, htlc_outputs: htlcs.clone(), commitment_number: self.context.cur_counterparty_commitment_transaction_number, - their_per_commitment_point: self.context.counterparty_cur_commitment_point.unwrap() + their_per_commitment_point: self.context.counterparty_cur_commitment_point.unwrap(), + feerate_per_kw: Some(counterparty_commitment_tx.feerate_per_kw()), + to_broadcaster_value_sat: Some(counterparty_commitment_tx.to_broadcaster_value_sat()), + to_countersignatory_value_sat: Some(counterparty_commitment_tx.to_countersignatory_value_sat()), }] }; self.context.channel_state |= ChannelState::AwaitingRemoteRevoke as u32; monitor_update } - fn build_commitment_no_state_update(&self, logger: &L) -> (Txid, Vec<(HTLCOutputInCommitment, Option<&HTLCSource>)>) where L::Target: Logger { + fn build_commitment_no_state_update(&self, logger: &L) + -> (Vec<(HTLCOutputInCommitment, Option<&HTLCSource>)>, CommitmentTransaction) + where L::Target: Logger + { let counterparty_keys = self.context.build_remote_transaction_keys(); let commitment_stats = self.context.build_commitment_transaction(self.context.cur_counterparty_commitment_transaction_number, &counterparty_keys, false, true, logger); - let counterparty_commitment_txid = commitment_stats.tx.trust().txid(); + let counterparty_commitment_tx = commitment_stats.tx; #[cfg(any(test, fuzzing))] { @@ -5334,7 +5342,7 @@ impl Channel where } } - (counterparty_commitment_txid, commitment_stats.htlcs_included) + (commitment_stats.htlcs_included, counterparty_commitment_tx) } /// Only fails in case of signer rejection. Used for channel_reestablish commitment_signed From 966465a282f91a91ec8b058a0dea5f7b0c6bf21c Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Wed, 9 Aug 2023 15:23:24 -0500 Subject: [PATCH 2/6] Build and expose counterparty commitments from monitor update --- lightning/src/chain/channelmonitor.rs | 78 ++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 56228906e..cb267524f 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -36,7 +36,7 @@ use bitcoin::secp256k1; use crate::ln::{PaymentHash, PaymentPreimage}; use crate::ln::msgs::DecodeError; use crate::ln::chan_utils; -use crate::ln::chan_utils::{CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HTLCClaim, ChannelTransactionParameters, HolderCommitmentTransaction}; +use crate::ln::chan_utils::{CommitmentTransaction, CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HTLCClaim, ChannelTransactionParameters, HolderCommitmentTransaction, TxCreationKeys}; use crate::ln::channelmanager::{HTLCSource, SentHTLCId}; use crate::chain; use crate::chain::{BestBlock, WatchedOutput}; @@ -1376,6 +1376,29 @@ impl ChannelMonitor { ret } + /// Gets all of the counterparty commitment transactions provided by the given update. 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 in the persistence pipeline are able to build + /// justice transactions for each counterparty commitment upon each update. It's intended to be + /// used within an implementation of [`Persist::update_persisted_channel`], which is provided + /// with a monitor and an update. + /// + /// 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 a later update in which + /// the monitor has been updated with the corresponding revocation data, at which point the + /// monitor can sign the justice transaction. + /// + /// This will only return a non-empty list for monitor updates that have been created after + /// upgrading to LDK 0.0.117+. Note that no restriction lies on the monitors themselves, which + /// may have been created prior to upgrading. + /// + /// [`Persist::update_persisted_channel`]: crate::chain::chainmonitor::Persist::update_persisted_channel + pub fn counterparty_commitment_txs_from_update(&self, update: &ChannelMonitorUpdate) -> Vec { + self.inner.lock().unwrap().counterparty_commitment_txs_from_update(update) + } + pub(crate) fn get_min_seen_secret(&self) -> u64 { self.inner.lock().unwrap().get_min_seen_secret() } @@ -2549,6 +2572,10 @@ impl ChannelMonitorImpl { } } + #[cfg(debug_assertions)] { + self.counterparty_commitment_txs_from_update(updates); + } + // If the updates succeeded and we were in an already closed channel state, then there's no // need to refuse any updates we expect to receive afer seeing a confirmed commitment. if ret.is_ok() && updates.update_id == CLOSED_CHANNEL_UPDATE_ID && self.latest_update_id == updates.update_id { @@ -2657,6 +2684,55 @@ impl ChannelMonitorImpl { ret } + fn build_counterparty_commitment_tx( + &self, commitment_number: u64, their_per_commitment_point: &PublicKey, + to_broadcaster_value: u64, to_countersignatory_value: u64, feerate_per_kw: u32, + mut nondust_htlcs: Vec<(HTLCOutputInCommitment, Option>)> + ) -> CommitmentTransaction { + let broadcaster_keys = &self.onchain_tx_handler.channel_transaction_parameters + .counterparty_parameters.as_ref().unwrap().pubkeys; + let countersignatory_keys = + &self.onchain_tx_handler.channel_transaction_parameters.holder_pubkeys; + + let broadcaster_funding_key = broadcaster_keys.funding_pubkey; + let countersignatory_funding_key = countersignatory_keys.funding_pubkey; + let keys = TxCreationKeys::from_channel_static_keys(&their_per_commitment_point, + &broadcaster_keys, &countersignatory_keys, &self.onchain_tx_handler.secp_ctx); + let channel_parameters = + &self.onchain_tx_handler.channel_transaction_parameters.as_counterparty_broadcastable(); + + CommitmentTransaction::new_with_auxiliary_htlc_data(commitment_number, + to_broadcaster_value, to_countersignatory_value, broadcaster_funding_key, + countersignatory_funding_key, keys, feerate_per_kw, &mut nondust_htlcs, + channel_parameters) + } + + pub(crate) fn counterparty_commitment_txs_from_update(&self, update: &ChannelMonitorUpdate) -> Vec { + update.updates.iter().filter_map(|update| { + match update { + &ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { commitment_txid, + ref htlc_outputs, commitment_number, their_per_commitment_point, + feerate_per_kw: Some(feerate_per_kw), + to_broadcaster_value_sat: Some(to_broadcaster_value), + to_countersignatory_value_sat: Some(to_countersignatory_value) } => { + + let nondust_htlcs = htlc_outputs.iter().filter_map(|(htlc, _)| { + htlc.transaction_output_index.map(|_| (htlc.clone(), None)) + }).collect::>(); + + let commitment_tx = self.build_counterparty_commitment_tx(commitment_number, + &their_per_commitment_point, to_broadcaster_value, + to_countersignatory_value, feerate_per_kw, nondust_htlcs); + + debug_assert_eq!(commitment_tx.trust().txid(), commitment_txid); + + Some(commitment_tx) + }, + _ => None, + } + }).collect() + } + /// Can only fail if idx is < get_min_seen_secret fn get_secret(&self, idx: u64) -> Option<[u8; 32]> { self.commitment_secrets.get_secret(idx) From 75c058670cdfd2db97102d70d869757a77f1ac04 Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Wed, 12 Jul 2023 13:14:10 -0500 Subject: [PATCH 3/6] Enable monitor to rebuild initial counterparty commitment tx Upon creating a channel monitor, it is provided with the initial counterparty commitment transaction info directly before the very first time it is persisted. Because of this, the very first counterparty commitment is not seen as an update in the persistence pipeline, and so our previous changes to the monitor and updates cannot be used to reconstruct this commitment. To be able to expose the counterparty's transaction for the very first commitment, we add a thin wrapper around `provide_latest_counterparty_commitment_tx`, that stores the necessary data needed to reconstruct the initial commitment transaction in the monitor. --- lightning/src/chain/channelmonitor.rs | 82 ++++++++++++++++++++++++++- lightning/src/ln/channel.rs | 21 +++++-- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index cb267524f..0c207fa8b 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -33,6 +33,7 @@ use bitcoin::secp256k1::{Secp256k1, ecdsa::Signature}; use bitcoin::secp256k1::{SecretKey, PublicKey}; use bitcoin::secp256k1; +use crate::ln::channel::INITIAL_COMMITMENT_NUMBER; use crate::ln::{PaymentHash, PaymentPreimage}; use crate::ln::msgs::DecodeError; use crate::ln::chan_utils; @@ -888,6 +889,14 @@ pub(crate) struct ChannelMonitorImpl { /// The node_id of our counterparty counterparty_node_id: Option, + + /// Initial counterparty commmitment data needed to recreate the commitment tx + /// in the persistence pipeline for third-party watchtowers. This will only be present on + /// monitors created after 0.0.117. + /// + /// Ordering of tuple data: (their_per_commitment_point, feerate_per_kw, to_broadcaster_sats, + /// to_countersignatory_sats) + initial_counterparty_commitment_info: Option<(PublicKey, u32, u64, u64)>, } /// Transaction outputs to watch for on-chain spends. @@ -1078,6 +1087,7 @@ impl Writeable for ChannelMonitorImpl ChannelMonitor { best_block, counterparty_node_id: Some(counterparty_node_id), + initial_counterparty_commitment_info: None, }) } @@ -1236,11 +1247,31 @@ impl ChannelMonitor { self.inner.lock().unwrap().provide_secret(idx, secret) } + /// A variant of `Self::provide_latest_counterparty_commitment_tx` used to provide + /// additional information to the monitor to store in order to recreate the initial + /// counterparty commitment transaction during persistence (mainly for use in third-party + /// watchtowers). + /// + /// This is used to provide the counterparty commitment information directly to the monitor + /// before the initial persistence of a new channel. + pub(crate) fn provide_initial_counterparty_commitment_tx( + &self, txid: Txid, htlc_outputs: Vec<(HTLCOutputInCommitment, Option>)>, + commitment_number: u64, their_cur_per_commitment_point: PublicKey, feerate_per_kw: u32, + to_broadcaster_value_sat: u64, to_countersignatory_value_sat: u64, logger: &L, + ) + where L::Target: Logger + { + self.inner.lock().unwrap().provide_initial_counterparty_commitment_tx(txid, + htlc_outputs, commitment_number, their_cur_per_commitment_point, feerate_per_kw, + to_broadcaster_value_sat, to_countersignatory_value_sat, logger); + } + /// Informs this monitor of the latest counterparty (ie non-broadcastable) commitment transaction. /// The monitor watches for it to be broadcasted and then uses the HTLC information (and /// possibly future revocation/preimage information) to claim outputs where possible. /// We cache also the mapping hash:commitment number to lighten pruning of old preimages by watchtowers. - pub(crate) fn provide_latest_counterparty_commitment_tx( + #[cfg(test)] + fn provide_latest_counterparty_commitment_tx( &self, txid: Txid, htlc_outputs: Vec<(HTLCOutputInCommitment, Option>)>, @@ -1376,6 +1407,22 @@ impl ChannelMonitor { ret } + /// Gets the counterparty's initial commitment transaction. The returned commitment + /// transaction is unsigned. This is intended to be called during the initial persistence of + /// the monitor (inside an implementation of [`Persist::persist_new_channel`]), to allow for + /// watchtowers in the persistence pipeline to have enough data to form justice transactions. + /// + /// This is similar to [`Self::counterparty_commitment_txs_from_update`], except + /// that for the initial commitment transaction, we don't have a corresponding update. + /// + /// This will only return `Some` for channel monitors that have been created after upgrading + /// to LDK 0.0.117+. + /// + /// [`Persist::persist_new_channel`]: crate::chain::chainmonitor::Persist::persist_new_channel + pub fn initial_counterparty_commitment_tx(&self) -> Option { + self.inner.lock().unwrap().initial_counterparty_commitment_tx() + } + /// Gets all of the counterparty commitment transactions provided by the given update. This /// may be empty if the update doesn't include any new counterparty commitments. Returned /// commitment transactions are unsigned. @@ -2255,6 +2302,25 @@ impl ChannelMonitorImpl { Ok(()) } + pub(crate) fn provide_initial_counterparty_commitment_tx( + &mut self, txid: Txid, htlc_outputs: Vec<(HTLCOutputInCommitment, Option>)>, + commitment_number: u64, their_per_commitment_point: PublicKey, feerate_per_kw: u32, + to_broadcaster_value: u64, to_countersignatory_value: u64, logger: &L + ) + where L::Target: Logger + { + self.initial_counterparty_commitment_info = Some((their_per_commitment_point.clone(), + feerate_per_kw, to_broadcaster_value, to_countersignatory_value)); + + #[cfg(debug_assertions)] { + let rebuilt_commitment_tx = self.initial_counterparty_commitment_tx().unwrap(); + debug_assert_eq!(rebuilt_commitment_tx.trust().txid(), txid); + } + + self.provide_latest_counterparty_commitment_tx(txid, htlc_outputs, commitment_number, + their_per_commitment_point, logger); + } + pub(crate) fn provide_latest_counterparty_commitment_tx(&mut self, txid: Txid, htlc_outputs: Vec<(HTLCOutputInCommitment, Option>)>, commitment_number: u64, their_per_commitment_point: PublicKey, logger: &L) where L::Target: Logger { // TODO: Encrypt the htlc_outputs data with the single-hash of the commitment transaction // so that a remote monitor doesn't learn anything unless there is a malicious close. @@ -2684,6 +2750,17 @@ impl ChannelMonitorImpl { ret } + pub(crate) fn initial_counterparty_commitment_tx(&mut self) -> Option { + let (their_per_commitment_point, feerate_per_kw, to_broadcaster_value, + to_countersignatory_value) = self.initial_counterparty_commitment_info?; + let htlc_outputs = vec![]; + + let commitment_tx = self.build_counterparty_commitment_tx(INITIAL_COMMITMENT_NUMBER, + &their_per_commitment_point, to_broadcaster_value, to_countersignatory_value, + feerate_per_kw, htlc_outputs); + Some(commitment_tx) + } + fn build_counterparty_commitment_tx( &self, commitment_number: u64, their_per_commitment_point: &PublicKey, to_broadcaster_value: u64, to_countersignatory_value: u64, feerate_per_kw: u32, @@ -4195,6 +4272,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP let mut confirmed_commitment_tx_counterparty_output = None; let mut spendable_txids_confirmed = Some(Vec::new()); let mut counterparty_fulfilled_htlcs = Some(HashMap::new()); + let mut initial_counterparty_commitment_info = None; read_tlv_fields!(reader, { (1, funding_spend_confirmed, option), (3, htlcs_resolved_on_chain, optional_vec), @@ -4204,6 +4282,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP (11, confirmed_commitment_tx_counterparty_output, option), (13, spendable_txids_confirmed, optional_vec), (15, counterparty_fulfilled_htlcs, option), + (17, initial_counterparty_commitment_info, option), }); Ok((best_block.block_hash(), ChannelMonitor::from_impl(ChannelMonitorImpl { @@ -4259,6 +4338,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP best_block, counterparty_node_id, + initial_counterparty_commitment_info, }))) } } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0dcb56ff6..a8e8c8053 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2534,7 +2534,13 @@ impl Channel where obscure_factor, holder_commitment_tx, best_block, self.context.counterparty_node_id); - channel_monitor.provide_latest_counterparty_commitment_tx(counterparty_initial_bitcoin_tx.txid, Vec::new(), self.context.cur_counterparty_commitment_transaction_number, self.context.counterparty_cur_commitment_point.unwrap(), logger); + channel_monitor.provide_initial_counterparty_commitment_tx( + counterparty_initial_bitcoin_tx.txid, Vec::new(), + self.context.cur_counterparty_commitment_transaction_number, + self.context.counterparty_cur_commitment_point.unwrap(), + counterparty_initial_commitment_tx.feerate_per_kw(), + counterparty_initial_commitment_tx.to_broadcaster_value_sat(), + counterparty_initial_commitment_tx.to_countersignatory_value_sat(), logger); assert_eq!(self.context.channel_state & (ChannelState::MonitorUpdateInProgress as u32), 0); // We have no had any monitor(s) yet to fail update! self.context.channel_state = ChannelState::FundingSent as u32; @@ -6464,7 +6470,7 @@ impl InboundV1Channel where SP::Target: SignerProvider { self.generate_accept_channel_message() } - fn funding_created_signature(&mut self, sig: &Signature, logger: &L) -> Result<(Txid, CommitmentTransaction, Signature), ChannelError> where L::Target: Logger { + fn funding_created_signature(&mut self, sig: &Signature, logger: &L) -> Result<(CommitmentTransaction, CommitmentTransaction, Signature), ChannelError> where L::Target: Logger { let funding_script = self.context.get_funding_redeemscript(); let keys = self.context.build_holder_transaction_keys(self.context.cur_holder_commitment_transaction_number); @@ -6496,7 +6502,7 @@ impl InboundV1Channel where SP::Target: SignerProvider { .map_err(|_| ChannelError::Close("Failed to get signatures for new commitment_signed".to_owned()))?.0; // We sign "counterparty" commitment transaction, allowing them to broadcast the tx if they wish. - Ok((counterparty_initial_bitcoin_tx.txid, initial_commitment_tx, counterparty_signature)) + Ok((counterparty_initial_commitment_tx, initial_commitment_tx, counterparty_signature)) } } } @@ -6528,7 +6534,7 @@ impl InboundV1Channel where SP::Target: SignerProvider { // funding_created_signature may fail. self.context.holder_signer.as_mut().provide_channel_parameters(&self.context.channel_transaction_parameters); - let (counterparty_initial_commitment_txid, initial_commitment_tx, signature) = match self.funding_created_signature(&msg.signature, logger) { + let (counterparty_initial_commitment_tx, initial_commitment_tx, signature) = match self.funding_created_signature(&msg.signature, logger) { Ok(res) => res, Err(ChannelError::Close(e)) => { self.context.channel_transaction_parameters.funding_outpoint = None; @@ -6569,7 +6575,12 @@ impl InboundV1Channel where SP::Target: SignerProvider { obscure_factor, holder_commitment_tx, best_block, self.context.counterparty_node_id); - channel_monitor.provide_latest_counterparty_commitment_tx(counterparty_initial_commitment_txid, Vec::new(), self.context.cur_counterparty_commitment_transaction_number, self.context.counterparty_cur_commitment_point.unwrap(), logger); + channel_monitor.provide_initial_counterparty_commitment_tx( + counterparty_initial_commitment_tx.trust().txid(), Vec::new(), + self.context.cur_counterparty_commitment_transaction_number, + self.context.counterparty_cur_commitment_point.unwrap(), self.context.feerate_per_kw, + counterparty_initial_commitment_tx.to_broadcaster_value_sat(), + counterparty_initial_commitment_tx.to_countersignatory_value_sat(), logger); self.context.channel_state = ChannelState::FundingSent as u32; self.context.channel_id = funding_txo.to_channel_id(); From 04475c809ef0258a63a45aa3df48585d53110a66 Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Tue, 11 Jul 2023 17:15:11 -0500 Subject: [PATCH 4/6] Expose revokeable output index and building a justice tx from commitment For watchtowers to be able to build justice transactions for our counterparty's revoked commitments, they need to be able to find the revokeable output for them to sweep. Here we cache `to_self_delay` in `CommitmentTransaction` to allow for finding this output on the struct directly. We also add a simple helper method to aid in building the initial spending transaction. This also adds a unit test for both of these helpers, and refactors a bit of a previous `CommitmentTransaction` unit test to make adding these easier. --- lightning/src/ln/chan_utils.rs | 276 ++++++++++++++++++++++++--------- 1 file changed, 201 insertions(+), 75 deletions(-) diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index 85490afae..a12665dba 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -21,6 +21,8 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::ripemd160::Hash as Ripemd160; use bitcoin::hash_types::{Txid, PubkeyHash}; +use crate::chain::chaininterface::fee_for_weight; +use crate::chain::package::WEIGHT_REVOKED_OUTPUT; use crate::sign::EntropySource; use crate::ln::{PaymentHash, PaymentPreimage}; use crate::ln::msgs::DecodeError; @@ -1308,6 +1310,7 @@ pub struct CommitmentTransaction { commitment_number: u64, to_broadcaster_value_sat: u64, to_countersignatory_value_sat: u64, + to_broadcaster_delay: Option, // Added in 0.0.117 feerate_per_kw: u32, htlcs: Vec, // Note that on upgrades, some features of existing outputs may be missed. @@ -1341,6 +1344,7 @@ impl Writeable for CommitmentTransaction { let legacy_deserialization_prevention_marker = legacy_deserialization_prevention_marker_for_channel_type_features(&self.channel_type_features); write_tlv_fields!(writer, { (0, self.commitment_number, required), + (1, self.to_broadcaster_delay, option), (2, self.to_broadcaster_value_sat, required), (4, self.to_countersignatory_value_sat, required), (6, self.feerate_per_kw, required), @@ -1358,6 +1362,7 @@ impl Readable for CommitmentTransaction { fn read(reader: &mut R) -> Result { _init_and_read_tlv_fields!(reader, { (0, commitment_number, required), + (1, to_broadcaster_delay, option), (2, to_broadcaster_value_sat, required), (4, to_countersignatory_value_sat, required), (6, feerate_per_kw, required), @@ -1376,6 +1381,7 @@ impl Readable for CommitmentTransaction { commitment_number: commitment_number.0.unwrap(), to_broadcaster_value_sat: to_broadcaster_value_sat.0.unwrap(), to_countersignatory_value_sat: to_countersignatory_value_sat.0.unwrap(), + to_broadcaster_delay, feerate_per_kw: feerate_per_kw.0.unwrap(), keys: keys.0.unwrap(), built: built.0.unwrap(), @@ -1407,6 +1413,7 @@ impl CommitmentTransaction { commitment_number, to_broadcaster_value_sat, to_countersignatory_value_sat, + to_broadcaster_delay: Some(channel_parameters.contest_delay()), feerate_per_kw, htlcs, channel_type_features: channel_parameters.channel_type_features().clone(), @@ -1724,6 +1731,69 @@ impl<'a> TrustedCommitmentTransaction<'a> { ); htlc_tx } + + /// Returns the index of the revokeable output, i.e. the `to_local` output sending funds to + /// the broadcaster, in the built transaction, if any exists. + /// + /// There are two cases where this may return `None`: + /// - The balance of the revokeable output is below the dust limit (only found on commitments + /// early in the channel's lifetime, i.e. before the channel reserve is met). + /// - This commitment was created before LDK 0.0.117. In this case, the + /// commitment transaction previously didn't contain enough information to locate the + /// revokeable output. + pub fn revokeable_output_index(&self) -> Option { + let revokeable_redeemscript = get_revokeable_redeemscript( + &self.keys.revocation_key, + self.to_broadcaster_delay?, + &self.keys.broadcaster_delayed_payment_key, + ); + let revokeable_p2wsh = revokeable_redeemscript.to_v0_p2wsh(); + let outputs = &self.inner.built.transaction.output; + outputs.iter().enumerate() + .find(|(_, out)| out.script_pubkey == revokeable_p2wsh) + .map(|(idx, _)| idx) + } + + /// Helper method to build an unsigned justice transaction spending the revokeable + /// `to_local` output to a destination script. Fee estimation accounts for the expected + /// revocation witness data that will be added when signed. + /// + /// This method will error if the given fee rate results in a fee greater than the value + /// of the output being spent, or if there exists no revokeable `to_local` output on this + /// commitment transaction. See [`Self::revokeable_output_index`] for more details. + /// + /// The built transaction will allow fee bumping with RBF, and this method takes + /// `feerate_per_kw` as an input such that multiple copies of a justice transaction at different + /// fee rates may be built. + pub fn build_to_local_justice_tx(&self, feerate_per_kw: u64, destination_script: Script) + -> Result { + let output_idx = self.revokeable_output_index().ok_or(())?; + let input = vec![TxIn { + previous_output: OutPoint { + txid: self.trust().txid(), + vout: output_idx as u32, + }, + script_sig: Script::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }]; + let value = self.inner.built.transaction.output[output_idx].value; + let output = vec![TxOut { + script_pubkey: destination_script, + value, + }]; + let mut justice_tx = Transaction { + version: 2, + lock_time: PackedLockTime::ZERO, + input, + output, + }; + let weight = justice_tx.weight() as u64 + WEIGHT_REVOKED_OUTPUT; + let fee = fee_for_weight(feerate_per_kw as u32, weight); + justice_tx.output[0].value = value.checked_sub(fee).ok_or(())?; + Ok(justice_tx) + } + } /// Commitment transaction numbers which appear in the transactions themselves are XOR'd with a @@ -1758,14 +1828,14 @@ pub fn get_commitment_transaction_number_obscure_factor( #[cfg(test)] mod tests { - use super::CounterpartyCommitmentSecrets; + use super::{CounterpartyCommitmentSecrets, ChannelPublicKeys}; use crate::{hex, chain}; use crate::prelude::*; use crate::ln::chan_utils::{get_htlc_redeemscript, get_to_countersignatory_with_anchors_redeemscript, CommitmentTransaction, TxCreationKeys, ChannelTransactionParameters, CounterpartyChannelTransactionParameters, HTLCOutputInCommitment}; use bitcoin::secp256k1::{PublicKey, SecretKey, Secp256k1}; use crate::util::test_utils; use crate::sign::{ChannelSigner, SignerProvider}; - use bitcoin::{Network, Txid}; + use bitcoin::{Network, Txid, Script}; use bitcoin::hashes::Hash; use crate::ln::PaymentHash; use bitcoin::hashes::hex::ToHex; @@ -1773,74 +1843,86 @@ mod tests { use bitcoin::PublicKey as BitcoinPublicKey; use crate::ln::features::ChannelTypeFeatures; + struct TestCommitmentTxBuilder { + commitment_number: u64, + holder_funding_pubkey: PublicKey, + counterparty_funding_pubkey: PublicKey, + keys: TxCreationKeys, + feerate_per_kw: u32, + htlcs_with_aux: Vec<(HTLCOutputInCommitment, ())>, + channel_parameters: ChannelTransactionParameters, + counterparty_pubkeys: ChannelPublicKeys, + } + + impl TestCommitmentTxBuilder { + fn new() -> Self { + let secp_ctx = Secp256k1::new(); + let seed = [42; 32]; + let network = Network::Testnet; + let keys_provider = test_utils::TestKeysInterface::new(&seed, network); + let signer = keys_provider.derive_channel_signer(3000, keys_provider.generate_channel_keys_id(false, 1_000_000, 0)); + let counterparty_signer = keys_provider.derive_channel_signer(3000, keys_provider.generate_channel_keys_id(true, 1_000_000, 1)); + let delayed_payment_base = &signer.pubkeys().delayed_payment_basepoint; + let per_commitment_secret = SecretKey::from_slice(&hex::decode("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100").unwrap()[..]).unwrap(); + let per_commitment_point = PublicKey::from_secret_key(&secp_ctx, &per_commitment_secret); + let htlc_basepoint = &signer.pubkeys().htlc_basepoint; + let holder_pubkeys = signer.pubkeys(); + let counterparty_pubkeys = counterparty_signer.pubkeys().clone(); + let keys = TxCreationKeys::derive_new(&secp_ctx, &per_commitment_point, delayed_payment_base, htlc_basepoint, &counterparty_pubkeys.revocation_basepoint, &counterparty_pubkeys.htlc_basepoint); + let channel_parameters = ChannelTransactionParameters { + holder_pubkeys: holder_pubkeys.clone(), + holder_selected_contest_delay: 0, + is_outbound_from_holder: false, + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys.clone(), selected_contest_delay: 0 }), + funding_outpoint: Some(chain::transaction::OutPoint { txid: Txid::all_zeros(), index: 0 }), + channel_type_features: ChannelTypeFeatures::only_static_remote_key(), + }; + let htlcs_with_aux = Vec::new(); + + Self { + commitment_number: 0, + holder_funding_pubkey: holder_pubkeys.funding_pubkey, + counterparty_funding_pubkey: counterparty_pubkeys.funding_pubkey, + keys, + feerate_per_kw: 1, + htlcs_with_aux, + channel_parameters, + counterparty_pubkeys, + } + } + + fn build(&mut self, to_broadcaster_sats: u64, to_countersignatory_sats: u64) -> CommitmentTransaction { + CommitmentTransaction::new_with_auxiliary_htlc_data( + self.commitment_number, to_broadcaster_sats, to_countersignatory_sats, + self.holder_funding_pubkey.clone(), + self.counterparty_funding_pubkey.clone(), + self.keys.clone(), self.feerate_per_kw, + &mut self.htlcs_with_aux, &self.channel_parameters.as_holder_broadcastable() + ) + } + } + #[test] fn test_anchors() { - let secp_ctx = Secp256k1::new(); - - let seed = [42; 32]; - let network = Network::Testnet; - let keys_provider = test_utils::TestKeysInterface::new(&seed, network); - let signer = keys_provider.derive_channel_signer(3000, keys_provider.generate_channel_keys_id(false, 1_000_000, 0)); - let counterparty_signer = keys_provider.derive_channel_signer(3000, keys_provider.generate_channel_keys_id(true, 1_000_000, 1)); - let delayed_payment_base = &signer.pubkeys().delayed_payment_basepoint; - let per_commitment_secret = SecretKey::from_slice(&hex::decode("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100").unwrap()[..]).unwrap(); - let per_commitment_point = PublicKey::from_secret_key(&secp_ctx, &per_commitment_secret); - let htlc_basepoint = &signer.pubkeys().htlc_basepoint; - let holder_pubkeys = signer.pubkeys(); - let counterparty_pubkeys = counterparty_signer.pubkeys(); - let keys = TxCreationKeys::derive_new(&secp_ctx, &per_commitment_point, delayed_payment_base, htlc_basepoint, &counterparty_pubkeys.revocation_basepoint, &counterparty_pubkeys.htlc_basepoint); - let mut channel_parameters = ChannelTransactionParameters { - holder_pubkeys: holder_pubkeys.clone(), - holder_selected_contest_delay: 0, - is_outbound_from_holder: false, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { pubkeys: counterparty_pubkeys.clone(), selected_contest_delay: 0 }), - funding_outpoint: Some(chain::transaction::OutPoint { txid: Txid::all_zeros(), index: 0 }), - channel_type_features: ChannelTypeFeatures::only_static_remote_key(), - }; - - let mut htlcs_with_aux: Vec<(_, ())> = Vec::new(); + let mut builder = TestCommitmentTxBuilder::new(); // Generate broadcaster and counterparty outputs - let tx = CommitmentTransaction::new_with_auxiliary_htlc_data( - 0, 1000, 2000, - holder_pubkeys.funding_pubkey, - counterparty_pubkeys.funding_pubkey, - keys.clone(), 1, - &mut htlcs_with_aux, &channel_parameters.as_holder_broadcastable() - ); + let tx = builder.build(1000, 2000); assert_eq!(tx.built.transaction.output.len(), 2); - assert_eq!(tx.built.transaction.output[1].script_pubkey, Payload::p2wpkh(&BitcoinPublicKey::new(counterparty_pubkeys.payment_point)).unwrap().script_pubkey()); + assert_eq!(tx.built.transaction.output[1].script_pubkey, Payload::p2wpkh(&BitcoinPublicKey::new(builder.counterparty_pubkeys.payment_point)).unwrap().script_pubkey()); // Generate broadcaster and counterparty outputs as well as two anchors - channel_parameters.channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); - let tx = CommitmentTransaction::new_with_auxiliary_htlc_data( - 0, 1000, 2000, - holder_pubkeys.funding_pubkey, - counterparty_pubkeys.funding_pubkey, - keys.clone(), 1, - &mut htlcs_with_aux, &channel_parameters.as_holder_broadcastable() - ); + builder.channel_parameters.channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let tx = builder.build(1000, 2000); assert_eq!(tx.built.transaction.output.len(), 4); - assert_eq!(tx.built.transaction.output[3].script_pubkey, get_to_countersignatory_with_anchors_redeemscript(&counterparty_pubkeys.payment_point).to_v0_p2wsh()); + assert_eq!(tx.built.transaction.output[3].script_pubkey, get_to_countersignatory_with_anchors_redeemscript(&builder.counterparty_pubkeys.payment_point).to_v0_p2wsh()); // Generate broadcaster output and anchor - let tx = CommitmentTransaction::new_with_auxiliary_htlc_data( - 0, 3000, 0, - holder_pubkeys.funding_pubkey, - counterparty_pubkeys.funding_pubkey, - keys.clone(), 1, - &mut htlcs_with_aux, &channel_parameters.as_holder_broadcastable() - ); + let tx = builder.build(3000, 0); assert_eq!(tx.built.transaction.output.len(), 2); // Generate counterparty output and anchor - let tx = CommitmentTransaction::new_with_auxiliary_htlc_data( - 0, 0, 3000, - holder_pubkeys.funding_pubkey, - counterparty_pubkeys.funding_pubkey, - keys.clone(), 1, - &mut htlcs_with_aux, &channel_parameters.as_holder_broadcastable() - ); + let tx = builder.build(0, 3000); assert_eq!(tx.built.transaction.output.len(), 2); let received_htlc = HTLCOutputInCommitment { @@ -1860,15 +1942,10 @@ mod tests { }; // Generate broadcaster output and received and offered HTLC outputs, w/o anchors - channel_parameters.channel_type_features = ChannelTypeFeatures::only_static_remote_key(); - let tx = CommitmentTransaction::new_with_auxiliary_htlc_data( - 0, 3000, 0, - holder_pubkeys.funding_pubkey, - counterparty_pubkeys.funding_pubkey, - keys.clone(), 1, - &mut vec![(received_htlc.clone(), ()), (offered_htlc.clone(), ())], - &channel_parameters.as_holder_broadcastable() - ); + builder.channel_parameters.channel_type_features = ChannelTypeFeatures::only_static_remote_key(); + builder.htlcs_with_aux = vec![(received_htlc.clone(), ()), (offered_htlc.clone(), ())]; + let tx = builder.build(3000, 0); + let keys = &builder.keys.clone(); assert_eq!(tx.built.transaction.output.len(), 3); assert_eq!(tx.built.transaction.output[0].script_pubkey, get_htlc_redeemscript(&received_htlc, &ChannelTypeFeatures::only_static_remote_key(), &keys).to_v0_p2wsh()); assert_eq!(tx.built.transaction.output[1].script_pubkey, get_htlc_redeemscript(&offered_htlc, &ChannelTypeFeatures::only_static_remote_key(), &keys).to_v0_p2wsh()); @@ -1878,15 +1955,9 @@ mod tests { "0020215d61bba56b19e9eadb6107f5a85d7f99c40f65992443f69229c290165bc00d"); // Generate broadcaster output and received and offered HTLC outputs, with anchors - channel_parameters.channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); - let tx = CommitmentTransaction::new_with_auxiliary_htlc_data( - 0, 3000, 0, - holder_pubkeys.funding_pubkey, - counterparty_pubkeys.funding_pubkey, - keys.clone(), 1, - &mut vec![(received_htlc.clone(), ()), (offered_htlc.clone(), ())], - &channel_parameters.as_holder_broadcastable() - ); + builder.channel_parameters.channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + builder.htlcs_with_aux = vec![(received_htlc.clone(), ()), (offered_htlc.clone(), ())]; + let tx = builder.build(3000, 0); assert_eq!(tx.built.transaction.output.len(), 5); assert_eq!(tx.built.transaction.output[2].script_pubkey, get_htlc_redeemscript(&received_htlc, &ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(), &keys).to_v0_p2wsh()); assert_eq!(tx.built.transaction.output[3].script_pubkey, get_htlc_redeemscript(&offered_htlc, &ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(), &keys).to_v0_p2wsh()); @@ -1896,6 +1967,61 @@ mod tests { "002087a3faeb1950a469c0e2db4a79b093a41b9526e5a6fc6ef5cb949bde3be379c7"); } + #[test] + fn test_finding_revokeable_output_index() { + let mut builder = TestCommitmentTxBuilder::new(); + + // Revokeable output present + let tx = builder.build(1000, 2000); + assert_eq!(tx.built.transaction.output.len(), 2); + assert_eq!(tx.trust().revokeable_output_index(), Some(0)); + + // Revokeable output present (but to_broadcaster_delay missing) + let tx = CommitmentTransaction { to_broadcaster_delay: None, ..tx }; + assert_eq!(tx.built.transaction.output.len(), 2); + assert_eq!(tx.trust().revokeable_output_index(), None); + + // Revokeable output not present (our balance is dust) + let tx = builder.build(0, 2000); + assert_eq!(tx.built.transaction.output.len(), 1); + assert_eq!(tx.trust().revokeable_output_index(), None); + } + + #[test] + fn test_building_to_local_justice_tx() { + let mut builder = TestCommitmentTxBuilder::new(); + + // Revokeable output not present (our balance is dust) + let tx = builder.build(0, 2000); + assert_eq!(tx.built.transaction.output.len(), 1); + assert!(tx.trust().build_to_local_justice_tx(253, Script::new()).is_err()); + + // Revokeable output present + let tx = builder.build(1000, 2000); + assert_eq!(tx.built.transaction.output.len(), 2); + + // Too high feerate + assert!(tx.trust().build_to_local_justice_tx(100_000, Script::new()).is_err()); + + // Generate a random public key for destination script + let secret_key = SecretKey::from_slice( + &hex::decode("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100") + .unwrap()[..]).unwrap(); + let pubkey_hash = BitcoinPublicKey::new( + PublicKey::from_secret_key(&Secp256k1::new(), &secret_key)).wpubkey_hash().unwrap(); + let destination_script = Script::new_v0_p2wpkh(&pubkey_hash); + + let justice_tx = tx.trust().build_to_local_justice_tx(253, destination_script.clone()).unwrap(); + assert_eq!(justice_tx.input.len(), 1); + assert_eq!(justice_tx.input[0].previous_output.txid, tx.built.transaction.txid()); + assert_eq!(justice_tx.input[0].previous_output.vout, tx.trust().revokeable_output_index().unwrap() as u32); + assert!(justice_tx.input[0].sequence.is_rbf()); + + assert_eq!(justice_tx.output.len(), 1); + assert!(justice_tx.output[0].value < 1000); + assert_eq!(justice_tx.output[0].script_pubkey, destination_script); + } + #[test] fn test_per_commitment_storage() { // Test vectors from BOLT 3: From 2cb25576697f368f1807d6a1f2db3d6086beb84b Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Wed, 14 Jun 2023 22:58:10 -0500 Subject: [PATCH 5/6] Enable signing a justice tx using the channel monitor --- lightning/src/chain/chainmonitor.rs | 11 ++++++ lightning/src/chain/channelmonitor.rs | 51 +++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index 16a02b54a..7d698a220 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -94,6 +94,17 @@ impl MonitorUpdateId { /// [`ChannelMonitorUpdateStatus::PermanentFailure`], in which case the channel will likely be /// closed without broadcasting the latest state. See /// [`ChannelMonitorUpdateStatus::PermanentFailure`] for more details. +/// +/// Third-party watchtowers may be built as a part of an implementation of this trait, with the +/// advantage that you can control whether to resume channel operation depending on if an update +/// has been persisted to a watchtower. For this, you may find the following methods useful: +/// [`ChannelMonitor::initial_counterparty_commitment_tx`], +/// [`ChannelMonitor::counterparty_commitment_txs_from_update`], +/// [`ChannelMonitor::sign_to_local_justice_tx`], [`TrustedCommitmentTransaction::revokeable_output_index`], +/// [`TrustedCommitmentTransaction::build_to_local_justice_tx`]. +/// +/// [`TrustedCommitmentTransaction::revokeable_output_index`]: crate::ln::chan_utils::TrustedCommitmentTransaction::revokeable_output_index +/// [`TrustedCommitmentTransaction::build_to_local_justice_tx`]: crate::ln::chan_utils::TrustedCommitmentTransaction::build_to_local_justice_tx pub trait Persist { /// Persist a new channel's data in response to a [`chain::Watch::watch_channel`] call. This is /// called by [`ChannelManager`] for new channels, or may be called directly, e.g. on startup. diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 0c207fa8b..da3970da8 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -31,7 +31,7 @@ use bitcoin::hash_types::{Txid, BlockHash, WPubkeyHash}; use bitcoin::secp256k1::{Secp256k1, ecdsa::Signature}; use bitcoin::secp256k1::{SecretKey, PublicKey}; -use bitcoin::secp256k1; +use bitcoin::{secp256k1, EcdsaSighashType}; use crate::ln::channel::INITIAL_COMMITMENT_NUMBER; use crate::ln::{PaymentHash, PaymentPreimage}; @@ -1430,7 +1430,8 @@ impl ChannelMonitor { /// This is provided so that watchtower clients in the persistence pipeline are able to build /// justice transactions for each counterparty commitment upon each update. It's intended to be /// used within an implementation of [`Persist::update_persisted_channel`], which is provided - /// with a monitor and an update. + /// with a monitor and an update. Once revoked, signing a justice transaction can be done using + /// [`Self::sign_to_local_justice_tx`]. /// /// 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 a later update in which @@ -1446,6 +1447,27 @@ impl ChannelMonitor { self.inner.lock().unwrap().counterparty_commitment_txs_from_update(update) } + /// Wrapper around [`EcdsaChannelSigner::sign_justice_revoked_output`] to make + /// signing the justice transaction easier for implementors of + /// [`chain::chainmonitor::Persist`]. On success this method returns the provided transaction + /// signing the input at `input_idx`. 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` is the value of the output being spent by the input at `input_idx`, committed + /// in the BIP 143 signature. + /// + /// This method will only succeed if this monitor has received the revocation secret for the + /// provided `commitment_number`. If a commitment number is provided that does not correspond + /// to the commitment transaction being revoked, this will return a signed transaction, but + /// the signature will not be valid. + /// + /// [`EcdsaChannelSigner::sign_justice_revoked_output`]: crate::sign::EcdsaChannelSigner::sign_justice_revoked_output + /// [`Persist`]: crate::chain::chainmonitor::Persist + pub fn sign_to_local_justice_tx(&self, justice_tx: Transaction, input_idx: usize, value: u64, commitment_number: u64) -> Result { + self.inner.lock().unwrap().sign_to_local_justice_tx(justice_tx, input_idx, value, commitment_number) + } + pub(crate) fn get_min_seen_secret(&self) -> u64 { self.inner.lock().unwrap().get_min_seen_secret() } @@ -2810,6 +2832,31 @@ impl ChannelMonitorImpl { }).collect() } + pub(crate) fn sign_to_local_justice_tx( + &self, mut justice_tx: Transaction, input_idx: usize, value: u64, commitment_number: u64 + ) -> Result { + let secret = self.get_secret(commitment_number).ok_or(())?; + let per_commitment_key = SecretKey::from_slice(&secret).map_err(|_| ())?; + let their_per_commitment_point = PublicKey::from_secret_key( + &self.onchain_tx_handler.secp_ctx, &per_commitment_key); + + let revocation_pubkey = chan_utils::derive_public_revocation_key( + &self.onchain_tx_handler.secp_ctx, &their_per_commitment_point, + &self.holder_revocation_basepoint); + let delayed_key = chan_utils::derive_public_key(&self.onchain_tx_handler.secp_ctx, + &their_per_commitment_point, + &self.counterparty_commitment_params.counterparty_delayed_payment_base_key); + let revokeable_redeemscript = chan_utils::get_revokeable_redeemscript(&revocation_pubkey, + self.counterparty_commitment_params.on_counterparty_tx_csv, &delayed_key); + + let sig = self.onchain_tx_handler.signer.sign_justice_revoked_output( + &justice_tx, input_idx, value, &per_commitment_key, &self.onchain_tx_handler.secp_ctx)?; + justice_tx.input[input_idx].witness.push_bitcoin_signature(&sig.serialize_der(), EcdsaSighashType::All); + justice_tx.input[input_idx].witness.push(&[1u8]); + justice_tx.input[input_idx].witness.push(revokeable_redeemscript.as_bytes()); + Ok(justice_tx) + } + /// Can only fail if idx is < get_min_seen_secret fn get_secret(&self, idx: u64) -> Option<[u8; 32]> { self.commitment_secrets.get_secret(idx) From b20b1dbe678d12480f79c736b37ab10ab79a9b06 Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Tue, 11 Jul 2023 17:20:54 -0500 Subject: [PATCH 6/6] Test justice tx formation from persistence Here we implement `WatchtowerPersister`, which provides a test-only sample implementation of `Persist` similar to how we might imagine a user to build watchtower-like functionality in the persistence pipeline. We test that the `WatchtowerPersister` is able to successfully build and sign a valid justice transaction that sweeps a counterparty's funds if they broadcast an old commitment. --- lightning/src/ln/functional_test_utils.rs | 8 +- lightning/src/ln/functional_tests.rs | 70 ++++++++++++++- lightning/src/util/test_utils.rs | 104 ++++++++++++++++++++++ 3 files changed, 178 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 1db4e8734..34568b07c 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -10,7 +10,7 @@ //! A bunch of useful utilities for building networks of nodes and exchanging messages between //! nodes for functional tests. -use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; +use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch, chainmonitor::Persist}; use crate::sign::EntropySource; use crate::chain::channelmonitor::ChannelMonitor; use crate::chain::transaction::OutPoint; @@ -2643,10 +2643,14 @@ pub fn create_chanmon_cfgs(node_count: usize) -> Vec { } pub fn create_node_cfgs<'a>(node_count: usize, chanmon_cfgs: &'a Vec) -> Vec> { + create_node_cfgs_with_persisters(node_count, chanmon_cfgs, chanmon_cfgs.iter().map(|c| &c.persister).collect()) +} + +pub fn create_node_cfgs_with_persisters<'a>(node_count: usize, chanmon_cfgs: &'a Vec, persisters: Vec<&'a impl Persist>) -> Vec> { let mut nodes = Vec::new(); for i in 0..node_count { - let chain_monitor = test_utils::TestChainMonitor::new(Some(&chanmon_cfgs[i].chain_source), &chanmon_cfgs[i].tx_broadcaster, &chanmon_cfgs[i].logger, &chanmon_cfgs[i].fee_estimator, &chanmon_cfgs[i].persister, &chanmon_cfgs[i].keys_manager); + let chain_monitor = test_utils::TestChainMonitor::new(Some(&chanmon_cfgs[i].chain_source), &chanmon_cfgs[i].tx_broadcaster, &chanmon_cfgs[i].logger, &chanmon_cfgs[i].fee_estimator, persisters[i], &chanmon_cfgs[i].keys_manager); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &chanmon_cfgs[i].logger)); let seed = [i as u8; 32]; nodes.push(NodeCfg { diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index f5ba7166c..2fbc36ce9 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -17,7 +17,7 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::channelmonitor; use crate::chain::channelmonitor::{CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY}; use crate::chain::transaction::OutPoint; -use crate::sign::{EcdsaChannelSigner, EntropySource}; +use crate::sign::{ChannelSigner, EcdsaChannelSigner, EntropySource, SignerProvider}; use crate::events::{Event, MessageSendEvent, MessageSendEventsProvider, PathFailure, PaymentPurpose, ClosureReason, HTLCDestination, PaymentFailureReason}; use crate::ln::{PaymentPreimage, PaymentSecret, PaymentHash}; use crate::ln::channel::{commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, CONCURRENT_INBOUND_HTLC_FEE_BUFFER, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT, get_holder_selected_channel_reserve_satoshis, OutboundV1Channel, InboundV1Channel}; @@ -31,7 +31,7 @@ use crate::ln::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures}; use crate::ln::msgs; use crate::ln::msgs::{ChannelMessageHandler, RoutingMessageHandler, ErrorAction}; use crate::util::enforcing_trait_impls::EnforcingSigner; -use crate::util::test_utils; +use crate::util::test_utils::{self, WatchtowerPersister}; use crate::util::errors::APIError; use crate::util::ser::{Writeable, ReadableArgs}; use crate::util::string::UntrustedString; @@ -2554,6 +2554,72 @@ fn revoked_output_claim() { check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed, [nodes[1].node.get_our_node_id()], 100000); } +#[test] +fn test_forming_justice_tx_from_monitor_updates() { + do_test_forming_justice_tx_from_monitor_updates(true); + do_test_forming_justice_tx_from_monitor_updates(false); +} + +fn do_test_forming_justice_tx_from_monitor_updates(broadcast_initial_commitment: bool) { + // Simple test to make sure that the justice tx formed in WatchtowerPersister + // is properly formed and can be broadcasted/confirmed successfully in the event + // that a revoked commitment transaction is broadcasted + // (Similar to `revoked_output_claim` test but we get the justice tx + broadcast manually) + let chanmon_cfgs = create_chanmon_cfgs(2); + let destination_script0 = chanmon_cfgs[0].keys_manager.get_destination_script().unwrap(); + let destination_script1 = chanmon_cfgs[1].keys_manager.get_destination_script().unwrap(); + let persisters = vec![WatchtowerPersister::new(destination_script0), + WatchtowerPersister::new(destination_script1)]; + let node_cfgs = create_node_cfgs_with_persisters(2, &chanmon_cfgs, persisters.iter().collect()); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let (_, _, channel_id, funding_tx) = create_announced_chan_between_nodes(&nodes, 0, 1); + let funding_txo = OutPoint { txid: funding_tx.txid(), index: 0 }; + + if !broadcast_initial_commitment { + // Send a payment to move the channel forward + send_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000); + } + + // node[0] is gonna to revoke an old state thus node[1] should be able to claim the revoked output. + // We'll keep this commitment transaction to broadcast once it's revoked. + let revoked_local_txn = get_local_commitment_txn!(nodes[0], channel_id); + assert_eq!(revoked_local_txn.len(), 1); + let revoked_commitment_tx = &revoked_local_txn[0]; + + // Send another payment, now revoking the previous commitment tx + send_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000); + + let justice_tx = persisters[1].justice_tx(funding_txo, &revoked_commitment_tx.txid()).unwrap(); + check_spends!(justice_tx, revoked_commitment_tx); + + mine_transactions(&nodes[1], &[revoked_commitment_tx, &justice_tx]); + mine_transactions(&nodes[0], &[revoked_commitment_tx, &justice_tx]); + + check_added_monitors!(nodes[1], 1); + check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, false, + &[nodes[0].node.get_our_node_id()], 100_000); + get_announce_close_broadcast_events(&nodes, 1, 0); + + check_added_monitors!(nodes[0], 1); + check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, false, + &[nodes[1].node.get_our_node_id()], 100_000); + + // Check that the justice tx has sent the revoked output value to nodes[1] + let monitor = get_monitor!(nodes[1], channel_id); + let total_claimable_balance = monitor.get_claimable_balances().iter().fold(0, |sum, balance| { + match balance { + channelmonitor::Balance::ClaimableAwaitingConfirmations { amount_satoshis, .. } => sum + amount_satoshis, + _ => panic!("Unexpected balance type"), + } + }); + // On the first commitment, node[1]'s balance was below dust so it didn't have an output + let node1_channel_balance = if broadcast_initial_commitment { 0 } else { revoked_commitment_tx.output[0].value }; + let expected_claimable_balance = node1_channel_balance + justice_tx.output[0].value; + assert_eq!(total_claimable_balance, expected_claimable_balance); +} + + #[test] fn claim_htlc_outputs_shared_tx() { // Node revoked old state, htlcs haven't time out yet, claim them in shared justice tx diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index e7e29600d..bcd460ee1 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -11,6 +11,7 @@ use crate::chain; use crate::chain::WatchedOutput; use crate::chain::chaininterface; use crate::chain::chaininterface::ConfirmationTarget; +use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; use crate::chain::chainmonitor; use crate::chain::chainmonitor::MonitorUpdateId; use crate::chain::channelmonitor; @@ -20,6 +21,7 @@ use crate::sign; use crate::events; use crate::events::bump_transaction::{WalletSource, Utxo}; use crate::ln::channelmanager; +use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures}; use crate::ln::{msgs, wire}; use crate::ln::msgs::LightningError; @@ -272,6 +274,108 @@ impl<'a> chain::Watch for TestChainMonitor<'a> { } } +struct JusticeTxData { + justice_tx: Transaction, + value: u64, + commitment_number: u64, +} + +pub(crate) struct WatchtowerPersister { + persister: TestPersister, + /// Upon a new commitment_signed, we'll get a + /// ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTxInfo. We'll store the justice tx + /// amount, and commitment number so we can build the justice tx after our counterparty + /// revokes it. + unsigned_justice_tx_data: Mutex>>, + /// After receiving a revoke_and_ack for a commitment number, we'll form and store the justice + /// tx which would be used to provide a watchtower with the data it needs. + watchtower_state: Mutex>>, + destination_script: Script, +} + +impl WatchtowerPersister { + pub(crate) fn new(destination_script: Script) -> Self { + WatchtowerPersister { + persister: TestPersister::new(), + unsigned_justice_tx_data: Mutex::new(HashMap::new()), + watchtower_state: Mutex::new(HashMap::new()), + destination_script, + } + } + + pub(crate) fn justice_tx(&self, funding_txo: OutPoint, commitment_txid: &Txid) + -> Option { + self.watchtower_state.lock().unwrap().get(&funding_txo).unwrap().get(commitment_txid).cloned() + } + + fn form_justice_data_from_commitment(&self, counterparty_commitment_tx: &CommitmentTransaction) + -> Option { + let trusted_tx = counterparty_commitment_tx.trust(); + let output_idx = trusted_tx.revokeable_output_index()?; + let built_tx = trusted_tx.built_transaction(); + let value = built_tx.transaction.output[output_idx as usize].value; + let justice_tx = trusted_tx.build_to_local_justice_tx( + FEERATE_FLOOR_SATS_PER_KW as u64, self.destination_script.clone()).ok()?; + let commitment_number = counterparty_commitment_tx.commitment_number(); + Some(JusticeTxData { justice_tx, value, commitment_number }) + } +} + +impl chainmonitor::Persist for WatchtowerPersister { + fn persist_new_channel(&self, funding_txo: OutPoint, + data: &channelmonitor::ChannelMonitor, id: MonitorUpdateId + ) -> chain::ChannelMonitorUpdateStatus { + let res = self.persister.persist_new_channel(funding_txo, data, id); + + assert!(self.unsigned_justice_tx_data.lock().unwrap() + .insert(funding_txo, VecDeque::new()).is_none()); + assert!(self.watchtower_state.lock().unwrap() + .insert(funding_txo, HashMap::new()).is_none()); + + let initial_counterparty_commitment_tx = data.initial_counterparty_commitment_tx() + .expect("First and only call expects Some"); + if let Some(justice_data) + = self.form_justice_data_from_commitment(&initial_counterparty_commitment_tx) { + self.unsigned_justice_tx_data.lock().unwrap() + .get_mut(&funding_txo).unwrap() + .push_back(justice_data); + } + res + } + + fn update_persisted_channel( + &self, funding_txo: OutPoint, update: Option<&channelmonitor::ChannelMonitorUpdate>, + data: &channelmonitor::ChannelMonitor, update_id: MonitorUpdateId + ) -> chain::ChannelMonitorUpdateStatus { + let res = self.persister.update_persisted_channel(funding_txo, update, data, update_id); + + if let Some(update) = update { + let commitment_txs = data.counterparty_commitment_txs_from_update(update); + let justice_datas = commitment_txs.into_iter() + .filter_map(|commitment_tx| self.form_justice_data_from_commitment(&commitment_tx)); + let mut channels_justice_txs = self.unsigned_justice_tx_data.lock().unwrap(); + let channel_state = channels_justice_txs.get_mut(&funding_txo).unwrap(); + channel_state.extend(justice_datas); + + while let Some(JusticeTxData { justice_tx, value, commitment_number }) = channel_state.front() { + let input_idx = 0; + let commitment_txid = justice_tx.input[input_idx].previous_output.txid; + match data.sign_to_local_justice_tx(justice_tx.clone(), input_idx, *value, *commitment_number) { + Ok(signed_justice_tx) => { + let dup = self.watchtower_state.lock().unwrap() + .get_mut(&funding_txo).unwrap() + .insert(commitment_txid, signed_justice_tx); + assert!(dup.is_none()); + channel_state.pop_front(); + }, + Err(_) => break, + } + } + } + res + } +} + pub struct TestPersister { /// The queue of update statuses we'll return. If none are queued, ::Completed will always be /// returned.