Skip to content

Commit a9a7221

Browse files
Add ChannelMonitor justice tx API for simplified watchtower integration
Adds sign_initial_justice_txs(), sign_justice_txs_from_update(), and get_pending_justice_txs() to ChannelMonitor, enabling Persist implementors to obtain signed justice transactions for both to_local and HTLC outputs without maintaining external state. Storage uses cur/prev counterparty commitment fields on FundingScope, matching the existing pattern and supporting splicing. The API is crash-safe: commitment data is cloned rather than consumed, and get_pending_justice_txs() allows recovery after restart. Simplifies WatchtowerPersister in test_utils by removing manual queue and signing logic. Addresses feedback from lightningdevkit/ldk-node#813 and picks up the intent of #2552.
1 parent db42ad6 commit a9a7221

8 files changed

Lines changed: 499 additions & 149 deletions

File tree

lightning/src/chain/channelmonitor.rs

Lines changed: 252 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
2323
use bitcoin::amount::Amount;
2424
use bitcoin::block::Header;
25+
use bitcoin::locktime::absolute::LockTime;
2526
use bitcoin::script::{Script, ScriptBuf};
26-
use bitcoin::transaction::{OutPoint as BitcoinOutPoint, Transaction, TxOut};
27+
use bitcoin::transaction::{OutPoint as BitcoinOutPoint, Transaction, TxIn, TxOut, Version};
28+
use bitcoin::{Sequence, Witness};
2729

2830
use bitcoin::hash_types::{BlockHash, Txid};
2931
use bitcoin::hashes::sha256::Hash as Sha256;
3032
use bitcoin::hashes::Hash;
3133

3234
use bitcoin::ecdsa::Signature as BitcoinSignature;
3335
use bitcoin::secp256k1::{self, ecdsa::Signature, PublicKey, Secp256k1, SecretKey};
36+
use bitcoin::sighash::EcdsaSighashType;
3437

