Skip to content

Commit d6c7903

Browse files
Add ChannelMonitor::get_justice_txs for simplified watchtower integration
Adds a single-call API that returns signed justice transactions for all revoked counterparty commitments, eliminating the need for Persist implementors to maintain external state between commitment receipt and revocation. - New JusticeTransaction struct and get_justice_txs() method - Stores recent counterparty CommitmentTransactions in ChannelMonitorImpl - TLV field 39 (optional, backwards-compatible) - Simplified test WatchtowerPersister (~80 lines of queue management removed) Implements the approach suggested by TheBlueMatt in ldk-node#813 and in review of #2552.
1 parent ec03159 commit d6c7903

File tree

2 files changed

+125
-84
lines changed

2 files changed

+125
-84
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,19 @@ impl_writeable_tlv_based!(HTLCUpdate, {
262262
(4, payment_preimage, option),
263263
});
264264

265+
/// A signed justice transaction ready for broadcast or watchtower submission.
266+
///
267+
/// Returned by [`ChannelMonitor::get_justice_txs`].
268+
#[derive(Clone, Debug)]
269+
pub struct JusticeTransaction {
270+
/// The fully signed justice transaction.
271+
pub tx: Transaction,
272+
/// The txid of the revoked counterparty commitment transaction.
273+
pub revoked_commitment_txid: Txid,
274+
/// The commitment number of the revoked commitment transaction.
275+
pub commitment_number: u64,
276+
}
277+
265278
/// If an output goes from claimable only by us to claimable by us or our counterparty within this
266279
/// many blocks, we consider it pinnable for the purposes of aggregating claims in a single
267280
/// transaction.
@@ -1372,6 +1385,13 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> {
13721385
/// we now provide the transaction outright.
13731386
initial_counterparty_commitment_tx: Option<CommitmentTransaction>,
13741387

1388+
/// The latest counterparty commitment transaction(s), stored so that justice
1389+
/// transactions can be built and signed in a single call via [`ChannelMonitor::get_justice_txs`].
1390+
/// Contains the current and previous counterparty commitment(s). With splicing,
1391+
/// there may be multiple entries per commitment number (one per funding scope).
1392+
/// Pruned to remove entries more than one revocation old.
1393+
latest_counterparty_commitment_txs: Vec<CommitmentTransaction>,
1394+
13751395
/// The first block height at which we had no remaining claimable balances.
13761396
balances_empty_height: Option<u32>,
13771397

@@ -1755,6 +1775,7 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>(
17551775
(34, channel_monitor.alternative_funding_confirmed, option),
17561776
(35, channel_monitor.is_manual_broadcast, required),
17571777
(37, channel_monitor.funding_seen_onchain, required),
1778+
(39, channel_monitor.latest_counterparty_commitment_txs, optional_vec),
17581779
});
17591780

17601781
Ok(())
@@ -1960,6 +1981,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
19601981
counterparty_node_id: counterparty_node_id,
19611982
initial_counterparty_commitment_info: None,
19621983
initial_counterparty_commitment_tx: None,
1984+
latest_counterparty_commitment_txs: Vec::new(),
19631985
balances_empty_height: None,
19641986

19651987
failed_back_htlc_ids: new_hash_set(),
@@ -2271,6 +2293,25 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
22712293
self.inner.lock().unwrap().sign_to_local_justice_tx(justice_tx, input_idx, value, commitment_number)
22722294
}
22732295

