Skip to content

Commit 034c377

Browse files
Add ChannelMonitor justice tx API for simplified watchtower integration
Adds sign_initial_justice_tx() and sign_justice_txs_from_update() to ChannelMonitor, allowing Persist implementors to obtain signed justice transactions without maintaining external state. Storage uses cur/prev counterparty commitment fields on FundingScope, matching the existing pattern and supporting splicing. Simplifies WatchtowerPersister in test_utils by removing the manual queue and signing logic. Addresses feedback from lightningdevkit/ldk-node#813 and picks up the intent of #2552.
1 parent ec03159 commit 034c377

File tree

2 files changed

+158
-80
lines changed

2 files changed

+158
-80
lines changed

lightning/src/chain/channelmonitor.rs

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

265+
/// A signed justice transaction ready for broadcast or watchtower submission.
266+
#[derive(Clone, Debug)]
267+
pub struct JusticeTransaction {
268+
/// The fully signed justice transaction.
269+
pub tx: Transaction,
270+
/// The txid of the revoked counterparty commitment transaction.
271+
pub revoked_commitment_txid: Txid,
272+
/// The commitment number of the revoked commitment transaction.
273+
pub commitment_number: u64,
274+
}
275+
265276
/// If an output goes from claimable only by us to claimable by us or our counterparty within this
266277
/// many blocks, we consider it pinnable for the purposes of aggregating claims in a single
267278
/// transaction.
@@ -1166,6 +1177,11 @@ struct FundingScope {
11661177
// transaction for which we have deleted claim information on some watchtowers.
11671178
current_holder_commitment_tx: HolderCommitmentTransaction,
11681179
prev_holder_commitment_tx: Option<HolderCommitmentTransaction>,
1180+
1181+
/// The current counterparty commitment transaction, stored for justice tx signing.
1182+
cur_counterparty_commitment_tx: Option<CommitmentTransaction>,
1183+
/// The previous counterparty commitment transaction, stored for justice tx signing.
1184+
prev_counterparty_commitment_tx: Option<CommitmentTransaction>,
11691185
}
11701186

11711187
impl FundingScope {
@@ -1194,6 +1210,8 @@ impl_writeable_tlv_based!(FundingScope, {
11941210
(7, current_holder_commitment_tx, required),
11951211
(9, prev_holder_commitment_tx, option),
11961212
(11, counterparty_claimable_outpoints, required),
1213+
(13, cur_counterparty_commitment_tx, option),
1214+
(15, prev_counterparty_commitment_tx, option),
11971215
});
11981216

11991217
#[derive(Clone, PartialEq)]
@@ -1755,6 +1773,8 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>(
17551773
(34, channel_monitor.alternative_funding_confirmed, option),
17561774
(35, channel_monitor.is_manual_broadcast, required),
17571775
(37, channel_monitor.funding_seen_onchain, required),
1776+
(39, channel_monitor.funding.cur_counterparty_commitment_tx, option),
1777+
(41, channel_monitor.funding.prev_counterparty_commitment_tx, option),
17581778
});
17591779

17601780
Ok(())
@@ -1904,6 +1924,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
19041924

19051925
current_holder_commitment_tx: initial_holder_commitment_tx,
19061926
prev_holder_commitment_tx: None,
1927+
1928+
cur_counterparty_commitment_tx: None,
1929+
prev_counterparty_commitment_tx: None,
19071930
},
19081931
pending_funding: vec![],
19091932

@@ -2271,6 +2294,37 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
22712294
self.inner.lock().unwrap().sign_to_local_justice_tx(justice_tx, input_idx, value, commitment_number)
22722295
}
22732296

