From 1f02ca6889a39211402c477e4c1ee840138f732c Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sat, 8 Aug 2026 18:16:00 +0530 Subject: [PATCH 1/4] smite: validate channel type variants in accept_channel oracle Signed-off-by: Nishant Bansal --- smite/src/oracles/accept_channel.rs | 244 ++++++++++++++++++++++++---- 1 file changed, 216 insertions(+), 28 deletions(-) diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index c3c7add2..408154ef 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,9 +10,14 @@ 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; +// The least-significant bit of `channel_flags` in `open_channel`, indicating +// whether the initiator wishes to announce the channel publicly. +const ANNOUNCE_CHANNEL_FLAG: u8 = 1; + /// Context for `AcceptChannelOracle` pub struct AcceptChannelContext<'a> { /// The `accept_channel` received from the peer. @@ -114,13 +119,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 & ANNOUNCE_CHANNEL_FLAG != 0; + 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 +203,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 +229,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 +257,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 +305,15 @@ fn verify_initial_commitment( Ok(()) } +/// Returns the maximum number of inbound HTLCs allowed by the channel type. +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 +427,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 +497,53 @@ mod tests { } #[test] - fn open_channel_max_accepted_htlcs_above_the_limit() { + fn open_channel_with_unknown_channel_type_variant() { + let bits = [ + Features::OPTION_STATIC_REMOTEKEY, + Features::OPTION_ANCHORS, + 30, // not part of any known channel_type variant + ]; + let mut oc = open_channel(); - oc.max_accepted_htlcs = MAX_ACCEPTED_HTLCS_LIMIT + 1; + oc.tlvs.channel_type = Some(Features::from_bits(&bits).into_bytes()); + + 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.channel_flags = ANNOUNCE_CHANNEL_FLAG; + 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 +552,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(); @@ -467,7 +594,7 @@ mod tests { fn opener_cannot_cover_anchor_outputs() { let mut oc = open_channel(); oc.push_msat = oc.funding_satoshis * 1000 - 17_000_000; - oc.tlvs.channel_type = Some(vec![0x40, 0x10, 0x00]); + oc.tlvs.channel_type = Some(ChannelTypeVariant::Anchors.encode()); assert_fail( &accept_channel(), @@ -503,7 +630,7 @@ mod tests { #[test] fn accept_channel_channel_type_mismatch_with_open_channel() { let mut ac = accept_channel(); - ac.tlvs.channel_type = Some(vec![0x40, 0x10, 0x00]); + ac.tlvs.channel_type = Some(ChannelTypeVariant::Anchors.encode()); assert_fail( &ac, @@ -520,6 +647,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 +689,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 +700,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 +741,35 @@ mod tests { ); } + // NOTE: Validation is skipped, but once we add support for 0FC, this should + // still be accepted since the `shared_anchor` amount is 0. + #[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))); + } + + // FIXME: Validation is skipped, but once we add support for the Taproot + // commitment fee and anchors, this should be rejected. + #[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 e6450a48ff8b85cb0173a52bada249164ac1b69c Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Sat, 8 Aug 2026 18:27:24 +0530 Subject: [PATCH 2/4] 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 | 6 ++++-- smite-scenarios/src/executor/tests/harness.rs | 2 +- smite-scenarios/src/scenarios/setup.rs | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index 7873ff77..eeb6afac 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -136,8 +136,10 @@ 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. Even and odd + /// feature bits are treated equivalently, and the distinction carries no + /// meaning here. + 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 5c6bdccb..65509ab5 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -139,7 +139,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 165f6cfbc475c73cd9e80568d610aebc7030c451 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Mon, 10 Aug 2026 23:22:15 +0530 Subject: [PATCH 3/4] smite: add supports_features 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..115ef4b2 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 in `other` is supported here, where the + /// feature's required (even) or optional (odd) bit both count as support. + #[must_use] + pub fn supports_features(&self, other: &Features) -> bool { + for bit in 0..(other.0.len() * 8) { + if other.is_bit_set(bit) && !self.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 supports_features_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.supports_features(&empty)); + assert!(single_bit.supports_features(&empty)); + assert!(multiple_bits.supports_features(&empty)); + + assert!(!empty.supports_features(&single_bit)); + assert!(single_bit.supports_features(&single_bit)); + assert!(multiple_bits.supports_features(&single_bit)); + + assert!(!empty.supports_features(&multiple_bits)); + assert!(!single_bit.supports_features(&multiple_bits)); + assert!(multiple_bits.supports_features(&multiple_bits)); + } + + #[test] + fn supports_features_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!(superset.supports_features(&subset1)); + assert!(superset.supports_features(&subset2)); + + assert!(!subset1.supports_features(&subset2)); + assert!(!subset2.supports_features(&subset1)); + + assert!(!subset1.supports_features(&superset)); + assert!(!subset2.supports_features(&superset)); + } + + #[test] + fn supports_features_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!(!remotekey_and_dual_fund.supports_features(&anchors_and_remotekey)); + assert!(!anchors_and_remotekey.supports_features(&remotekey_and_dual_fund)); + } + + #[test] + fn supports_features_with_different_lengths() { + let short = Features::from(vec![0x01]); + let long = Features::from(vec![0x10, 0x01]); + + assert!(long.supports_features(&short)); + assert!(!short.supports_features(&long)); + + let short = Features::from(vec![0x01]); + let long = Features::from(vec![0x01, 0x00]); + + assert!(!long.supports_features(&short)); + assert!(!short.supports_features(&long)); + + let short = Features::from(vec![0x80]); + let long = Features::from(vec![0x00, 0x80]); + + assert!(long.supports_features(&short)); + assert!(short.supports_features(&long)); + } + + #[test] + fn supports_features_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!(negotiated.supports_features(&channel_type)); + + // 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!(negotiated.supports_features(&channel_type)); + + // A feature advertised in neither parity is still not negotiated. + let anchors = Features::from_bits(&[Features::OPTION_ANCHORS]); + assert!(!negotiated.supports_features(&anchors)); + } + + #[test] + fn supports_features_accepts_required_bit_for_optional_bit() { + // The pairing is symmetric: an optional bit on the right is satisfied + // by the required bit on the left and vice versa. + let optional = Features::from_bits(&[Features::OPTION_SCID_ALIAS ^ 1]); + let required = Features::from_bits(&[Features::OPTION_SCID_ALIAS]); + + assert!(required.supports_features(&optional)); + assert!(optional.supports_features(&required)); + } + #[test] fn equality_with_same_and_different_bits() { let lease = Features::from_bits(&[Features::OPTION_SCRIPT_ENFORCED_LEASE]); From b56ea18b5c215a51741bac95784cf3cb64fd86c8 Mon Sep 17 00:00:00 2001 From: Nishant Bansal Date: Tue, 11 Aug 2026 01:22:41 +0530 Subject: [PATCH 4/4] 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 | 5 +- smite/src/bolt/features.rs | 4 + smite/src/oracles/accept_channel.rs | 285 ++++++++++++++++-- 4 files changed, 275 insertions(+), 20 deletions(-) diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index eeb6afac..4488d045 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -498,6 +498,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 65509ab5..490ab35c 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -139,7 +139,10 @@ 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, + ]), } } diff --git a/smite/src/bolt/features.rs b/smite/src/bolt/features.rs index 115ef4b2..5307229a 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). diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index 408154ef..54af06a1 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; @@ -25,6 +29,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 @@ -50,7 +56,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}"), @@ -58,7 +65,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}"), @@ -88,13 +99,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, )); } @@ -107,9 +125,17 @@ 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) { + let Some(script) = &open_channel.tlvs.upfront_shutdown_script else { + return Err("open_channel does not include upfront_shutdown_script".to_string()); + }; + if !script.is_empty() && !is_acceptable_shutdown_script(script, negotiated_features) { + return Err("upfront_shutdown_script is not valid".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 @@ -119,6 +145,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 !negotiated_features.supports_features(&channel_type) { + 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() @@ -180,7 +211,18 @@ 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) { + let Some(script) = &accept_channel.tlvs.upfront_shutdown_script else { + return Err("accept_channel does not include upfront_shutdown_script".to_string()); + }; + if !script.is_empty() && !is_standard_shutdown_script(script, negotiated_features) { + return Err("upfront_shutdown_script is not valid".to_string()); + } + } + // Check that the channel type was included. let Some(channel_type) = accept_channel .tlvs @@ -305,6 +347,15 @@ fn verify_initial_commitment( Ok(()) } +/// Returns the maximum funding amount allowed by the negotiated features. +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. fn max_accepted_htlcs_limit(channel_type: &Features) -> u16 { if channel_type.supports_feature(Features::ZERO_FEE_COMMITMENTS) { @@ -318,7 +369,9 @@ 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"); @@ -388,11 +441,29 @@ 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_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}"); } @@ -402,11 +473,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); @@ -424,6 +497,7 @@ mod tests { assert_pass( &accept_channel(), Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), ); } @@ -437,7 +511,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] @@ -448,7 +526,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] @@ -456,19 +553,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", ); } @@ -480,10 +608,40 @@ 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_without_a_channel_type() { let mut oc = open_channel(); @@ -492,10 +650,26 @@ 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(ChannelTypeVariant::StaticRemoteKey.encode()); + + let negotiated_features = Features::from_bits(&[Features::ZERO_FEE_COMMITMENTS]); + + 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 bits = [ @@ -506,10 +680,12 @@ mod tests { let mut oc = open_channel(); oc.tlvs.channel_type = Some(Features::from_bits(&bits).into_bytes()); + let negotiated_features = Features::from_bits(&bits); assert_fail( &accept_channel(), Some(&pending_negotiation(oc)), + &negotiated_features, "invalid open_channel: channel_type is not a known variant", ); } @@ -523,6 +699,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", ); } @@ -536,6 +713,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", ); } @@ -548,6 +726,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", ); } @@ -562,6 +741,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", ); } @@ -574,6 +754,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", ); } @@ -586,6 +767,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", ); } @@ -599,6 +781,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)", ); } @@ -611,10 +794,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(); @@ -623,6 +844,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", ); } @@ -635,6 +857,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", ); } @@ -644,7 +867,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] @@ -658,6 +885,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(oc)), + &sample_negotiated_features(), "invalid accept_channel: option_zeroconf requires minimum_depth to be 0", ); } @@ -671,6 +899,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", ); } @@ -684,6 +913,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", ); } @@ -696,6 +926,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", ); } @@ -713,6 +944,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", ); } @@ -725,6 +957,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", ); } @@ -737,6 +970,7 @@ mod tests { assert_fail( &ac, Some(&pending_negotiation(open_channel())), + &sample_negotiated_features(), "invalid accept_channel: neither side exceeds channel reserve", ); } @@ -754,7 +988,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(), + ); } // FIXME: Validation is skipped, but once we add support for the Taproot @@ -767,7 +1005,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] @@ -778,6 +1020,7 @@ mod tests { assert_fail( &accept_channel(), Some(&negotiation), + &sample_negotiated_features(), "temporary_channel_id reuse: previous negotiation has not reached funding_created", ); } @@ -788,6 +1031,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(), + ); } }