2296+
/// Returns signed justice transactions for all revoked counterparty commitment
2297+
/// transactions that this monitor knows about.
2298+
///
2299+
/// This is a convenience method that combines the functionality of
2300+
/// [`Self::counterparty_commitment_txs_from_update`], building justice transactions,
2301+
/// and [`Self::sign_to_local_justice_tx`] into a single call, eliminating the need
2302+
/// for callers to track intermediate state.
2303+
///
2304+
/// `feerate_per_kw` is used for the justice transaction fee calculation.
2305+
/// `destination_script` is the script where swept funds will be sent.
2306+
///
2307+
/// Returns a list of [`JusticeTransaction`]s, each containing a fully signed
2308+
/// transaction and metadata about the revoked commitment it punishes.
2309+
pub fn get_justice_txs(
2310+
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
2311+
) -> Vec<JusticeTransaction> {
2312+
self.inner.lock().unwrap().get_justice_txs(feerate_per_kw, destination_script)
2313+
}
2314+
22742315
pub(crate) fn get_min_seen_secret(&self) -> u64 {
22752316
self.inner.lock().unwrap().get_min_seen_secret()
22762317
}
@@ -3483,6 +3524,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
34833524
self.provide_latest_counterparty_commitment_tx(commitment_tx.trust().txid(), Vec::new(), commitment_tx.commitment_number(),
34843525
commitment_tx.per_commitment_point());
34853526
// Soon, we will only populate this field
3527+
self.latest_counterparty_commitment_txs = vec![commitment_tx.clone()];
34863528
self.initial_counterparty_commitment_tx = Some(commitment_tx);
34873529
}
34883530

@@ -4284,8 +4326,21 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
42844326
}
42854327
}
42864328

4287-
#[cfg(debug_assertions)] {
4288-
self.counterparty_commitment_txs_from_update(updates);
4329+
let new_commitment_txs = self.counterparty_commitment_txs_from_update(updates);
4330+
if !new_commitment_txs.is_empty() {
4331+
self.latest_counterparty_commitment_txs.extend(new_commitment_txs);
4332+
}
4333+
// Prune commitment txs that are two or more revocations old. We keep one
4334+
// revocation depth so that get_justice_txs can sign the just-revoked
4335+
// commitment during this update_persisted_channel call.
4336+
if self.latest_counterparty_commitment_txs.len() > 1 {
4337+
let current = self.current_counterparty_commitment_number;
4338+
self.latest_counterparty_commitment_txs.retain(|tx| {
4339+
// Commitment numbers count down. Keep entries within 1 of current
4340+
// (current and the one just prior, which may have just been revoked).
4341+
// Also keep anything with a matching number (splicing).
4342+
tx.commitment_number() <= current + 1
4343+
});
42894344
}
42904345

42914346
self.latest_update_id = updates.update_id;
@@ -4563,6 +4618,52 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
45634618
Ok(justice_tx)
45644619
}
45654620

4621+
fn get_justice_txs(
4622+
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
4623+
) -> Vec<JusticeTransaction> {
4624+
let mut result = Vec::new();
4625+
4626+
for commitment_tx in &self.latest_counterparty_commitment_txs {
4627+
let commitment_number = commitment_tx.commitment_number();
4628+
4629+
// Check if we have the revocation secret (i.e., this commitment is revoked)
4630+
let _secret = match self.get_secret(commitment_number) {
4631+
Some(s) => s,
4632+
None => continue,
4633+
};
4634+
4635+
let trusted = commitment_tx.trust();
4636+
let output_idx = match trusted.revokeable_output_index() {
4637+
Some(idx) => idx,
4638+
None => continue,
4639+
};
4640+
4641+
let built = trusted.built_transaction();
4642+
let value = built.transaction.output[output_idx].value;
4643+
let txid = built.txid;
4644+
4645+
let justice_tx = match trusted
4646+
.build_to_local_justice_tx(feerate_per_kw, destination_script.clone())
4647+
{
4648+
Ok(tx) => tx,
4649+
Err(_) => continue,
4650+
};
4651+
4652+
match self.sign_to_local_justice_tx(justice_tx, 0, value.to_sat(), commitment_number) {
4653+
Ok(signed_tx) => {
4654+
result.push(JusticeTransaction {
4655+
tx: signed_tx,
4656+
revoked_commitment_txid: txid,
4657+
commitment_number,
4658+
});
4659+
},
4660+
Err(_) => continue,
4661+
}
4662+
}
4663+
4664+
result
4665+
}
4666+
45664667
/// Can only fail if idx is < get_min_seen_secret
45674668
fn get_secret(&self, idx: u64) -> Option<[u8; 32]> {
45684669
self.commitment_secrets.get_secret(idx)
@@ -6521,6 +6622,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
65216622
let mut alternative_funding_confirmed = None;
65226623
let mut is_manual_broadcast = RequiredWrapper(None);
65236624
let mut funding_seen_onchain = RequiredWrapper(None);
6625+
let mut latest_counterparty_commitment_txs: Option<Vec<CommitmentTransaction>> = Some(Vec::new());
65246626
read_tlv_fields!(reader, {
65256627
(1, funding_spend_confirmed, option),
65266628
(3, htlcs_resolved_on_chain, optional_vec),
@@ -6543,6 +6645,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
65436645
(34, alternative_funding_confirmed, option),
65446646
(35, is_manual_broadcast, (default_value, false)),
65456647
(37, funding_seen_onchain, (default_value, true)),
6648+
(39, latest_counterparty_commitment_txs, optional_vec),
65466649
});
65476650
// Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so
65486651
// we can use it to determine if this monitor was last written by LDK 0.1 or later.
@@ -6714,6 +6817,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
67146817
counterparty_node_id: counterparty_node_id.unwrap_or(dummy_node_id),
67156818
initial_counterparty_commitment_info,
67166819
initial_counterparty_commitment_tx,
6820+
latest_counterparty_commitment_txs: latest_counterparty_commitment_txs.unwrap_or_default(),
67176821
balances_empty_height,
67186822
failed_back_htlc_ids: new_hash_set(),
67196823

lightning/src/util/test_utils.rs

Lines changed: 19 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ use crate::chain::channelmonitor::{
2121
};
2222
use crate::chain::transaction::OutPoint;
2323
use crate::chain::WatchedOutput;
24-
#[cfg(any(test, feature = "_externalize_tests"))]
25-
use crate::ln::chan_utils::CommitmentTransaction;
2624
use crate::ln::channel_state::ChannelDetails;
2725
use crate::ln::channelmanager;
2826
use crate::ln::inbound_payment::ExpandedKey;
@@ -679,21 +677,9 @@ impl<'a> chain::Watch<TestChannelSigner> for TestChainMonitor<'a> {
679677
}
680678
}
681679

