Skip to content

Commit

Permalink
Merge pull request #1339 from TheBlueMatt/2022-02-0.0.105-sec
Browse files Browse the repository at this point in the history
0.0.105 Security Fixes
  • Loading branch information
TheBlueMatt authored Mar 1, 2022
2 parents 82b8d85 + d798ac1 commit 6259e7a
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 37 deletions.
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,19 @@
0.0.104 or before and then upgrading again will invalidate existing phantom
SCIDs which may be included in invoices (#1199).

In total, this release features 108 files changed, 6914 insertions, 2095
deletions in 102 commits from 15 authors, in alphabetical order:
## Security
0.0.105 fixes two denial-of-service vulnerabilities which may be reachable from
untrusted input in certain application designs.

* Route calculation spuriously panics when a routing decision is made for a
path where the second-to-last hop is a private channel, included due to a
multi-hop route hint in an invoice.
* `ChannelMonitor::get_claimable_balances` spuriously panics in some scenarios
when the LDK application's local commitment transaction is confirmed while
HTLCs are still pending resolution.

In total, this release features 109 files changed, 7270 insertions, 2131
deletions in 108 commits from 15 authors, in alphabetical order:
* Conor Okus
* Devrandom
* Elias Rohrer
Expand Down
19 changes: 17 additions & 2 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1392,8 +1392,23 @@ impl<Signer: Sign> ChannelMonitor<Signer> {
($holder_commitment: expr, $htlc_iter: expr) => {
for htlc in $htlc_iter {
if let Some(htlc_input_idx) = htlc.transaction_output_index {
if us.htlcs_resolved_on_chain.iter().any(|v| v.input_idx == htlc_input_idx) {
assert!(us.funding_spend_confirmed.is_some());
if let Some(conf_thresh) = us.onchain_events_awaiting_threshold_conf.iter().find_map(|event| {
if let OnchainEvent::MaturingOutput { descriptor: SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) } = &event.event {
if descriptor.outpoint.index as u32 == htlc_input_idx { Some(event.confirmation_threshold()) } else { None }
} else { None }
}) {
debug_assert!($holder_commitment);
res.push(Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: htlc.amount_msat / 1000,
confirmation_height: conf_thresh,
});
} else if us.htlcs_resolved_on_chain.iter().any(|v| v.input_idx == htlc_input_idx) {
// Funding transaction spends should be fully confirmed by the time any
// HTLC transactions are resolved, unless we're talking about a holder
// commitment tx, whose resolution is delayed until the CSV timeout is
// reached, even though HTLCs may be resolved after only
// ANTI_REORG_DELAY confirmations.
debug_assert!($holder_commitment || us.funding_spend_confirmed.is_some());
} else if htlc.offered == $holder_commitment {
// If the payment was outbound, check if there's an HTLCUpdate
// indicating we have spent this HTLC with a timeout, claiming it back
Expand Down
13 changes: 13 additions & 0 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,29 @@ pub fn connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: &Block)
do_connect_block(node, block, false);
}

fn call_claimable_balances<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>) {
// Ensure `get_claimable_balances`' self-tests never panic
for funding_outpoint in node.chain_monitor.chain_monitor.list_monitors() {
node.chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances();
}
}

fn do_connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: &Block, skip_intermediaries: bool) {
call_claimable_balances(node);
let height = node.best_block_info().1 + 1;
if !skip_intermediaries {
let txdata: Vec<_> = block.txdata.iter().enumerate().collect();
match *node.connect_style.borrow() {
ConnectStyle::BestBlockFirst|ConnectStyle::BestBlockFirstSkippingBlocks => {
node.chain_monitor.chain_monitor.best_block_updated(&block.header, height);
call_claimable_balances(node);
node.chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height);
node.node.best_block_updated(&block.header, height);
node.node.transactions_confirmed(&block.header, &txdata, height);
},
ConnectStyle::TransactionsFirst|ConnectStyle::TransactionsFirstSkippingBlocks => {
node.chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height);
call_claimable_balances(node);
node.chain_monitor.chain_monitor.best_block_updated(&block.header, height);
node.node.transactions_confirmed(&block.header, &txdata, height);
node.node.best_block_updated(&block.header, height);
Expand All @@ -146,11 +156,13 @@ fn do_connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: &Block, s
}
}
}
call_claimable_balances(node);
node.node.test_process_background_events();
node.blocks.lock().unwrap().push((block.header, height));
}

pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32) {
call_claimable_balances(node);
for i in 0..count {
let orig_header = node.blocks.lock().unwrap().pop().unwrap();
assert!(orig_header.1 > 0); // Cannot disconnect genesis
Expand All @@ -172,6 +184,7 @@ pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32)
node.node.best_block_updated(&prev_header.0, prev_header.1);
},
}
call_claimable_balances(node);
}
}

Expand Down
190 changes: 190 additions & 0 deletions lightning/src/ln/monitor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,193 @@ fn test_claim_value_force_close() {
do_test_claim_value_force_close(true);
do_test_claim_value_force_close(false);
}

#[test]
fn test_balances_on_local_commitment_htlcs() {
// Previously, when handling the broadcast of a local commitment transactions (with associated
// CSV delays prior to spendability), we incorrectly handled the CSV delays on HTLC
// transactions. This caused us to miss spendable outputs for HTLCs which were awaiting a CSV
// delay prior to spendability.
//
// Further, because of this, we could hit an assertion as `get_claimable_balances` asserted
// that HTLCs were resolved after the funding spend was resolved, which was not true if the
// HTLC did not have a CSV delay attached (due to the above bug or due to it being an HTLC
// claim by our counterparty).
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);

// Create a single channel with two pending HTLCs from nodes[0] to nodes[1], one which nodes[1]
// knows the preimage for, one which it does not.
let (_, _, chan_id, funding_tx) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0, InitFeatures::known(), InitFeatures::known());
let funding_outpoint = OutPoint { txid: funding_tx.txid(), index: 0 };

let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 10_000_000);
let htlc_cltv_timeout = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + 1; // Note ChannelManager adds one to CLTV timeouts for safety
nodes[0].node.send_payment(&route, payment_hash, &Some(payment_secret)).unwrap();
check_added_monitors!(nodes[0], 1);

let updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id());
nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]);
commitment_signed_dance!(nodes[1], nodes[0], updates.commitment_signed, false);

expect_pending_htlcs_forwardable!(nodes[1]);
expect_payment_received!(nodes[1], payment_hash, payment_secret, 10_000_000);

let (route_2, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 20_000_000);
nodes[0].node.send_payment(&route_2, payment_hash_2, &Some(payment_secret_2)).unwrap();
check_added_monitors!(nodes[0], 1);

let updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id());
nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]);
commitment_signed_dance!(nodes[1], nodes[0], updates.commitment_signed, false);

expect_pending_htlcs_forwardable!(nodes[1]);
expect_payment_received!(nodes[1], payment_hash_2, payment_secret_2, 20_000_000);
assert!(nodes[1].node.claim_funds(payment_preimage_2));
get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id());
check_added_monitors!(nodes[1], 1);

let chan_feerate = get_feerate!(nodes[0], chan_id) as u64;
let opt_anchors = get_opt_anchors!(nodes[0], chan_id);

// Get nodes[0]'s commitment transaction and HTLC-Timeout transactions
let as_txn = get_local_commitment_txn!(nodes[0], chan_id);
assert_eq!(as_txn.len(), 3);
check_spends!(as_txn[1], as_txn[0]);
check_spends!(as_txn[2], as_txn[0]);
check_spends!(as_txn[0], funding_tx);

// First confirm the commitment transaction on nodes[0], which should leave us with three
// claimable balances.
let node_a_commitment_claimable = nodes[0].best_block_info().1 + BREAKDOWN_TIMEOUT as u32;
mine_transaction(&nodes[0], &as_txn[0]);
check_added_monitors!(nodes[0], 1);
check_closed_broadcast!(nodes[0], true);
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed);

assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 1_000_000 - 10_000 - 20_000 - chan_feerate *
(channel::commitment_tx_base_weight(opt_anchors) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
confirmation_height: node_a_commitment_claimable,
}, Balance::MaybeClaimableHTLCAwaitingTimeout {
claimable_amount_satoshis: 10_000,
claimable_height: htlc_cltv_timeout,
}, Balance::MaybeClaimableHTLCAwaitingTimeout {
claimable_amount_satoshis: 20_000,
claimable_height: htlc_cltv_timeout,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));

