Skip to content
Open
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
256 changes: 252 additions & 4 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@

use bitcoin::amount::Amount;
use bitcoin::block::Header;
use bitcoin::locktime::absolute::LockTime;
use bitcoin::script::{Script, ScriptBuf};
use bitcoin::transaction::{OutPoint as BitcoinOutPoint, Transaction, TxOut};
use bitcoin::transaction::{OutPoint as BitcoinOutPoint, Transaction, TxIn, TxOut, Version};
use bitcoin::{Sequence, Witness};

use bitcoin::hash_types::{BlockHash, Txid};
use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;

use bitcoin::ecdsa::Signature as BitcoinSignature;
use bitcoin::secp256k1::{self, ecdsa::Signature, PublicKey, Secp256k1, SecretKey};
use bitcoin::sighash::EcdsaSighashType;

use crate::chain;
use crate::chain::chaininterface::{
Expand All @@ -47,7 +50,7 @@ use crate::events::bump_transaction::{AnchorDescriptor, BumpTransactionEvent};
use crate::events::{ClosureReason, Event, EventHandler, ReplayEvent};
use crate::ln::chan_utils::{
self, ChannelTransactionParameters, CommitmentTransaction, CounterpartyCommitmentSecrets,
HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction,
HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction, TxCreationKeys,
};
use crate::ln::channel::INITIAL_COMMITMENT_NUMBER;
use crate::ln::channel_keys::{
Expand Down Expand Up @@ -141,6 +144,20 @@ impl ChannelMonitorUpdate {
pub fn renegotiated_funding_data(&self) -> impl Iterator<Item = (OutPoint, ScriptBuf)> + '_ {
self.internal_renegotiated_funding_data()
}

/// Returns `true` if this update contains counterparty commitment data
/// relevant to a watchtower (a new commitment or a revocation secret).
pub fn updates_watchtower_state(&self) -> bool {
self.updates.iter().any(|step| {
matches!(
step,
ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { .. }
| ChannelMonitorUpdateStep::LatestCounterpartyCommitment { .. }
| ChannelMonitorUpdateStep::CommitmentSecret { .. }
| ChannelMonitorUpdateStep::RenegotiatedFunding { .. }
)
})
}
}

/// LDK prior to 0.1 used this constant as the [`ChannelMonitorUpdate::update_id`] for any
Expand Down Expand Up @@ -262,6 +279,17 @@ impl_writeable_tlv_based!(HTLCUpdate, {
(4, payment_preimage, option),
});

/// A signed justice transaction ready for broadcast or watchtower submission.
#[derive(Clone, Debug)]
pub struct JusticeTransaction {
/// The fully signed justice transaction.
pub tx: Transaction,
/// The txid of the revoked counterparty commitment transaction.
pub revoked_commitment_txid: Txid,
/// The commitment number of the revoked commitment transaction.
pub commitment_number: u64,
}

/// If an output goes from claimable only by us to claimable by us or our counterparty within this
/// many blocks, we consider it pinnable for the purposes of aggregating claims in a single
/// transaction.
Expand Down Expand Up @@ -1166,6 +1194,11 @@ struct FundingScope {
// transaction for which we have deleted claim information on some watchtowers.
current_holder_commitment_tx: HolderCommitmentTransaction,
prev_holder_commitment_tx: Option<HolderCommitmentTransaction>,

/// The current counterparty commitment transaction, stored for justice tx signing.
cur_counterparty_commitment_tx: Option<CommitmentTransaction>,
/// The previous counterparty commitment transaction, stored for justice tx signing.
prev_counterparty_commitment_tx: Option<CommitmentTransaction>,
}

impl FundingScope {
Expand Down Expand Up @@ -1194,6 +1227,8 @@ impl_writeable_tlv_based!(FundingScope, {
(7, current_holder_commitment_tx, required),
(9, prev_holder_commitment_tx, option),
(11, counterparty_claimable_outpoints, required),
(13, cur_counterparty_commitment_tx, option),
(15, prev_counterparty_commitment_tx, option),
Comment on lines +1230 to +1231
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fields are added to FundingScope's impl_writeable_tlv_based! (at TLV 13/15, which applies to pending_funding entries), and to the top-level write_chanmon_internal (at TLV 39/41, for the main funding scope). During deserialization, the main funding scope reads from TLV 39/41 while pending_funding entries read from their per-FundingScope TLV 13/15.

This split serialization works but is subtle and fragile — a future change that serializes the main funding as a whole FundingScope would silently double-write the data. Worth adding a comment explaining why these live in two places.

});

#[derive(Clone, PartialEq)]
Expand Down Expand Up @@ -1756,6 +1791,8 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>(
(35, channel_monitor.is_manual_broadcast, required),
(37, channel_monitor.funding_seen_onchain, required),
(39, channel_monitor.best_block.previous_blocks, required),
(43, channel_monitor.funding.cur_counterparty_commitment_tx, option),
(45, channel_monitor.funding.prev_counterparty_commitment_tx, option),
});

Ok(())
Expand Down Expand Up @@ -1905,6 +1942,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

current_holder_commitment_tx: initial_holder_commitment_tx,
prev_holder_commitment_tx: None,

cur_counterparty_commitment_tx: None,
prev_counterparty_commitment_tx: None,
},
pending_funding: vec![],