682-
#[cfg(any(test, feature = "_externalize_tests"))]
683-
struct JusticeTxData {
684-
justice_tx: Transaction,
685-
value: Amount,
686-
commitment_number: u64,
687-
}
688-
689680
#[cfg(any(test, feature = "_externalize_tests"))]
690681
pub(crate) struct WatchtowerPersister {
691682
persister: TestPersister,
692-
/// Upon a new commitment_signed, we'll get a
693-
/// ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTxInfo. We'll store the justice tx
694-
/// amount, and commitment number so we can build the justice tx after our counterparty
695-
/// revokes it.
696-
unsigned_justice_tx_data: Mutex<HashMap<ChannelId, VecDeque<JusticeTxData>>>,
697683
/// After receiving a revoke_and_ack for a commitment number, we'll form and store the justice
698684
/// tx which would be used to provide a watchtower with the data it needs.
699685
watchtower_state: Mutex<HashMap<ChannelId, HashMap<Txid, Transaction>>>,
@@ -703,12 +689,9 @@ pub(crate) struct WatchtowerPersister {
703689
#[cfg(any(test, feature = "_externalize_tests"))]
704690
impl WatchtowerPersister {
705691
pub(crate) fn new(destination_script: ScriptBuf) -> Self {
706-
let unsigned_justice_tx_data = Mutex::new(new_hash_map());
707-
let watchtower_state = Mutex::new(new_hash_map());
708692
WatchtowerPersister {
709693
persister: TestPersister::new(),
710-
unsigned_justice_tx_data,
711-
watchtower_state,
694+
watchtower_state: Mutex::new(new_hash_map()),
712695
destination_script,
713696
}
714697
}
@@ -724,23 +707,6 @@ impl WatchtowerPersister {
724707
.get(commitment_txid)
725708
.cloned()
726709
}
727-
728-
fn form_justice_data_from_commitment(
729-
&self, counterparty_commitment_tx: &CommitmentTransaction,
730-
) -> Option<JusticeTxData> {
731-
let trusted_tx = counterparty_commitment_tx.trust();
732-
let output_idx = trusted_tx.revokeable_output_index()?;
733-
let built_tx = trusted_tx.built_transaction();
734-
let value = built_tx.transaction.output[output_idx as usize].value;
735-
let justice_tx = trusted_tx
736-
.build_to_local_justice_tx(
737-
FEERATE_FLOOR_SATS_PER_KW as u64,
738-
self.destination_script.clone(),
739-
)
740-
.ok()?;
741-
let commitment_number = counterparty_commitment_tx.commitment_number();
742-
Some(JusticeTxData { justice_tx, value, commitment_number })
743-
}
744710
}
745711

746712
#[cfg(any(test, feature = "_externalize_tests"))]
@@ -750,31 +716,25 @@ impl<Signer: sign::ecdsa::EcdsaChannelSigner> Persist<Signer> for WatchtowerPers
750716
) -> chain::ChannelMonitorUpdateStatus {
751717
let res = self.persister.persist_new_channel(monitor_name, data);
752718

753-
assert!(self
754-
.unsigned_justice_tx_data
755-
.lock()
756-
.unwrap()
757-
.insert(data.channel_id(), VecDeque::new())
758-
.is_none());
759719
assert!(self
760720
.watchtower_state
761721
.lock()
762722
.unwrap()
763723
.insert(data.channel_id(), new_hash_map())
764724
.is_none());
765725

766-
let initial_counterparty_commitment_tx =
767-
data.initial_counterparty_commitment_tx().expect("First and only call expects Some");
768-
if let Some(justice_data) =
769-
self.form_justice_data_from_commitment(&initial_counterparty_commitment_tx)
770-
{
771-
self.unsigned_justice_tx_data
726+
// Use the simplified get_justice_txs API
727+
let justice_txs =
728+
data.get_justice_txs(FEERATE_FLOOR_SATS_PER_KW as u64, self.destination_script.clone());
729+
for jtx in justice_txs {
730+
self.watchtower_state
772731
.lock()
773732
.unwrap()
774733
.get_mut(&data.channel_id())
775734
.unwrap()
776-
.push_back(justice_data);
735+
.insert(jtx.revoked_commitment_txid, jtx.tx);
777736
}
737+
778738
res
779739
}
780740

@@ -784,41 +744,18 @@ impl<Signer: sign::ecdsa::EcdsaChannelSigner> Persist<Signer> for WatchtowerPers
784744
) -> chain::ChannelMonitorUpdateStatus {
785745
let res = self.persister.update_persisted_channel(monitor_name, update, data);
786746

787-
if let Some(update) = update {
788-
let commitment_txs = data.counterparty_commitment_txs_from_update(update);
789-
let justice_datas = commitment_txs
790-
.into_iter()
791-
.filter_map(|commitment_tx| self.form_justice_data_from_commitment(&commitment_tx));
792-
let mut channels_justice_txs = self.unsigned_justice_tx_data.lock().unwrap();
793-
let channel_state = channels_justice_txs.get_mut(&data.channel_id()).unwrap();
794-
channel_state.extend(justice_datas);
795-
796-
while let Some(JusticeTxData { justice_tx, value, commitment_number }) =
797-
channel_state.front()
798-
{
799-
let input_idx = 0;
800-
let commitment_txid = justice_tx.input[input_idx].previous_output.txid;
801-
match data.sign_to_local_justice_tx(
802-
justice_tx.clone(),
803-
input_idx,
804-
value.to_sat(),
805-
*commitment_number,
806-
) {
807-
Ok(signed_justice_tx) => {
808-
let dup = self
809-
.watchtower_state
810-
.lock()
811-
.unwrap()
812-
.get_mut(&data.channel_id())
813-
.unwrap()
814-
.insert(commitment_txid, signed_justice_tx);
815-
assert!(dup.is_none());
816-
channel_state.pop_front();
817-
},
818-
Err(_) => break,
819-
}
820-
}
747+
// Use the simplified get_justice_txs API
748+
let justice_txs =
749+
data.get_justice_txs(FEERATE_FLOOR_SATS_PER_KW as u64, self.destination_script.clone());
750+
for jtx in justice_txs {
751+
self.watchtower_state
752+
.lock()
753+
.unwrap()
754+
.get_mut(&data.channel_id())
755+
.unwrap()
756+
.insert(jtx.revoked_commitment_txid, jtx.tx);
821757
}
758+
822759
res
823760
}
824761

0 commit comments

Comments
 (0)