diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 60644f27..17590b02 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -14,7 +14,8 @@ use std::fmt::Write; use bitcoin::{opcodes::all as opcodes, script::Builder, script::PushBytes}; use rand::{Rng, RngExt}; use serde::{Deserialize, Serialize}; -use smite::bolt::{FeatureBit, Features, ShortChannelId}; +pub use smite::bolt::ChannelTypeVariant; +use smite::bolt::ShortChannelId; use super::VariableType; @@ -423,200 +424,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 [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, - ], - } - } - - /// Encodes the channel type as a BOLT feature bitmap (big-endian bytes). - #[must_use] - pub fn encode(self) -> Vec { - Features::from_bits(self.bits()).into_bytes() - } -} - -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 665acc43..3e5550b5 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -134,8 +134,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 @@ -490,6 +490,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)) diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index 7039c04b..9bd8c358 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -126,7 +126,11 @@ pub fn sample_context() -> ProgramContext { 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 1daf89e9..c626279d 100644 --- a/smite-scenarios/src/scenarios/setup.rs +++ b/smite-scenarios/src/scenarios/setup.rs @@ -73,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. @@ -86,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 5572124f..0461786f 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -74,9 +74,10 @@ 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, TemporaryChannelId, 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, TemporaryChannelId, 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 index 1f791ca0..d5c6f5f6 100644 --- a/smite/src/bolt/features.rs +++ b/smite/src/bolt/features.rs @@ -9,12 +9,16 @@ pub type FeatureBit = usize; 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). @@ -25,6 +29,8 @@ impl Features { 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). @@ -120,6 +126,18 @@ impl Features { 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 { @@ -301,6 +319,111 @@ mod tests { 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]); diff --git a/smite/src/bolt/types.rs b/smite/src/bolt/types.rs index c13925fc..c47935c2 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). @@ -80,6 +82,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/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index c3c7add2..867a0124 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, Features, 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,9 +121,24 @@ 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 @@ -114,13 +148,40 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String 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, )); } @@ -153,7 +214,19 @@ 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 @@ -176,6 +249,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!( @@ -193,12 +275,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, )); } @@ -223,22 +303,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: &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); @@ -269,11 +351,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, OpenChannelTlvs, TemporaryChannelId}; + 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"); @@ -343,11 +445,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}"); } @@ -357,11 +478,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); @@ -379,27 +502,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", ); } @@ -411,10 +613,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(); @@ -423,22 +667,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.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_LIMIT + 1; + 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(); @@ -447,6 +770,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", ); } @@ -459,6 +783,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", ); } @@ -472,6 +797,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)", ); } @@ -484,10 +810,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(); @@ -496,6 +860,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", ); } @@ -508,6 +873,7 @@ 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", ); } @@ -517,7 +883,27 @@ mod tests { let mut oc = open_channel(); oc.tlvs.channel_type = Some(vec![0x00, 0x00, 0x10, 0x00]); - assert_pass(&accept_channel(), Some(&pending_negotiation(oc))); + assert_pass( + &accept_channel(), + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); + } + + #[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] @@ -529,6 +915,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", ); } @@ -542,22 +929,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(); @@ -566,6 +973,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", ); } @@ -578,10 +986,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()); @@ -590,6 +1032,7 @@ mod tests { assert_fail( &accept_channel(), Some(&negotiation), + &sample_negotiated_features(), "temporary_channel_id reuse: previous negotiation has not reached funding_created", ); } @@ -600,6 +1043,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(), + ); } }