diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 20b1f64b..906f7393 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -14,6 +14,7 @@ use std::fmt::Write; use bitcoin::{opcodes::all as opcodes, script::Builder, script::PushBytes}; use rand::{Rng, RngExt}; use serde::{Deserialize, Serialize}; +pub use smite::bolt::ChannelTypeVariant; use smite::bolt::ShortChannelId; use super::VariableType; @@ -422,163 +423,6 @@ impl fmt::Display for ShutdownScriptVariant { } } -/// A specific BOLT 2 `channel_type` feature-bit combination. -/// -/// Each variant corresponds to a channel type accepted by at least one target -/// implementation: -/// -/// - `option_static_remotekey` (bit 12) -/// - `option_anchors` (bits 22 and 12) -/// - `zero_fee_commitments` (bit 40) -/// - `option_simple_taproot` (bit 80) -/// - `option_simple_taproot_staging` (bit 180) -/// - `option_script_enforced_lease` (bits 2022, 22, 12) -/// -/// Additionally, the following bits can be added to any channel type: -/// - `option_scid_alias` (bit 46) -/// - `option_zeroconf` (bit 50) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum ChannelTypeVariant { - /// bit 12 - StaticRemoteKey, - /// bits 12, 46 - StaticRemoteKeyScidAlias, - /// bits 12, 50 - StaticRemoteKeyZeroConf, - /// bits 12, 46, 50 - StaticRemoteKeyScidAliasZeroConf, - /// bits 12, 22 - Anchors, - /// bits 12, 22, 46 - AnchorsScidAlias, - /// bits 12, 22, 50 - AnchorsZeroConf, - /// bits 12, 22, 46, 50 - AnchorsScidAliasZeroConf, - /// bit 40 - ZeroFeeCommitments, - /// bits 40, 46 - ZeroFeeCommitmentsScidAlias, - /// bits 40, 50 - ZeroFeeCommitmentsZeroConf, - /// bits 40, 46, 50 - ZeroFeeCommitmentsScidAliasZeroConf, - /// bit 80 - SimpleTaproot, - /// bits 80, 46 - SimpleTaprootScidAlias, - /// bits 80, 50 - SimpleTaprootZeroConf, - /// bits 80, 46, 50 - SimpleTaprootScidAliasZeroConf, - /// bit 180 - SimpleTaprootStaging, - /// bits 180, 46 - SimpleTaprootStagingScidAlias, - /// bits 180, 50 - SimpleTaprootStagingZeroConf, - /// bits 180, 46, 50 - SimpleTaprootStagingScidAliasZeroConf, - /// bits 12, 22, 2022 - ScriptEnforcedLease, - /// bits 12, 22, 2022, 46 - ScriptEnforcedLeaseScidAlias, - /// bits 12, 22, 2022, 50 - ScriptEnforcedLeaseZeroConf, - /// bits 12, 22, 2022, 46, 50 - ScriptEnforcedLeaseScidAliasZeroConf, -} - -impl ChannelTypeVariant { - /// All variants. Keep in sync with the enum definition. - pub const ALL: &[Self] = &[ - Self::StaticRemoteKey, - Self::StaticRemoteKeyScidAlias, - Self::StaticRemoteKeyZeroConf, - Self::StaticRemoteKeyScidAliasZeroConf, - Self::Anchors, - Self::AnchorsScidAlias, - Self::AnchorsZeroConf, - Self::AnchorsScidAliasZeroConf, - Self::ZeroFeeCommitments, - Self::ZeroFeeCommitmentsScidAlias, - Self::ZeroFeeCommitmentsZeroConf, - Self::ZeroFeeCommitmentsScidAliasZeroConf, - Self::SimpleTaproot, - Self::SimpleTaprootScidAlias, - Self::SimpleTaprootZeroConf, - Self::SimpleTaprootScidAliasZeroConf, - Self::SimpleTaprootStaging, - Self::SimpleTaprootStagingScidAlias, - Self::SimpleTaprootStagingZeroConf, - Self::SimpleTaprootStagingScidAliasZeroConf, - Self::ScriptEnforcedLease, - Self::ScriptEnforcedLeaseScidAlias, - Self::ScriptEnforcedLeaseZeroConf, - Self::ScriptEnforcedLeaseScidAliasZeroConf, - ]; - - /// The feature bits (even/required) contained in this channel type. - #[must_use] - pub fn bits(self) -> &'static [usize] { - // BOLT 9 feature bits: - // 12 = option_static_remotekey - // 22 = option_anchors - // 40 = zero_fee_commitments - // 46 = option_scid_alias - // 50 = option_zeroconf - // 80 = option_simple_taproot - // 180 = option_simple_taproot_staging - // 2022 = option_script_enforced_lease - match self { - Self::StaticRemoteKey => &[12], - Self::StaticRemoteKeyScidAlias => &[12, 46], - Self::StaticRemoteKeyZeroConf => &[12, 50], - Self::StaticRemoteKeyScidAliasZeroConf => &[12, 46, 50], - Self::Anchors => &[12, 22], - Self::AnchorsScidAlias => &[12, 22, 46], - Self::AnchorsZeroConf => &[12, 22, 50], - Self::AnchorsScidAliasZeroConf => &[12, 22, 46, 50], - Self::ZeroFeeCommitments => &[40], - Self::ZeroFeeCommitmentsScidAlias => &[40, 46], - Self::ZeroFeeCommitmentsZeroConf => &[40, 50], - Self::ZeroFeeCommitmentsScidAliasZeroConf => &[40, 46, 50], - Self::SimpleTaproot => &[80], - Self::SimpleTaprootScidAlias => &[80, 46], - Self::SimpleTaprootZeroConf => &[80, 50], - Self::SimpleTaprootScidAliasZeroConf => &[80, 46, 50], - Self::SimpleTaprootStaging => &[180], - Self::SimpleTaprootStagingScidAlias => &[180, 46], - Self::SimpleTaprootStagingZeroConf => &[180, 50], - Self::SimpleTaprootStagingScidAliasZeroConf => &[180, 46, 50], - Self::ScriptEnforcedLease => &[12, 22, 2022], - Self::ScriptEnforcedLeaseScidAlias => &[12, 22, 2022, 46], - Self::ScriptEnforcedLeaseZeroConf => &[12, 22, 2022, 50], - Self::ScriptEnforcedLeaseScidAliasZeroConf => &[12, 22, 2022, 46, 50], - } - } - - /// Encodes the channel type as a BOLT feature bitmap (big-endian bytes). - #[must_use] - #[allow(clippy::missing_panics_doc)] // bits() is always non-empty - pub fn encode(self) -> Vec { - let bits = self.bits(); - let max_bit = *bits.iter().max().expect("non-empty bits"); - let num_bytes = max_bit / 8 + 1; - let mut out = vec![0u8; num_bytes]; - for &bit in bits { - out[num_bytes - 1 - bit / 8] |= 1 << (bit % 8); - } - out - } -} - -impl fmt::Display for ChannelTypeVariant { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - /// Fields that can be extracted from an `AcceptChannel` compound variable. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AcceptChannelField { diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 1c01b145..4c23bfab 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -9,8 +9,8 @@ use bitcoin::{OutPoint, ScriptBuf, Txid}; use smite::bitcoin::{BitcoinCli, TxBlockPosition, Utxo}; use smite::bolt::{ AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelId, ChannelReady, - ChannelReadyTlvs, ChannelUpdate, FundingCreated, FundingSigned, Message, NodeAnnouncement, - OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, msg_type, + ChannelReadyTlvs, ChannelUpdate, Features, FundingCreated, FundingSigned, Message, + NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, msg_type, }; use smite::channel_tx::{ ChannelConfig, ChannelPartyConfig, ChannelState, FundingTransaction, HolderIdentity, Side, @@ -133,8 +133,8 @@ pub struct ProgramContext { pub chain_hash: [u8; 32], /// Current block height at snapshot time. pub block_height: u32, - /// Target's advertised feature bits from init message. - pub target_features: Vec, + /// Features negotiated between the target node and Smite. + pub negotiated_features: Features, } /// Abstraction over a Noise-encrypted connection, allowing mock implementations @@ -481,6 +481,7 @@ impl Executor { AcceptChannelOracle.evaluate(&AcceptChannelContext { accept_channel: &ac, negotiation: self.negotiations.get(&ac.temporary_channel_id), + negotiated_features: &self.context.negotiated_features, })?; record_recv_accept_channel(&mut self.negotiations, &ac); Some(Variable::AcceptChannel(ac)) @@ -932,7 +933,7 @@ fn build_funding_created( let config = ChannelConfig { funding_outpoint, funding_satoshis: open_channel.funding_satoshis, - channel_type: open_channel.tlvs.channel_type.clone().unwrap_or_default(), + channel_type: Features::from(open_channel.tlvs.channel_type.clone().unwrap_or_default()), opener, acceptor, minimum_depth: accept_channel.minimum_depth, @@ -1557,7 +1558,11 @@ mod tests { target_pubkey: sample_pubkey(1), chain_hash: [0xcc; 32], block_height: 800_000, - target_features: vec![], + negotiated_features: Features::from_bits(&[ + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_ANCHORS, + Features::OPTION_CHANNEL_TYPE, + ]), } } diff --git a/smite-scenarios/src/scenarios/setup.rs b/smite-scenarios/src/scenarios/setup.rs index 08422d86..c626279d 100644 --- a/smite-scenarios/src/scenarios/setup.rs +++ b/smite-scenarios/src/scenarios/setup.rs @@ -2,7 +2,7 @@ use std::time::Duration; -use smite::bolt::{Init, InitTlvs, Message}; +use smite::bolt::{FeatureBit, Features, Init, InitTlvs, Message}; use smite::noise::NoiseConnection; use smite::scenarios::ScenarioError; @@ -30,49 +30,34 @@ pub trait SnapshotSetup { fn setup(target: &T) -> Result<(NoiseConnection, ProgramContext), ScenarioError>; } -/// Clears a feature bit from a feature vector. -/// -/// Feature vectors are encoded as big-endian byte arrays where bit N lives in -/// byte `features[len - 1 - N/8]` at position `N % 8`. -fn clear_feature_bit(features: &mut [u8], bit: usize) { - let byte_index = features.len().checked_sub(1 + bit / 8); - if let Some(i) = byte_index { - features[i] &= !(1 << (bit % 8)); - } -} - -/// Gossip-related feature bits (BOLT 9): `gossip_queries` (6/7), -/// `gossip_queries_ex` (10/11). Stripped so the target doesn't send -/// `gossip_timestamp_filter` or other gossip noise during execution. -const GOSSIP_FEATURE_BITS: &[usize] = &[6, 7, 10, 11]; - -/// Feature bits that force a dual-funded flow when both peers support them: -/// `option_dual_fund` (28/29). Eclair in particular will not allow -/// single-funded flows if either of these feature bits is set, so we strip them -/// when fuzzing the single-funded flow. -const DUAL_FUNDING_FEATURE_BITS: &[usize] = &[28, 29]; - -/// Peer storage feature bits: `option_provide_storage` (42/43). When enabled, -/// peers may send `peer_storage` and `peer_storage_retrieval` messages at -/// arbitrary times. Disabling these bits eliminates peer storage noise. -const PEER_STORAGE_FEATURE_BITS: &[usize] = &[42, 43]; +/// Features stripped from our echoed `init` so the target stays on the single +/// funded flow and doesn't emit unrelated noise: +/// - `gossip_queries` (6/7), `gossip_queries_ex` (10/11): Stripped so the +/// target doesn't send `gossip_timestamp_filter` or other gossip noise during +/// execution. +/// - `option_dual_fund` (28/29): Eclair in particular will not allow +/// single-funded flows if either of these feature bits is set. +/// - `option_provide_storage` (42/43): When enabled, peers may send +/// `peer_storage` and `peer_storage_retrieval` messages at arbitrary times. +const STRIPPED_FEATURES: &[FeatureBit] = &[ + Features::GOSSIP_QUERIES, + Features::GOSSIP_QUERIES_EX, + Features::OPTION_DUAL_FUND, + Features::OPTION_PROVIDE_STORAGE, +]; /// Creates an `init` that echoes the received features with bits stripped that /// would steer the target away from the single-funded `open_channel` flow. fn init_for_single_funded(received: &Init) -> Init { - let mut globalfeatures = received.globalfeatures.clone(); - let mut features = received.features.clone(); - for &bit in GOSSIP_FEATURE_BITS - .iter() - .chain(DUAL_FUNDING_FEATURE_BITS) - .chain(PEER_STORAGE_FEATURE_BITS) - { - clear_feature_bit(&mut globalfeatures, bit); - clear_feature_bit(&mut features, bit); + let mut globalfeatures = Features::from(received.globalfeatures.clone()); + let mut features = Features::from(received.features.clone()); + for &bit in STRIPPED_FEATURES { + globalfeatures.clear_feature(bit); + features.clear_feature(bit); } Init { - globalfeatures, - features, + globalfeatures: globalfeatures.into_bytes(), + features: features.into_bytes(), tlvs: InitTlvs::default(), } } @@ -88,7 +73,7 @@ impl SnapshotSetup for PostInitSetup { // Echo features but strip the bits that would take us off the // single-funded `open_channel` path this setup is built for. let our_init = init_for_single_funded(&target_init); - conn.send_message(&Message::Init(our_init).encode())?; + conn.send_message(&Message::Init(our_init.clone()).encode())?; // Drain any remaining post-init noise so the snapshot starts with a // clean connection. @@ -101,7 +86,11 @@ impl SnapshotSetup for PostInitSetup { // this is the floor. Dynamic per-target queries can replace it // later. block_height: u32::try_from(INITIAL_BLOCKS).expect("fits in u32"), - target_features: target_init.features, + // Since we echo the same features the target sent, but strip both + // required and optional bits to exercise only the single funded + // flow and avoid unrelated noise, negotiated features are just the + // features we sent in our init. + negotiated_features: Features::from(our_init.features), }; Ok((conn, context)) diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index 69eda957..7008cb00 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -14,6 +14,7 @@ mod closing_complete; mod closing_sig; mod commitment_signed; mod error; +mod features; mod funding_created; mod funding_signed; mod gossip_timestamp_filter; @@ -52,6 +53,7 @@ pub use closing_complete::{ClosingComplete, ClosingTlvs}; pub use closing_sig::ClosingSig; pub use commitment_signed::{CommitmentSigned, CommitmentSignedTlvs}; pub use error::Error; +pub use features::{FeatureBit, Features}; pub use funding_created::FundingCreated; pub use funding_signed::FundingSigned; pub use gossip_timestamp_filter::GossipTimestampFilter; @@ -62,7 +64,7 @@ pub use open_channel2::{OpenChannel2, OpenChannel2Tlvs}; pub use ping::Ping; pub use pong::Pong; pub use revoke_and_ack::RevokeAndAck; -pub use shutdown::Shutdown; +pub use shutdown::{Shutdown, is_acceptable_shutdown_script, is_standard_shutdown_script}; pub use tlv::{TlvRecord, TlvStream}; pub use tx_abort::TxAbort; pub use tx_ack_rbf::{TxAckRbf, TxAckRbfTlvs}; @@ -72,9 +74,9 @@ pub use tx_init_rbf::{TxInitRbf, TxInitRbfTlvs}; pub use tx_remove_input::TxRemoveInput; pub use tx_remove_output::TxRemoveOutput; pub use types::{ - BigSize, CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, ChannelId, MAX_MESSAGE_SIZE, - PAYMENT_ONION_PACKET_SIZE, PER_COMMITMENT_SECRET_SIZE, PUBLIC_KEY_SIZE, SHA256_HASH_SIZE, - SHORT_CHANNEL_ID_SIZE, ShortChannelId, TXID_SIZE, Tu32, Tu64, + BigSize, CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, ChannelId, ChannelTypeVariant, + MAX_MESSAGE_SIZE, PAYMENT_ONION_PACKET_SIZE, PER_COMMITMENT_SECRET_SIZE, PUBLIC_KEY_SIZE, + SHA256_HASH_SIZE, SHORT_CHANNEL_ID_SIZE, ShortChannelId, TXID_SIZE, Tu32, Tu64, }; pub use update_add_htlc::{UpdateAddHtlc, UpdateAddHtlcTlvs}; pub use update_fail_htlc::{UpdateFailHtlc, UpdateFailHtlcTlvs}; diff --git a/smite/src/bolt/features.rs b/smite/src/bolt/features.rs new file mode 100644 index 00000000..2b70d1b9 --- /dev/null +++ b/smite/src/bolt/features.rs @@ -0,0 +1,451 @@ +//! BOLT 9 feature bitfield primitives. + +/// BOLT 9 feature bit index. Even bits are required (reject if unknown); odd +/// bits are optional (safe to ignore if unknown): "it's ok to be odd" rule. +pub type FeatureBit = usize; + +/// BOLT 9 feature bitfield, encoded as big-endian bytes. +#[derive(Debug, Clone, Default)] +pub struct Features(Vec); + +impl Features { + /// `option_upfront_shutdown_script` (bits 4/5). + pub const OPTION_UPFRONT_SHUTDOWN_SCRIPT: FeatureBit = 4; + /// `gossip_queries` (bits 6/7). + pub const GOSSIP_QUERIES: FeatureBit = 6; + /// `gossip_queries_ex` (bits 10/11). + pub const GOSSIP_QUERIES_EX: FeatureBit = 10; + /// `option_static_remotekey` (bits 12/13). + pub const OPTION_STATIC_REMOTEKEY: FeatureBit = 12; + /// `option_support_large_channel` (bits 18/19). + pub const OPTION_SUPPORT_LARGE_CHANNEL: FeatureBit = 18; + /// `option_anchors` (bits 22/23). + pub const OPTION_ANCHORS: FeatureBit = 22; + /// `option_shutdown_anysegwit` (bits 26/27). + pub const OPTION_SHUTDOWN_ANYSEGWIT: FeatureBit = 26; + /// `option_dual_fund` (bits 28/29). + pub const OPTION_DUAL_FUND: FeatureBit = 28; + /// `zero_fee_commitments` (bits 40/41). + pub const ZERO_FEE_COMMITMENTS: FeatureBit = 40; + /// `option_provide_storage` (bits 42/43). + pub const OPTION_PROVIDE_STORAGE: FeatureBit = 42; + /// `option_channel_type` (bits 44/45). + pub const OPTION_CHANNEL_TYPE: FeatureBit = 44; + /// `option_scid_alias` (bits 46/47). + pub const OPTION_SCID_ALIAS: FeatureBit = 46; + /// `option_zeroconf` (bits 50/51). + pub const OPTION_ZEROCONF: FeatureBit = 50; + /// `option_simple_close` (bits 60/61). + pub const OPTION_SIMPLE_CLOSE: FeatureBit = 60; + /// `option_simple_taproot` (bits 80/81). + pub const OPTION_SIMPLE_TAPROOT: FeatureBit = 80; + /// `option_simple_taproot_staging` (bits 180/181). + pub const OPTION_SIMPLE_TAPROOT_STAGING: FeatureBit = 180; + /// `option_script_enforced_lease` (bits 2022/2023). + /// Note: Currently, this feature is LND-specific and is not defined in BOLT 9. + pub const OPTION_SCRIPT_ENFORCED_LEASE: FeatureBit = 2022; + + /// Creates an empty set of features. + #[must_use] + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Creates features with the given bits set. + #[must_use] + pub fn from_bits(bits: &[FeatureBit]) -> Self { + let mut features = Self::new(); + for &bit in bits { + features.set_bit(bit); + } + features + } + + /// Consumes the features into their underlying bytes. + #[must_use] + pub fn into_bytes(self) -> Vec { + self.0 + } + + /// Sets the bit, extending the features with leading zero bytes if needed. + /// + /// # Panics + /// + /// Panics if bit exceeds the BOLT 9 feature bitfield's u16-byte length + /// limit. + pub fn set_bit(&mut self, bit: FeatureBit) { + let byte_offset = bit / 8; + assert!( + byte_offset < u16::MAX as usize, + "feature bit {bit} exceeds the feature bitfield length limit" + ); + + let mut len = self.0.len(); + if len <= byte_offset { + let new_len = byte_offset + 1; + let mut new_features = vec![0u8; new_len]; + new_features[(new_len - len)..].copy_from_slice(&self.0); + self.0 = new_features; + len = new_len; + } + + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] |= mask; + } + + /// Clears the bit, no-op if beyond the feature's length. + pub fn clear_bit(&mut self, bit: FeatureBit) { + let byte_offset = bit / 8; + let len = self.0.len(); + if byte_offset < len { + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] &= !mask; + } + } + + /// Returns whether the bit is set. + #[must_use] + pub fn is_bit_set(&self, bit: FeatureBit) -> bool { + let byte_offset = bit / 8; + let len = self.0.len(); + if len <= byte_offset { + return false; + } + + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] & mask != 0 + } + + /// Returns whether the feature is supported by checking its required or + /// optional bit. + #[must_use] + pub fn supports_feature(&self, bit: FeatureBit) -> bool { + self.is_bit_set(bit) || self.is_bit_set(bit ^ 1) + } + + /// Clears the feature's required (even) and optional (odd) bits. + pub fn clear_feature(&mut self, bit: FeatureBit) { + self.clear_bit(bit); + self.clear_bit(bit ^ 1); + } + + /// Returns whether every bit set here is supported by `other`, where the + /// feature's required (even) or optional (odd) bit both count as support. + #[must_use] + pub fn is_supported(&self, other: &Features) -> bool { + for bit in 0..(self.0.len() * 8) { + if self.is_bit_set(bit) && !other.supports_feature(bit) { + return false; + } + } + true + } +} + +impl PartialEq for Features { + /// Returns true if this set and other have the same feature bits set. + fn eq(&self, other: &Self) -> bool { + for bit in 0..(self.0.len().max(other.0.len()) * 8) { + if self.is_bit_set(bit) != other.is_bit_set(bit) { + return false; + } + } + true + } +} + +impl Eq for Features {} + +impl From> for Features { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From<&[u8]> for Features { + fn from(bytes: &[u8]) -> Self { + Self(bytes.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_features_support_no_features() { + let features = Features::new(); + assert!(!features.supports_feature(Features::OPTION_ANCHORS)); + assert!(!features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + assert!(!features.supports_feature(Features::GOSSIP_QUERIES)); + } + + #[test] + fn from_bits_sets_requested_bits() { + assert_eq!(Features::from_bits(&[]), Features::new()); + assert_eq!( + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY]).into_bytes(), + vec![0x10, 0x00] + ); + assert_eq!( + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY, Features::OPTION_ANCHORS]), + Features::from(vec![0x40, 0x10, 0x00]) + ); + } + + #[test] + fn supports_multiple_set_features() { + let features = Features::from_bits(&[ + Features::OPTION_ANCHORS, + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_DUAL_FUND, + ]); + + assert!(features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + assert!(features.supports_feature(Features::OPTION_DUAL_FUND)); + assert!(!features.supports_feature(Features::OPTION_ZEROCONF)); + } + + #[test] + fn set_bit_within_existing_length() { + let mut fv = Features::from(vec![0x00, 0x00]); + fv.set_bit(0); + assert_eq!(fv, Features::from(vec![0x00, 0x01])); + fv.set_bit(8); + assert_eq!(fv, Features::from(vec![0x01, 0x01])); + } + + #[test] + fn set_bit_grows_and_preserves_existing_bits() { + let mut fv = Features::from(vec![0x01]); + fv.set_bit(12); + assert_eq!(fv, Features::from(vec![0x10, 0x01])); + assert!(fv.is_bit_set(0)); + assert!(fv.is_bit_set(12)); + } + + #[test] + fn set_bit_accepts_the_highest_bit_within_the_length_limit() { + // The bitfield is length-prefixed with a `u16`, so `u16::MAX` bytes + // hold bits 0 through `u16::MAX * 8 - 1`. + let highest = u16::MAX as usize * 8 - 1; + let mut fv = Features::new(); + fv.set_bit(highest); + assert!(fv.is_bit_set(highest)); + assert_eq!(fv.into_bytes().len(), u16::MAX as usize); + } + + #[test] + #[should_panic(expected = "exceeds the feature bitfield length limit")] + fn set_bit_panics_beyond_the_length_limit() { + Features::new().set_bit(u16::MAX as usize * 8); + } + + #[test] + fn clear_bit_within_bounds_and_noop_out_of_bounds() { + let mut fv = Features::from(vec![0xff, 0xff]); + fv.clear_bit(0); + assert_eq!(fv, Features::from(vec![0xff, 0xfe])); + // Out of range: no-op. + fv.clear_bit(100); + assert_eq!(fv, Features::from(vec![0xff, 0xfe])); + } + + #[test] + fn is_bit_set_uses_big_endian_bit_order() { + let fv = Features::from(vec![0x00, 0x01]); + assert!(fv.is_bit_set(0)); + assert!(!fv.is_bit_set(1)); + + let fv = Features::from(vec![0x01, 0x00]); + assert!(fv.is_bit_set(8)); + assert!(!fv.is_bit_set(0)); + } + + #[test] + fn is_bit_set_out_of_bounds_returns_false() { + assert!(!Features::new().is_bit_set(0)); + assert!(!Features::from(vec![0xff]).is_bit_set(8)); + } + + #[test] + fn supports_feature_uses_big_endian_bit_order() { + // Required (bit 22), optional (bit 23). + assert!(Features::from(vec![0x40, 0x00, 0x00]).supports_feature(Features::OPTION_ANCHORS)); + assert!(Features::from(vec![0x80, 0x00, 0x00]).supports_feature(Features::OPTION_ANCHORS)); + // No support. + assert!(!Features::from(vec![0x00, 0x00, 0x40]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0x00, 0x00, 0x80]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0xff, 0xff]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0x00, 0x10]).supports_feature(Features::OPTION_ANCHORS)); + } + + #[test] + fn supports_feature_matches_either_bit_set() { + // Only the required (even) bit set. + let required_only = Features::from_bits(&[22]); + assert!(required_only.supports_feature(Features::OPTION_ANCHORS)); + assert!(required_only.supports_feature(23)); + // Only the optional (odd) bit set. + let optional_only = Features::from_bits(&[23]); + assert!(optional_only.supports_feature(Features::OPTION_ANCHORS)); + assert!(optional_only.supports_feature(23)); + // Neither bit set. + assert!( + !Features::from_bits(&[Features::OPTION_ZEROCONF]) + .supports_feature(Features::OPTION_ANCHORS) + ); + } + + #[test] + fn clear_feature_clears_both_bits() { + // Passing the even bit clears the pair. + let mut fv = Features::from_bits(&[22, 23]); + fv.clear_feature(Features::OPTION_ANCHORS); + assert!(!fv.supports_feature(Features::OPTION_ANCHORS)); + // Passing the odd bit clears the pair. + let mut fv = Features::from_bits(&[22, 23]); + fv.clear_feature(23); + assert!(!fv.supports_feature(Features::OPTION_ANCHORS)); + } + + #[test] + fn clear_feature_removes_support() { + let mut features = + Features::from_bits(&[Features::OPTION_ANCHORS, Features::OPTION_STATIC_REMOTEKEY]); + + assert!(features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + + features.clear_feature(Features::OPTION_ANCHORS); + assert!(!features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + } + + #[test] + fn is_supported_with_empty_and_nonempty_features() { + let empty = Features::new(); + let single_bit = Features::from_bits(&[Features::OPTION_ANCHORS]); + let multiple_bits = + Features::from_bits(&[Features::OPTION_ANCHORS, Features::OPTION_STATIC_REMOTEKEY]); + + assert!(empty.is_supported(&empty)); + assert!(empty.is_supported(&single_bit)); + assert!(empty.is_supported(&multiple_bits)); + + assert!(!single_bit.is_supported(&empty)); + assert!(single_bit.is_supported(&single_bit)); + assert!(single_bit.is_supported(&multiple_bits)); + + assert!(!multiple_bits.is_supported(&empty)); + assert!(!multiple_bits.is_supported(&single_bit)); + assert!(multiple_bits.is_supported(&multiple_bits)); + } + + #[test] + fn is_supported_with_fewer_bits() { + let superset = Features::from(vec![0xff, 0xff]); + let subset1 = Features::from(vec![0x0f, 0xff]); + let subset2 = Features::from(vec![0xff, 0x0f]); + + assert!(subset1.is_supported(&superset)); + assert!(subset2.is_supported(&superset)); + + assert!(!subset2.is_supported(&subset1)); + assert!(!subset1.is_supported(&subset2)); + + assert!(!superset.is_supported(&subset1)); + assert!(!superset.is_supported(&subset2)); + } + + #[test] + fn is_supported_with_partial_overlap() { + let anchors_and_remotekey = + Features::from_bits(&[Features::OPTION_ANCHORS, Features::OPTION_STATIC_REMOTEKEY]); + let remotekey_and_dual_fund = Features::from_bits(&[ + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_DUAL_FUND, + ]); + + assert!(!anchors_and_remotekey.is_supported(&remotekey_and_dual_fund)); + assert!(!remotekey_and_dual_fund.is_supported(&anchors_and_remotekey)); + } + + #[test] + fn is_supported_with_different_lengths() { + let short = Features::from(vec![0x01]); + let long = Features::from(vec![0x10, 0x01]); + + assert!(short.is_supported(&long)); + assert!(!long.is_supported(&short)); + + let short = Features::from(vec![0x01]); + let long = Features::from(vec![0x01, 0x00]); + + assert!(!short.is_supported(&long)); + assert!(!long.is_supported(&short)); + + let short = Features::from(vec![0x80]); + let long = Features::from(vec![0x00, 0x80]); + + assert!(short.is_supported(&long)); + assert!(long.is_supported(&short)); + } + + #[test] + fn is_supported_accepts_optional_bit_for_required_bit() { + // A channel type carries `option_scid_alias` as required (bit 46), + // while peers advertise it as optional (bit 47) in `init`. + let channel_type = Features::from_bits(&[ + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_SCID_ALIAS, + ]); + let mut negotiated = Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY]); + negotiated.set_bit(Features::OPTION_SCID_ALIAS ^ 1); + + assert!(!negotiated.is_bit_set(Features::OPTION_SCID_ALIAS)); + assert!(channel_type.is_supported(&negotiated)); + + // A required bit is also satisfied by the same required bit. + let mut negotiated = Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY]); + negotiated.set_bit(Features::OPTION_SCID_ALIAS); + assert!(channel_type.is_supported(&negotiated)); + + // A feature advertised in neither parity is still not negotiated. + let anchors = Features::from_bits(&[Features::OPTION_ANCHORS]); + assert!(!anchors.is_supported(&negotiated)); + } + + #[test] + fn is_supported_accepts_required_bit_for_optional_bit() { + // The pairing is symmetric: an optional bit on the left is satisfied + // by the required bit on the right. + let optional = Features::from_bits(&[Features::OPTION_SCID_ALIAS ^ 1]); + let required = Features::from_bits(&[Features::OPTION_SCID_ALIAS]); + + assert!(optional.is_supported(&required)); + assert!(required.is_supported(&optional)); + } + + #[test] + fn equality_with_same_and_different_bits() { + let lease = Features::from_bits(&[Features::OPTION_SCRIPT_ENFORCED_LEASE]); + assert_eq!(lease, Features::from_bits(&[2022])); + assert_eq!( + Features::from(vec![0x02, 0x01]), + Features::from(vec![0x02, 0x01]) + ); + + // equality ignores leading zero bytes. + assert_eq!(Features::from(vec![0x01]), Features::from(vec![0x00, 0x01])); + assert_eq!(Features::new(), Features::from(vec![0x00, 0x00])); + + assert_ne!(Features::from(vec![0x01]), Features::from(vec![0x02])); + assert_ne!(Features::from(vec![0x01]), Features::from(vec![0x01, 0x00])); + assert_ne!(Features::from(vec![0x01]), Features::from(vec![0x01, 0x01])); + assert_ne!(Features::from(vec![0x01, 0x00]), Features::from(vec![0x00])); + assert_ne!(lease, Features::from_bits(&[2023])); + } +} diff --git a/smite/src/bolt/shutdown.rs b/smite/src/bolt/shutdown.rs index 8246b380..767ddb6c 100644 --- a/smite/src/bolt/shutdown.rs +++ b/smite/src/bolt/shutdown.rs @@ -1,6 +1,11 @@ //! BOLT 2 shutdown message. +use bitcoin::opcodes::all::OP_RETURN; +use bitcoin::script::Instruction; +use bitcoin::{Script, WitnessVersion}; + use super::BoltError; +use super::features::Features; use super::types::ChannelId; use super::wire::WireFormat; @@ -51,11 +56,77 @@ impl Shutdown { } } +/// Returns `true` if `spk` is a standard `shutdown` scriptpubkey per BOLT 2. +/// +/// Legacy P2PKH/P2SH are rejected. A receiver may accept them for backward +/// compatibility, but this oracle judges the sender's output. +#[must_use] +pub fn is_standard_shutdown_script(spk: &[u8], features: &Features) -> bool { + let script = Script::from_bytes(spk); + let witness_v0 = script.is_p2wpkh() || script.is_p2wsh(); + let anysegwit = features.supports_feature(Features::OPTION_SHUTDOWN_ANYSEGWIT) + && matches!(script.witness_version(), Some(v) if v != WitnessVersion::V0); + let simple_close = features.supports_feature(Features::OPTION_SIMPLE_CLOSE) + && is_simple_close_op_return(script); + witness_v0 || anysegwit || simple_close +} + +/// Returns `true` if `spk` is a shutdown scriptpubkey a receiver may accept per +/// BOLT 2. +/// +/// This includes the standard shutdown scriptpubkey forms, as well as legacy +/// P2PKH/P2SH outputs accepted for backward compatibility. +#[must_use] +pub fn is_acceptable_shutdown_script(spk: &[u8], features: &Features) -> bool { + let script = Script::from_bytes(spk); + is_standard_shutdown_script(spk, features) || script.is_p2pkh() || script.is_p2sh() +} + +/// Returns `true` if `script` is a BOLT 2 `option_simple_close` `OP_RETURN` script: `OP_RETURN` +/// followed by a single minimal push of 6..=80 bytes. +/// +/// A non-minimal push here would be `OP_PUSHDATA1` used for a payload of fewer than 76 bytes. +fn is_simple_close_op_return(script: &Script) -> bool { + let mut instrs = script.instructions_minimal(); + if !matches!(instrs.next(), Some(Ok(Instruction::Op(op))) if op == OP_RETURN) { + return false; + } + match instrs.next() { + // matches any minimal push + Some(Ok(Instruction::PushBytes(bytes))) => { + (6..=80).contains(&bytes.len()) && instrs.next().is_none() + } + _ => false, + } +} + #[cfg(test)] mod tests { + use bitcoin::opcodes::all::{ + OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHBYTES_0, OP_PUSHBYTES_1, + OP_PUSHBYTES_20, OP_PUSHBYTES_21, OP_PUSHBYTES_32, OP_PUSHBYTES_41, OP_PUSHDATA1, + OP_PUSHNUM_1, OP_PUSHNUM_16, OP_RETURN, + }; + use super::super::CHANNEL_ID_SIZE; use super::*; + fn none() -> Features { + Features::new() + } + fn anysegwit() -> Features { + Features::from_bits(&[Features::OPTION_SHUTDOWN_ANYSEGWIT]) + } + fn simple_close() -> Features { + Features::from_bits(&[Features::OPTION_SIMPLE_CLOSE]) + } + fn all() -> Features { + Features::from_bits(&[ + Features::OPTION_SHUTDOWN_ANYSEGWIT, + Features::OPTION_SIMPLE_CLOSE, + ]) + } + #[test] fn shutdown_for_channel() { let channel_id = ChannelId::new([0x42; CHANNEL_ID_SIZE]); @@ -151,4 +222,142 @@ mod tests { assert_eq!(original, decoded); assert!(decoded.scriptpubkey.is_empty()); } + + #[test] + #[allow(clippy::similar_names)] + fn is_standard_shutdown_script_rejects_legacy_accepts_witness_v0() { + // Legacy P2PKH/P2SH are non-standard even with all features negotiated. + let mut p2pkh = vec![OP_DUP.to_u8(), OP_HASH160.to_u8(), OP_PUSHBYTES_20.to_u8()]; + p2pkh.extend_from_slice(&[0x11; 20]); + p2pkh.extend_from_slice(&[OP_EQUALVERIFY.to_u8(), OP_CHECKSIG.to_u8()]); + assert!(!is_standard_shutdown_script(&p2pkh, &all())); + + let mut p2sh = vec![OP_HASH160.to_u8(), OP_PUSHBYTES_20.to_u8()]; + p2sh.extend_from_slice(&[0x22; 20]); + p2sh.push(OP_EQUAL.to_u8()); + assert!(!is_standard_shutdown_script(&p2sh, &all())); + + // Witness v0 forms are accepted regardless of features. + let v0 = OP_PUSHBYTES_0.to_u8(); + let mut p2wpkh = vec![v0, OP_PUSHBYTES_20.to_u8()]; + p2wpkh.extend_from_slice(&[0x33; 20]); + assert!(is_standard_shutdown_script(&p2wpkh, &none())); + assert!(is_standard_shutdown_script(&p2wpkh, &all())); + + let mut p2wsh = vec![v0, OP_PUSHBYTES_32.to_u8()]; + p2wsh.extend_from_slice(&[0x44; 32]); + assert!(is_standard_shutdown_script(&p2wsh, &none())); + assert!(is_standard_shutdown_script(&p2wsh, &all())); + } + + #[test] + fn is_standard_shutdown_script_accepts_anysegwit() { + for v in OP_PUSHNUM_1.to_u8()..=OP_PUSHNUM_16.to_u8() { + let mut spk = vec![v, OP_PUSHBYTES_20.to_u8()]; + spk.extend_from_slice(&[0x00; 20]); + // Gated on option_shutdown_anysegwit; option_simple_close alone doesn't help. + assert!(!is_standard_shutdown_script(&spk, &none())); + assert!(!is_standard_shutdown_script(&spk, &simple_close())); + assert!(is_standard_shutdown_script(&spk, &anysegwit())); + assert!(is_standard_shutdown_script(&spk, &all())); + } + } + + #[test] + fn is_standard_shutdown_script_rejects_other() { + // These are non-standard even with all features negotiated. Because + // features only widen the accepted set, rejection under `all()` implies + // rejection under any subset. + let empty_spk = vec![]; + assert!(!is_standard_shutdown_script(&empty_spk, &all())); + + let random_spk = vec![0x00, 0x01, 0x02]; + assert!(!is_standard_shutdown_script(&random_spk, &all())); + + // OP_RETURN with a 1-byte push: below the simple_close minimum of 6. + let op_return_spk = vec![OP_RETURN.to_u8(), OP_PUSHBYTES_1.to_u8(), 0xff]; + assert!(!is_standard_shutdown_script(&op_return_spk, &all())); + + // Witness version 0 with a non-{20,32} program length + let v0 = OP_PUSHBYTES_0.to_u8(); + let mut witness_v0_invalid_prog_length_spk = vec![v0, OP_PUSHBYTES_21.to_u8()]; + witness_v0_invalid_prog_length_spk.extend_from_slice(&[0x00; 21]); + assert!(!is_standard_shutdown_script( + &witness_v0_invalid_prog_length_spk, + &all() + )); + + // Witness version 1 with a program length outside 2..=40 + let mut witness_v1_invalid_prog_length_spk = + vec![OP_PUSHNUM_1.to_u8(), OP_PUSHBYTES_41.to_u8()]; + witness_v1_invalid_prog_length_spk.extend_from_slice(&[0x00; 41]); + assert!(!is_standard_shutdown_script( + &witness_v1_invalid_prog_length_spk, + &all() + )); + + // Length prefix disagrees with the actual program length + let mut witness_v0_invalid_length_prefix_spk = vec![v0, OP_PUSHBYTES_20.to_u8()]; + witness_v0_invalid_length_prefix_spk.extend_from_slice(&[0x00; 19]); + assert!(!is_standard_shutdown_script( + &witness_v0_invalid_length_prefix_spk, + &all() + )); + } + + #[test] + fn is_standard_shutdown_script_accepts_simple_close_op_return() { + // OP_RETURN + a single direct push of 6..=75 bytes. + for len in [6u8, 40, 75] { + let mut spk = vec![OP_RETURN.to_u8(), len]; + spk.extend_from_slice(&vec![0xab; usize::from(len)]); + // Gated on option_simple_close; option_shutdown_anysegwit alone doesn't help. + assert!(!is_standard_shutdown_script(&spk, &none())); + assert!(!is_standard_shutdown_script(&spk, &anysegwit())); + assert!(is_standard_shutdown_script(&spk, &simple_close())); + assert!(is_standard_shutdown_script(&spk, &all())); + } + + // OP_RETURN + OP_PUSHDATA1 + a single push of 76..=80 bytes. + for len in [76u8, 80] { + let mut spk = vec![OP_RETURN.to_u8(), OP_PUSHDATA1.to_u8(), len]; + spk.extend_from_slice(&vec![0xab; usize::from(len)]); + assert!(!is_standard_shutdown_script(&spk, &none())); + assert!(is_standard_shutdown_script(&spk, &simple_close())); + } + } + + #[test] + fn is_standard_shutdown_script_rejects_invalid_simple_close_op_return() { + // Rejected even with option_simple_close negotiated (via `all()`). + + // Bare OP_RETURN with no push. + assert!(!is_standard_shutdown_script(&[OP_RETURN.to_u8()], &all())); + + // Direct push below the 6-byte minimum. + let mut too_short = vec![OP_RETURN.to_u8(), 5]; + too_short.extend_from_slice(&[0xab; 5]); + assert!(!is_standard_shutdown_script(&too_short, &all())); + + // Push length disagrees with the trailing data (claims 6, has 5). + let mut len_mismatch = vec![OP_RETURN.to_u8(), 6]; + len_mismatch.extend_from_slice(&[0xab; 5]); + assert!(!is_standard_shutdown_script(&len_mismatch, &all())); + + // Extra bytes after the single push (multiple pushes not allowed). + let mut trailing = vec![OP_RETURN.to_u8(), 6]; + trailing.extend_from_slice(&[0xab; 6]); + trailing.extend_from_slice(&[OP_PUSHBYTES_1.to_u8(), 0xff]); + assert!(!is_standard_shutdown_script(&trailing, &all())); + + // OP_PUSHDATA1 with a length above the 80-byte maximum. + let mut too_long = vec![OP_RETURN.to_u8(), OP_PUSHDATA1.to_u8(), 81]; + too_long.extend_from_slice(&[0xab; 81]); + assert!(!is_standard_shutdown_script(&too_long, &all())); + + // Non-minimal push: OP_PUSHDATA1 used for less than 76 bytes. + let mut non_minimal = vec![OP_RETURN.to_u8(), OP_PUSHDATA1.to_u8(), 10]; + non_minimal.extend_from_slice(&[0xab; 10]); + assert!(!is_standard_shutdown_script(&non_minimal, &all())); + } } diff --git a/smite/src/bolt/types.rs b/smite/src/bolt/types.rs index d3851b86..5b1d6d2a 100644 --- a/smite/src/bolt/types.rs +++ b/smite/src/bolt/types.rs @@ -1,8 +1,10 @@ //! Fundamental types for BOLT message encoding. +use super::{FeatureBit, Features}; use bitcoin::OutPoint; use bitcoin::hashes::Hash; use bitcoin::hex::DisplayHex; +use serde::{Deserialize, Serialize}; use std::fmt; /// Maximum Lightning message size (2-byte length prefix limit). @@ -73,6 +75,206 @@ impl fmt::Display for ChannelId { } } +/// A specific BOLT 2 `channel_type` feature-bit combination. +/// +/// Each variant corresponds to a channel type accepted by at least one target +/// implementation: +/// +/// - `option_static_remotekey` (bit 12) +/// - `option_anchors` (bits 22 and 12) +/// - `zero_fee_commitments` (bit 40) +/// - `option_simple_taproot` (bit 80) +/// - `option_simple_taproot_staging` (bit 180) +/// - `option_script_enforced_lease` (bits 2022, 22, 12) +/// +/// Additionally, the following bits can be added to any channel type: +/// - `option_scid_alias` (bit 46) +/// - `option_zeroconf` (bit 50) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ChannelTypeVariant { + /// bit 12 + StaticRemoteKey, + /// bits 12, 46 + StaticRemoteKeyScidAlias, + /// bits 12, 50 + StaticRemoteKeyZeroConf, + /// bits 12, 46, 50 + StaticRemoteKeyScidAliasZeroConf, + /// bits 12, 22 + Anchors, + /// bits 12, 22, 46 + AnchorsScidAlias, + /// bits 12, 22, 50 + AnchorsZeroConf, + /// bits 12, 22, 46, 50 + AnchorsScidAliasZeroConf, + /// bit 40 + ZeroFeeCommitments, + /// bits 40, 46 + ZeroFeeCommitmentsScidAlias, + /// bits 40, 50 + ZeroFeeCommitmentsZeroConf, + /// bits 40, 46, 50 + ZeroFeeCommitmentsScidAliasZeroConf, + /// bit 80 + SimpleTaproot, + /// bits 80, 46 + SimpleTaprootScidAlias, + /// bits 80, 50 + SimpleTaprootZeroConf, + /// bits 80, 46, 50 + SimpleTaprootScidAliasZeroConf, + /// bit 180 + SimpleTaprootStaging, + /// bits 180, 46 + SimpleTaprootStagingScidAlias, + /// bits 180, 50 + SimpleTaprootStagingZeroConf, + /// bits 180, 46, 50 + SimpleTaprootStagingScidAliasZeroConf, + /// bits 12, 22, 2022 + ScriptEnforcedLease, + /// bits 12, 22, 2022, 46 + ScriptEnforcedLeaseScidAlias, + /// bits 12, 22, 2022, 50 + ScriptEnforcedLeaseZeroConf, + /// bits 12, 22, 2022, 46, 50 + ScriptEnforcedLeaseScidAliasZeroConf, +} + +impl ChannelTypeVariant { + /// All variants. Keep in sync with the enum definition. + pub const ALL: &[Self] = &[ + Self::StaticRemoteKey, + Self::StaticRemoteKeyScidAlias, + Self::StaticRemoteKeyZeroConf, + Self::StaticRemoteKeyScidAliasZeroConf, + Self::Anchors, + Self::AnchorsScidAlias, + Self::AnchorsZeroConf, + Self::AnchorsScidAliasZeroConf, + Self::ZeroFeeCommitments, + Self::ZeroFeeCommitmentsScidAlias, + Self::ZeroFeeCommitmentsZeroConf, + Self::ZeroFeeCommitmentsScidAliasZeroConf, + Self::SimpleTaproot, + Self::SimpleTaprootScidAlias, + Self::SimpleTaprootZeroConf, + Self::SimpleTaprootScidAliasZeroConf, + Self::SimpleTaprootStaging, + Self::SimpleTaprootStagingScidAlias, + Self::SimpleTaprootStagingZeroConf, + Self::SimpleTaprootStagingScidAliasZeroConf, + Self::ScriptEnforcedLease, + Self::ScriptEnforcedLeaseScidAlias, + Self::ScriptEnforcedLeaseZeroConf, + Self::ScriptEnforcedLeaseScidAliasZeroConf, + ]; + + /// The feature bits (even/required) contained in this channel type. + #[must_use] + pub fn bits(self) -> &'static [FeatureBit] { + use Features as F; + match self { + Self::StaticRemoteKey => &[F::OPTION_STATIC_REMOTEKEY], + Self::StaticRemoteKeyScidAlias => &[F::OPTION_STATIC_REMOTEKEY, F::OPTION_SCID_ALIAS], + Self::StaticRemoteKeyZeroConf => &[F::OPTION_STATIC_REMOTEKEY, F::OPTION_ZEROCONF], + Self::StaticRemoteKeyScidAliasZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::Anchors => &[F::OPTION_STATIC_REMOTEKEY, F::OPTION_ANCHORS], + Self::AnchorsScidAlias => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCID_ALIAS, + ], + Self::AnchorsZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_ZEROCONF, + ], + Self::AnchorsScidAliasZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::ZeroFeeCommitments => &[F::ZERO_FEE_COMMITMENTS], + Self::ZeroFeeCommitmentsScidAlias => &[F::ZERO_FEE_COMMITMENTS, F::OPTION_SCID_ALIAS], + Self::ZeroFeeCommitmentsZeroConf => &[F::ZERO_FEE_COMMITMENTS, F::OPTION_ZEROCONF], + Self::ZeroFeeCommitmentsScidAliasZeroConf => &[ + F::ZERO_FEE_COMMITMENTS, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::SimpleTaproot => &[F::OPTION_SIMPLE_TAPROOT], + Self::SimpleTaprootScidAlias => &[F::OPTION_SIMPLE_TAPROOT, F::OPTION_SCID_ALIAS], + Self::SimpleTaprootZeroConf => &[F::OPTION_SIMPLE_TAPROOT, F::OPTION_ZEROCONF], + Self::SimpleTaprootScidAliasZeroConf => &[ + F::OPTION_SIMPLE_TAPROOT, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::SimpleTaprootStaging => &[F::OPTION_SIMPLE_TAPROOT_STAGING], + Self::SimpleTaprootStagingScidAlias => { + &[F::OPTION_SIMPLE_TAPROOT_STAGING, F::OPTION_SCID_ALIAS] + } + Self::SimpleTaprootStagingZeroConf => { + &[F::OPTION_SIMPLE_TAPROOT_STAGING, F::OPTION_ZEROCONF] + } + Self::SimpleTaprootStagingScidAliasZeroConf => &[ + F::OPTION_SIMPLE_TAPROOT_STAGING, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + Self::ScriptEnforcedLease => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + ], + Self::ScriptEnforcedLeaseScidAlias => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + F::OPTION_SCID_ALIAS, + ], + Self::ScriptEnforcedLeaseZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + F::OPTION_ZEROCONF, + ], + Self::ScriptEnforcedLeaseScidAliasZeroConf => &[ + F::OPTION_STATIC_REMOTEKEY, + F::OPTION_ANCHORS, + F::OPTION_SCRIPT_ENFORCED_LEASE, + F::OPTION_SCID_ALIAS, + F::OPTION_ZEROCONF, + ], + } + } + + /// Converts the channel type variant to a `Features` bitmap. + #[must_use] + pub fn to_features(self) -> Features { + Features::from_bits(self.bits()) + } + + /// Encodes the channel type as a BOLT feature bitmap (big-endian bytes). + #[must_use] + pub fn encode(self) -> Vec { + self.to_features().into_bytes() + } +} + +impl fmt::Display for ChannelTypeVariant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + /// A BOLT 7 `short_channel_id`. /// /// Per [BOLT 7]: diff --git a/smite/src/channel_tx/commitment.rs b/smite/src/channel_tx/commitment.rs index 0b451b60..bc6870c5 100644 --- a/smite/src/channel_tx/commitment.rs +++ b/smite/src/channel_tx/commitment.rs @@ -1,6 +1,7 @@ //! BOLT 3 commitment transaction construction and signing. use super::funding::build_funding_witness_script; +use crate::bolt::Features; use bitcoin::absolute::LockTime; use bitcoin::hashes::sha256::Hash as Sha256; @@ -24,9 +25,6 @@ const COMMITMENT_TX_BASE_WEIGHT_NON_ANCHOR: u64 = 724; /// Weight of an anchor commitment transaction without HTLCs. const COMMITMENT_TX_BASE_WEIGHT_ANCHOR: u64 = 1124; -/// `option_anchors` feature bits (BOLT 9, bits 22/23). -const OPTION_ANCHORS_FEATURE_BITS: &[usize] = &[22, 23]; - /// Errors that can occur when constructing or validating commitment transactions. #[derive(Debug, thiserror::Error)] pub enum CommitmentError { @@ -77,7 +75,7 @@ pub struct ChannelConfig { pub funding_satoshis: u64, /// Channel type feature bits. The commitment format (anchor / legacy) is /// derived from the bits set here. - pub channel_type: Vec, + pub channel_type: Features, /// Opener's static keys and parameters. pub opener: ChannelPartyConfig, /// Acceptor's static keys and parameters. @@ -383,7 +381,7 @@ impl ChannelConfig { /// `local_side` selects whose commitment outputs are built: the /// opener's or the acceptor's. fn build_commitment_outputs(&self, state: &CommitmentState, local_side: &Side) -> Vec { - let anchor = supports_option_anchors(&self.channel_type); + let anchor = self.channel_type.supports_feature(Features::OPTION_ANCHORS); // Fee and balances. let commitment_cost = CommitmentCost::new(state.feerate_per_kw, &self.channel_type); @@ -471,7 +469,7 @@ impl CommitmentState { impl CommitmentCost { /// Calculates the total cost of a commitment transaction. #[must_use] - pub fn new(feerate_per_kw: u32, channel_type: &[u8]) -> CommitmentCost { + pub fn new(feerate_per_kw: u32, channel_type: &Features) -> CommitmentCost { CommitmentCost { fee_sat: commit_tx_fee_sat(feerate_per_kw, channel_type), anchor_cost_sat: total_anchors_sat(channel_type), @@ -486,8 +484,8 @@ impl CommitmentCost { } /// Get the fee cost of a commitment tx in satoshis. -fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &[u8]) -> u64 { - let commitment_weight = if supports_option_anchors(channel_type) { +fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &Features) -> u64 { + let commitment_weight = if channel_type.supports_feature(Features::OPTION_ANCHORS) { COMMITMENT_TX_BASE_WEIGHT_ANCHOR } else { COMMITMENT_TX_BASE_WEIGHT_NON_ANCHOR @@ -497,8 +495,8 @@ fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &[u8]) -> u64 { } /// Get the anchor cost of a commitment tx in satoshis. -fn total_anchors_sat(channel_type: &[u8]) -> u64 { - if supports_option_anchors(channel_type) { +fn total_anchors_sat(channel_type: &Features) -> u64 { + if channel_type.supports_feature(Features::OPTION_ANCHORS) { ANCHOR_OUTPUT_VALUE * 2 } else { 0 @@ -528,24 +526,6 @@ fn compute_obscuring_factor( u64::from_be_bytes(buf) } -/// Checks whether `option_anchors` (BOLT 9, bits 22/23) is set in a -/// big-endian `channel_type` feature bitfield. -/// -/// Per BOLT 9, even bit (22) = required, odd bit (23) = optional. -/// Either bit indicates anchor support. -fn supports_option_anchors(channel_type: &[u8]) -> bool { - let byte_offset = OPTION_ANCHORS_FEATURE_BITS[0] / 8; - let len = channel_type.len(); - if len <= byte_offset { - return false; - } - - let required_mask = 1 << (OPTION_ANCHORS_FEATURE_BITS[0] % 8); - let optional_mask = 1 << (OPTION_ANCHORS_FEATURE_BITS[1] % 8); - - channel_type[len - 1 - byte_offset] & (required_mask | optional_mask) != 0 -} - /// Derives a public key from a basepoint and per-commitment point per BOLT 3. fn derive_pubkey(basepoint: &PublicKey, per_commitment_point: &PublicKey) -> PublicKey { let secp = Secp256k1::new(); @@ -688,19 +668,6 @@ mod tests { assert_eq!(factor, 0x2bb0_3852_1914); } - #[test] - fn supports_option_anchors_detection() { - // Required (bit 22), optional (bit 23). - assert!(supports_option_anchors(&[0x40, 0x00, 0x00])); - assert!(supports_option_anchors(&[0x80, 0x00, 0x00])); - // No support. - assert!(!supports_option_anchors(&[0x00, 0x00, 0x40])); - assert!(!supports_option_anchors(&[0x00, 0x00, 0x80])); - assert!(!supports_option_anchors(&[])); - assert!(!supports_option_anchors(&[0xff, 0xff])); - assert!(!supports_option_anchors(&[0x00, 0x10])); - } - fn bolt3_commitment_params( feerate_per_kw: u32, to_opener_msat: u64, @@ -721,7 +688,7 @@ mod tests { vout: 0, }, funding_satoshis: 10_000_000, - channel_type, + channel_type: Features::from(channel_type), opener: ChannelPartyConfig { funding_pubkey: pubkey( "023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb", @@ -1349,7 +1316,7 @@ mod tests { vout: 0, }, funding_satoshis, - channel_type, + channel_type: Features::from(channel_type), opener: sample_party(), acceptor: sample_party(), minimum_depth: 8, @@ -1377,28 +1344,32 @@ mod tests { #[test] fn opener_balance_after_commitment_cost_total_sat_checks() { let feerate_per_kw: u32 = 15_000; - let anchor_channel_type = [0x40, 0x00, 0x00]; + let legacy = Features::new(); + let anchor = Features::from_bits(&[Features::OPTION_ANCHORS]); // Legacy fee: 15000 * 724 / 1000 = 10_860 sat // Anchor fee: 15000 * 1124 / 1000 = 16_860 sat; anchor_cost = 660 sat // Comfortably affordable let opener_balance_sat: u64 = 20_000; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), Some(9_140), ); // Exact zero opener balance let opener_balance_sat: u64 = 10_860; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), Some(0), ); // Balance does not cover the fee let opener_balance_sat: u64 = 10_000; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), None ); @@ -1406,7 +1377,7 @@ mod tests { let opener_balance_sat: u64 = 17_500; assert_eq!( opener_balance_sat - .checked_sub(CommitmentCost::new(feerate_per_kw, &anchor_channel_type).total_sat()), + .checked_sub(CommitmentCost::new(feerate_per_kw, &anchor).total_sat()), None, ); } diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index 3ab16320..3cf15575 100644 --- a/smite/src/oracles/accept_channel.rs +++ b/smite/src/oracles/accept_channel.rs @@ -1,7 +1,10 @@ //! BOLT 2 `accept_channel` oracle, for the v1 outbound channel funding flow. use super::Oracle; -use crate::bolt::{AcceptChannel, OpenChannel}; +use crate::bolt::{ + AcceptChannel, ChannelTypeVariant, Features, OpenChannel, is_acceptable_shutdown_script, + is_standard_shutdown_script, +}; use crate::channel_tx::CommitmentCost; use crate::pending_channel::PendingChannel; use crate::violation::Violation; @@ -10,7 +13,9 @@ use bitcoin::Amount; // Constants from the BOLT 2 `open_channel` and `accept_channel` requirements: // https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#requirements-8 -const MAX_ACCEPTED_HTLCS_LIMIT: u16 = 483; +const MAX_FUNDING_SATOSHIS_NO_WUMBO: u64 = (1 << 24) - 1; +const MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS: u16 = 114; +const MAX_ACCEPTED_HTLCS_DEFAULT: u16 = 483; const MIN_DUST_LIMIT_SATOSHIS: u64 = 354; /// Context for `AcceptChannelOracle` @@ -20,6 +25,8 @@ pub struct AcceptChannelContext<'a> { /// The negotiation the `accept_channel` answers, identified by its /// `temporary_channel_id`, or `None` if no matching `open_channel` was sent. pub negotiation: Option<&'a PendingChannel>, + /// Features negotiated between the target node and Smite. + pub negotiated_features: &'a Features, } /// Checks whether the `open_channel` answered by an `accept_channel` satisfied @@ -45,7 +52,8 @@ impl Oracle> for AcceptChannelOracle { }; // Check that the `open_channel` was valid to accept. - if let Err(reason) = verify_accepted_open_channel(open_channel) { + if let Err(reason) = verify_accepted_open_channel(open_channel, context.negotiated_features) + { return Err(Violation::InvalidAcceptChannel( context.accept_channel.temporary_channel_id, format!("accepted invalid open_channel: {reason}"), @@ -53,7 +61,11 @@ impl Oracle> for AcceptChannelOracle { } // Check that the `accept_channel` itself is valid. - if let Err(reason) = verify_accept_channel(context.accept_channel, open_channel) { + if let Err(reason) = verify_accept_channel( + context.accept_channel, + open_channel, + context.negotiated_features, + ) { return Err(Violation::InvalidAcceptChannel( context.accept_channel.temporary_channel_id, format!("invalid accept_channel: {reason}"), @@ -83,13 +95,20 @@ impl Oracle> for AcceptChannelOracle { /// be less than or equal to the channel reserve. However, implementations /// such as LDK accept zero channel reserves on the receiving side, so we do /// not enforce this check on the target's receiving side. -fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String> { +fn verify_accepted_open_channel( + open_channel: &OpenChannel, + negotiated_features: &Features, +) -> Result<(), String> { + // Check that option_dual_fund has not been negotiated. + if negotiated_features.supports_feature(Features::OPTION_DUAL_FUND) { + return Err("option_dual_fund has been negotiated".to_string()); + } + // Check that the funding amounts are valid. - // FIXME: Varies if `option_support_large_channel` is not negotiated. - let total_supply_satoshis = Amount::MAX_MONEY.to_sat(); - if open_channel.funding_satoshis > total_supply_satoshis { + let max_funding = max_funding_satoshis(negotiated_features); + if open_channel.funding_satoshis > max_funding { return Err(format!( - "funding_satoshis {} exceeds maximum funding of {total_supply_satoshis} sat", + "funding_satoshis {} exceeds maximum funding of {max_funding} sat", open_channel.funding_satoshis, )); } @@ -102,20 +121,67 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String )); } + // Check that the upfront shutdown script is present and valid when negotiated. + if negotiated_features.supports_feature(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT) { + if let Some(script) = &open_channel.tlvs.upfront_shutdown_script { + if !script.is_empty() && !is_acceptable_shutdown_script(script, negotiated_features) { + return Err("upfront_shutdown_script is not valid".to_string()); + } + } else { + return Err("open_channel does not include upfront_shutdown_script".to_string()); + } + } + + // Check option_channel_type in negotiated features since it is assumed to + // be supported. + if !negotiated_features.supports_feature(Features::OPTION_CHANNEL_TYPE) { + return Err("option_channel_type is not supported".to_string()); + } + // Check that the channel type was included. - // TODO: Check option_channel_type in negotiated features since it is - // assumed to be supported. - let Some(channel_type) = open_channel.tlvs.channel_type.as_deref() else { + let Some(channel_type) = open_channel + .tlvs + .channel_type + .as_deref() + .map(Features::from) + else { return Err("open_channel does not include a channel_type".to_string()); }; + // Check that the channel type only contains negotiated features. + if !channel_type.is_supported(negotiated_features) { + return Err("channel_type contains features that were not negotiated".to_string()); + } + + // Check that the channel type is one of the known variants. + if !ChannelTypeVariant::ALL + .iter() + .any(|variant| channel_type == variant.to_features()) + { + return Err("channel_type is not a known variant".to_string()); + } + + // Check that feerate_per_kw is 0 when `zero_fee_commitments` is negotiated. + if channel_type.supports_feature(Features::ZERO_FEE_COMMITMENTS) + && open_channel.feerate_per_kw != 0 + { + return Err(format!( + "zero_fee_commitments requires feerate_per_kw to be 0, but got {}", + open_channel.feerate_per_kw, + )); + } + + // Check that option_scid_alias is only negotiated for private channels. + let announce_channel = (open_channel.channel_flags & 1) == 1; + if announce_channel && channel_type.supports_feature(Features::OPTION_SCID_ALIAS) { + return Err("option_scid_alias requires the channel to be private".to_string()); + } + // Check the HTLC limit is within the maximum. - // FIXME: Does not apply to channels whose `channel_type` includes - // `zero_fee_commitments`. These channel types have a lower upper limit on - // `max_accepted_htlcs`, so we are currently safe. - if open_channel.max_accepted_htlcs > MAX_ACCEPTED_HTLCS_LIMIT { + let htlc_limit = max_accepted_htlcs_limit(&channel_type); + if open_channel.max_accepted_htlcs > htlc_limit { return Err(format!( - "max_accepted_htlcs {} exceeds the limit of {MAX_ACCEPTED_HTLCS_LIMIT}", + "max_accepted_htlcs {} exceeds the limit of {htlc_limit}", open_channel.max_accepted_htlcs, )); } @@ -131,7 +197,7 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String // Check the initial commitment satisfies the channel reserve. verify_initial_commitment( open_channel, - channel_type, + &channel_type, open_channel.channel_reserve_satoshis, ) } @@ -148,9 +214,26 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String fn verify_accept_channel( accept_channel: &AcceptChannel, open_channel: &OpenChannel, + negotiated_features: &Features, ) -> Result<(), String> { + // Check that the upfront shutdown script is present and valid when negotiated. + if negotiated_features.supports_feature(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT) { + if let Some(script) = &accept_channel.tlvs.upfront_shutdown_script { + if !script.is_empty() && !is_standard_shutdown_script(script, negotiated_features) { + return Err("upfront_shutdown_script is not valid".to_string()); + } + } else { + return Err("accept_channel does not include upfront_shutdown_script".to_string()); + } + } + // Check that the channel type was included. - let Some(channel_type) = accept_channel.tlvs.channel_type.as_deref() else { + let Some(channel_type) = accept_channel + .tlvs + .channel_type + .as_deref() + .map(Features::from) + else { return Err("accept_channel does not include a channel_type".to_string()); }; @@ -159,6 +242,15 @@ fn verify_accept_channel( return Err("accept_channel channel_type does not match open_channel".to_string()); } + // Check that option_zeroconf has a minimum depth of 0. + if channel_type.supports_feature(Features::OPTION_ZEROCONF) && accept_channel.minimum_depth != 0 + { + return Err(format!( + "option_zeroconf requires minimum_depth to be 0, but got {}", + accept_channel.minimum_depth, + )); + } + // Check the acceptor's channel reserve covers the opener's dust limit. if accept_channel.channel_reserve_satoshis < open_channel.dust_limit_satoshis { return Err(format!( @@ -176,12 +268,10 @@ fn verify_accept_channel( } // Check the HTLC limit is within the maximum. - // FIXME: Does not apply to channels whose `channel_type` includes - // `zero_fee_commitments`. These channel types have a lower upper limit on - // `max_accepted_htlcs`, so we are currently safe. - if accept_channel.max_accepted_htlcs > MAX_ACCEPTED_HTLCS_LIMIT { + let htlc_limit = max_accepted_htlcs_limit(&channel_type); + if accept_channel.max_accepted_htlcs > htlc_limit { return Err(format!( - "max_accepted_htlcs {} exceeds the limit of {MAX_ACCEPTED_HTLCS_LIMIT}", + "max_accepted_htlcs {} exceeds the limit of {htlc_limit}", accept_channel.max_accepted_htlcs, )); } @@ -197,7 +287,7 @@ fn verify_accept_channel( // Check the initial commitment satisfies the channel reserve. verify_initial_commitment( open_channel, - channel_type, + &channel_type, accept_channel.channel_reserve_satoshis, ) } @@ -206,22 +296,24 @@ fn verify_accept_channel( /// channel reserve requirement, returning an error if it breaches either, or /// `Ok(())` if both are met. /// -/// NOTE: This check is safe from false positives for `zero_fee_commitments` -/// and `option_simple_taproot`, although the reported error may be misleading: +/// NOTE: Validation is skipped for channel types we do not yet fully support, +/// such as 0FC and Taproot, to avoid misleading errors. /// -/// - `zero_fee_commitments` requires `feerate_per_kw == 0`, which we currently -/// do not enforce. A non-zero feerate may cause the error to be reported here -/// even though it is invalid for this channel type. -/// - `option_simple_taproot` has a different commitment fee (968-byte weight), -/// but we calculate it using the lower 724-byte weight. This may allow some -/// invalid cases through, but cannot cause a false positive. -/// - Anchor costs are only included when `option_anchors` is negotiated, so -/// they are not unnecessarily subtracted for these channel types. +/// TODO: Enable validation once we support commitment handling for these +/// channel types. fn verify_initial_commitment( open_channel: &OpenChannel, - channel_type: &[u8], + channel_type: &Features, channel_reserve_satoshis: u64, ) -> Result<(), String> { + // Skip validation for channel types we don't yet fully support. + if channel_type.supports_feature(Features::ZERO_FEE_COMMITMENTS) + || channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT) + || channel_type.supports_feature(Features::OPTION_SIMPLE_TAPROOT_STAGING) + { + return Ok(()); + } + // Check that the opener can afford the proposed feerate. let opener_balance_sat = (open_channel.funding_satoshis * 1000 - open_channel.push_msat) / 1000; let commitment_cost = CommitmentCost::new(open_channel.feerate_per_kw, channel_type); @@ -252,11 +344,31 @@ fn verify_initial_commitment( Ok(()) } +/// Returns the maximum funding amount allowed by the negotiated features. +pub fn max_funding_satoshis(negotiated_features: &Features) -> u64 { + if negotiated_features.supports_feature(Features::OPTION_SUPPORT_LARGE_CHANNEL) { + Amount::MAX_MONEY.to_sat() + } else { + MAX_FUNDING_SATOSHIS_NO_WUMBO + } +} + +/// Returns the maximum number of inbound HTLCs allowed by the channel type. +pub fn max_accepted_htlcs_limit(channel_type: &Features) -> u16 { + if channel_type.supports_feature(Features::ZERO_FEE_COMMITMENTS) { + MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS + } else { + MAX_ACCEPTED_HTLCS_DEFAULT + } +} + #[cfg(test)] mod tests { use super::*; use crate::bolt::{AcceptChannelTlvs, ChannelId, OpenChannelTlvs}; + use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use bitcoin::{PubkeyHash, ScriptBuf, WPubkeyHash}; fn pubkey(seed: u8) -> PublicKey { let sk = SecretKey::from_slice(&[seed; 32]).expect("valid secret key"); @@ -326,11 +438,30 @@ mod tests { } } + /// Valid negotiated features for testing. + fn sample_negotiated_features() -> Features { + Features::from_bits(&[ + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_ANCHORS, + Features::ZERO_FEE_COMMITMENTS, + Features::OPTION_CHANNEL_TYPE, + Features::OPTION_SCID_ALIAS, + Features::OPTION_ZEROCONF, + Features::OPTION_SIMPLE_TAPROOT, + Features::OPTION_SIMPLE_TAPROOT_STAGING, + ]) + } + #[track_caller] - fn assert_pass(accept_channel: &AcceptChannel, negotiation: Option<&PendingChannel>) { + fn assert_pass( + accept_channel: &AcceptChannel, + negotiation: Option<&PendingChannel>, + negotiated_features: &Features, + ) { if let Err(err) = AcceptChannelOracle.evaluate(&AcceptChannelContext { accept_channel, negotiation, + negotiated_features, }) { panic!("expected pass, got: {err}"); } @@ -340,11 +471,13 @@ mod tests { fn assert_fail( accept_channel: &AcceptChannel, negotiation: Option<&PendingChannel>, + negotiated_features: &Features, expected: &str, ) { match AcceptChannelOracle.evaluate(&AcceptChannelContext { accept_channel, negotiation, + negotiated_features, }) { Err(Violation::InvalidAcceptChannel(chan_id, reason)) => { assert_eq!(accept_channel.temporary_channel_id, chan_id); @@ -362,27 +495,106 @@ mod tests { assert_pass( &accept_channel(), Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), + ); + } + + #[test] + fn conforming_zero_fee_commitments_channel_passes() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + oc.feerate_per_kw = 0; + oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS; + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + ac.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS; + + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), ); } + #[test] + fn conforming_option_zeroconf_with_valid_minimum_depth_passes() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyZeroConf.encode()); + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyZeroConf.encode()); + ac.minimum_depth = 0; + + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); + } + + #[test] + fn conforming_compliant_shutdown_script_passes() { + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT); + let legacy_script = ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()).into_bytes(); + let segwit_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()).into_bytes(); + + let mut oc = open_channel(); + oc.tlvs.upfront_shutdown_script = Some(legacy_script.clone()); + let mut ac = accept_channel(); + ac.tlvs.upfront_shutdown_script = Some(segwit_script); + + assert_pass(&ac, Some(&pending_negotiation(oc)), &negotiated_features); + } + #[test] fn accept_channel_for_unknown_temporary_channel_id() { assert_fail( &accept_channel(), None, + &sample_negotiated_features(), "unknown temporary_channel_id: no open_channel was sent for this negotiation", ); } #[test] - fn funding_satoshis_above_bitcoins_total_supply() { + fn open_channel_option_dual_fund_negotiated() { + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_DUAL_FUND); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(open_channel())), + &negotiated_features, + "invalid open_channel: option_dual_fund has been negotiated", + ); + } + + #[test] + fn funding_satoshis_above_non_wumbo_limit_without_option_support_large_channel() { + let mut oc = open_channel(); + oc.funding_satoshis = MAX_FUNDING_SATOSHIS_NO_WUMBO + 1; + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + "invalid open_channel: funding_satoshis 16777216 exceeds maximum funding of 16777215 sat", + ); + } + + #[test] + fn funding_satoshis_above_bitcoins_total_supply_with_option_support_large_channel() { let mut oc = open_channel(); oc.funding_satoshis = Amount::MAX_MONEY.to_sat() + 1; + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_SUPPORT_LARGE_CHANNEL); + assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), - "invalid open_channel: funding_satoshis 2100000000000001 exceeds maximum funding", + &negotiated_features, + "invalid open_channel: funding_satoshis 2100000000000001 exceeds maximum funding of 2100000000000000 sat", ); } @@ -394,10 +606,52 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: push_msat 10000000001 exceeds funding amount", ); } + #[test] + fn open_channel_invalid_upfront_shutdown_script() { + let mut oc = open_channel(); + oc.tlvs.upfront_shutdown_script = Some(vec![0xFF, 0xFF]); + + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &negotiated_features, + "invalid open_channel: upfront_shutdown_script is not valid", + ); + } + + #[test] + fn open_channel_missing_upfront_shutdown_script() { + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(open_channel())), + &negotiated_features, + "invalid open_channel: open_channel does not include upfront_shutdown_script", + ); + } + + #[test] + fn open_channel_option_channel_type_not_supported() { + let negotiated_features = Features::new(); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(open_channel())), + &negotiated_features, + "invalid open_channel: option_channel_type is not supported", + ); + } + #[test] fn open_channel_without_a_channel_type() { let mut oc = open_channel(); @@ -406,22 +660,101 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: open_channel does not include a channel_type", ); } #[test] - fn open_channel_max_accepted_htlcs_above_the_limit() { + fn open_channel_channel_type_contains_non_negotiated_features() { let mut oc = open_channel(); - oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_LIMIT + 1; + oc.tlvs.channel_type = Some(vec![0x10, 0x00]); + + let negotiated_features = Features::from_bits(&[ + Features::ZERO_FEE_COMMITMENTS, + Features::OPTION_CHANNEL_TYPE, + ]); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &negotiated_features, + "invalid open_channel: channel_type contains features that were not negotiated", + ); + } + + #[test] + fn open_channel_with_unknown_channel_type_variant() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(vec![0x40, 0x40, 0x10, 0x00]); + + let mut negotiated_features = Features::from(vec![0x40, 0x40, 0x10, 0x00]); + negotiated_features.set_bit(Features::OPTION_CHANNEL_TYPE); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &negotiated_features, + "invalid open_channel: channel_type is not a known variant", + ); + } + + #[test] + fn open_channel_zero_fee_commitments_with_nonzero_feerate() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + oc.feerate_per_kw = 1; + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + "invalid open_channel: zero_fee_commitments requires feerate_per_kw to be 0", + ); + } + + #[test] + fn open_channel_option_scid_alias_for_public_channel() { + let mut oc = open_channel(); + oc.channel_flags = 1; + oc.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyScidAlias.encode()); assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + "invalid open_channel: option_scid_alias requires the channel to be private", + ); + } + + #[test] + fn open_channel_max_accepted_htlcs_above_the_default_limit() { + let mut oc = open_channel(); + oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_DEFAULT + 1; + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: max_accepted_htlcs 484 exceeds the limit of 483", ); } + #[test] + fn open_channel_max_accepted_htlcs_above_the_zero_fee_commitments_limit() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + oc.feerate_per_kw = 0; + oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS + 1; + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + "invalid open_channel: max_accepted_htlcs 115 exceeds the limit of 114", + ); + } + #[test] fn open_channel_dust_limit_below_the_minimum() { let mut oc = open_channel(); @@ -430,6 +763,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: dust_limit_satoshis 353 is below the minimum of 354 sat", ); } @@ -442,6 +776,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: opener balance 10000 sat cannot cover the commitment fee", ); } @@ -455,6 +790,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: opener balance 17000 sat cannot cover anchor cost of 660 sat (after fee deduction)", ); } @@ -467,10 +803,48 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: neither side exceeds channel reserve", ); } + #[test] + fn accept_channel_invalid_upfront_shutdown_script() { + let mut oc = open_channel(); + let legacy_script = ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()).into_bytes(); + oc.tlvs.upfront_shutdown_script = Some(legacy_script.clone()); + + let mut ac = accept_channel(); + ac.tlvs.upfront_shutdown_script = Some(legacy_script); + + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT); + + assert_fail( + &ac, + Some(&pending_negotiation(oc)), + &negotiated_features, + "invalid accept_channel: upfront_shutdown_script is not valid", + ); + } + + #[test] + fn accept_channel_missing_upfront_shutdown_script() { + let mut oc = open_channel(); + let valid_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()).into_bytes(); + oc.tlvs.upfront_shutdown_script = Some(valid_script); + + let mut negotiated_features = sample_negotiated_features(); + negotiated_features.set_bit(Features::OPTION_UPFRONT_SHUTDOWN_SCRIPT); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + &negotiated_features, + "accept_channel does not include upfront_shutdown_script", + ); + } + #[test] fn accept_channel_without_a_channel_type() { let mut ac = accept_channel(); @@ -479,6 +853,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: accept_channel does not include a channel_type", ); } @@ -491,10 +866,27 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: accept_channel channel_type does not match open_channel", ); } + #[test] + fn accept_channel_option_zeroconf_with_nonzero_minimum_depth() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyZeroConf.encode()); + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyZeroConf.encode()); + ac.minimum_depth = 1; + + assert_fail( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + "invalid accept_channel: option_zeroconf requires minimum_depth to be 0", + ); + } + #[test] fn accept_channel_reserve_below_the_open_channel_dust_limit() { let oc = open_channel(); @@ -504,6 +896,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid accept_channel: channel_reserve_satoshis 545 is below the open_channel dust_limit_satoshis 546", ); } @@ -517,22 +910,42 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: dust_limit_satoshis 5000 exceeds channel_reserve_satoshis 4000", ); } #[test] - fn accept_channel_max_accepted_htlcs_above_the_limit() { + fn accept_channel_max_accepted_htlcs_above_the_default_limit() { let mut ac = accept_channel(); - ac.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_LIMIT + 1; + ac.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_DEFAULT + 1; assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: max_accepted_htlcs 484 exceeds the limit of 483", ); } + #[test] + fn accept_channel_max_accepted_htlcs_above_the_zero_fee_commitments_limit() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + oc.feerate_per_kw = 0; + oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS; + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + ac.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS + 1; + + assert_fail( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + "invalid accept_channel: max_accepted_htlcs 115 exceeds the limit of 114", + ); + } + #[test] fn accept_channel_dust_limit_below_the_minimum() { let mut ac = accept_channel(); @@ -541,6 +954,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: dust_limit_satoshis 353 is below the minimum of 354 sat", ); } @@ -553,10 +967,44 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: neither side exceeds channel reserve", ); } + #[test] + fn commitment_validation_skipped_for_zero_fee_commitments() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + oc.feerate_per_kw = 0; + oc.push_msat = oc.funding_satoshis * 1000; + oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS; + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(ChannelTypeVariant::ZeroFeeCommitments.encode()); + ac.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS; + + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); + } + + #[test] + fn commitment_validation_skipped_for_option_simple_taproot() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(ChannelTypeVariant::SimpleTaproot.encode()); + oc.push_msat = oc.funding_satoshis * 1000; + let mut ac = accept_channel(); + ac.tlvs.channel_type = Some(ChannelTypeVariant::SimpleTaproot.encode()); + + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); + } + #[test] fn temporary_channel_id_reuse_before_funding_created() { let mut negotiation = pending_negotiation(open_channel()); @@ -565,6 +1013,7 @@ mod tests { assert_fail( &accept_channel(), Some(&negotiation), + &sample_negotiated_features(), "temporary_channel_id reuse: previous negotiation has not reached funding_created", ); } @@ -575,6 +1024,10 @@ mod tests { negotiation.accept_channel = Some(accept_channel()); negotiation.funding_built = true; - assert_pass(&accept_channel(), Some(&negotiation)); + assert_pass( + &accept_channel(), + Some(&negotiation), + &sample_negotiated_features(), + ); } }