From d84c084df7b7dae62ea6b1a51951fb01391a7ebd Mon Sep 17 00:00:00 2001 From: Antoine Riard Date: Tue, 11 Sep 2018 01:37:31 +0000 Subject: [PATCH 1/3] Implement set_their_delayed_payment_base_key in ChannelMonitor Needed to build redeemscript on HTLC-Success/HTLC-Timeout tx from remote revoked commitment tx --- src/ln/channel.rs | 4 ++-- src/ln/channelmonitor.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ln/channel.rs b/src/ln/channel.rs index 0ed2dde04ec..4a927119695 100644 --- a/src/ln/channel.rs +++ b/src/ln/channel.rs @@ -570,7 +570,7 @@ impl Channel { &PublicKey::from_secret_key(&secp_ctx, &chan_keys.delayed_payment_base_key), &chan_keys.htlc_base_key, BREAKDOWN_TIMEOUT, our_channel_monitor_claim_script); - channel_monitor.set_their_htlc_base_key(&msg.htlc_basepoint); + channel_monitor.set_their_base_keys(&msg.htlc_basepoint, &msg.delayed_payment_basepoint); channel_monitor.set_their_to_self_delay(msg.to_self_delay); let mut chan = Channel { @@ -1236,7 +1236,7 @@ impl Channel { // max_accepted_htlcs too small // dust_limit_satoshis too small - self.channel_monitor.set_their_htlc_base_key(&msg.htlc_basepoint); + self.channel_monitor.set_their_base_keys(&msg.htlc_basepoint, &msg.delayed_payment_basepoint); self.their_dust_limit_satoshis = msg.dust_limit_satoshis; self.their_max_htlc_value_in_flight_msat = cmp::min(msg.max_htlc_value_in_flight_msat, self.channel_value_satoshis * 1000); diff --git a/src/ln/channelmonitor.rs b/src/ln/channelmonitor.rs index 39146daa9b2..81efedcbffd 100644 --- a/src/ln/channelmonitor.rs +++ b/src/ln/channelmonitor.rs @@ -166,6 +166,7 @@ pub struct ChannelMonitor { key_storage: KeyStorage, delayed_payment_base_key: PublicKey, their_htlc_base_key: Option, + their_delayed_payment_base_key: Option, // first is the idx of the first of the two revocation points their_cur_revocation_points: Option<(u64, PublicKey, Option)>, @@ -207,6 +208,7 @@ impl Clone for ChannelMonitor { key_storage: self.key_storage.clone(), delayed_payment_base_key: self.delayed_payment_base_key.clone(), their_htlc_base_key: self.their_htlc_base_key.clone(), + their_delayed_payment_base_key: self.their_delayed_payment_base_key.clone(), their_cur_revocation_points: self.their_cur_revocation_points.clone(), our_to_self_delay: self.our_to_self_delay, @@ -238,6 +240,7 @@ impl PartialEq for ChannelMonitor { self.key_storage != other.key_storage || self.delayed_payment_base_key != other.delayed_payment_base_key || self.their_htlc_base_key != other.their_htlc_base_key || + self.their_delayed_payment_base_key != other.their_delayed_payment_base_key || self.their_cur_revocation_points != other.their_cur_revocation_points || self.our_to_self_delay != other.our_to_self_delay || self.their_to_self_delay != other.their_to_self_delay || @@ -274,6 +277,7 @@ impl ChannelMonitor { }, delayed_payment_base_key: delayed_payment_base_key.clone(), their_htlc_base_key: None, + their_delayed_payment_base_key: None, their_cur_revocation_points: None, our_to_self_delay: our_to_self_delay, @@ -478,8 +482,10 @@ impl ChannelMonitor { self.funding_txo = Some(funding_info); } - pub(super) fn set_their_htlc_base_key(&mut self, their_htlc_base_key: &PublicKey) { + /// We log these base keys at channel opening to being able to rebuild redeemscript in case of leaked revoked commit tx + pub(super) fn set_their_base_keys(&mut self, their_htlc_base_key: &PublicKey, their_delayed_payment_base_key: &PublicKey) { self.their_htlc_base_key = Some(their_htlc_base_key.clone()); + self.their_delayed_payment_base_key = Some(their_delayed_payment_base_key.clone()); } pub(super) fn set_their_to_self_delay(&mut self, their_to_self_delay: u16) { @@ -531,6 +537,7 @@ impl ChannelMonitor { res.extend_from_slice(&self.delayed_payment_base_key.serialize()); res.extend_from_slice(&self.their_htlc_base_key.as_ref().unwrap().serialize()); + res.extend_from_slice(&self.their_delayed_payment_base_key.as_ref().unwrap().serialize()); match self.their_cur_revocation_points { Some((idx, pubkey, second_option)) => { @@ -705,6 +712,7 @@ impl ChannelMonitor { let delayed_payment_base_key = unwrap_obj!(PublicKey::from_slice(&secp_ctx, read_bytes!(33))); let their_htlc_base_key = Some(unwrap_obj!(PublicKey::from_slice(&secp_ctx, read_bytes!(33)))); + let their_delayed_payment_base_key = Some(unwrap_obj!(PublicKey::from_slice(&secp_ctx, read_bytes!(33)))); let their_cur_revocation_points = { let first_idx = byte_utils::slice_to_be48(read_bytes!(6)); @@ -867,6 +875,7 @@ impl ChannelMonitor { key_storage, delayed_payment_base_key, their_htlc_base_key, + their_delayed_payment_base_key, their_cur_revocation_points, our_to_self_delay, From 3e149b1fb6624eef99b055bde772842f364abdca Mon Sep 17 00:00:00 2001 From: Antoine Riard Date: Tue, 11 Sep 2018 01:40:53 +0000 Subject: [PATCH 2/3] Implement claiming of revoked HTLC transactions by ChannelMonitor Refactor check_spend_remote_transaction in part to check_spend_remote_htlc to avoid lock mess in block_connected. We need remote_commitment_txn_on_chain to match remote HTLC tx --- src/ln/channelmonitor.rs | 104 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 7 deletions(-) diff --git a/src/ln/channelmonitor.rs b/src/ln/channelmonitor.rs index 81efedcbffd..6de0a63609a 100644 --- a/src/ln/channelmonitor.rs +++ b/src/ln/channelmonitor.rs @@ -924,8 +924,7 @@ impl ChannelMonitor { /// Attempts to claim a remote commitment transaction's outputs using the revocation key and /// data in remote_claimable_outpoints. Will directly claim any HTLC outputs which expire at a /// height > height + CLTV_SHARED_CLAIM_BUFFER. In any case, will install monitoring for - /// HTLC-Success/HTLC-Timeout transactions, and claim them using the revocation key (if - /// applicable) as well. + /// HTLC-Success/HTLC-Timeout transactions. fn check_spend_remote_transaction(&self, tx: &Transaction, height: u32) -> (Vec, (Sha256dHash, Vec)) { // Most secp and related errors trying to create keys means we have no hope of constructing // a spend transaction...so we return no transactions to broadcast @@ -1205,13 +1204,97 @@ impl ChannelMonitor { txn_to_broadcast.push(spend_tx); } } - } else { - //TODO: For each input check if its in our remote_commitment_txn_on_chain map! } (txn_to_broadcast, (commitment_txid, watch_outputs)) } + /// Attempst to claim a remote HTLC-Success/HTLC-Timeout s outputs using the revocation key + fn check_spend_remote_htlc(&self, tx: &Transaction, commitment_number: u64) -> Vec { + let mut txn_to_broadcast = Vec::new(); + + let htlc_txid = tx.txid(); //TODO: This is gonna be a performance bottleneck for watchtowers! + + macro_rules! ignore_error { + ( $thing : expr ) => { + match $thing { + Ok(a) => a, + Err(_) => return txn_to_broadcast + } + }; + } + + let secret = self.get_secret(commitment_number).unwrap(); + let per_commitment_key = ignore_error!(SecretKey::from_slice(&self.secp_ctx, &secret)); + let revocation_pubkey = match self.key_storage { + KeyStorage::PrivMode { ref revocation_base_key, .. } => { + let per_commitment_point = PublicKey::from_secret_key(&self.secp_ctx, &per_commitment_key); + ignore_error!(chan_utils::derive_public_revocation_key(&self.secp_ctx, &per_commitment_point, &PublicKey::from_secret_key(&self.secp_ctx, &revocation_base_key))) + }, + KeyStorage::SigsMode { ref revocation_base_key, .. } => { + let per_commitment_point = PublicKey::from_secret_key(&self.secp_ctx, &per_commitment_key); + ignore_error!(chan_utils::derive_public_revocation_key(&self.secp_ctx, &per_commitment_point, &revocation_base_key)) + }, + }; + let delayed_key = match self.their_delayed_payment_base_key { + None => return txn_to_broadcast, + Some(their_delayed_payment_base_key) => ignore_error!(chan_utils::derive_public_key(&self.secp_ctx, &PublicKey::from_secret_key(&self.secp_ctx, &per_commitment_key), &their_delayed_payment_base_key)), + }; + let redeemscript = chan_utils::get_revokeable_redeemscript(&revocation_pubkey, self.their_to_self_delay.unwrap(), &delayed_key); + let revokeable_p2wsh = redeemscript.to_v0_p2wsh(); + + let mut inputs = Vec::new(); + let mut amount = 0; + + if tx.output[0].script_pubkey == revokeable_p2wsh { //HTLC transactions have one txin, one txout + inputs.push(TxIn { + previous_output: BitcoinOutPoint { + txid: htlc_txid, + vout: 0, + }, + script_sig: Script::new(), + sequence: 0xfffffffd, + witness: Vec::new(), + }); + amount = tx.output[0].value; + } + + if !inputs.is_empty() { + let outputs = vec!(TxOut { + script_pubkey: self.destination_script.clone(), + value: amount, //TODO: - fee + }); + + let mut spend_tx = Transaction { + version: 2, + lock_time: 0, + input: inputs, + output: outputs, + }; + + + let sighash_parts = bip143::SighashComponents::new(&spend_tx); + + let sig = match self.key_storage { + KeyStorage::PrivMode { ref revocation_base_key, .. } => { + let sighash = ignore_error!(Message::from_slice(&sighash_parts.sighash_all(&spend_tx.input[0], &redeemscript, amount)[..])); + let revocation_key = ignore_error!(chan_utils::derive_private_revocation_key(&self.secp_ctx, &per_commitment_key, &revocation_base_key)); + self.secp_ctx.sign(&sighash, &revocation_key) + } + KeyStorage::SigsMode { .. } => { + unimplemented!(); + } + }; + spend_tx.input[0].witness.push(sig.serialize_der(&self.secp_ctx).to_vec()); + spend_tx.input[0].witness[0].push(SigHashType::All as u8); + spend_tx.input[0].witness.push(vec!(1)); + spend_tx.input[0].witness.push(redeemscript.into_bytes()); + + txn_to_broadcast.push(spend_tx); + } + txn_to_broadcast + } + fn broadcast_by_local_state(&self, local_tx: &LocalSignedTx) -> Vec { let mut res = Vec::with_capacity(local_tx.htlc_outputs.len()); @@ -1273,19 +1356,26 @@ impl ChannelMonitor { fn block_connected(&self, txn_matched: &[&Transaction], height: u32, broadcaster: &BroadcasterInterface)-> Vec<(Sha256dHash, Vec)> { let mut watch_outputs = Vec::new(); for tx in txn_matched { + let mut txn: Vec = Vec::new(); for txin in tx.input.iter() { if self.funding_txo.is_none() || (txin.previous_output.txid == self.funding_txo.as_ref().unwrap().0.txid && txin.previous_output.vout == self.funding_txo.as_ref().unwrap().0.index as u32) { - let (mut txn, new_outputs) = self.check_spend_remote_transaction(tx, height); + let (remote_txn, new_outputs) = self.check_spend_remote_transaction(tx, height); + txn = remote_txn; if !new_outputs.1.is_empty() { watch_outputs.push(new_outputs); } if txn.is_empty() { txn = self.check_spend_local_transaction(tx, height); } - for tx in txn.iter() { - broadcaster.broadcast_transaction(tx); + } else { + let remote_commitment_txn_on_chain = self.remote_commitment_txn_on_chain.lock().unwrap(); + for commitment_number in remote_commitment_txn_on_chain.get(&txin.previous_output.txid) { + txn = self.check_spend_remote_htlc(tx, *commitment_number); } } + for tx in txn.iter() { + broadcaster.broadcast_transaction(tx); + } } } if let Some(ref cur_local_tx) = self.current_local_signed_commitment_tx { From 383bd90a481bc146b3a3b1d84e7c92bbd98afbed Mon Sep 17 00:00:00 2001 From: Antoine Riard Date: Tue, 11 Sep 2018 01:44:50 +0000 Subject: [PATCH 3/3] Add test of claiming HTLC-Timeout outputs based on a revoked commitment tx --- src/ln/channelmanager.rs | 48 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/ln/channelmanager.rs b/src/ln/channelmanager.rs index e111a53163c..07e6306ec2d 100644 --- a/src/ln/channelmanager.rs +++ b/src/ln/channelmanager.rs @@ -3089,9 +3089,11 @@ mod tests { #[derive(PartialEq)] enum HTLCType { NONE, TIMEOUT, SUCCESS } - fn test_txn_broadcast(node: &Node, chan: &(msgs::ChannelUpdate, msgs::ChannelUpdate, [u8; 32], Transaction), commitment_tx: Option, has_htlc_tx: HTLCType) -> Vec { + #[derive(PartialEq)] + enum PenaltyType { NONE, HTLC } + fn test_txn_broadcast(node: &Node, chan: &(msgs::ChannelUpdate, msgs::ChannelUpdate, [u8; 32], Transaction), commitment_tx: Option, revoked_tx: Option, has_htlc_tx: HTLCType, has_penalty_tx: PenaltyType) -> Vec { let mut node_txn = node.tx_broadcaster.txn_broadcasted.lock().unwrap(); - assert!(node_txn.len() >= if commitment_tx.is_some() { 0 } else { 1 } + if has_htlc_tx == HTLCType::NONE { 0 } else { 1 }); + assert!(node_txn.len() >= if has_htlc_tx == HTLCType::NONE { 0 } else { 1 } + if has_penalty_tx == PenaltyType::NONE { 0 } else { 1 }); let mut res = Vec::with_capacity(2); @@ -3107,7 +3109,9 @@ mod tests { } } } - assert_eq!(res.len(), 1); + if !revoked_tx.is_some() && !(has_penalty_tx == PenaltyType::HTLC) { + assert_eq!(res.len(), 1); + } if has_htlc_tx != HTLCType::NONE { for tx in node_txn.iter() { @@ -3126,6 +3130,20 @@ mod tests { } assert_eq!(res.len(), 2); } + + if has_penalty_tx == PenaltyType::HTLC { + let revoked_tx = revoked_tx.unwrap(); + for tx in node_txn.iter() { + if tx.input.len() == 1 && tx.input[0].previous_output.txid == revoked_tx.txid() { + let mut funding_tx_map = HashMap::new(); + funding_tx_map.insert(revoked_tx.txid(), revoked_tx.clone()); + tx.verify(&funding_tx_map).unwrap(); + res.push(tx.clone()); + break; + } + } + assert_eq!(res.len(), 1); + } node_txn.clear(); res } @@ -3203,10 +3221,10 @@ mod tests { // Simple case with no pending HTLCs: nodes[1].node.peer_disconnected(&nodes[0].node.get_our_node_id(), true); { - let mut node_txn = test_txn_broadcast(&nodes[1], &chan_1, None, HTLCType::NONE); + let mut node_txn = test_txn_broadcast(&nodes[1], &chan_1, None, None, HTLCType::NONE, PenaltyType::NONE); let header = BlockHeader { version: 0x20000000, prev_blockhash: Default::default(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }; nodes[0].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![node_txn.drain(..).next().unwrap()] }, 1); - test_txn_broadcast(&nodes[0], &chan_1, None, HTLCType::NONE); + test_txn_broadcast(&nodes[0], &chan_1, None, None, HTLCType::NONE, PenaltyType::NONE); } get_announce_close_broadcast_events(&nodes, 0, 1); assert_eq!(nodes[0].node.list_channels().len(), 0); @@ -3218,10 +3236,10 @@ mod tests { // Simple case of one pending HTLC to HTLC-Timeout nodes[1].node.peer_disconnected(&nodes[2].node.get_our_node_id(), true); { - let mut node_txn = test_txn_broadcast(&nodes[1], &chan_2, None, HTLCType::TIMEOUT); + let mut node_txn = test_txn_broadcast(&nodes[1], &chan_2, None, None, HTLCType::TIMEOUT, PenaltyType::NONE); let header = BlockHeader { version: 0x20000000, prev_blockhash: Default::default(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }; nodes[2].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![node_txn.drain(..).next().unwrap()] }, 1); - test_txn_broadcast(&nodes[2], &chan_2, None, HTLCType::NONE); + test_txn_broadcast(&nodes[2], &chan_2, None, None, HTLCType::NONE, PenaltyType::NONE); } get_announce_close_broadcast_events(&nodes, 1, 2); assert_eq!(nodes[1].node.list_channels().len(), 0); @@ -3255,7 +3273,7 @@ mod tests { // HTLC-Timeout and a nodes[3] claim against it (+ its own announces) nodes[2].node.peer_disconnected(&nodes[3].node.get_our_node_id(), true); { - let node_txn = test_txn_broadcast(&nodes[2], &chan_3, None, HTLCType::TIMEOUT); + let node_txn = test_txn_broadcast(&nodes[2], &chan_3, None, None, HTLCType::TIMEOUT, PenaltyType::NONE); // Claim the payment on nodes[3], giving it knowledge of the preimage claim_funds!(nodes[3], nodes[2], payment_preimage_1); @@ -3280,9 +3298,9 @@ mod tests { nodes[3].chain_monitor.block_connected_checked(&header, i, &Vec::new()[..], &[0; 0]); } - let node_txn = test_txn_broadcast(&nodes[3], &chan_4, None, HTLCType::TIMEOUT); + let node_txn = test_txn_broadcast(&nodes[3], &chan_4, None, None, HTLCType::TIMEOUT, PenaltyType::NONE); - // Claim the payment on nodes[3], giving it knowledge of the preimage + // Claim the payment on nodes[4], giving it knowledge of the preimage claim_funds!(nodes[4], nodes[3], payment_preimage_2); header = BlockHeader { version: 0x20000000, prev_blockhash: Default::default(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }; @@ -3292,7 +3310,7 @@ mod tests { nodes[4].chain_monitor.block_connected_checked(&header, i, &Vec::new()[..], &[0; 0]); } - test_txn_broadcast(&nodes[4], &chan_4, None, HTLCType::SUCCESS); + test_txn_broadcast(&nodes[4], &chan_4, None, None, HTLCType::SUCCESS, PenaltyType::NONE); header = BlockHeader { version: 0x20000000, prev_blockhash: header.bitcoin_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }; nodes[4].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![node_txn[0].clone()] }, TEST_FINAL_CLTV - 5); @@ -3327,15 +3345,13 @@ mod tests { node_txn[0].verify(&funding_tx_map).unwrap(); node_txn.swap_remove(0); } - test_txn_broadcast(&nodes[1], &chan_5, None, HTLCType::NONE); + test_txn_broadcast(&nodes[1], &chan_5, None, None, HTLCType::NONE, PenaltyType::NONE); nodes[0].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![revoked_local_txn[0].clone()] }, 1); - let node_txn = test_txn_broadcast(&nodes[0], &chan_5, Some(revoked_local_txn[0].clone()), HTLCType::TIMEOUT); + let node_txn = test_txn_broadcast(&nodes[0], &chan_5, Some(revoked_local_txn[0].clone()), None, HTLCType::TIMEOUT, PenaltyType::NONE); header = BlockHeader { version: 0x20000000, prev_blockhash: header.bitcoin_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }; nodes[1].chain_monitor.block_connected_with_filtering(&Block { header, txdata: vec![node_txn[1].clone()] }, 1); - - //TODO: At this point nodes[1] should claim the revoked HTLC-Timeout output, but that's - //not yet implemented in ChannelMonitor + test_txn_broadcast(&nodes[1], &chan_5, None, Some(node_txn[1].clone()), HTLCType::NONE, PenaltyType::HTLC); } get_announce_close_broadcast_events(&nodes, 0, 1); assert_eq!(nodes[0].node.list_channels().len(), 0);