Skip to content

Commit 9fd6127

Browse files
authored
Merge pull request #2082 from wpaulino/bump-htlc-resolution-tx-locktime
Expose HTLC transaction locktime in BumpTransactionEvent::HTLCResolution
2 parents 31e78ff + 23e233b commit 9fd6127

File tree

6 files changed

+72
-33
lines changed

6 files changed

+72
-33
lines changed

lightning/src/chain/channelmonitor.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2426,7 +2426,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
24262426
}));
24272427
},
24282428
ClaimEvent::BumpHTLC {
2429-
target_feerate_sat_per_1000_weight, htlcs,
2429+
target_feerate_sat_per_1000_weight, htlcs, tx_lock_time,
24302430
} => {
24312431
let mut htlc_descriptors = Vec::with_capacity(htlcs.len());
24322432
for htlc in htlcs {
@@ -2444,6 +2444,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
24442444
ret.push(Event::BumpTransaction(BumpTransactionEvent::HTLCResolution {
24452445
target_feerate_sat_per_1000_weight,
24462446
htlc_descriptors,
2447+
tx_lock_time,
24472448
}));
24482449
}
24492450
}

lightning/src/chain/onchaintx.rs

+12-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//! OnchainTxHandler objects are fully-part of ChannelMonitor and encapsulates all
1313
//! building, tracking, bumping and notifications functions.
1414
15+
#[cfg(anchors)]
16+
use bitcoin::PackedLockTime;
1517
use bitcoin::blockdata::transaction::Transaction;
1618
use bitcoin::blockdata::transaction::OutPoint as BitcoinOutPoint;
1719
use bitcoin::blockdata::script::Script;
@@ -201,6 +203,7 @@ pub(crate) enum ClaimEvent {
201203
BumpHTLC {
202204
target_feerate_sat_per_1000_weight: u32,
203205
htlcs: Vec<ExternalHTLCClaim>,
206+
tx_lock_time: PackedLockTime,
204207
},
205208
}
206209

@@ -544,6 +547,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
544547
OnchainClaim::Event(ClaimEvent::BumpHTLC {
545548
target_feerate_sat_per_1000_weight,
546549
htlcs,
550+
tx_lock_time: PackedLockTime(cached_request.package_locktime(cur_height)),
547551
}),
548552
));
549553
} else {
@@ -558,7 +562,9 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
558562
) {
559563
assert!(new_feerate != 0);
560564

561-
let transaction = cached_request.finalize_malleable_package(self, output_value, self.destination_script.clone(), logger).unwrap();
565+
let transaction = cached_request.finalize_malleable_package(
566+
cur_height, self, output_value, self.destination_script.clone(), logger
567+
).unwrap();
562568
log_trace!(logger, "...with timer {} and feerate {}", new_timer.unwrap(), new_feerate);
563569
assert!(predicted_weight >= transaction.weight());
564570
return Some((new_timer, new_feerate, OnchainClaim::Tx(transaction)));
@@ -654,16 +660,17 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
654660
.find(|locked_package| locked_package.outpoints() == req.outpoints());
655661
if let Some(package) = timelocked_equivalent_package {
656662
log_info!(logger, "Ignoring second claim for outpoint {}:{}, we already have one which we're waiting on a timelock at {} for.",
657-
req.outpoints()[0].txid, req.outpoints()[0].vout, package.package_timelock());
663+
req.outpoints()[0].txid, req.outpoints()[0].vout, package.package_locktime(cur_height));
658664
continue;
659665
}
660666