2297+
/// Stores the initial counterparty commitment and returns a signed justice transaction
2298+
/// if the commitment has already been revoked, or `None` otherwise.
2299+
///
2300+
/// Intended to be called during [`Persist::persist_new_channel`].
2301+
///
2302+
/// [`Persist::persist_new_channel`]: crate::chain::chainmonitor::Persist::persist_new_channel
2303+
pub fn sign_initial_justice_tx(
2304+
&self, feerate_per_kw: u64, destination_script: ScriptBuf,
2305+
) -> Option<JusticeTransaction> {
2306+
self.inner.lock().unwrap().sign_initial_justice_tx(feerate_per_kw, destination_script)
2307+
}
2308+
2309+
/// Returns signed justice transactions for any counterparty commitments that
2310+
/// became revoked as a result of applying the given update.
2311+
///
2312+
/// Stores new counterparty commitment(s) from the update and signs any
2313+
/// previously-stored commitments whose revocation secrets are now available.
2314+
///
2315+
/// Intended to be called during [`Persist::update_persisted_channel`].
2316+
///
2317+
/// [`Persist::update_persisted_channel`]: crate::chain::chainmonitor::Persist::update_persisted_channel
2318+
pub fn sign_justice_txs_from_update(
2319+
&self, update: &ChannelMonitorUpdate, feerate_per_kw: u64, destination_script: ScriptBuf,
2320+
) -> Vec<JusticeTransaction> {
2321+
self.inner.lock().unwrap().sign_justice_txs_from_update(
2322+
update,
2323+
feerate_per_kw,
2324+
destination_script,
2325+
)
2326+
}
2327+
22742328
pub(crate) fn get_min_seen_secret(&self) -> u64 {
22752329
self.inner.lock().unwrap().get_min_seen_secret()
22762330
}
@@ -3482,6 +3536,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
34823536

34833537
self.provide_latest_counterparty_commitment_tx(commitment_tx.trust().txid(), Vec::new(), commitment_tx.commitment_number(),
34843538
commitment_tx.per_commitment_point());
3539+
self.funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
34853540
// Soon, we will only populate this field
34863541
self.initial_counterparty_commitment_tx = Some(commitment_tx);
34873542
}
@@ -4022,6 +4077,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
40224077
counterparty_claimable_outpoints,
40234078
current_holder_commitment_tx: alternative_holder_commitment_tx.clone(),
40244079
prev_holder_commitment_tx: None,
4080+
cur_counterparty_commitment_tx: None,
4081+
prev_counterparty_commitment_tx: None,
40254082
};
40264083
let alternative_funding_outpoint = alternative_funding.funding_outpoint();
40274084

@@ -4563,6 +4620,77 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
45634620
Ok(justice_tx)
45644621
}
45654622

