From d6a3b0ffd1b804d52a72f37b289b78c0f3634b52 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sat, 8 Aug 2026 14:51:50 +0530 Subject: [PATCH 1/5] smite: move ChannelTypeVariant to bolt::types The `accept_channel` oracle needs to verify that `channel_type` uses the smallest possible bitmap and represents a defined channel type. Keeping `ChannelTypeVariant` in `bolt::types` gives the oracle a canonical set of channel types to validate against and avoids duplicating the definition across modules. Signed-off-by: Nishant Bansal --- smite-ir/src/operation.rs | 197 +------------------------------------ smite/src/bolt.rs | 7 +- smite/src/bolt/types.rs | 202 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 198 deletions(-) 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/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/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]: From 918df0d7458f6a9b4694d34a7e3f43a04694dba2 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sat, 8 Aug 2026 18:16:00 +0530 Subject: [PATCH 2/5] smite: validate channel type variants in accept_channel oracle Signed-off-by: Nishant Bansal --- smite/src/oracles/accept_channel.rs | 226 ++++++++++++++++++++++++---- 1 file changed, 200 insertions(+), 26 deletions(-) diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index c3c7add2..1b07fdf7 100644 --- a/smite/src/oracles/accept_channel.rs +++ b/smite/src/oracles/accept_channel.rs @@ -1,7 +1,7 @@ //! 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}; use crate::channel_tx::CommitmentCost; use crate::pending_channel::PendingChannel; use crate::violation::Violation; @@ -10,7 +10,8 @@ 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_ACCEPTED_HTLCS_ZERO_FEE_COMMITMENTS: u16 = 114; +const MAX_ACCEPTED_HTLCS_DEFAULT: u16 = 483; const MIN_DUST_LIMIT_SATOSHIS: u64 = 354; /// Context for `AcceptChannelOracle` @@ -114,13 +115,35 @@ 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 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, )); } @@ -176,6 +199,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 +225,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 +253,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,6 +301,15 @@ fn verify_initial_commitment( Ok(()) } +/// 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::*; @@ -382,6 +423,30 @@ mod tests { ); } + #[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))); + } + + #[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))); + } + #[test] fn accept_channel_for_unknown_temporary_channel_id() { assert_fail( @@ -428,9 +493,47 @@ mod tests { } #[test] - fn open_channel_max_accepted_htlcs_above_the_limit() { + fn open_channel_with_unknown_channel_type_variant() { + let mut oc = open_channel(); + oc.tlvs.channel_type = Some(vec![0xff, 0x40, 0x10, 0x00]); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + "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)), + "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.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_LIMIT + 1; + oc.channel_flags = 1; + oc.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyScidAlias.encode()); + + assert_fail( + &accept_channel(), + Some(&pending_negotiation(oc)), + "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(), @@ -439,6 +542,20 @@ mod tests { ); } + #[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)), + "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(); @@ -520,6 +637,21 @@ mod tests { assert_pass(&accept_channel(), Some(&pending_negotiation(oc))); } + #[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)), + "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(); @@ -547,9 +679,9 @@ mod tests { } #[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, @@ -558,6 +690,23 @@ mod tests { ); } + #[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)), + "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(); @@ -582,6 +731,31 @@ mod tests { ); } + #[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))); + } + + #[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))); + } + #[test] fn temporary_channel_id_reuse_before_funding_created() { let mut negotiation = pending_negotiation(open_channel()); From 05df5d8473da6d6ae9b0d37be5d2eb8b8517bf72 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sat, 8 Aug 2026 18:27:24 +0530 Subject: [PATCH 3/5] smite-scenarios: rename target_features to negotiated_features We strip certain feature bits during setup to exercise only the single funded flow, so the features stored here are what both sides have agreed to continue with, the negotiated feature set, not just the target's advertised features. This prepares for oracle validation that will use negotiated features to validate field constraints. If the target didn't disconnect after our init, that confirms it also conforms to our negotiated features, not its original advertised feature set. Signed-off-by: Nishant Bansal --- smite-scenarios/src/executor.rs | 4 ++-- smite-scenarios/src/executor/tests/harness.rs | 2 +- smite-scenarios/src/scenarios/setup.rs | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 665acc43..c7ed620d 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 diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index 7039c04b..26be8ef4 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -126,7 +126,7 @@ 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(vec![0x40, 0x10, 0x00]), } } 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)) From 5250f9f4e3c1c13050ac889a25ccf6fbfde69697 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Mon, 10 Aug 2026 23:22:15 +0530 Subject: [PATCH 4/5] smite: add is_supported for negotiated feature validation This is useful when comparing features in message fields against negotiated features. For eg., comparing channel_type in open_channel and accept_channel to ensure they match the features negotiated during setup. Signed-off-by: Nishant Bansal --- smite/src/bolt/features.rs | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/smite/src/bolt/features.rs b/smite/src/bolt/features.rs index 1f791ca0..48344c7e 100644 --- a/smite/src/bolt/features.rs +++ b/smite/src/bolt/features.rs @@ -120,6 +120,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 +313,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]); From 05bce1c6eaee5d01f43b36974a2e839fbdb8334d Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Tue, 11 Aug 2026 01:22:41 +0530 Subject: [PATCH 5/5] smite: add negotiated feature validation to accept_channel oracle Signed-off-by: Nishant Bansal --- smite-scenarios/src/executor.rs | 1 + smite-scenarios/src/executor/tests/harness.rs | 6 +- smite/src/bolt/features.rs | 6 + smite/src/oracles/accept_channel.rs | 313 ++++++++++++++++-- 4 files changed, 305 insertions(+), 21 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index c7ed620d..3e5550b5 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -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 26be8ef4..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, - negotiated_features: Features::from(vec![0x40, 0x10, 0x00]), + negotiated_features: Features::from_bits(&[ + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_ANCHORS, + Features::OPTION_CHANNEL_TYPE, + ]), } } diff --git a/smite/src/bolt/features.rs b/smite/src/bolt/features.rs index 48344c7e..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). diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index 1b07fdf7..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, ChannelTypeVariant, 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,6 +13,7 @@ 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_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; @@ -21,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 @@ -46,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}"), @@ -54,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}"), @@ -84,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, )); } @@ -103,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 @@ -115,6 +148,11 @@ 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() @@ -176,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 @@ -301,6 +351,15 @@ 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) { @@ -314,7 +373,9 @@ pub fn max_accepted_htlcs_limit(channel_type: &Features) -> u16 { 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"); @@ -384,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}"); } @@ -398,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); @@ -420,6 +502,7 @@ mod tests { assert_pass( &accept_channel(), Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), ); } @@ -433,7 +516,11 @@ mod tests { 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))); + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); } #[test] @@ -444,7 +531,26 @@ mod tests { ac.tlvs.channel_type = Some(ChannelTypeVariant::StaticRemoteKeyZeroConf.encode()); ac.minimum_depth = 0; - assert_pass(&ac, Some(&pending_negotiation(oc))); + 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] @@ -452,19 +558,50 @@ mod tests { 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", ); } @@ -476,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(); @@ -488,18 +667,41 @@ 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_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![0xff, 0x40, 0x10, 0x00]); + 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", ); } @@ -513,6 +715,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: zero_fee_commitments requires feerate_per_kw to be 0", ); } @@ -526,6 +729,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: option_scid_alias requires the channel to be private", ); } @@ -538,6 +742,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: max_accepted_htlcs 484 exceeds the limit of 483", ); } @@ -552,6 +757,7 @@ mod tests { assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid open_channel: max_accepted_htlcs 115 exceeds the limit of 114", ); } @@ -564,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", ); } @@ -576,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", ); } @@ -589,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)", ); } @@ -601,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(); @@ -613,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", ); } @@ -625,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", ); } @@ -634,7 +883,11 @@ 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] @@ -648,6 +901,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid accept_channel: option_zeroconf requires minimum_depth to be 0", ); } @@ -661,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", ); } @@ -674,6 +929,7 @@ 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", ); } @@ -686,6 +942,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: max_accepted_htlcs 484 exceeds the limit of 483", ); } @@ -703,6 +960,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid accept_channel: max_accepted_htlcs 115 exceeds the limit of 114", ); } @@ -715,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", ); } @@ -727,6 +986,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: neither side exceeds channel reserve", ); } @@ -742,7 +1002,11 @@ mod tests { 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))); + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); } #[test] @@ -753,7 +1017,11 @@ mod tests { let mut ac = accept_channel(); ac.tlvs.channel_type = Some(ChannelTypeVariant::SimpleTaproot.encode()); - assert_pass(&ac, Some(&pending_negotiation(oc))); + assert_pass( + &ac, + Some(&pending_negotiation(oc)), + &sample_negotiated_features(), + ); } #[test] @@ -764,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", ); } @@ -774,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(), + ); } }