Expand Down Expand Up @@ -2272,6 +2312,27 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
self.inner.lock().unwrap().sign_to_local_justice_tx(justice_tx, input_idx, value, commitment_number)
}

/// Returns signed justice transactions for all revoked counterparty commitments
/// currently stored in this monitor.
///
/// Call this after persisting the monitor when
/// [`ChannelMonitorUpdate::updates_watchtower_state`] returns `true`. Also call on
/// startup for each loaded monitor to recover any justice transactions not yet
/// delivered to a watchtower.
///
/// To avoid losing justice data when the watchtower is unreachable, the
/// [`Persist`] implementation should delay completing monitor updates until
/// previously obtained justice transactions have been delivered.
///
/// Idempotent: returns the same results on repeated calls for the same monitor state.
///
/// [`Persist`]: crate::chain::chainmonitor::Persist
pub fn get_pending_justice_txs(
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
) -> Vec<JusticeTransaction> {
self.inner.lock().unwrap().get_pending_justice_txs(feerate_per_kw, destination_script)
}

pub(crate) fn get_min_seen_secret(&self) -> u64 {
self.inner.lock().unwrap().get_min_seen_secret()
}
Expand Down Expand Up @@ -3486,6 +3547,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.provide_latest_counterparty_commitment_tx(commitment_tx.trust().txid(), Vec::new(), commitment_tx.commitment_number(),
commitment_tx.per_commitment_point());
// Soon, we will only populate this field
self.funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
self.initial_counterparty_commitment_tx = Some(commitment_tx);
}

Expand Down Expand Up @@ -3563,6 +3625,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
current_funding_commitment_tx.commitment_number(),
current_funding_commitment_tx.per_commitment_point(),
);
self.funding.prev_counterparty_commitment_tx =
self.funding.cur_counterparty_commitment_tx.take();
self.funding.cur_counterparty_commitment_tx = Some(current_funding_commitment_tx.clone());

for (pending_funding, commitment_tx) in
self.pending_funding.iter_mut().zip(commitment_txs.iter().skip(1))
Expand All @@ -3574,6 +3639,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
pending_funding
.counterparty_claimable_outpoints
.insert(commitment_txid, htlcs_for_commitment(commitment_tx));
pending_funding.prev_counterparty_commitment_tx =
pending_funding.cur_counterparty_commitment_tx.take();
pending_funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
}

Ok(())
Expand Down Expand Up @@ -4025,6 +4093,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
counterparty_claimable_outpoints,
current_holder_commitment_tx: alternative_holder_commitment_tx.clone(),
prev_holder_commitment_tx: None,

cur_counterparty_commitment_tx: None,
prev_counterparty_commitment_tx: None,
};
let alternative_funding_outpoint = alternative_funding.funding_outpoint();

Expand Down Expand Up @@ -4294,8 +4365,21 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}
}

#[cfg(debug_assertions)] {
self.counterparty_commitment_txs_from_update(updates);
// Populate cur/prev for the LatestCounterpartyCommitmentTXInfo path, which
// doesn't go through update_counterparty_commitment_data.
Comment on lines +4368 to +4369
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "for the LatestCounterpartyCommitmentTXInfo path" but this post-loop is also the only mechanism that sets cur_counterparty_commitment_tx for RenegotiatedFunding scopes (created at line 4097 with cur_counterparty_commitment_tx: None).