// Get nodes[1]'s HTLC claim tx for the second HTLC
mine_transaction(&nodes[1], &as_txn[0]);
check_added_monitors!(nodes[1], 1);
check_closed_broadcast!(nodes[1], true);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let bs_htlc_claim_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
assert_eq!(bs_htlc_claim_txn.len(), 3);
check_spends!(bs_htlc_claim_txn[0], as_txn[0]);
check_spends!(bs_htlc_claim_txn[1], funding_tx);
check_spends!(bs_htlc_claim_txn[2], bs_htlc_claim_txn[1]);

// Connect blocks until the HTLCs expire, allowing us to (validly) broadcast the HTLC-Timeout
// transaction.
connect_blocks(&nodes[0], TEST_FINAL_CLTV - 1);
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 1_000_000 - 10_000 - 20_000 - chan_feerate *
(channel::commitment_tx_base_weight(opt_anchors) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
confirmation_height: node_a_commitment_claimable,
}, Balance::MaybeClaimableHTLCAwaitingTimeout {
claimable_amount_satoshis: 10_000,
claimable_height: htlc_cltv_timeout,
}, Balance::MaybeClaimableHTLCAwaitingTimeout {
claimable_amount_satoshis: 20_000,
claimable_height: htlc_cltv_timeout,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));
assert_eq!(as_txn[1].lock_time, nodes[0].best_block_info().1 + 1); // as_txn[1] can be included in the next block

// Now confirm nodes[0]'s HTLC-Timeout transaction, which changes the claimable balance to an
// "awaiting confirmations" one.
let node_a_htlc_claimable = nodes[0].best_block_info().1 + BREAKDOWN_TIMEOUT as u32;
mine_transaction(&nodes[0], &as_txn[1]);
// Note that prior to the fix in the commit which introduced this test, this (and the next
// balance) check failed. With this check removed, the code panicked in the `connect_blocks`
// call, as described, two hunks down.
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 1_000_000 - 10_000 - 20_000 - chan_feerate *
(channel::commitment_tx_base_weight(opt_anchors) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
confirmation_height: node_a_commitment_claimable,
}, Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 10_000,
confirmation_height: node_a_htlc_claimable,
}, Balance::MaybeClaimableHTLCAwaitingTimeout {
claimable_amount_satoshis: 20_000,
claimable_height: htlc_cltv_timeout,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));

// Now confirm nodes[1]'s HTLC claim, giving nodes[0] the preimage. Note that the "maybe
// claimable" balance remains until we see ANTI_REORG_DELAY blocks.
mine_transaction(&nodes[0], &bs_htlc_claim_txn[0]);
expect_payment_sent!(nodes[0], payment_preimage_2);
assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 1_000_000 - 10_000 - 20_000 - chan_feerate *
(channel::commitment_tx_base_weight(opt_anchors) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
confirmation_height: node_a_commitment_claimable,
}, Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 10_000,
confirmation_height: node_a_htlc_claimable,
}, Balance::MaybeClaimableHTLCAwaitingTimeout {
claimable_amount_satoshis: 20_000,
claimable_height: htlc_cltv_timeout,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));

// Finally make the HTLC transactions have ANTI_REORG_DELAY blocks. This call previously
// panicked as described in the test introduction. This will remove the "maybe claimable"
// spendable output as nodes[1] has fully claimed the second HTLC.
connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1);
expect_payment_failed!(nodes[0], payment_hash, true);

assert_eq!(sorted_vec(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 1_000_000 - 10_000 - 20_000 - chan_feerate *
(channel::commitment_tx_base_weight(opt_anchors) + 2 * channel::COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000,
confirmation_height: node_a_commitment_claimable,
}, Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 10_000,
confirmation_height: node_a_htlc_claimable,
}]),
sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances()));

// Connect blocks until the commitment transaction's CSV expires, providing us the relevant
// `SpendableOutputs` event and removing the claimable balance entry.
connect_blocks(&nodes[0], node_a_commitment_claimable - nodes[0].best_block_info().1);
assert_eq!(vec![Balance::ClaimableAwaitingConfirmations {
claimable_amount_satoshis: 10_000,
confirmation_height: node_a_htlc_claimable,
}],
nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances());
let mut node_a_spendable = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
assert_eq!(node_a_spendable.len(), 1);
if let Event::SpendableOutputs { outputs } = node_a_spendable.pop().unwrap() {
assert_eq!(outputs.len(), 1);
let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(),
Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &Secp256k1::new()).unwrap();
check_spends!(spend_tx, as_txn[0]);
}

