Skip to content

Commit de4d6c4

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 lightningdevkit#2552.
1 parent db42ad6 commit de4d6c4

8 files changed

Lines changed: 495 additions & 151 deletions

File tree

lightning/src/chain/channelmonitor.rs

Lines changed: 246 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,21 @@ 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+
/// Idempotent: returns the same results on repeated calls for the same monitor state.
2324+
pub fn get_pending_justice_txs(
2325+
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
2326+
) -> Vec<JusticeTransaction> {
2327+
self.inner.lock().unwrap().get_pending_justice_txs(feerate_per_kw, destination_script)
2328+
}
2329+
22752330
pub(crate) fn get_min_seen_secret(&self) -> u64 {
22762331
self.inner.lock().unwrap().get_min_seen_secret()
22772332
}
@@ -3486,6 +3541,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
34863541
self.provide_latest_counterparty_commitment_tx(commitment_tx.trust().txid(), Vec::new(), commitment_tx.commitment_number(),
34873542
commitment_tx.per_commitment_point());
34883543
// Soon, we will only populate this field
3544+
self.funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
34893545
self.initial_counterparty_commitment_tx = Some(commitment_tx);
34903546
}
34913547

@@ -3563,6 +3619,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35633619
current_funding_commitment_tx.commitment_number(),
35643620
current_funding_commitment_tx.per_commitment_point(),
35653621
);
3622+
self.funding.prev_counterparty_commitment_tx =
3623+
self.funding.cur_counterparty_commitment_tx.take();
3624+
self.funding.cur_counterparty_commitment_tx = Some(current_funding_commitment_tx.clone());
35663625

35673626
for (pending_funding, commitment_tx) in
35683627
self.pending_funding.iter_mut().zip(commitment_txs.iter().skip(1))
@@ -3574,6 +3633,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35743633
pending_funding
35753634
.counterparty_claimable_outpoints
35763635
.insert(commitment_txid, htlcs_for_commitment(commitment_tx));
3636+
pending_funding.prev_counterparty_commitment_tx =
3637+
pending_funding.cur_counterparty_commitment_tx.take();
3638+
pending_funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
35773639
}
35783640

35793641
Ok(())
@@ -4025,6 +4087,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
40254087
counterparty_claimable_outpoints,
40264088
current_holder_commitment_tx: alternative_holder_commitment_tx.clone(),
40274089
prev_holder_commitment_tx: None,
4090+
4091+
cur_counterparty_commitment_tx: None,
4092+
prev_counterparty_commitment_tx: None,
40284093
};
40294094
let alternative_funding_outpoint = alternative_funding.funding_outpoint();
40304095

@@ -4294,8 +4359,21 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
42944359
}
42954360
}
42964361

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

43014379
self.latest_update_id = updates.update_id;
@@ -4742,6 +4820,163 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
47424820
self.commitment_secrets.get_secret(idx)
47434821
}
47444822

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

68387077
current_holder_commitment_tx,
68397078
prev_holder_commitment_tx,
7079+
7080+
cur_counterparty_commitment_tx,
7081+
prev_counterparty_commitment_tx: prev_counterparty_commitment_tx_deser,
68407082
},
68417083
pending_funding: pending_funding.unwrap_or(vec![]),
68427084
is_manual_broadcast: is_manual_broadcast.0.unwrap(),

0 commit comments

Comments
 (0)