If this code is removed during the planned LatestCounterpartyCommitmentTXInfo deprecation, RenegotiatedFunding (splice) scopes would silently lose their initial commitment tracking — cur would stay None, so after the next commitment update, prev would be None instead of the initial splice commitment, and the watchtower would miss that revocation.

Suggest updating the comment:

Suggested change
// Populate cur/prev for the LatestCounterpartyCommitmentTXInfo path, which
// doesn't go through update_counterparty_commitment_data.
// Populate cur/prev for paths that don't go through
// update_counterparty_commitment_data: LatestCounterpartyCommitmentTXInfo
// (legacy, to be removed) and RenegotiatedFunding (splice).

for commitment_tx in self.counterparty_commitment_txs_from_update(updates) {
let txid = commitment_tx.trust().built_transaction().txid;
let funding = core::iter::once(&mut self.funding)
.chain(self.pending_funding.iter_mut())
.find(|f| f.current_counterparty_commitment_txid == Some(txid));
if let Some(funding) = funding {
if funding.cur_counterparty_commitment_tx.as_ref()
.map(|c| c.trust().built_transaction().txid) != Some(txid)
{
funding.prev_counterparty_commitment_tx = funding.cur_counterparty_commitment_tx.take();
funding.cur_counterparty_commitment_tx = Some(commitment_tx);
}
}
}

self.latest_update_id = updates.update_id;
Expand Down Expand Up @@ -4742,6 +4826,163 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.commitment_secrets.get_secret(idx)
}

/// Returns signed justice transactions for all revoked counterparty commitments
/// currently stored in this monitor. Idempotent.
fn get_pending_justice_txs(
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
) -> Vec<JusticeTransaction> {
let mut result = Vec::new();
for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) {
if let Some(ref prev) = funding.prev_counterparty_commitment_tx {
if self.commitment_secrets.get_secret(prev.commitment_number()).is_some() {
result.extend(self.try_sign_justice_txs(
prev,
feerate_per_kw,
destination_script.clone(),
));
}
}
}
result
}