3538
use crate::chain;
3639
use crate::chain::chaininterface::{
@@ -47,7 +50,7 @@ use crate::events::bump_transaction::{AnchorDescriptor, BumpTransactionEvent};
4750
use crate::events::{ClosureReason, Event, EventHandler, ReplayEvent};
4851
use crate::ln::chan_utils::{
4952
self, ChannelTransactionParameters, CommitmentTransaction, CounterpartyCommitmentSecrets,
50-
HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction,
53+
HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction, TxCreationKeys,
5154
};
5255
use crate::ln::channel::INITIAL_COMMITMENT_NUMBER;
5356
use crate::ln::channel_keys::{
@@ -141,6 +144,20 @@ impl ChannelMonitorUpdate {
141144
pub fn renegotiated_funding_data(&self) -> impl Iterator<Item = (OutPoint, ScriptBuf)> + '_ {
142145
self.internal_renegotiated_funding_data()
143146
}
147+
148+
/// Returns `true` if this update contains counterparty commitment data
149+
/// relevant to a watchtower (a new commitment or a revocation secret).
150+
pub fn updates_watchtower_state(&self) -> bool {
151+
self.updates.iter().any(|step| {
152+
matches!(
153+
step,
154+
ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { .. }
155+
| ChannelMonitorUpdateStep::LatestCounterpartyCommitment { .. }
156+
| ChannelMonitorUpdateStep::CommitmentSecret { .. }
157+
| ChannelMonitorUpdateStep::RenegotiatedFunding { .. }
158+
)
159+
})
160+
}
144161
}
145162

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

282+
/// A signed justice transaction ready for broadcast or watchtower submission.
283+
#[derive(Clone, Debug)]
284+
pub struct JusticeTransaction {
285+
/// The fully signed justice transaction.
286+
pub tx: Transaction,
287+
/// The txid of the revoked counterparty commitment transaction.
288+
pub revoked_commitment_txid: Txid,
289+
/// The commitment number of the revoked commitment transaction.
290+
pub commitment_number: u64,
291+
}
292+
265293
/// If an output goes from claimable only by us to claimable by us or our counterparty within this
266294
/// many blocks, we consider it pinnable for the purposes of aggregating claims in a single
267295
/// transaction.
@@ -1166,6 +1194,11 @@ struct FundingScope {
11661194
// transaction for which we have deleted claim information on some watchtowers.
11671195
current_holder_commitment_tx: HolderCommitmentTransaction,
11681196
prev_holder_commitment_tx: Option<HolderCommitmentTransaction>,
1197+
1198+
/// The current counterparty commitment transaction, stored for justice tx signing.
1199+
cur_counterparty_commitment_tx: Option<CommitmentTransaction>,
1200+
/// The previous counterparty commitment transaction, stored for justice tx signing.
1201+
prev_counterparty_commitment_tx: Option<CommitmentTransaction>,
11691202
}
11701203

11711204
impl FundingScope {
@@ -1194,6 +1227,8 @@ impl_writeable_tlv_based!(FundingScope, {
11941227
(7, current_holder_commitment_tx, required),
11951228
(9, prev_holder_commitment_tx, option),
11961229
(11, counterparty_claimable_outpoints, required),
1230+
(13, cur_counterparty_commitment_tx, option),
1231+
(15, prev_counterparty_commitment_tx, option),
11971232
});
11981233

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

17611798
Ok(())
@@ -1905,6 +1942,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
19051942

19061943
current_holder_commitment_tx: initial_holder_commitment_tx,
19071944
prev_holder_commitment_tx: None,
1945+
1946+
cur_counterparty_commitment_tx: None,
1947+
prev_counterparty_commitment_tx: None,
19081948
},
19091949
pending_funding: vec![],
19101950

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

2315+
/// Returns signed justice transactions for all revoked counterparty commitments
2316+
/// currently stored in this monitor.
2317+
///
2318+
/// Call this after persisting the monitor when
2319+
/// [`ChannelMonitorUpdate::updates_watchtower_state`] returns `true`. Also call on
2320+
/// startup for each loaded monitor to recover any justice transactions not yet
2321+
/// delivered to a watchtower.
2322+
///
2323+
/// To avoid losing justice data when the watchtower is unreachable, the
2324+
/// [`Persist`] implementation should delay completing monitor updates until
2325+
/// previously obtained justice transactions have been delivered.
2326+
///
2327+
/// Idempotent: returns the same results on repeated calls for the same monitor state.
2328+
///
2329+
/// [`Persist`]: crate::chain::chainmonitor::Persist
2330+
pub fn get_pending_justice_txs(
2331+
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
2332+
) -> Vec<JusticeTransaction> {
2333+
self.inner.lock().unwrap().get_pending_justice_txs(feerate_per_kw, destination_script)
2334+
}
2335+
22752336
pub(crate) fn get_min_seen_secret(&self) -> u64 {
22762337
self.inner.lock().unwrap().get_min_seen_secret()
22772338
}
@@ -3486,6 +3547,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
34863547
self.provide_latest_counterparty_commitment_tx(commitment_tx.trust().txid(), Vec::new(), commitment_tx.commitment_number(),
34873548
commitment_tx.per_commitment_point());
34883549
// Soon, we will only populate this field
3550+
self.funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
34893551
self.initial_counterparty_commitment_tx = Some(commitment_tx);
34903552
}
34913553

@@ -3563,6 +3625,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35633625
current_funding_commitment_tx.commitment_number(),
35643626
current_funding_commitment_tx.per_commitment_point(),
35653627
);
3628+
self.funding.prev_counterparty_commitment_tx =
3629+
self.funding.cur_counterparty_commitment_tx.take();
3630+
self.funding.cur_counterparty_commitment_tx = Some(current_funding_commitment_tx.clone());
35663631

35673632
for (pending_funding, commitment_tx) in
35683633
self.pending_funding.iter_mut().zip(commitment_txs.iter().skip(1))
@@ -3574,6 +3639,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35743639
pending_funding
35753640
.counterparty_claimable_outpoints
35763641
.insert(commitment_txid, htlcs_for_commitment(commitment_tx));
3642+
pending_funding.prev_counterparty_commitment_tx =
3643+
pending_funding.cur_counterparty_commitment_tx.take();
3644+
pending_funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
35773645
}
35783646

35793647
Ok(())
@@ -4025,6 +4093,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
40254093
counterparty_claimable_outpoints,
40264094
current_holder_commitment_tx: alternative_holder_commitment_tx.clone(),
40274095
prev_holder_commitment_tx: None,
4096+
4097+
cur_counterparty_commitment_tx: None,
4098+
prev_counterparty_commitment_tx: None,
40284099
};
40294100
let alternative_funding_outpoint = alternative_funding.funding_outpoint();
40304101

@@ -4294,8 +4365,21 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
42944365
}
42954366
}
42964367