// Connect blocks until the HTLC-Timeout's CSV expires, providing us the relevant
// `SpendableOutputs` event and removing the claimable balance entry.
connect_blocks(&nodes[0], node_a_htlc_claimable - nodes[0].best_block_info().1);
assert!(nodes[0].chain_monitor.chain_monitor.get_monitor(funding_outpoint).unwrap().get_claimable_balances().is_empty());
let mut node_a_spendable = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
assert_eq!(node_a_spendable.len(), 1);
if let Event::SpendableOutputs { outputs } = node_a_spendable.pop().unwrap() {
assert_eq!(outputs.len(), 1);
let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(),
Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, &Secp256k1::new()).unwrap();
check_spends!(spend_tx, as_txn[1]);
}
}
10 changes: 8 additions & 2 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,9 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) {
check_spends!(bs_htlc_claim_txn[0], as_commitment_tx);
expect_payment_forwarded!(nodes[1], None, false);

mine_transaction(&nodes[0], &as_commitment_tx);
if !confirm_before_reload {
mine_transaction(&nodes[0], &as_commitment_tx);
}
mine_transaction(&nodes[0], &bs_htlc_claim_txn[0]);
expect_payment_sent!(nodes[0], payment_preimage_1);
connect_blocks(&nodes[0], TEST_FINAL_CLTV*4 + 20);
Expand Down Expand Up @@ -515,6 +517,10 @@ fn do_test_dup_htlc_onchain_fails_on_reload(persist_manager_post_event: bool, co
check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
let claim_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
assert_eq!(claim_txn.len(), 3);
check_spends!(claim_txn[0], node_txn[1]);
check_spends!(claim_txn[1], funding_tx);
check_spends!(claim_txn[2], claim_txn[1]);

header.prev_blockhash = nodes[0].best_block_hash();
connect_block(&nodes[0], &Block { header, txdata: vec![node_txn[1].clone()]});
Expand All @@ -524,7 +530,7 @@ fn do_test_dup_htlc_onchain_fails_on_reload(persist_manager_post_event: bool, co
}

header.prev_blockhash = nodes[0].best_block_hash();
let claim_block = Block { header, txdata: if payment_timeout { timeout_txn } else { claim_txn } };
let claim_block = Block { header, txdata: if payment_timeout { timeout_txn } else { vec![claim_txn[0].clone()] } };

if payment_timeout {
assert!(confirm_commitment_tx); // Otherwise we're spending below our CSV!
Expand Down
10 changes: 4 additions & 6 deletions lightning/src/ln/reorg_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fn do_test_onchain_htlc_reorg(local_commitment: bool, claim: bool) {
vec![node_1_commitment_txn[0].clone(), node_2_commitment_txn[0].clone()]
} else {
// Broadcast node 2 commitment txn
let node_2_commitment_txn = get_local_commitment_txn!(nodes[2], chan_2.2);
let mut node_2_commitment_txn = get_local_commitment_txn!(nodes[2], chan_2.2);
assert_eq!(node_2_commitment_txn.len(), 2); // 1 local commitment tx, 1 Received HTLC-Claim
assert_eq!(node_2_commitment_txn[0].output.len(), 2); // to-remote and Received HTLC (to-self is dust)
check_spends!(node_2_commitment_txn[0], chan_2.3);
Expand All @@ -113,12 +113,10 @@ fn do_test_onchain_htlc_reorg(local_commitment: bool, claim: bool) {
check_spends!(node_1_commitment_txn[0], chan_2.3);
check_spends!(node_1_commitment_txn[1], node_2_commitment_txn[0]);

// Confirm node 2's commitment txn (and node 1's HTLC-Timeout) on node 1
header.prev_blockhash = nodes[1].best_block_hash();
let block = Block { header, txdata: vec![node_2_commitment_txn[0].clone(), node_1_commitment_txn[1].clone()] };
connect_block(&nodes[1], &block);
// Confirm node 1's HTLC-Timeout on node 1
mine_transaction(&nodes[1], &node_1_commitment_txn[1]);
// ...but return node 2's commitment tx (and claim) in case claim is set and we're preparing to reorg
node_2_commitment_txn
vec![node_2_commitment_txn.pop().unwrap()]
};
check_added_monitors!(nodes[1], 1);
check_closed_broadcast!(nodes[1], true); // We should get a BroadcastChannelUpdate (and *only* a BroadcstChannelUpdate)
Expand Down
Loading

0 comments on commit 6259e7a

Please sign in to comment.