fn try_sign_justice_txs(
&self, commitment_tx: &CommitmentTransaction, feerate_per_kw: u64,
destination_script: ScriptBuf,
) -> Vec<JusticeTransaction> {
let commitment_number = commitment_tx.commitment_number();
let secret = match self.get_secret(commitment_number) {
Some(s) => s,
None => return Vec::new(),
};
let per_commitment_key = match SecretKey::from_slice(&secret) {
Ok(k) => k,
Err(_) => return Vec::new(),
};

let trusted = commitment_tx.trust();
let built = trusted.built_transaction();
let txid = built.txid;
let mut result = Vec::new();

// to_local justice tx
if let Some(output_idx) = trusted.revokeable_output_index() {
let value = built.transaction.output[output_idx].value;
if let Ok(justice_tx) =
trusted.build_to_local_justice_tx(feerate_per_kw, destination_script.clone())
{
if let Ok(signed) =
self.sign_to_local_justice_tx(justice_tx, 0, value.to_sat(), commitment_number)
{
result.push(JusticeTransaction {
tx: signed,
revoked_commitment_txid: txid,
commitment_number,
});
}
}
}

// HTLC justice txs
let channel_parameters = core::iter::once(&self.funding)
.chain(&self.pending_funding)
.find(|funding| funding.counterparty_claimable_outpoints.contains_key(&txid))
.map(|funding| &funding.channel_parameters);
if let Some(channel_parameters) = channel_parameters {
let per_commitment_point =
PublicKey::from_secret_key(&self.onchain_tx_handler.secp_ctx, &per_commitment_key);
let directed = channel_parameters.as_counterparty_broadcastable();
let keys = TxCreationKeys::from_channel_static_keys(
&per_commitment_point,
directed.broadcaster_pubkeys(),
directed.countersignatory_pubkeys(),
&self.onchain_tx_handler.secp_ctx,
);

for htlc in commitment_tx.nondust_htlcs() {
if let Some(output_index) = htlc.transaction_output_index {
let htlc_value = built.transaction.output[output_index as usize].value;
let witness_script = chan_utils::get_htlc_redeemscript(
htlc,
&channel_parameters.channel_type_features,
&keys,
);

// Build a spending tx for this HTLC output
let input = vec![TxIn {
previous_output: bitcoin::OutPoint { txid, vout: output_index },
script_sig: ScriptBuf::new(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::new(),
}];
let weight_estimate = if htlc.offered {
crate::chain::package::weight_revoked_offered_htlc(
&channel_parameters.channel_type_features,
)
} else {
crate::chain::package::weight_revoked_received_htlc(
&channel_parameters.channel_type_features,
)
};
let fee = Amount::from_sat(crate::chain::chaininterface::fee_for_weight(
feerate_per_kw as u32,
// Base tx weight + witness weight
Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: input.clone(),
output: vec![TxOut {
script_pubkey: destination_script.clone(),
value: htlc_value,
}],
}
.weight()
.to_wu() + weight_estimate,
));
Comment on lines +4927 to +4941
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fee estimation constructs a throwaway Transaction with input.clone() and destination_script.clone() per HTLC, but the base tx weight (excluding witness) is identical for every HTLC justice tx in this loop — same version, locktime, single input (empty witness), single output to the same destination script.

Consider hoisting the base weight calculation before the loop and only varying the witness weight per HTLC type:

let base_weight = Transaction {
    version: Version::TWO,
    lock_time: LockTime::ZERO,
    input: vec![TxIn { ..Default::default() }],
    output: vec![TxOut { script_pubkey: destination_script.clone(), value: Amount::ZERO }],
}.weight().to_wu();

// Then inside the loop:
let fee = fee_for_weight(feerate_per_kw as u32, base_weight + weight_estimate);

This avoids cloning input and destination_script on every iteration. (Reiterating prior comment #5 with a concrete suggestion since it's in a per-HTLC loop that could be large.)

let output_value = match htlc_value.checked_sub(fee) {
Some(v) => v,
None => continue, // Dust, skip
};

let mut justice_tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input,
output: vec![TxOut {
script_pubkey: destination_script.clone(),
value: output_value,
}],
};

if let Ok(sig) = self.onchain_tx_handler.signer.sign_justice_revoked_htlc(
channel_parameters,
&justice_tx,
0,
htlc_value.to_sat(),
&per_commitment_key,
htlc,
&self.onchain_tx_handler.secp_ctx,
) {
let mut ser_sig = sig.serialize_der().to_vec();
ser_sig.push(EcdsaSighashType::All as u8);
justice_tx.input[0].witness.push(ser_sig);
justice_tx.input[0]
.witness
.push(keys.revocation_key.to_public_key().serialize().to_vec());
justice_tx.input[0].witness.push(witness_script.into_bytes());
result.push(JusticeTransaction {
tx: justice_tx,
revoked_commitment_txid: txid,
commitment_number,
});
}
}
}
}

result
}

fn get_min_seen_secret(&self) -> u64 {
self.commitment_secrets.get_min_seen_secret()
}
Expand Down Expand Up @@ -6696,6 +6937,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
let mut is_manual_broadcast = RequiredWrapper(None);
let mut funding_seen_onchain = RequiredWrapper(None);
let mut best_block_previous_blocks = None;
let mut cur_counterparty_commitment_tx: Option<CommitmentTransaction> = None;
let mut prev_counterparty_commitment_tx_deser: Option<CommitmentTransaction> = None;
read_tlv_fields!(reader, {
(1, funding_spend_confirmed, option),
(3, htlcs_resolved_on_chain, optional_vec),
Expand All @@ -6719,6 +6962,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
(35, is_manual_broadcast, (default_value, false)),
(37, funding_seen_onchain, (default_value, true)),
(39, best_block_previous_blocks, option), // Added and always set in 0.3
(43, cur_counterparty_commitment_tx, option),
(45, prev_counterparty_commitment_tx_deser, option),
});
if let Some(previous_blocks) = best_block_previous_blocks {
best_block.previous_blocks = previous_blocks;
Expand Down Expand Up @@ -6837,6 +7082,9 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP

current_holder_commitment_tx,
prev_holder_commitment_tx,

cur_counterparty_commitment_tx,
prev_counterparty_commitment_tx: prev_counterparty_commitment_tx_deser,
},
pending_funding: pending_funding.unwrap_or(vec![]),
is_manual_broadcast: is_manual_broadcast.0.unwrap(),
Expand Down
Loading
Loading