661-
if req.package_timelock() > cur_height + 1 {
662-
log_info!(logger, "Delaying claim of package until its timelock at {} (current height {}), the following outpoints are spent:", req.package_timelock(), cur_height);
667+
let package_locktime = req.package_locktime(cur_height);
668+
if package_locktime > cur_height + 1 {
669+
log_info!(logger, "Delaying claim of package until its timelock at {} (current height {}), the following outpoints are spent:", package_locktime, cur_height);
663670
for outpoint in req.outpoints() {
664671
log_info!(logger, " Outpoint {}", outpoint);
665672
}
666-
self.locktimed_packages.entry(req.package_timelock()).or_insert(Vec::new()).push(req);
673+
self.locktimed_packages.entry(package_locktime).or_insert(Vec::new()).push(req);
667674
continue;
668675
}
669676

lightning/src/chain/package.rs

+49-17
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,6 @@ impl PackageSolvingData {
434434
let chan_keys = TxCreationKeys::derive_new(&onchain_handler.secp_ctx, &outp.per_commitment_point, &outp.counterparty_delayed_payment_base_key, &outp.counterparty_htlc_base_key, &onchain_handler.signer.pubkeys().revocation_basepoint, &onchain_handler.signer.pubkeys().htlc_basepoint);
435435
let witness_script = chan_utils::get_htlc_redeemscript_with_explicit_keys(&outp.htlc, onchain_handler.opt_anchors(), &chan_keys.broadcaster_htlc_key, &chan_keys.countersignatory_htlc_key, &chan_keys.revocation_key);
436436

437-
bumped_tx.lock_time = PackedLockTime(outp.htlc.cltv_expiry); // Right now we don't aggregate time-locked transaction, if we do we should set lock_time before to avoid breaking hash computation
438437
if let Ok(sig) = onchain_handler.signer.sign_counterparty_htlc_transaction(&bumped_tx, i, &outp.htlc.amount_msat / 1000, &outp.per_commitment_point, &outp.htlc, &onchain_handler.secp_ctx) {
439438
let mut ser_sig = sig.serialize_der().to_vec();
440439
ser_sig.push(EcdsaSighashType::All as u8);
@@ -460,18 +459,23 @@ impl PackageSolvingData {
460459
_ => { panic!("API Error!"); }
461460
}
462461
}
463-
fn absolute_tx_timelock(&self, output_conf_height: u32) -> u32 {
464-
// Get the absolute timelock at which this output can be spent given the height at which
465-
// this output was confirmed. We use `output_conf_height + 1` as a safe default as we can
466-
// be confirmed in the next block and transactions with time lock `current_height + 1`
467-
// always propagate.
462+
fn absolute_tx_timelock(&self, current_height: u32) -> u32 {
463+
// We use `current_height + 1` as our default locktime to discourage fee sniping and because
464+
// transactions with it always propagate.
468465
let absolute_timelock = match self {
469-
PackageSolvingData::RevokedOutput(_) => output_conf_height + 1,
470-
PackageSolvingData::RevokedHTLCOutput(_) => output_conf_height + 1,
471-
PackageSolvingData::CounterpartyOfferedHTLCOutput(_) => output_conf_height + 1,
472-
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => cmp::max(outp.htlc.cltv_expiry, output_conf_height + 1),
473-
PackageSolvingData::HolderHTLCOutput(ref outp) => cmp::max(outp.cltv_expiry, output_conf_height + 1),
474-
PackageSolvingData::HolderFundingOutput(_) => output_conf_height + 1,
466+
PackageSolvingData::RevokedOutput(_) => current_height + 1,
467+
PackageSolvingData::RevokedHTLCOutput(_) => current_height + 1,
468+
PackageSolvingData::CounterpartyOfferedHTLCOutput(_) => current_height + 1,
469+
PackageSolvingData::CounterpartyReceivedHTLCOutput(ref outp) => cmp::max(outp.htlc.cltv_expiry, current_height + 1),
470+
// HTLC timeout/success transactions rely on a fixed timelock due to the counterparty's
471+
// signature.
472+
PackageSolvingData::HolderHTLCOutput(ref outp) => {
473+
if outp.preimage.is_some() {
474+
debug_assert_eq!(outp.cltv_expiry, 0);
475+
}
476+
outp.cltv_expiry
477+
},
478+
PackageSolvingData::HolderFundingOutput(_) => current_height + 1,
475479
};
476480
absolute_timelock
477481
}
@@ -638,9 +642,36 @@ impl PackageTemplate {
638642
}
639643
amounts
640644
}
641-
pub(crate) fn package_timelock(&self) -> u32 {
642-
self.inputs.iter().map(|(_, outp)| outp.absolute_tx_timelock(self.height_original))
643-
.max().expect("There must always be at least one output to spend in a PackageTemplate")
645+
pub(crate) fn package_locktime(&self, current_height: u32) -> u32 {
646+
let locktime = self.inputs.iter().map(|(_, outp)| outp.absolute_tx_timelock(current_height))
647+
.max().expect("There must always be at least one output to spend in a PackageTemplate");
648+
649+
// If we ever try to aggregate a `HolderHTLCOutput`s with another output type, we'll likely
650+
// end up with an incorrect transaction locktime since the counterparty has included it in
651+
// its HTLC signature. This should never happen unless we decide to aggregate outputs across
652+
// different channel commitments.
653+
#[cfg(debug_assertions)] {
654+
if self.inputs.iter().any(|(_, outp)|
655+
if let PackageSolvingData::HolderHTLCOutput(outp) = outp {
656+
outp.preimage.is_some()
657+
} else {
658+
false
659+
}
660+
) {
661+
debug_assert_eq!(locktime, 0);
662+
};
663+
for timeout_htlc_expiry in self.inputs.iter().filter_map(|(_, outp)|
664+
if let PackageSolvingData::HolderHTLCOutput(outp) = outp {
665+
if outp.preimage.is_none() {
666+
Some(outp.cltv_expiry)
667+
} else { None }
668+
} else { None }
669+
) {
670+
debug_assert_eq!(locktime, timeout_htlc_expiry);
671+
}
672+
}
673+
674+
locktime
644675
}
645676
pub(crate) fn package_weight(&self, destination_script: &Script) -> usize {
646677
let mut inputs_weight = 0;
@@ -676,12 +707,13 @@ impl PackageTemplate {
676707
htlcs
677708
}
678709
pub(crate) fn finalize_malleable_package<L: Deref, Signer: WriteableEcdsaChannelSigner>(
679-
&self, onchain_handler: &mut OnchainTxHandler<Signer>, value: u64, destination_script: Script, logger: &L
710+
&self, current_height: u32, onchain_handler: &mut OnchainTxHandler<Signer>, value: u64,
711+
destination_script: Script, logger: &L
680712
) -> Option<Transaction> where L::Target: Logger {
681713
debug_assert!(self.is_malleable());
682714
let mut bumped_tx = Transaction {
683715
version: 2,
684-
lock_time: PackedLockTime::ZERO,
716+
lock_time: PackedLockTime(self.package_locktime(current_height)),
685717
input: vec![],
686718
output: vec![TxOut {
687719
script_pubkey: destination_script,

lightning/src/events/bump_transaction.rs

+2
Original file line numberDiff line numberDiff line change
@@ -227,5 +227,7 @@ pub enum BumpTransactionEvent {
227227
/// The set of pending HTLCs on the confirmed commitment that need to be claimed, preferably
228228
/// by the same transaction.
229229
htlc_descriptors: Vec<HTLCDescriptor>,
230+
/// The locktime required for the resulting HTLC transaction.
231+
tx_lock_time: PackedLockTime,
230232
},
231233
}

lightning/src/ln/functional_tests.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -2843,7 +2843,7 @@ fn test_htlc_on_chain_success() {
28432843
assert_eq!(commitment_spend.input.len(), 2);
28442844
assert_eq!(commitment_spend.input[0].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
28452845
assert_eq!(commitment_spend.input[1].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
2846-
assert_eq!(commitment_spend.lock_time.0, 0);
2846+
assert_eq!(commitment_spend.lock_time.0, nodes[1].best_block_info().1 + 1);
28472847
assert!(commitment_spend.output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
28482848
// We don't bother to check that B can claim the HTLC output on its commitment tx here as
28492849
// we already checked the same situation with A.
@@ -4699,7 +4699,7 @@ fn test_onchain_to_onchain_claim() {
46994699
check_spends!(b_txn[0], commitment_tx[0]);
47004700
assert_eq!(b_txn[0].input[0].witness.clone().last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
47014701
assert!(b_txn[0].output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
4702-
assert_eq!(b_txn[0].lock_time.0, 0); // Success tx
4702+
assert_eq!(b_txn[0].lock_time.0, nodes[1].best_block_info().1 + 1); // Success tx
47034703

47044704
check_closed_broadcast!(nodes[1], true);
47054705
check_added_monitors!(nodes[1], 1);
@@ -6860,7 +6860,7 @@ fn do_test_sweep_outbound_htlc_failure_update(revoked: bool, local: bool) {
68606860
if !revoked {
68616861
assert_eq!(timeout_tx[0].input[0].witness.last().unwrap().len(), ACCEPTED_HTLC_SCRIPT_WEIGHT);
68626862
} else {
6863-
assert_eq!(timeout_tx[0].lock_time.0, 0);
6863+
assert_eq!(timeout_tx[0].lock_time.0, 12);
68646864
}
68656865
// We fail non-dust-HTLC 2 by broadcast of local timeout/revocation-claim tx
68666866
mine_transaction(&nodes[0], &timeout_tx[0]);

lightning/src/ln/monitor_tests.rs

+4-7
Original file line numberDiff line numberDiff line change
@@ -1775,7 +1775,7 @@ fn test_yield_anchors_events() {
17751775
let mut htlc_txs = Vec::with_capacity(2);
17761776
for event in holder_events {
17771777
match event {
1778-
Event::BumpTransaction(BumpTransactionEvent::HTLCResolution { htlc_descriptors, .. }) => {
1778+
Event::BumpTransaction(BumpTransactionEvent::HTLCResolution { htlc_descriptors, tx_lock_time, .. }) => {
17791779
assert_eq!(htlc_descriptors.len(), 1);
17801780
let htlc_descriptor = &htlc_descriptors[0];
17811781
let signer = nodes[0].keys_manager.derive_channel_keys(
@@ -1784,11 +1784,7 @@ fn test_yield_anchors_events() {
17841784
let per_commitment_point = signer.get_per_commitment_point(htlc_descriptor.per_commitment_number, &secp);
17851785
let mut htlc_tx = Transaction {
17861786
version: 2,
1787-
lock_time: if htlc_descriptor.htlc.offered {
1788-
PackedLockTime(htlc_descriptor.htlc.cltv_expiry)
1789-
} else {
1790-
PackedLockTime::ZERO
1791-
},
1787+
lock_time: tx_lock_time,
17921788
input: vec![
17931789
htlc_descriptor.unsigned_tx_input(), // HTLC input
17941790
TxIn { ..Default::default() } // Fee input
@@ -2064,7 +2060,7 @@ fn test_anchors_aggregated_revoked_htlc_tx() {
20642060
};
20652061
let mut descriptors = Vec::with_capacity(4);
20662062
for event in events {
2067-
if let Event::BumpTransaction(BumpTransactionEvent::HTLCResolution { mut htlc_descriptors, .. }) = event {
2063+
if let Event::BumpTransaction(BumpTransactionEvent::HTLCResolution { mut htlc_descriptors, tx_lock_time, .. }) = event {
20682064
assert_eq!(htlc_descriptors.len(), 2);
20692065
for htlc_descriptor in &htlc_descriptors {
20702066
assert!(!htlc_descriptor.htlc.offered);
@@ -2076,6 +2072,7 @@ fn test_anchors_aggregated_revoked_htlc_tx() {
20762072
htlc_tx.output.push(htlc_descriptor.tx_output(&per_commitment_point, &secp));
20772073
}
20782074
descriptors.append(&mut htlc_descriptors);
2075+
htlc_tx.lock_time = tx_lock_time;
20792076
} else {
20802077
panic!("Unexpected event");
20812078
}

0 commit comments

Comments
 (0)