From 2833786084af7ca108b99d5ce0baf6ab1fe98f63 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 29 Jul 2021 12:41:03 -0500 Subject: [PATCH 01/11] Clean up and add shutdown script functional tests --- lightning/src/ln/functional_tests.rs | 129 ++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 23 deletions(-) diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 6e5395e65c6..f6f3084b939 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -7521,24 +7521,69 @@ fn test_upfront_shutdown_script() { } #[test] -fn test_upfront_shutdown_script_unsupport_segwit() { - // We test that channel is closed early - // if a segwit program is passed as upfront shutdown script, - // but the peer does not support segwit. +fn test_unsupported_anysegwit_upfront_shutdown_script() { 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 nodes = create_network(2, &node_cfgs, &node_chanmgrs); + // Use a non-v0 segwit script supported by option_shutdown_anysegwit + let node_features = InitFeatures::known().clear_shutdown_anysegwit(); + let anysegwit_shutdown_script = Builder::new() + .push_int(16) + .push_slice(&[0, 40]) + .into_script(); + + // Check script when handling an open_channel message nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100000, 10001, 42, None).unwrap(); + let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id()); + open_channel.shutdown_scriptpubkey = Present(anysegwit_shutdown_script.clone()); + nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), node_features.clone(), &open_channel); + let events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + match events[0] { + MessageSendEvent::HandleError { action: ErrorAction::SendErrorMessage { ref msg }, node_id } => { + assert_eq!(node_id, nodes[0].node.get_our_node_id()); + assert!(regex::Regex::new(r"Peer is signaling upfront_shutdown but has provided a non-accepted scriptpubkey format. script: (\([A-Fa-f0-9]+\))").unwrap().is_match(&*msg.data)); + }, + _ => panic!("Unexpected event"), + } + + // Check script when handling an accept_channel message + nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100000, 10001, 42, None).unwrap(); + let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id()); + nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), InitFeatures::known(), &open_channel); + let mut accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, nodes[0].node.get_our_node_id()); + accept_channel.shutdown_scriptpubkey = Present(anysegwit_shutdown_script.clone()); + nodes[0].node.handle_accept_channel(&nodes[1].node.get_our_node_id(), node_features, &accept_channel); + + let events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + match events[0] { + MessageSendEvent::HandleError { action: ErrorAction::SendErrorMessage { ref msg }, node_id } => { + assert_eq!(node_id, nodes[1].node.get_our_node_id()); + assert!(regex::Regex::new(r"Peer is signaling upfront_shutdown but has provided a non-accepted scriptpubkey format. script: (\([A-Fa-f0-9]+\))").unwrap().is_match(&*msg.data)); + }, + _ => panic!("Unexpected event"), + } +} + +#[test] +fn test_invalid_upfront_shutdown_script() { + 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 nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100000, 10001, 42, None).unwrap(); + + // Use a segwit v0 script with an unsupported witness program let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id()); - open_channel.shutdown_scriptpubkey = Present(Builder::new().push_int(16) + open_channel.shutdown_scriptpubkey = Present(Builder::new().push_int(0) .push_slice(&[0, 0]) .into_script()); - - let features = InitFeatures::known().clear_shutdown_anysegwit(); - nodes[0].node.handle_open_channel(&nodes[0].node.get_our_node_id(), features, &open_channel); + nodes[0].node.handle_open_channel(&nodes[0].node.get_our_node_id(), InitFeatures::known(), &open_channel); let events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -7552,7 +7597,7 @@ fn test_upfront_shutdown_script_unsupport_segwit() { } #[test] -fn test_shutdown_script_any_segwit_allowed() { +fn test_segwit_v0_shutdown_script() { let mut config = UserConfig::default(); config.channel_options.announced_channel = true; config.peer_channel_config_limits.force_announced_channel_preference = false; @@ -7563,14 +7608,50 @@ fn test_shutdown_script_any_segwit_allowed() { let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &user_cfgs); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); - //// We test if the remote peer accepts opt_shutdown_anysegwit, a witness program can be used on shutdown - let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 1000000, InitFeatures::known(), InitFeatures::known()); + let chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); nodes[1].node.close_channel(&OutPoint { txid: chan.3.txid(), index: 0 }.to_channel_id()).unwrap(); + + // Use a segwit v0 script supported even without option_shutdown_anysegwit + let mut node_0_shutdown = get_event_msg!(nodes[1], MessageSendEvent::SendShutdown, nodes[0].node.get_our_node_id()); + node_0_shutdown.scriptpubkey = Builder::new().push_int(0) + .push_slice(&[0; 20]) + .into_script(); + nodes[0].node.handle_shutdown(&nodes[1].node.get_our_node_id(), &InitFeatures::known(), &node_0_shutdown); + + let events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + match events[0] { + MessageSendEvent::SendShutdown { node_id, .. } => { assert_eq!(node_id, nodes[1].node.get_our_node_id()) } + _ => panic!("Unexpected event"), + } + match events[1] { + MessageSendEvent::SendClosingSigned { node_id, .. } => { assert_eq!(node_id, nodes[1].node.get_our_node_id()) } + _ => panic!("Unexpected event"), + } +} + +#[test] +fn test_anysegwit_shutdown_script() { + let mut config = UserConfig::default(); + config.channel_options.announced_channel = true; + config.peer_channel_config_limits.force_announced_channel_preference = false; + config.channel_options.commit_upfront_shutdown_pubkey = false; + let user_cfgs = [None, Some(config), None]; + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &user_cfgs); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); + nodes[1].node.close_channel(&OutPoint { txid: chan.3.txid(), index: 0 }.to_channel_id()).unwrap(); + + // Use a non-v0 segwit script supported by option_shutdown_anysegwit let mut node_0_shutdown = get_event_msg!(nodes[1], MessageSendEvent::SendShutdown, nodes[0].node.get_our_node_id()); node_0_shutdown.scriptpubkey = Builder::new().push_int(16) .push_slice(&[0, 0]) .into_script(); nodes[0].node.handle_shutdown(&nodes[1].node.get_our_node_id(), &InitFeatures::known(), &node_0_shutdown); + let events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 2); match events[0] { @@ -7584,7 +7665,7 @@ fn test_shutdown_script_any_segwit_allowed() { } #[test] -fn test_shutdown_script_any_segwit_not_allowed() { +fn test_unsupported_anysegwit_shutdown_script() { let mut config = UserConfig::default(); config.channel_options.announced_channel = true; config.peer_channel_config_limits.force_announced_channel_preference = false; @@ -7595,22 +7676,23 @@ fn test_shutdown_script_any_segwit_not_allowed() { let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &user_cfgs); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); - //// We test that if the remote peer does not accept opt_shutdown_anysegwit, the witness program cannot be used on shutdown - let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 1000000, InitFeatures::known(), InitFeatures::known()); + let node_features = InitFeatures::known().clear_shutdown_anysegwit(); + let chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), node_features.clone()); nodes[1].node.close_channel(&OutPoint { txid: chan.3.txid(), index: 0 }.to_channel_id()).unwrap(); + + // Use a non-v0 segwit script supported by option_shutdown_anysegwit let mut node_0_shutdown = get_event_msg!(nodes[1], MessageSendEvent::SendShutdown, nodes[0].node.get_our_node_id()); - // Make an any segwit version script node_0_shutdown.scriptpubkey = Builder::new().push_int(16) - .push_slice(&[0, 0]) + .push_slice(&[0, 40]) .into_script(); - let flags_no = InitFeatures::known().clear_shutdown_anysegwit(); - nodes[0].node.handle_shutdown(&nodes[1].node.get_our_node_id(), &flags_no, &node_0_shutdown); + nodes[0].node.handle_shutdown(&nodes[1].node.get_our_node_id(), &node_features, &node_0_shutdown); + let events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 2); match events[1] { MessageSendEvent::HandleError { action: ErrorAction::SendErrorMessage { ref msg }, node_id } => { assert_eq!(node_id, nodes[1].node.get_our_node_id()); - assert_eq!(msg.data, "Got a nonstandard scriptpubkey (60020000) from remote peer".to_owned()) + assert_eq!(msg.data, "Got a nonstandard scriptpubkey (60020028) from remote peer".to_owned()); }, _ => panic!("Unexpected event"), } @@ -7618,7 +7700,7 @@ fn test_shutdown_script_any_segwit_not_allowed() { } #[test] -fn test_shutdown_script_segwit_but_not_anysegwit() { +fn test_invalid_shutdown_script() { let mut config = UserConfig::default(); config.channel_options.announced_channel = true; config.peer_channel_config_limits.force_announced_channel_preference = false; @@ -7629,15 +7711,16 @@ fn test_shutdown_script_segwit_but_not_anysegwit() { let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &user_cfgs); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); - //// We test that if shutdown any segwit is supported and we send a witness script with 0 version, this is not accepted - let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 1000000, InitFeatures::known(), InitFeatures::known()); + let chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()); nodes[1].node.close_channel(&OutPoint { txid: chan.3.txid(), index: 0 }.to_channel_id()).unwrap(); + + // Use a segwit v0 script with an unsupported witness program let mut node_0_shutdown = get_event_msg!(nodes[1], MessageSendEvent::SendShutdown, nodes[0].node.get_our_node_id()); - // Make a segwit script that is not a valid as any segwit node_0_shutdown.scriptpubkey = Builder::new().push_int(0) .push_slice(&[0, 0]) .into_script(); nodes[0].node.handle_shutdown(&nodes[1].node.get_our_node_id(), &InitFeatures::known(), &node_0_shutdown); + let events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 2); match events[1] { From ecc70757f9863a76ab9d4cc8b6bfe1a9cc9f4977 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 21 Jul 2021 20:12:14 -0500 Subject: [PATCH 02/11] Add ShutdownScript for BOLT 2 acceptable scripts BOLT 2 enumerates the script formats that may be used for a shutdown script. KeysInterface::get_shutdown_pubkey returns a PublicKey used to form one of the acceptable formats (P2WPKH). Add a ShutdownScript abstraction to encapsulate all accept formats and be backwards compatible with P2WPKH scripts serialized as the corresponding PublicKey. --- lightning/src/ln/mod.rs | 1 + lightning/src/ln/script.rs | 268 +++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 lightning/src/ln/script.rs diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 7500b93c005..5f928eea072 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -27,6 +27,7 @@ pub mod msgs; pub mod peer_handler; pub mod chan_utils; pub mod features; +pub mod script; #[cfg(feature = "fuzztarget")] pub mod peer_channel_encryptor; diff --git a/lightning/src/ln/script.rs b/lightning/src/ln/script.rs new file mode 100644 index 00000000000..b6fcdbd1904 --- /dev/null +++ b/lightning/src/ln/script.rs @@ -0,0 +1,268 @@ +//! Abstractions for scripts used in the Lightning Network. + +use bitcoin::blockdata::opcodes::all::OP_PUSHBYTES_0 as SEGWIT_V0; +use bitcoin::blockdata::script::{Builder, Script}; +use bitcoin::hashes::Hash; +use bitcoin::hash_types::{PubkeyHash, ScriptHash, WPubkeyHash, WScriptHash}; +use bitcoin::secp256k1::key::PublicKey; + +use ln::features::InitFeatures; + +use core::convert::TryFrom; +use core::num::NonZeroU8; + +/// A script pubkey for shutting down a channel as defined by [BOLT #2]. +/// +/// [BOLT #2]: https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md +pub struct ShutdownScript(ShutdownScriptImpl); + +/// An error occurring when converting from [`Script`] to [`ShutdownScript`]. +#[derive(Debug)] +pub struct InvalidShutdownScript { + /// The script that did not meet the requirements from [BOLT #2]. + /// + /// [BOLT #2]: https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md + pub script: Script +} + +enum ShutdownScriptImpl { + /// [`PublicKey`] used to form a P2WPKH script pubkey. Used to support backward-compatible + /// serialization. + Legacy(PublicKey), + + /// [`Script`] adhering to a script pubkey format specified in BOLT #2. + Bolt2(Script), +} + +impl ShutdownScript { + /// Generates a P2WPKH script pubkey from the given [`PublicKey`]. + pub fn new_p2wpkh_from_pubkey(pubkey: PublicKey) -> Self { + Self(ShutdownScriptImpl::Legacy(pubkey)) + } + + /// Generates a P2PKH script pubkey from the given [`PubkeyHash`]. + pub fn new_p2pkh(pubkey_hash: &PubkeyHash) -> Self { + Self(ShutdownScriptImpl::Bolt2(Script::new_p2pkh(pubkey_hash))) + } + + /// Generates a P2SH script pubkey from the given [`ScriptHash`]. + pub fn new_p2sh(script_hash: &ScriptHash) -> Self { + Self(ShutdownScriptImpl::Bolt2(Script::new_p2sh(script_hash))) + } + + /// Generates a P2WPKH script pubkey from the given [`WPubkeyHash`]. + pub fn new_p2wpkh(pubkey_hash: &WPubkeyHash) -> Self { + Self(ShutdownScriptImpl::Bolt2(Script::new_v0_wpkh(pubkey_hash))) + } + + /// Generates a P2WSH script pubkey from the given [`WScriptHash`]. + pub fn new_p2wsh(script_hash: &WScriptHash) -> Self { + Self(ShutdownScriptImpl::Bolt2(Script::new_v0_wsh(script_hash))) + } + + /// Generates a P2WSH script pubkey from the given segwit version and program. + /// + /// # Errors + /// + /// This function may return an error if `program` is invalid for the segwit `version`. + pub fn new_witness_program(version: NonZeroU8, program: &[u8]) -> Result { + let script = Builder::new() + .push_int(version.get().into()) + .push_slice(&program) + .into_script(); + Self::try_from(script) + } + + /// Converts the shutdown script into the underlying [`Script`]. + pub fn into_inner(self) -> Script { + self.into() + } + + /// Returns the [`PublicKey`] used for a P2WPKH shutdown script if constructed directly from it. + pub fn as_legacy_pubkey(&self) -> Option<&PublicKey> { + match &self.0 { + ShutdownScriptImpl::Legacy(pubkey) => Some(pubkey), + ShutdownScriptImpl::Bolt2(_) => None, + } + } + + /// Returns whether the shutdown script is compatible with the features as defined by BOLT #2. + /// + /// Specifically, checks for compliance with feature `option_shutdown_anysegwit`. + pub fn is_compatible(&self, features: &InitFeatures) -> bool { + match &self.0 { + ShutdownScriptImpl::Legacy(_) => true, + ShutdownScriptImpl::Bolt2(script) => is_bolt2_compliant(script, features), + } + } +} + +fn is_bolt2_compliant(script: &Script, features: &InitFeatures) -> bool { + if script.is_p2pkh() || script.is_p2sh() || script.is_v0_p2wpkh() || script.is_v0_p2wsh() { + true + } else if features.supports_shutdown_anysegwit() { + script.is_witness_program() && script.as_bytes()[0] != SEGWIT_V0.into_u8() + } else { + false + } +} + +impl TryFrom