From 695d504c438ec74efa3fb6b35f8d149c3e7a7b77 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Wed, 5 Aug 2026 13:41:10 +0530 Subject: [PATCH 1/5] smite: add BOLT 9 feature bitfield primitives Signed-off-by: Nishant Bansal --- smite/src/bolt.rs | 2 + smite/src/bolt/features.rs | 255 +++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 smite/src/bolt/features.rs diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index 69eda957..0e2ddcce 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -14,6 +14,7 @@ mod closing_complete; mod closing_sig; mod commitment_signed; mod error; +mod features; mod funding_created; mod funding_signed; mod gossip_timestamp_filter; @@ -52,6 +53,7 @@ pub use closing_complete::{ClosingComplete, ClosingTlvs}; pub use closing_sig::ClosingSig; pub use commitment_signed::{CommitmentSigned, CommitmentSignedTlvs}; pub use error::Error; +pub use features::{FeatureBit, Features}; pub use funding_created::FundingCreated; pub use funding_signed::FundingSigned; pub use gossip_timestamp_filter::GossipTimestampFilter; diff --git a/smite/src/bolt/features.rs b/smite/src/bolt/features.rs new file mode 100644 index 00000000..d36f90c6 --- /dev/null +++ b/smite/src/bolt/features.rs @@ -0,0 +1,255 @@ +//! BOLT 9 feature bitfield primitives. + +/// BOLT 9 feature bit index. Even bits are required; odd bits are optional. +pub type FeatureBit = usize; + +/// BOLT 9 feature bitfield, encoded as big-endian bytes. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Features(Vec); + +impl Features { + /// `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_anchors` (bits 22/23). + pub const OPTION_ANCHORS: FeatureBit = 22; + /// `option_dual_fund` (bits 28/29). + pub const OPTION_DUAL_FUND: FeatureBit = 28; + /// `zero_fee_commitments` (bits 40/41). + pub const ZERO_FEE_COMMITMENTS: FeatureBit = 40; + /// `option_provide_storage` (bits 42/43). + pub const OPTION_PROVIDE_STORAGE: FeatureBit = 42; + /// `option_scid_alias` (bits 46/47). + pub const OPTION_SCID_ALIAS: FeatureBit = 46; + /// `option_zeroconf` (bits 50/51). + pub const OPTION_ZEROCONF: FeatureBit = 50; + /// `option_simple_taproot` (bits 80/81). + pub const OPTION_SIMPLE_TAPROOT: FeatureBit = 80; + /// `option_simple_taproot_staging` (bits 180/181). + pub const OPTION_SIMPLE_TAPROOT_STAGING: FeatureBit = 180; + /// `option_script_enforced_lease` (bits 2022/2023). + pub const OPTION_SCRIPT_ENFORCED_LEASE: FeatureBit = 2022; + + /// Creates an empty set of features. + #[must_use] + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Creates features with the given bits set. + #[must_use] + pub fn from_bits(bits: &[FeatureBit]) -> Self { + let mut features = Self::new(); + for &bit in bits { + features.set_bit(bit); + } + features + } + + /// Consumes the features into their underlying bytes. + #[must_use] + pub fn into_bytes(self) -> Vec { + self.0 + } + + /// Sets the bit, extending the features with leading zero bytes if needed. + pub fn set_bit(&mut self, bit: FeatureBit) { + let byte_offset = bit / 8; + let mut len = self.0.len(); + if len <= byte_offset { + let new_len = byte_offset + 1; + let mut new_features = vec![0u8; new_len]; + new_features[(new_len - len)..].copy_from_slice(&self.0); + self.0 = new_features; + len = new_len; + } + + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] |= mask; + } + + /// Clears the bit, no-op if beyond the feature's length. + pub fn clear_bit(&mut self, bit: FeatureBit) { + let byte_offset = bit / 8; + let len = self.0.len(); + if byte_offset < len { + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] &= !mask; + } + } + + /// Returns whether the bit is set. + #[must_use] + pub fn is_bit_set(&self, bit: FeatureBit) -> bool { + let byte_offset = bit / 8; + let len = self.0.len(); + if len <= byte_offset { + return false; + } + + let mask = 1 << (bit % 8); + self.0[len - 1 - byte_offset] & mask != 0 + } + + /// Returns whether the feature is supported by checking its required or + /// optional bit. + #[must_use] + pub fn supports_feature(&self, bit: FeatureBit) -> bool { + self.is_bit_set(bit) || self.is_bit_set(bit ^ 1) + } + + /// Clears the feature's required (even) and optional (odd) bits. + pub fn clear_feature(&mut self, bit: FeatureBit) { + self.clear_bit(bit); + self.clear_bit(bit ^ 1); + } +} + +impl From> for Features { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_features_support_no_features() { + let features = Features::new(); + assert!(!features.supports_feature(Features::OPTION_ANCHORS)); + assert!(!features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + assert!(!features.supports_feature(Features::GOSSIP_QUERIES)); + } + + #[test] + fn from_bits_sets_requested_bits() { + assert_eq!(Features::from_bits(&[]), Features::new()); + assert_eq!( + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY]).into_bytes(), + vec![0x10, 0x00] + ); + assert_eq!( + Features::from_bits(&[Features::OPTION_STATIC_REMOTEKEY, Features::OPTION_ANCHORS]), + Features::from(vec![0x40, 0x10, 0x00]) + ); + } + + #[test] + fn supports_multiple_set_features() { + let features = Features::from_bits(&[ + Features::OPTION_ANCHORS, + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_DUAL_FUND, + ]); + + assert!(features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + assert!(features.supports_feature(Features::OPTION_DUAL_FUND)); + assert!(!features.supports_feature(Features::OPTION_ZEROCONF)); + } + + #[test] + fn set_bit_within_existing_length() { + let mut fv = Features::from(vec![0x00, 0x00]); + fv.set_bit(0); + assert_eq!(fv, Features::from(vec![0x00, 0x01])); + fv.set_bit(8); + assert_eq!(fv, Features::from(vec![0x01, 0x01])); + } + + #[test] + fn set_bit_grows_and_preserves_existing_bits() { + let mut fv = Features::from(vec![0x01]); + fv.set_bit(12); + assert_eq!(fv, Features::from(vec![0x10, 0x01])); + assert!(fv.is_bit_set(0)); + assert!(fv.is_bit_set(12)); + } + + #[test] + fn clear_bit_within_bounds_and_noop_out_of_bounds() { + let mut fv = Features::from(vec![0xff, 0xff]); + fv.clear_bit(0); + assert_eq!(fv, Features::from(vec![0xff, 0xfe])); + // Out of range: no-op. + fv.clear_bit(100); + assert_eq!(fv, Features::from(vec![0xff, 0xfe])); + } + + #[test] + fn is_bit_set_uses_big_endian_bit_order() { + let fv = Features::from(vec![0x00, 0x01]); + assert!(fv.is_bit_set(0)); + assert!(!fv.is_bit_set(1)); + + let fv = Features::from(vec![0x01, 0x00]); + assert!(fv.is_bit_set(8)); + assert!(!fv.is_bit_set(0)); + } + + #[test] + fn is_bit_set_out_of_bounds_returns_false() { + assert!(!Features::new().is_bit_set(0)); + assert!(!Features::from(vec![0xff]).is_bit_set(8)); + } + + #[test] + fn supports_feature_uses_big_endian_bit_order() { + // Required (bit 22), optional (bit 23). + assert!(Features::from(vec![0x40, 0x00, 0x00]).supports_feature(Features::OPTION_ANCHORS)); + assert!(Features::from(vec![0x80, 0x00, 0x00]).supports_feature(Features::OPTION_ANCHORS)); + // No support. + assert!(!Features::from(vec![0x00, 0x00, 0x40]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0x00, 0x00, 0x80]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0xff, 0xff]).supports_feature(Features::OPTION_ANCHORS)); + assert!(!Features::from(vec![0x00, 0x10]).supports_feature(Features::OPTION_ANCHORS)); + } + + #[test] + fn supports_feature_matches_either_bit_set() { + // Only the required (even) bit set. + let required_only = Features::from_bits(&[22]); + assert!(required_only.supports_feature(Features::OPTION_ANCHORS)); + assert!(required_only.supports_feature(23)); + // Only the optional (odd) bit set. + let optional_only = Features::from_bits(&[23]); + assert!(optional_only.supports_feature(Features::OPTION_ANCHORS)); + assert!(optional_only.supports_feature(23)); + // Neither bit set. + assert!( + !Features::from_bits(&[Features::OPTION_ZEROCONF]) + .supports_feature(Features::OPTION_ANCHORS) + ); + } + + #[test] + fn clear_feature_clears_both_bits() { + // Passing the even bit clears the pair. + let mut fv = Features::from_bits(&[22, 23]); + fv.clear_feature(Features::OPTION_ANCHORS); + assert!(!fv.supports_feature(Features::OPTION_ANCHORS)); + // Passing the odd bit clears the pair. + let mut fv = Features::from_bits(&[22, 23]); + fv.clear_feature(23); + assert!(!fv.supports_feature(Features::OPTION_ANCHORS)); + } + + #[test] + fn clear_feature_removes_support() { + let mut features = + Features::from_bits(&[Features::OPTION_ANCHORS, Features::OPTION_STATIC_REMOTEKEY]); + + assert!(features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + + features.clear_feature(Features::OPTION_ANCHORS); + assert!(!features.supports_feature(Features::OPTION_ANCHORS)); + assert!(features.supports_feature(Features::OPTION_STATIC_REMOTEKEY)); + } +} From 5f235ede692c496cc8621780178e84fef0c7e807 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Wed, 5 Aug 2026 14:09:26 +0530 Subject: [PATCH 2/5] smite: use Features for channel_type in commitment Signed-off-by: Nishant Bansal --- smite-scenarios/src/executor.rs | 6 +-- smite/src/channel_tx/commitment.rs | 67 +++++++++--------------------- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 1c01b145..a8e42dfa 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -9,8 +9,8 @@ use bitcoin::{OutPoint, ScriptBuf, Txid}; use smite::bitcoin::{BitcoinCli, TxBlockPosition, Utxo}; use smite::bolt::{ AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelId, ChannelReady, - ChannelReadyTlvs, ChannelUpdate, FundingCreated, FundingSigned, Message, NodeAnnouncement, - OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, msg_type, + ChannelReadyTlvs, ChannelUpdate, Features, FundingCreated, FundingSigned, Message, + NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, msg_type, }; use smite::channel_tx::{ ChannelConfig, ChannelPartyConfig, ChannelState, FundingTransaction, HolderIdentity, Side, @@ -932,7 +932,7 @@ fn build_funding_created( let config = ChannelConfig { funding_outpoint, funding_satoshis: open_channel.funding_satoshis, - channel_type: open_channel.tlvs.channel_type.clone().unwrap_or_default(), + channel_type: Features::from(open_channel.tlvs.channel_type.clone().unwrap_or_default()), opener, acceptor, minimum_depth: accept_channel.minimum_depth, diff --git a/smite/src/channel_tx/commitment.rs b/smite/src/channel_tx/commitment.rs index 0b451b60..bc6870c5 100644 --- a/smite/src/channel_tx/commitment.rs +++ b/smite/src/channel_tx/commitment.rs @@ -1,6 +1,7 @@ //! BOLT 3 commitment transaction construction and signing. use super::funding::build_funding_witness_script; +use crate::bolt::Features; use bitcoin::absolute::LockTime; use bitcoin::hashes::sha256::Hash as Sha256; @@ -24,9 +25,6 @@ const COMMITMENT_TX_BASE_WEIGHT_NON_ANCHOR: u64 = 724; /// Weight of an anchor commitment transaction without HTLCs. const COMMITMENT_TX_BASE_WEIGHT_ANCHOR: u64 = 1124; -/// `option_anchors` feature bits (BOLT 9, bits 22/23). -const OPTION_ANCHORS_FEATURE_BITS: &[usize] = &[22, 23]; - /// Errors that can occur when constructing or validating commitment transactions. #[derive(Debug, thiserror::Error)] pub enum CommitmentError { @@ -77,7 +75,7 @@ pub struct ChannelConfig { pub funding_satoshis: u64, /// Channel type feature bits. The commitment format (anchor / legacy) is /// derived from the bits set here. - pub channel_type: Vec, + pub channel_type: Features, /// Opener's static keys and parameters. pub opener: ChannelPartyConfig, /// Acceptor's static keys and parameters. @@ -383,7 +381,7 @@ impl ChannelConfig { /// `local_side` selects whose commitment outputs are built: the /// opener's or the acceptor's. fn build_commitment_outputs(&self, state: &CommitmentState, local_side: &Side) -> Vec { - let anchor = supports_option_anchors(&self.channel_type); + let anchor = self.channel_type.supports_feature(Features::OPTION_ANCHORS); // Fee and balances. let commitment_cost = CommitmentCost::new(state.feerate_per_kw, &self.channel_type); @@ -471,7 +469,7 @@ impl CommitmentState { impl CommitmentCost { /// Calculates the total cost of a commitment transaction. #[must_use] - pub fn new(feerate_per_kw: u32, channel_type: &[u8]) -> CommitmentCost { + pub fn new(feerate_per_kw: u32, channel_type: &Features) -> CommitmentCost { CommitmentCost { fee_sat: commit_tx_fee_sat(feerate_per_kw, channel_type), anchor_cost_sat: total_anchors_sat(channel_type), @@ -486,8 +484,8 @@ impl CommitmentCost { } /// Get the fee cost of a commitment tx in satoshis. -fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &[u8]) -> u64 { - let commitment_weight = if supports_option_anchors(channel_type) { +fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &Features) -> u64 { + let commitment_weight = if channel_type.supports_feature(Features::OPTION_ANCHORS) { COMMITMENT_TX_BASE_WEIGHT_ANCHOR } else { COMMITMENT_TX_BASE_WEIGHT_NON_ANCHOR @@ -497,8 +495,8 @@ fn commit_tx_fee_sat(feerate_per_kw: u32, channel_type: &[u8]) -> u64 { } /// Get the anchor cost of a commitment tx in satoshis. -fn total_anchors_sat(channel_type: &[u8]) -> u64 { - if supports_option_anchors(channel_type) { +fn total_anchors_sat(channel_type: &Features) -> u64 { + if channel_type.supports_feature(Features::OPTION_ANCHORS) { ANCHOR_OUTPUT_VALUE * 2 } else { 0 @@ -528,24 +526,6 @@ fn compute_obscuring_factor( u64::from_be_bytes(buf) } -/// Checks whether `option_anchors` (BOLT 9, bits 22/23) is set in a -/// big-endian `channel_type` feature bitfield. -/// -/// Per BOLT 9, even bit (22) = required, odd bit (23) = optional. -/// Either bit indicates anchor support. -fn supports_option_anchors(channel_type: &[u8]) -> bool { - let byte_offset = OPTION_ANCHORS_FEATURE_BITS[0] / 8; - let len = channel_type.len(); - if len <= byte_offset { - return false; - } - - let required_mask = 1 << (OPTION_ANCHORS_FEATURE_BITS[0] % 8); - let optional_mask = 1 << (OPTION_ANCHORS_FEATURE_BITS[1] % 8); - - channel_type[len - 1 - byte_offset] & (required_mask | optional_mask) != 0 -} - /// Derives a public key from a basepoint and per-commitment point per BOLT 3. fn derive_pubkey(basepoint: &PublicKey, per_commitment_point: &PublicKey) -> PublicKey { let secp = Secp256k1::new(); @@ -688,19 +668,6 @@ mod tests { assert_eq!(factor, 0x2bb0_3852_1914); } - #[test] - fn supports_option_anchors_detection() { - // Required (bit 22), optional (bit 23). - assert!(supports_option_anchors(&[0x40, 0x00, 0x00])); - assert!(supports_option_anchors(&[0x80, 0x00, 0x00])); - // No support. - assert!(!supports_option_anchors(&[0x00, 0x00, 0x40])); - assert!(!supports_option_anchors(&[0x00, 0x00, 0x80])); - assert!(!supports_option_anchors(&[])); - assert!(!supports_option_anchors(&[0xff, 0xff])); - assert!(!supports_option_anchors(&[0x00, 0x10])); - } - fn bolt3_commitment_params( feerate_per_kw: u32, to_opener_msat: u64, @@ -721,7 +688,7 @@ mod tests { vout: 0, }, funding_satoshis: 10_000_000, - channel_type, + channel_type: Features::from(channel_type), opener: ChannelPartyConfig { funding_pubkey: pubkey( "023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb", @@ -1349,7 +1316,7 @@ mod tests { vout: 0, }, funding_satoshis, - channel_type, + channel_type: Features::from(channel_type), opener: sample_party(), acceptor: sample_party(), minimum_depth: 8, @@ -1377,28 +1344,32 @@ mod tests { #[test] fn opener_balance_after_commitment_cost_total_sat_checks() { let feerate_per_kw: u32 = 15_000; - let anchor_channel_type = [0x40, 0x00, 0x00]; + let legacy = Features::new(); + let anchor = Features::from_bits(&[Features::OPTION_ANCHORS]); // Legacy fee: 15000 * 724 / 1000 = 10_860 sat // Anchor fee: 15000 * 1124 / 1000 = 16_860 sat; anchor_cost = 660 sat // Comfortably affordable let opener_balance_sat: u64 = 20_000; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), Some(9_140), ); // Exact zero opener balance let opener_balance_sat: u64 = 10_860; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), Some(0), ); // Balance does not cover the fee let opener_balance_sat: u64 = 10_000; assert_eq!( - opener_balance_sat.checked_sub(CommitmentCost::new(feerate_per_kw, &[]).total_sat()), + opener_balance_sat + .checked_sub(CommitmentCost::new(feerate_per_kw, &legacy).total_sat()), None ); @@ -1406,7 +1377,7 @@ mod tests { let opener_balance_sat: u64 = 17_500; assert_eq!( opener_balance_sat - .checked_sub(CommitmentCost::new(feerate_per_kw, &anchor_channel_type).total_sat()), + .checked_sub(CommitmentCost::new(feerate_per_kw, &anchor).total_sat()), None, ); } From 41ffc3ca72e40f9c1c9a69fc7749c188b9292230 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Wed, 5 Aug 2026 14:58:18 +0530 Subject: [PATCH 3/5] smite-ir: use Features for channel_type bits Signed-off-by: Nishant Bansal --- smite-ir/src/operation.rs | 125 ++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 44 deletions(-) diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 20b1f64b..fccde02a 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -14,7 +14,7 @@ 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::ShortChannelId; +use smite::bolt::{FeatureBit, Features, ShortChannelId}; use super::VariableType; @@ -520,56 +520,93 @@ impl ChannelTypeVariant { /// The feature bits (even/required) contained in this channel type. #[must_use] - pub fn bits(self) -> &'static [usize] { - // BOLT 9 feature bits: - // 12 = option_static_remotekey - // 22 = option_anchors - // 40 = zero_fee_commitments - // 46 = option_scid_alias - // 50 = option_zeroconf - // 80 = option_simple_taproot - // 180 = option_simple_taproot_staging - // 2022 = option_script_enforced_lease + pub fn bits(self) -> &'static [FeatureBit] { + use Features as F; match self { - Self::StaticRemoteKey => &[12], - Self::StaticRemoteKeyScidAlias => &[12, 46], - Self::StaticRemoteKeyZeroConf => &[12, 50], - Self::StaticRemoteKeyScidAliasZeroConf => &[12, 46, 50], - Self::Anchors => &[12, 22], - Self::AnchorsScidAlias => &[12, 22, 46], - Self::AnchorsZeroConf => &[12, 22, 50], - Self::AnchorsScidAliasZeroConf => &[12, 22, 46, 50], - Self::ZeroFeeCommitments => &[40], - Self::ZeroFeeCommitmentsScidAlias => &[40, 46], - Self::ZeroFeeCommitmentsZeroConf => &[40, 50], - Self::ZeroFeeCommitmentsScidAliasZeroConf => &[40, 46, 50], - Self::SimpleTaproot => &[80], - Self::SimpleTaprootScidAlias => &[80, 46], - Self::SimpleTaprootZeroConf => &[80, 50], - Self::SimpleTaprootScidAliasZeroConf => &[80, 46, 50], - Self::SimpleTaprootStaging => &[180], - Self::SimpleTaprootStagingScidAlias => &[180, 46], - Self::SimpleTaprootStagingZeroConf => &[180, 50], - Self::SimpleTaprootStagingScidAliasZeroConf => &[180, 46, 50], - Self::ScriptEnforcedLease => &[12, 22, 2022], - Self::ScriptEnforcedLeaseScidAlias => &[12, 22, 2022, 46], - Self::ScriptEnforcedLeaseZeroConf => &[12, 22, 2022, 50], - Self::ScriptEnforcedLeaseScidAliasZeroConf => &[12, 22, 2022, 46, 50], + 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] - #[allow(clippy::missing_panics_doc)] // bits() is always non-empty pub fn encode(self) -> Vec { - let bits = self.bits(); - let max_bit = *bits.iter().max().expect("non-empty bits"); - let num_bytes = max_bit / 8 + 1; - let mut out = vec![0u8; num_bytes]; - for &bit in bits { - out[num_bytes - 1 - bit / 8] |= 1 << (bit % 8); - } - out + Features::from_bits(self.bits()).into_bytes() } } From a9a6fb246e78304c16b72f26cf7822e1ed58ab6f Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Wed, 5 Aug 2026 15:08:36 +0530 Subject: [PATCH 4/5] smite-scenarios: use Features for feature bits in setup Signed-off-by: Nishant Bansal --- smite-scenarios/src/scenarios/setup.rs | 61 ++++++++++---------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/smite-scenarios/src/scenarios/setup.rs b/smite-scenarios/src/scenarios/setup.rs index 08422d86..1daf89e9 100644 --- a/smite-scenarios/src/scenarios/setup.rs +++ b/smite-scenarios/src/scenarios/setup.rs @@ -2,7 +2,7 @@ use std::time::Duration; -use smite::bolt::{Init, InitTlvs, Message}; +use smite::bolt::{FeatureBit, Features, Init, InitTlvs, Message}; use smite::noise::NoiseConnection; use smite::scenarios::ScenarioError; @@ -30,49 +30,34 @@ pub trait SnapshotSetup { fn setup(target: &T) -> Result<(NoiseConnection, ProgramContext), ScenarioError>; } -/// Clears a feature bit from a feature vector. -/// -/// Feature vectors are encoded as big-endian byte arrays where bit N lives in -/// byte `features[len - 1 - N/8]` at position `N % 8`. -fn clear_feature_bit(features: &mut [u8], bit: usize) { - let byte_index = features.len().checked_sub(1 + bit / 8); - if let Some(i) = byte_index { - features[i] &= !(1 << (bit % 8)); - } -} - -/// Gossip-related feature bits (BOLT 9): `gossip_queries` (6/7), -/// `gossip_queries_ex` (10/11). Stripped so the target doesn't send -/// `gossip_timestamp_filter` or other gossip noise during execution. -const GOSSIP_FEATURE_BITS: &[usize] = &[6, 7, 10, 11]; - -/// Feature bits that force a dual-funded flow when both peers support them: -/// `option_dual_fund` (28/29). Eclair in particular will not allow -/// single-funded flows if either of these feature bits is set, so we strip them -/// when fuzzing the single-funded flow. -const DUAL_FUNDING_FEATURE_BITS: &[usize] = &[28, 29]; - -/// Peer storage feature bits: `option_provide_storage` (42/43). When enabled, -/// peers may send `peer_storage` and `peer_storage_retrieval` messages at -/// arbitrary times. Disabling these bits eliminates peer storage noise. -const PEER_STORAGE_FEATURE_BITS: &[usize] = &[42, 43]; +/// Features stripped from our echoed `init` so the target stays on the single +/// funded flow and doesn't emit unrelated noise: +/// - `gossip_queries` (6/7), `gossip_queries_ex` (10/11): Stripped so the +/// target doesn't send `gossip_timestamp_filter` or other gossip noise during +/// execution. +/// - `option_dual_fund` (28/29): Eclair in particular will not allow +/// single-funded flows if either of these feature bits is set. +/// - `option_provide_storage` (42/43): When enabled, peers may send +/// `peer_storage` and `peer_storage_retrieval` messages at arbitrary times. +const STRIPPED_FEATURES: &[FeatureBit] = &[ + Features::GOSSIP_QUERIES, + Features::GOSSIP_QUERIES_EX, + Features::OPTION_DUAL_FUND, + Features::OPTION_PROVIDE_STORAGE, +]; /// Creates an `init` that echoes the received features with bits stripped that /// would steer the target away from the single-funded `open_channel` flow. fn init_for_single_funded(received: &Init) -> Init { - let mut globalfeatures = received.globalfeatures.clone(); - let mut features = received.features.clone(); - for &bit in GOSSIP_FEATURE_BITS - .iter() - .chain(DUAL_FUNDING_FEATURE_BITS) - .chain(PEER_STORAGE_FEATURE_BITS) - { - clear_feature_bit(&mut globalfeatures, bit); - clear_feature_bit(&mut features, bit); + let mut globalfeatures = Features::from(received.globalfeatures.clone()); + let mut features = Features::from(received.features.clone()); + for &bit in STRIPPED_FEATURES { + globalfeatures.clear_feature(bit); + features.clear_feature(bit); } Init { - globalfeatures, - features, + globalfeatures: globalfeatures.into_bytes(), + features: features.into_bytes(), tlvs: InitTlvs::default(), } } From a87f2221fed6d8ed2203b7b555bc05072ec322f7 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sat, 8 Aug 2026 14:34:13 +0530 Subject: [PATCH 5/5] smite: use Features for channel_type in accept_channel oracle Signed-off-by: Nishant Bansal --- smite/src/oracles/accept_channel.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index 3ab16320..c098a6c0 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, OpenChannel}; +use crate::bolt::{AcceptChannel, Features, OpenChannel}; use crate::channel_tx::CommitmentCost; use crate::pending_channel::PendingChannel; use crate::violation::Violation; @@ -105,7 +105,12 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String // Check that the channel type was included. // TODO: Check option_channel_type in negotiated features since it is // assumed to be supported. - let Some(channel_type) = open_channel.tlvs.channel_type.as_deref() else { + let Some(channel_type) = open_channel + .tlvs + .channel_type + .as_deref() + .map(|channel_type| Features::from(channel_type.to_vec())) + else { return Err("open_channel does not include a channel_type".to_string()); }; @@ -131,7 +136,7 @@ fn verify_accepted_open_channel(open_channel: &OpenChannel) -> Result<(), String // Check the initial commitment satisfies the channel reserve. verify_initial_commitment( open_channel, - channel_type, + &channel_type, open_channel.channel_reserve_satoshis, ) } @@ -150,7 +155,12 @@ fn verify_accept_channel( open_channel: &OpenChannel, ) -> Result<(), String> { // Check that the channel type was included. - let Some(channel_type) = accept_channel.tlvs.channel_type.as_deref() else { + let Some(channel_type) = accept_channel + .tlvs + .channel_type + .as_deref() + .map(|channel_type| Features::from(channel_type.to_vec())) + else { return Err("accept_channel does not include a channel_type".to_string()); }; @@ -197,7 +207,7 @@ fn verify_accept_channel( // Check the initial commitment satisfies the channel reserve. verify_initial_commitment( open_channel, - channel_type, + &channel_type, accept_channel.channel_reserve_satoshis, ) } @@ -219,7 +229,7 @@ fn verify_accept_channel( /// they are not unnecessarily subtracted for these channel types. fn verify_initial_commitment( open_channel: &OpenChannel, - channel_type: &[u8], + channel_type: &Features, channel_reserve_satoshis: u64, ) -> Result<(), String> { // Check that the opener can afford the proposed feerate.