Skip to content

Commit f1ad694

Browse files
committed
Ensure transactions_confirmed is idempotent
In many complexity-reduced implementations of chain syncing using esplora `transactions_confirmed` may be called redundantly for transactions which were already confirmed. To ensure this is idempotent we add two new `ConnectionStyle`s in our tests which (a) call `transactions_confirmed` twice for each call, ensuring simple idempotency is ensured and (b) call `transactions_confirmed` once for each historical block every time we're connecting a new block, ensuring we're fully idempotent even if every call is repeated constantly. In order to actually behave correctly this requires a simple already-confirmed check in `ChannelMonitor`, which is included.
1 parent 537e91c commit f1ad694

File tree

4 files changed

+90
-18
lines changed

4 files changed

+90
-18
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2869,7 +2869,37 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
28692869

28702870
let mut watch_outputs = Vec::new();
28712871
let mut claimable_outpoints = Vec::new();
2872-
for tx in &txn_matched {
2872+
'tx_iter: for tx in &txn_matched {
2873+
let txid = tx.txid();
2874+
// If a transaction has already been confirmed, ensure we don't bother processing it duplicatively.
2875+
if Some(txid) == self.funding_spend_confirmed {
2876+
log_debug!(logger, "Skipping redundant processing of funding-spend tx {} as it was previously confirmed", txid);
2877+
continue 'tx_iter;
2878+
}
2879+
for ev in self.onchain_events_awaiting_threshold_conf.iter() {
2880+
if ev.txid == txid {
2881+
if let Some(conf_hash) = ev.block_hash {
2882+
assert_eq!(header.block_hash(), conf_hash,
2883+
"Transaction {} was already confirmed and is being re-confirmed in a different block.\n\
2884+
This indicates a severe bug in the transaction connection logic - a reorg should have been processed first!", ev.txid);
2885+
}
2886+
log_debug!(logger, "Skipping redundant processing of confirming tx {} as it was previously confirmed", txid);
2887+
continue 'tx_iter;
2888+
}
2889+
}
2890+
for htlc in self.htlcs_resolved_on_chain.iter() {
2891+
if Some(txid) == htlc.resolving_txid {
2892+
log_debug!(logger, "Skipping redundant processing of HTLC resolution tx {} as it was previously confirmed", txid);
2893+
continue 'tx_iter;
2894+
}
2895+
}
2896+
for spendable_txid in self.spendable_txids_confirmed.iter() {
2897+
if txid == *spendable_txid {
2898+
log_debug!(logger, "Skipping redundant processing of spendable tx {} as it was previously confirmed", txid);
2899+
continue 'tx_iter;
2900+
}
2901+
}
2902+
28732903
if tx.input.len() == 1 {
28742904
// Assuming our keys were not leaked (in which case we're screwed no matter what),
28752905
// commitment transactions and HTLC transactions will all only ever have one input,
@@ -2879,7 +2909,7 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
28792909
if prevout.txid == self.funding_info.0.txid && prevout.vout == self.funding_info.0.index as u32 {
28802910
let mut balance_spendable_csv = None;
28812911
log_info!(logger, "Channel {} closed by funding output spend in txid {}.",
2882-
log_bytes!(self.funding_info.0.to_channel_id()), tx.txid());
2912+
log_bytes!(self.funding_info.0.to_channel_id()), txid);
28832913
self.funding_spend_seen = true;
28842914
let mut commitment_tx_to_counterparty_output = None;
28852915
if (tx.input[0].sequence.0 >> 8*3) as u8 == 0x80 && (tx.lock_time.0 >> 8*3) as u8 == 0x20 {
@@ -2902,7 +2932,6 @@ impl<Signer: Sign> ChannelMonitorImpl<Signer> {
29022932
}
29032933
}
29042934
}
2905-
let txid = tx.txid();
29062935
self.onchain_events_awaiting_threshold_conf.push(OnchainEventEntry {
29072936
txid,
29082937
transaction: Some((*tx).clone()),

lightning/src/ln/functional_test_utils.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ pub enum ConnectStyle {
107107
/// The same as `TransactionsFirst`, however when we have multiple blocks to connect, we only
108108
/// make a single `best_block_updated` call.
109109
TransactionsFirstSkippingBlocks,
110+
/// The same as `TransactionsFirst`, however when we have multiple blocks to connect, we only
111+
/// make a single `best_block_updated` call. Further, we call transactions_confirmed multiple
112+
/// times to ensure its idempotent.
113+
TransactionsDuplicativelyFirstSkippingBlocks,
114+
/// The same as `TransactionsFirst`, however when we have multiple blocks to connect, we only
115+
/// make a single `best_block_updated` call. Further, we call transactions_confirmed multiple
116+
/// times to ensure its idempotent.
117+
HighlyRedundantTransactionsFirstSkippingBlocks,
110118
/// The same as `TransactionsFirst` when connecting blocks. During disconnection only
111119
/// `transaction_unconfirmed` is called.
112120
TransactionsFirstReorgsOnlyTip,
@@ -121,14 +129,16 @@ impl ConnectStyle {
121129
use core::hash::{BuildHasher, Hasher};
122130
// Get a random value using the only std API to do so - the DefaultHasher
123131
let rand_val = std::collections::hash_map::RandomState::new().build_hasher().finish();
124-
let res = match rand_val % 7 {
132+
let res = match rand_val % 9 {
125133
0 => ConnectStyle::BestBlockFirst,
126134
1 => ConnectStyle::BestBlockFirstSkippingBlocks,
127135
2 => ConnectStyle::BestBlockFirstReorgsOnlyTip,
128136
3 => ConnectStyle::TransactionsFirst,
129137
4 => ConnectStyle::TransactionsFirstSkippingBlocks,
130-
5 => ConnectStyle::TransactionsFirstReorgsOnlyTip,
131-
6 => ConnectStyle::FullBlockViaListen,
138+
5 => ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks,
139+
6 => ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks,
140+
7 => ConnectStyle::TransactionsFirstReorgsOnlyTip,
141+
8 => ConnectStyle::FullBlockViaListen,
132142
_ => unreachable!(),
133143
};
134144
eprintln!("Using Block Connection Style: {:?}", res);
@@ -143,6 +153,7 @@ impl ConnectStyle {
143153
pub fn connect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, depth: u32) -> BlockHash {
144154
let skip_intermediaries = match *node.connect_style.borrow() {
145155
ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks|
156+
ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks|ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks|
146157
ConnectStyle::BestBlockFirstReorgsOnlyTip|ConnectStyle::TransactionsFirstReorgsOnlyTip => true,
147158
_ => false,
148159
};
@@ -193,8 +204,32 @@ fn do_connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: Block, sk
193204
node.node.best_block_updated(&block.header, height);
194205
node.node.transactions_confirmed(&block.header, &txdata, height);
195206
},
196-
ConnectStyle::TransactionsFirst|ConnectStyle::TransactionsFirstSkippingBlocks|ConnectStyle::TransactionsFirstReorgsOnlyTip => {
207+
ConnectStyle::TransactionsFirst|ConnectStyle::TransactionsFirstSkippingBlocks|
208+
ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks|ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks|
209+
ConnectStyle::TransactionsFirstReorgsOnlyTip => {
210+
if *node.connect_style.borrow() == ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks {
211+
let mut connections = Vec::new();
212+
for (block, height) in node.blocks.lock().unwrap().iter() {
213+
if !block.txdata.is_empty() {
214+
// Reconnect all transactions we've ever seen to ensure transaction connection
215+
// is *really* idempotent. This is a somewhat likely deployment for some
216+
// esplora implementations of chain sync which try to reduce state and
217+
// complexity as much as possible.
218+
//
219+
// Sadly we have to clone the block here to maintain lockorder. In the
220+
// future we should consider Arc'ing the blocks to avoid this.
221+
connections.push((block.clone(), *height));
222+
}
223+
}
224+
for (old_block, height) in connections {
225+
node.chain_monitor.chain_monitor.transactions_confirmed(&old_block.header,
226+
&old_block.txdata.iter().enumerate().collect::<Vec<_>>(), height);
227+
}
228+
}
197229
node.chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height);
230+
if *node.connect_style.borrow() == ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks {
231+
node.chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height);
232+
}
198233
call_claimable_balances(node);
199234
node.chain_monitor.chain_monitor.best_block_updated(&block.header, height);
200235
node.node.transactions_confirmed(&block.header, &txdata, height);
@@ -226,7 +261,8 @@ pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32)
226261
node.chain_monitor.chain_monitor.block_disconnected(&orig.0.header, orig.1);
227262
Listen::block_disconnected(node.node, &orig.0.header, orig.1);
228263
},
229-
ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks => {
264+
ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks|
265+
ConnectStyle::HighlyRedundantTransactionsFirstSkippingBlocks|ConnectStyle::TransactionsDuplicativelyFirstSkippingBlocks => {
230266
if i == count - 1 {
231267
node.chain_monitor.chain_monitor.best_block_updated(&prev.0.header, prev.1);
232268
node.node.best_block_updated(&prev.0.header, prev.1);

lightning/src/ln/functional_tests.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2814,12 +2814,17 @@ fn test_htlc_on_chain_success() {
28142814
check_added_monitors!(nodes[1], 1);
28152815
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
28162816
let node_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clone();
2817-
assert_eq!(node_txn.len(), 6); // ChannelManager : 3 (commitment tx + HTLC-Sucess * 2), ChannelMonitor : 3 (HTLC-Success, 2* RBF bumps of above HTLC txn)
2817+
assert!(node_txn.len() == 4 || node_txn.len() == 6); // ChannelManager : 3 (commitment tx + HTLC-Sucess * 2), ChannelMonitor : 3 (HTLC-Success, 2* RBF bumps of above HTLC txn)
28182818
let commitment_spend =
28192819
if node_txn[0].input[0].previous_output.txid == node_a_commitment_tx[0].txid() {
2820-
check_spends!(node_txn[1], commitment_tx[0]);
2821-
check_spends!(node_txn[2], commitment_tx[0]);
2822-
assert_ne!(node_txn[1].input[0].previous_output.vout, node_txn[2].input[0].previous_output.vout);
2820+
if node_txn.len() == 6 {
2821+
// In some block `ConnectionStyle`s we may avoid broadcasting the double-spending
2822+
// transactions spending the HTLC outputs of C's commitment transaction. Otherwise,
2823+
// check that the extra broadcasts (double-)spend those here.
2824+
check_spends!(node_txn[1], commitment_tx[0]);
2825+
check_spends!(node_txn[2], commitment_tx[0]);
2826+
assert_ne!(node_txn[1].input[0].previous_output.vout, node_txn[2].input[0].previous_output.vout);
2827+
}
28232828
&node_txn[0]
28242829
} else {
28252830
check_spends!(node_txn[0], commitment_tx[0]);
@@ -2834,10 +2839,11 @@ fn test_htlc_on_chain_success() {
28342839
assert_eq!(commitment_spend.input[1].witness.last().unwrap().len(), OFFERED_HTLC_SCRIPT_WEIGHT);
28352840
assert_eq!(commitment_spend.lock_time.0, 0);
28362841
assert!(commitment_spend.output[0].script_pubkey.is_v0_p2wpkh()); // direct payment
2837-
check_spends!(node_txn[3], chan_1.3);
2838-
assert_eq!(node_txn[3].input[0].witness.clone().last().unwrap().len(), 71);
2839-
check_spends!(node_txn[4], node_txn[3]);
2840-
check_spends!(node_txn[5], node_txn[3]);
2842+
let funding_spend_offset = if node_txn.len() == 6 { 3 } else { 1 };
2843+
check_spends!(node_txn[funding_spend_offset], chan_1.3);
2844+
assert_eq!(node_txn[funding_spend_offset].input[0].witness.clone().last().unwrap().len(), 71);
2845+
check_spends!(node_txn[funding_spend_offset + 1], node_txn[funding_spend_offset]);
2846+
check_spends!(node_txn[funding_spend_offset + 2], node_txn[funding_spend_offset]);
28412847
// We don't bother to check that B can claim the HTLC output on its commitment tx here as
28422848
// we already checked the same situation with A.
28432849

lightning/src/ln/payment_tests.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,8 +785,9 @@ fn do_test_dup_htlc_onchain_fails_on_reload(persist_manager_post_event: bool, co
785785
let funding_txo = OutPoint { txid: funding_tx.txid(), index: 0 };
786786
let mon_updates: Vec<_> = chanmon_cfgs[0].persister.chain_sync_monitor_persistences.lock().unwrap()
787787
.get_mut(&funding_txo).unwrap().drain().collect();
788-
// If we are using chain::Confirm instead of chain::Listen, we will get the same update twice
789-
assert!(mon_updates.len() == 1 || mon_updates.len() == 2);
788+
// If we are using chain::Confirm instead of chain::Listen, we will get the same update twice.
789+
// If we're testing connection idempotency we may get substantially more.
790+
assert!(mon_updates.len() >= 1);
790791
assert!(nodes[0].chain_monitor.release_pending_monitor_events().is_empty());
791792
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
792793

0 commit comments

Comments
 (0)