4623+
fn sign_initial_justice_tx(
4624+
&mut self, feerate_per_kw: u64, destination_script: ScriptBuf,
4625+
) -> Option<JusticeTransaction> {
4626+
let commitment_tx = self.initial_counterparty_commitment_tx()?;
4627+
self.funding.cur_counterparty_commitment_tx = Some(commitment_tx.clone());
4628+
self.try_sign_justice_tx(&commitment_tx, feerate_per_kw, destination_script)
4629+
}
4630+
4631+
fn sign_justice_txs_from_update(
4632+
&mut self, update: &ChannelMonitorUpdate, feerate_per_kw: u64,
4633+
destination_script: ScriptBuf,
4634+
) -> Vec<JusticeTransaction> {
4635+
let new_commitment_txs = self.counterparty_commitment_txs_from_update(update);
4636+
4637+
// Store new commitments, rotating cur -> prev per funding scope.
4638+
for commitment_tx in new_commitment_txs {
4639+
let txid = commitment_tx.trust().built_transaction().txid;
4640+
let funding = core::iter::once(&mut self.funding)
4641+
.chain(self.pending_funding.iter_mut())
4642+
.find(|f| f.current_counterparty_commitment_txid == Some(txid));
4643+
if let Some(funding) = funding {
4644+
funding.prev_counterparty_commitment_tx =
4645+
funding.cur_counterparty_commitment_tx.take();
4646+
funding.cur_counterparty_commitment_tx = Some(commitment_tx);
4647+
}
4648+
}
4649+
4650+
// Collect prev commitments that have revocation secrets available, clearing them
4651+
// from storage so they aren't signed again on subsequent calls.
4652+
let mut to_sign = Vec::new();
4653+
for funding in core::iter::once(&mut self.funding).chain(self.pending_funding.iter_mut()) {
4654+
let should_take =
4655+
funding.prev_counterparty_commitment_tx.as_ref().is_some_and(|prev| {
4656+
self.commitment_secrets.get_secret(prev.commitment_number()).is_some()
4657+
});
4658+
if should_take {
4659+
to_sign.push(funding.prev_counterparty_commitment_tx.take().unwrap());
4660+
}
4661+
}
4662+
4663+
let mut result = Vec::new();
4664+
for commitment_tx in &to_sign {
4665+
if let Some(jtx) =
4666+
self.try_sign_justice_tx(commitment_tx, feerate_per_kw, destination_script.clone())
4667+
{
4668+
result.push(jtx);
4669+
}
4670+
}
4671+
result
4672+
}
4673+
4674+
fn try_sign_justice_tx(
4675+
&self, commitment_tx: &CommitmentTransaction, feerate_per_kw: u64,
4676+
destination_script: ScriptBuf,
4677+
) -> Option<JusticeTransaction> {
4678+
let commitment_number = commitment_tx.commitment_number();
4679+
self.get_secret(commitment_number)?;
4680+
4681+
let trusted = commitment_tx.trust();
4682+
let output_idx = trusted.revokeable_output_index()?;
4683+
let built = trusted.built_transaction();
4684+
let value = built.transaction.output[output_idx].value;
4685+
let txid = built.txid;
4686+
4687+
let justice_tx =
4688+
trusted.build_to_local_justice_tx(feerate_per_kw, destination_script).ok()?;
4689+
let signed =
4690+
self.sign_to_local_justice_tx(justice_tx, 0, value.to_sat(), commitment_number).ok()?;
4691+
Some(JusticeTransaction { tx: signed, revoked_commitment_txid: txid, commitment_number })
4692+
}
4693+
45664694
/// Can only fail if idx is < get_min_seen_secret
45674695
fn get_secret(&self, idx: u64) -> Option<[u8; 32]> {
45684696
self.commitment_secrets.get_secret(idx)
@@ -6521,6 +6649,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
65216649
let mut alternative_funding_confirmed = None;
65226650
let mut is_manual_broadcast = RequiredWrapper(None);
65236651
let mut funding_seen_onchain = RequiredWrapper(None);
6652+
let mut cur_counterparty_commitment_tx: Option<CommitmentTransaction> = None;
6653+
let mut prev_counterparty_commitment_tx_deser: Option<CommitmentTransaction> = None;
65246654
read_tlv_fields!(reader, {
65256655
(1, funding_spend_confirmed, option),
65266656
(3, htlcs_resolved_on_chain, optional_vec),
@@ -6543,6 +6673,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
65436673
(34, alternative_funding_confirmed, option),
65446674
(35, is_manual_broadcast, (default_value, false)),
65456675
(37, funding_seen_onchain, (default_value, true)),
6676+
(39, cur_counterparty_commitment_tx, option),
6677+
(41, prev_counterparty_commitment_tx_deser, option),
65466678
});
65476679
// Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so
65486680
// we can use it to determine if this monitor was last written by LDK 0.1 or later.
@@ -6657,6 +6789,9 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
66576789

66586790
current_holder_commitment_tx,
66596791
prev_holder_commitment_tx,
6792+
6793+
cur_counterparty_commitment_tx,
6794+
prev_counterparty_commitment_tx: prev_counterparty_commitment_tx_deser,
66606795
},
66616796
pending_funding: pending_funding.unwrap_or(vec![]),
66626797
is_manual_broadcast: is_manual_broadcast.0.unwrap(),

lightning/src/util/test_utils.rs

Lines changed: 23 additions & 80 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+
if let Some(jtx) = data.sign_initial_justice_tx(
727+
FEERATE_FLOOR_SATS_PER_KW as u64,
728+
self.destination_script.clone(),
729+
) {
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

@@ -785,40 +745,23 @@ impl<Signer: sign::ecdsa::EcdsaChannelSigner> Persist<Signer> for WatchtowerPers
785745
let res = self.persister.update_persisted_channel(monitor_name, update, data);
786746

787747
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-
}
748+
let justice_txs = data.sign_justice_txs_from_update(
749+
update,
750+
FEERATE_FLOOR_SATS_PER_KW as u64,
751+
self.destination_script.clone(),
752+
);
753+
for jtx in justice_txs {
754+
let dup = self
755+
.watchtower_state
756+
.lock()
757+
.unwrap()
758+
.get_mut(&data.channel_id())
759+
.unwrap()
760+
.insert(jtx.revoked_commitment_txid, jtx.tx);
761+
assert!(dup.is_none());
820762
}
821763
}
764+
822765
res
823766
}
824767

0 commit comments

Comments
 (0)