4297-
#[cfg(debug_assertions)] {
4298-
self.counterparty_commitment_txs_from_update(updates);
4368+
// Populate cur/prev for the LatestCounterpartyCommitmentTXInfo path, which
4369+
// doesn't go through update_counterparty_commitment_data.
4370+
for commitment_tx in self.counterparty_commitment_txs_from_update(updates) {
4371+
let txid = commitment_tx.trust().built_transaction().txid;
4372+
let funding = core::iter::once(&mut self.funding)
4373+
.chain(self.pending_funding.iter_mut())
4374+
.find(|f| f.current_counterparty_commitment_txid == Some(txid));
4375+
if let Some(funding) = funding {
4376+
if funding.cur_counterparty_commitment_tx.as_ref()
4377+
.map(|c| c.trust().built_transaction().txid) != Some(txid)
4378+
{
4379+
funding.prev_counterparty_commitment_tx = funding.cur_counterparty_commitment_tx.take();
4380+
funding.cur_counterparty_commitment_tx = Some(commitment_tx);
4381+
}
4382+
}
42994383
}
43004384

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

4829+
/// Returns signed justice transactions for all revoked counterparty commitments
4830+
/// currently stored in this monitor. Idempotent.
4831+
fn get_pending_justice_txs(
4832+
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
4833+
) -> Vec<JusticeTransaction> {
4834+
let mut result = Vec::new();
4835+
for funding in core::iter::once(&self.funding).chain(self.pending_funding.iter()) {
4836+
if let Some(ref prev) = funding.prev_counterparty_commitment_tx {
4837+
if self.commitment_secrets.get_secret(prev.commitment_number()).is_some() {
4838+
result.extend(self.try_sign_justice_txs(
4839+
prev,
4840+
feerate_per_kw,
4841+
destination_script.clone(),
4842+
));
4843+
}
4844+
}
4845+
}
4846+
result
4847+
}
4848+
4849+
fn try_sign_justice_txs(
4850+
&self, commitment_tx: &CommitmentTransaction, feerate_per_kw: u64,
4851+
destination_script: ScriptBuf,
4852+
) -> Vec<JusticeTransaction> {
4853+
let commitment_number = commitment_tx.commitment_number();
4854+
let secret = match self.get_secret(commitment_number) {
4855+
Some(s) => s,
4856+
None => return Vec::new(),
4857+
};
4858+
let per_commitment_key = match SecretKey::from_slice(&secret) {
4859+
Ok(k) => k,
4860+
Err(_) => return Vec::new(),
4861+
};
4862+
4863+
let trusted = commitment_tx.trust();
4864+
let built = trusted.built_transaction();
4865+
let txid = built.txid;
4866+
let mut result = Vec::new();
4867+
4868+
// to_local justice tx
4869+
if let Some(output_idx) = trusted.revokeable_output_index() {
4870+
let value = built.transaction.output[output_idx].value;
4871+
if let Ok(justice_tx) =
4872+
trusted.build_to_local_justice_tx(feerate_per_kw, destination_script.clone())
4873+
{
4874+
if let Ok(signed) =
4875+
self.sign_to_local_justice_tx(justice_tx, 0, value.to_sat(), commitment_number)
4876+
{
4877+
result.push(JusticeTransaction {
4878+
tx: signed,
4879+
revoked_commitment_txid: txid,
4880+
commitment_number,
4881+
});
4882+
}
4883+
}
4884+
}
4885+
4886+
// HTLC justice txs
4887+
let channel_parameters = core::iter::once(&self.funding)
4888+
.chain(&self.pending_funding)
4889+
.find(|funding| funding.counterparty_claimable_outpoints.contains_key(&txid))
4890+
.map(|funding| &funding.channel_parameters);
4891+
if let Some(channel_parameters) = channel_parameters {
4892+
let per_commitment_point =
4893+
PublicKey::from_secret_key(&self.onchain_tx_handler.secp_ctx, &per_commitment_key);
4894+
let directed = channel_parameters.as_counterparty_broadcastable();
4895+
let keys = TxCreationKeys::from_channel_static_keys(
4896+
&per_commitment_point,
4897+
directed.broadcaster_pubkeys(),
4898+
directed.countersignatory_pubkeys(),
4899+
&self.onchain_tx_handler.secp_ctx,
4900+
);
4901+
4902+
for htlc in commitment_tx.nondust_htlcs() {
4903+
if let Some(output_index) = htlc.transaction_output_index {
4904+
let htlc_value = built.transaction.output[output_index as usize].value;
4905+
let witness_script = chan_utils::get_htlc_redeemscript(
4906+
htlc,
4907+
&channel_parameters.channel_type_features,
4908+
&keys,
4909+
);
4910+
4911+
// Build a spending tx for this HTLC output
4912+
let input = vec![TxIn {
4913+
previous_output: bitcoin::OutPoint { txid, vout: output_index },
4914+
script_sig: ScriptBuf::new(),
4915+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
4916+
witness: Witness::new(),
4917+
}];
4918+
let weight_estimate = if htlc.offered {
4919+
crate::chain::package::weight_revoked_offered_htlc(
4920+
&channel_parameters.channel_type_features,
4921+
)
4922+
} else {
4923+
crate::chain::package::weight_revoked_received_htlc(
4924+
&channel_parameters.channel_type_features,
4925+
)
4926+
};
4927+
let fee = Amount::from_sat(crate::chain::chaininterface::fee_for_weight(
4928+
feerate_per_kw as u32,
4929+
// Base tx weight + witness weight
4930+
Transaction {
4931+
version: Version::TWO,
4932+
lock_time: LockTime::ZERO,
4933+
input: input.clone(),
4934+
output: vec![TxOut {
4935+
script_pubkey: destination_script.clone(),
4936+
value: htlc_value,
4937+
}],
4938+
}
4939+
.weight()
4940+
.to_wu() + weight_estimate,
4941+
));
4942+
let output_value = match htlc_value.checked_sub(fee) {
4943+
Some(v) => v,
4944+
None => continue, // Dust, skip
4945+
};
4946+
4947+
let mut justice_tx = Transaction {
4948+
version: Version::TWO,
4949+
lock_time: LockTime::ZERO,
4950+
input,
4951+
output: vec![TxOut {
4952+
script_pubkey: destination_script.clone(),
4953+
value: output_value,
4954+
}],
4955+
};
4956+
4957+
if let Ok(sig) = self.onchain_tx_handler.signer.sign_justice_revoked_htlc(
4958+
channel_parameters,
4959+
&justice_tx,
4960+
0,
4961+
htlc_value.to_sat(),
4962+
&per_commitment_key,
4963+
htlc,
4964+
&self.onchain_tx_handler.secp_ctx,
4965+
) {
4966+
let mut ser_sig = sig.serialize_der().to_vec();
4967+
ser_sig.push(EcdsaSighashType::All as u8);
4968+
justice_tx.input[0].witness.push(ser_sig);
4969+
justice_tx.input[0]
4970+
.witness
4971+
.push(keys.revocation_key.to_public_key().serialize().to_vec());
4972+
justice_tx.input[0].witness.push(witness_script.into_bytes());
4973+
result.push(JusticeTransaction {
4974+
tx: justice_tx,
4975+
revoked_commitment_txid: txid,
4976+
commitment_number,
4977+
});
4978+
}
4979+
}
4980+
}
4981+
}
4982+
4983+
result
4984+
}
4985+
47454986
fn get_min_seen_secret(&self) -> u64 {
47464987
self.commitment_secrets.get_min_seen_secret()
47474988
}
@@ -6696,6 +6937,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
66966937
let mut is_manual_broadcast = RequiredWrapper(None);
66976938
let mut funding_seen_onchain = RequiredWrapper(None);
66986939
let mut best_block_previous_blocks = None;
6940+
let mut cur_counterparty_commitment_tx: Option<CommitmentTransaction> = None;
6941+
let mut prev_counterparty_commitment_tx_deser: Option<CommitmentTransaction> = None;
66996942
read_tlv_fields!(reader, {
67006943
(1, funding_spend_confirmed, option),
67016944
(3, htlcs_resolved_on_chain, optional_vec),
@@ -6719,6 +6962,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
67196962
(35, is_manual_broadcast, (default_value, false)),
67206963
(37, funding_seen_onchain, (default_value, true)),
67216964
(39, best_block_previous_blocks, option), // Added and always set in 0.3
6965+
(43, cur_counterparty_commitment_tx, option),
6966+
(45, prev_counterparty_commitment_tx_deser, option),
67226967
});
67236968
if let Some(previous_blocks) = best_block_previous_blocks {
67246969
best_block.previous_blocks = previous_blocks;
@@ -6837,6 +7082,9 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
68377082

68387083
current_holder_commitment_tx,
68397084
prev_holder_commitment_tx,
7085+
7086+
cur_counterparty_commitment_tx,
7087+
prev_counterparty_commitment_tx: prev_counterparty_commitment_tx_deser,
68407088
},
68417089
pending_funding: pending_funding.unwrap_or(vec![]),
68427090
is_manual_broadcast: is_manual_broadcast.0.unwrap(),

0 commit comments

Comments
 (0)