X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Fln%2Ffeatures.rs;h=8f3d5cb5a24f1b7bf5b3baf160e08eab6091f323;hb=a8bd4c097f7ae6620eee66c1cb49144e38568439;hp=ca6ea70b61da7312de22b1e28749a51cb3f28941;hpb=ed5f9f6a9b9f9fc84810cdc1c04ff0a1caa888ff;p=rust-lightning diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index ca6ea70b..8f3d5cb5 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -41,6 +41,12 @@ //! (see [BOLT-4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#basic-multi-part-payments) for more information). //! - `Wumbo` - requires/supports that a node create large channels. Called `option_support_large_channel` in the spec. //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#the-open_channel-message) for more information). +//! - `AnchorsZeroFeeHtlcTx` - requires/supports that commitment transactions include anchor outputs +//! and HTLC transactions are pre-signed with zero fee (see +//! [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md) for more +//! information). +//! - `RouteBlinding` - requires/supports that a node can relay payments over blinded paths +//! (see [BOLT-4](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#route-blinding) for more information). //! - `ShutdownAnySegwit` - requires/supports that future segwit versions are allowed in `shutdown` //! (see [BOLT-2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md) for more information). //! - `OnionMessages` - requires/supports forwarding onion messages @@ -60,10 +66,8 @@ //! for more info). //! - `Keysend` - send funds to a node without an invoice //! (see the [`Keysend` feature assignment proposal](https://github.com/lightning/bolts/issues/605#issuecomment-606679798) for more information). -//! - `AnchorsZeroFeeHtlcTx` - requires/supports that commitment transactions include anchor outputs -//! and HTLC transactions are pre-signed with zero fee (see -//! [BOLT-3](https://github.com/lightning/bolts/blob/master/03-transactions.md) for more -//! information). +//! - `Trampoline` - supports receiving and forwarding Trampoline payments +//! (see the [`Trampoline` feature proposal](https://github.com/lightning/bolts/pull/836) for more information). //! //! LDK knows about the following features, but does not support them: //! - `AnchorsNonzeroFeeHtlcTx` - the initial version of anchor outputs, which was later found to be @@ -74,19 +78,21 @@ //! [BOLT #9]: https://github.com/lightning/bolts/blob/master/09-features.md //! [messages]: crate::ln::msgs -use crate::{io, io_extras}; +#[allow(unused_imports)] use crate::prelude::*; + +use crate::{io, io_extras}; use core::{cmp, fmt}; use core::borrow::Borrow; use core::hash::{Hash, Hasher}; use core::marker::PhantomData; -use bitcoin::bech32; -use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5, WriteBase32}; +use bech32::{Base32Len, FromBase32, ToBase32, u5, WriteBase32}; use crate::ln::msgs::DecodeError; use crate::util::ser::{Readable, WithoutLength, Writeable, Writer}; mod sealed { + #[allow(unused_imports)] use crate::prelude::*; use crate::ln::features::Features; @@ -143,13 +149,15 @@ mod sealed { // Byte 2 BasicMPP | Wumbo | AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - ShutdownAnySegwit, + RouteBlinding | ShutdownAnySegwit | Taproot, // Byte 4 OnionMessages, // Byte 5 ChannelType | SCIDPrivacy, // Byte 6 ZeroConf, + // Byte 7 + Trampoline, ]); define_context!(NodeContext, [ // Byte 0 @@ -159,16 +167,18 @@ mod sealed { // Byte 2 BasicMPP | Wumbo | AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - ShutdownAnySegwit, + RouteBlinding | ShutdownAnySegwit | Taproot, // Byte 4 OnionMessages, // Byte 5 ChannelType | SCIDPrivacy, // Byte 6 ZeroConf | Keysend, + // Byte 7 + Trampoline, ]); define_context!(ChannelContext, []); - define_context!(InvoiceContext, [ + define_context!(Bolt11InvoiceContext, [ // Byte 0 , // Byte 1 @@ -183,6 +193,8 @@ mod sealed { , // Byte 6 PaymentMetadata, + // Byte 7 + Trampoline, ]); define_context!(OfferContext, []); define_context!(InvoiceRequestContext, []); @@ -205,7 +217,7 @@ mod sealed { // Byte 2 AnchorsNonzeroFeeHtlcTx | AnchorsZeroFeeHtlcTx, // Byte 3 - , + Taproot, // Byte 4 , // Byte 5 @@ -369,17 +381,17 @@ mod sealed { define_feature!(7, GossipQueries, [InitContext, NodeContext], "Feature flags for `gossip_queries`.", set_gossip_queries_optional, set_gossip_queries_required, supports_gossip_queries, requires_gossip_queries); - define_feature!(9, VariableLengthOnion, [InitContext, NodeContext, InvoiceContext], + define_feature!(9, VariableLengthOnion, [InitContext, NodeContext, Bolt11InvoiceContext], "Feature flags for `var_onion_optin`.", set_variable_length_onion_optional, set_variable_length_onion_required, supports_variable_length_onion, requires_variable_length_onion); define_feature!(13, StaticRemoteKey, [InitContext, NodeContext, ChannelTypeContext], "Feature flags for `option_static_remotekey`.", set_static_remote_key_optional, set_static_remote_key_required, supports_static_remote_key, requires_static_remote_key); - define_feature!(15, PaymentSecret, [InitContext, NodeContext, InvoiceContext], + define_feature!(15, PaymentSecret, [InitContext, NodeContext, Bolt11InvoiceContext], "Feature flags for `payment_secret`.", set_payment_secret_optional, set_payment_secret_required, supports_payment_secret, requires_payment_secret); - define_feature!(17, BasicMPP, [InitContext, NodeContext, InvoiceContext, Bolt12InvoiceContext], + define_feature!(17, BasicMPP, [InitContext, NodeContext, Bolt11InvoiceContext, Bolt12InvoiceContext], "Feature flags for `basic_mpp`.", set_basic_mpp_optional, set_basic_mpp_required, supports_basic_mpp, requires_basic_mpp); define_feature!(19, Wumbo, [InitContext, NodeContext], @@ -391,9 +403,15 @@ mod sealed { define_feature!(23, AnchorsZeroFeeHtlcTx, [InitContext, NodeContext, ChannelTypeContext], "Feature flags for `option_anchors_zero_fee_htlc_tx`.", set_anchors_zero_fee_htlc_tx_optional, set_anchors_zero_fee_htlc_tx_required, supports_anchors_zero_fee_htlc_tx, requires_anchors_zero_fee_htlc_tx); + define_feature!(25, RouteBlinding, [InitContext, NodeContext], + "Feature flags for `option_route_blinding`.", set_route_blinding_optional, + set_route_blinding_required, supports_route_blinding, requires_route_blinding); define_feature!(27, ShutdownAnySegwit, [InitContext, NodeContext], "Feature flags for `opt_shutdown_anysegwit`.", set_shutdown_any_segwit_optional, set_shutdown_any_segwit_required, supports_shutdown_anysegwit, requires_shutdown_anysegwit); + define_feature!(31, Taproot, [InitContext, NodeContext, ChannelTypeContext], + "Feature flags for `option_taproot`.", set_taproot_optional, + set_taproot_required, supports_taproot, requires_taproot); define_feature!(39, OnionMessages, [InitContext, NodeContext], "Feature flags for `option_onion_messages`.", set_onion_messages_optional, set_onion_messages_required, supports_onion_messages, requires_onion_messages); @@ -403,7 +421,7 @@ mod sealed { define_feature!(47, SCIDPrivacy, [InitContext, NodeContext, ChannelTypeContext], "Feature flags for only forwarding with SCID aliasing. Called `option_scid_alias` in the BOLTs", set_scid_privacy_optional, set_scid_privacy_required, supports_scid_privacy, requires_scid_privacy); - define_feature!(49, PaymentMetadata, [InvoiceContext], + define_feature!(49, PaymentMetadata, [Bolt11InvoiceContext], "Feature flags for payment metadata in invoices.", set_payment_metadata_optional, set_payment_metadata_required, supports_payment_metadata, requires_payment_metadata); define_feature!(51, ZeroConf, [InitContext, NodeContext, ChannelTypeContext], @@ -412,11 +430,14 @@ mod sealed { define_feature!(55, Keysend, [NodeContext], "Feature flags for keysend payments.", set_keysend_optional, set_keysend_required, supports_keysend, requires_keysend); + define_feature!(57, Trampoline, [InitContext, NodeContext, Bolt11InvoiceContext], + "Feature flags for Trampoline routing.", set_trampoline_routing_optional, set_trampoline_routing_required, + supports_trampoline_routing, requires_trampoline_routing); // Note: update the module-level docs when a new feature bit is added! #[cfg(test)] define_feature!(123456789, UnknownFeature, - [NodeContext, ChannelContext, InvoiceContext, OfferContext, InvoiceRequestContext, Bolt12InvoiceContext, BlindedHopContext], + [NodeContext, ChannelContext, Bolt11InvoiceContext, OfferContext, InvoiceRequestContext, Bolt12InvoiceContext, BlindedHopContext], "Feature flags for an unknown feature used in testing.", set_unknown_feature_optional, set_unknown_feature_required, supports_unknown_test_feature, requires_unknown_test_feature); } @@ -461,12 +482,24 @@ impl Clone for Features { } impl Hash for Features { fn hash(&self, hasher: &mut H) { - self.flags.hash(hasher); + let mut nonzero_flags = &self.flags[..]; + while nonzero_flags.last() == Some(&0) { + nonzero_flags = &nonzero_flags[..nonzero_flags.len() - 1]; + } + nonzero_flags.hash(hasher); } } impl PartialEq for Features { fn eq(&self, o: &Self) -> bool { - self.flags.eq(&o.flags) + let mut o_iter = o.flags.iter(); + let mut self_iter = self.flags.iter(); + loop { + match (o_iter.next(), self_iter.next()) { + (Some(o), Some(us)) => if o != us { return false }, + (Some(b), None) | (None, Some(b)) => if *b != 0 { return false }, + (None, None) => return true, + } + } } } impl PartialOrd for Features { @@ -492,7 +525,7 @@ pub type NodeFeatures = Features; /// Features used within a `channel_announcement` message. pub type ChannelFeatures = Features; /// Features used within an invoice. -pub type InvoiceFeatures = Features; +pub type Bolt11InvoiceFeatures = Features; /// Features used within an `offer`. pub type OfferFeatures = Features; /// Features used within an `invoice_request`. @@ -538,8 +571,8 @@ impl InitFeatures { } } -impl InvoiceFeatures { - /// Converts `InvoiceFeatures` to `Features`. Only known `InvoiceFeatures` relevant to +impl Bolt11InvoiceFeatures { + /// Converts `Bolt11InvoiceFeatures` to `Features`. Only known `Bolt11InvoiceFeatures` relevant to /// context `C` are included in the result. pub(crate) fn to_context(&self) -> Features { self.to_context_internal() @@ -549,15 +582,15 @@ impl InvoiceFeatures { /// features (since they were not announced in a node announcement). However, keysend payments /// don't have an invoice to pull the payee's features from, so this method is provided for use in /// [`PaymentParameters::for_keysend`], thus omitting the need for payers to manually construct an - /// `InvoiceFeatures` for [`find_route`]. + /// `Bolt11InvoiceFeatures` for [`find_route`]. /// /// MPP keysend is not widely supported yet, so we parameterize support to allow the user to /// choose whether their router should find multi-part routes. /// /// [`PaymentParameters::for_keysend`]: crate::routing::router::PaymentParameters::for_keysend /// [`find_route`]: crate::routing::router::find_route - pub(crate) fn for_keysend(allow_mpp: bool) -> InvoiceFeatures { - let mut res = InvoiceFeatures::empty(); + pub(crate) fn for_keysend(allow_mpp: bool) -> Bolt11InvoiceFeatures { + let mut res = Bolt11InvoiceFeatures::empty(); res.set_variable_length_onion_optional(); if allow_mpp { res.set_basic_mpp_optional(); @@ -567,8 +600,8 @@ impl InvoiceFeatures { } impl Bolt12InvoiceFeatures { - /// Converts `Bolt12InvoiceFeatures` to `Features`. Only known `Bolt12InvoiceFeatures` relevant - /// to context `C` are included in the result. + /// Converts [`Bolt12InvoiceFeatures`] to [`Features`]. Only known [`Bolt12InvoiceFeatures`] + /// relevant to context `C` are included in the result. pub(crate) fn to_context(&self) -> Features { self.to_context_internal() } @@ -604,7 +637,7 @@ impl ChannelTypeFeatures { } } -impl ToBase32 for InvoiceFeatures { +impl ToBase32 for Bolt11InvoiceFeatures { fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { // Explanation for the "4": the normal way to round up when dividing is to add the divisor // minus one before dividing @@ -634,16 +667,16 @@ impl ToBase32 for InvoiceFeatures { } } -impl Base32Len for InvoiceFeatures { +impl Base32Len for Bolt11InvoiceFeatures { fn base32_len(&self) -> usize { self.to_base32().len() } } -impl FromBase32 for InvoiceFeatures { +impl FromBase32 for Bolt11InvoiceFeatures { type Err = bech32::Error; - fn from_base32(field_data: &[u5]) -> Result { + fn from_base32(field_data: &[u5]) -> Result { // Explanation for the "7": the normal way to round up when dividing is to add the divisor // minus one before dividing let length_bytes = (field_data.len() * 5 + 7) / 8 as usize; @@ -662,7 +695,7 @@ impl FromBase32 for InvoiceFeatures { while !res_bytes.is_empty() && res_bytes[res_bytes.len() - 1] == 0 { res_bytes.pop(); } - Ok(InvoiceFeatures::from_le_bytes(res_bytes)) + Ok(Bolt11InvoiceFeatures::from_le_bytes(res_bytes)) } } @@ -715,7 +748,11 @@ impl Features { Ok(()) } - fn from_be_bytes(mut flags: Vec) -> Features { + /// Create a [`Features`] given a set of flags, in big-endian. This is in byte order from + /// most on-the-wire encodings. + /// + /// This is not exported to bindings users as we don't support export across multiple T + pub fn from_be_bytes(mut flags: Vec) -> Features { flags.reverse(); // Swap to little-endian Self { flags, @@ -728,7 +765,7 @@ impl Features { } /// Returns true if this `Features` object contains required features unknown by `other`. - pub fn requires_unknown_bits_from(&self, other: &Features) -> bool { + pub fn requires_unknown_bits_from(&self, other: &Self) -> bool { // Bitwise AND-ing with all even bits set except for known features will select required // unknown features. self.flags.iter().enumerate().any(|(i, &byte)| { @@ -795,6 +832,35 @@ impl Features { true } + /// Sets a required feature bit. Errors if `bit` is outside the feature range as defined + /// by [BOLT 9]. + /// + /// Note: Required bits are even. If an odd bit is given, then the corresponding even bit will + /// be set instead (i.e., `bit - 1`). + /// + /// [BOLT 9]: https://github.com/lightning/bolts/blob/master/09-features.md + pub fn set_required_feature_bit(&mut self, bit: usize) -> Result<(), ()> { + self.set_feature_bit(bit - (bit % 2)) + } + + /// Sets an optional feature bit. Errors if `bit` is outside the feature range as defined + /// by [BOLT 9]. + /// + /// Note: Optional bits are odd. If an even bit is given, then the corresponding odd bit will be + /// set instead (i.e., `bit + 1`). + /// + /// [BOLT 9]: https://github.com/lightning/bolts/blob/master/09-features.md + pub fn set_optional_feature_bit(&mut self, bit: usize) -> Result<(), ()> { + self.set_feature_bit(bit + (1 - (bit % 2))) + } + + fn set_feature_bit(&mut self, bit: usize) -> Result<(), ()> { + if bit > 255 { + return Err(()); + } + self.set_bit(bit, false) + } + /// Sets a required custom feature bit. Errors if `bit` is outside the custom range as defined /// by [bLIP 2] or if it is a known `T` feature. /// @@ -821,10 +887,13 @@ impl Features { if bit < 256 { return Err(()); } + self.set_bit(bit, true) + } + fn set_bit(&mut self, bit: usize, custom: bool) -> Result<(), ()> { let byte_offset = bit / 8; let mask = 1 << (bit - 8 * byte_offset); - if byte_offset < T::KNOWN_FEATURE_MASK.len() { + if byte_offset < T::KNOWN_FEATURE_MASK.len() && custom { if (T::KNOWN_FEATURE_MASK[byte_offset] & mask) != 0 { return Err(()); } @@ -876,6 +945,13 @@ impl Features { } } +impl Features { + #[cfg(test)] + pub(crate) fn clear_route_blinding(&mut self) { + ::clear_bits(&mut self.flags); + } +} + #[cfg(test)] impl Features { pub(crate) fn unknown() -> Self { @@ -903,7 +979,7 @@ macro_rules! impl_feature_len_prefixed_write { impl_feature_len_prefixed_write!(InitFeatures); impl_feature_len_prefixed_write!(ChannelFeatures); impl_feature_len_prefixed_write!(NodeFeatures); -impl_feature_len_prefixed_write!(InvoiceFeatures); +impl_feature_len_prefixed_write!(Bolt11InvoiceFeatures); impl_feature_len_prefixed_write!(Bolt12InvoiceFeatures); impl_feature_len_prefixed_write!(BlindedHopFeatures); @@ -943,8 +1019,8 @@ impl Readable for WithoutLength> { #[cfg(test)] mod tests { - use super::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, InvoiceFeatures, NodeFeatures, OfferFeatures, sealed}; - use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5}; + use super::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, Bolt11InvoiceFeatures, NodeFeatures, OfferFeatures, sealed}; + use bech32::{Base32Len, FromBase32, ToBase32, u5}; use crate::util::ser::{Readable, WithoutLength, Writeable}; #[test] @@ -1014,6 +1090,7 @@ mod tests { init_features.set_basic_mpp_optional(); init_features.set_wumbo_optional(); init_features.set_anchors_zero_fee_htlc_tx_optional(); + init_features.set_route_blinding_optional(); init_features.set_shutdown_any_segwit_optional(); init_features.set_onion_messages_optional(); init_features.set_channel_type_optional(); @@ -1029,8 +1106,8 @@ mod tests { // Check that the flags are as expected: // - option_data_loss_protect (req) // - var_onion_optin (req) | static_remote_key (req) | payment_secret(req) - // - basic_mpp | wumbo | anchors_zero_fee_htlc_tx - // - opt_shutdown_anysegwit + // - basic_mpp | wumbo | option_anchors_zero_fee_htlc_tx + // - option_route_blinding | opt_shutdown_anysegwit // - onion_messages // - option_channel_type | option_scid_alias // - option_zeroconf @@ -1038,7 +1115,7 @@ mod tests { assert_eq!(node_features.flags[0], 0b00000001); assert_eq!(node_features.flags[1], 0b01010001); assert_eq!(node_features.flags[2], 0b10001010); - assert_eq!(node_features.flags[3], 0b00001000); + assert_eq!(node_features.flags[3], 0b00001010); assert_eq!(node_features.flags[4], 0b10000000); assert_eq!(node_features.flags[5], 0b10100000); assert_eq!(node_features.flags[6], 0b00001000); @@ -1058,28 +1135,35 @@ mod tests { fn convert_to_context_with_unknown_flags() { // Ensure the `from` context has fewer known feature bytes than the `to` context. assert!(::KNOWN_FEATURE_MASK.len() < - ::KNOWN_FEATURE_MASK.len()); + ::KNOWN_FEATURE_MASK.len()); let mut channel_features = ChannelFeatures::empty(); channel_features.set_unknown_feature_optional(); assert!(channel_features.supports_unknown_bits()); - let invoice_features: InvoiceFeatures = channel_features.to_context_internal(); + let invoice_features: Bolt11InvoiceFeatures = channel_features.to_context_internal(); assert!(!invoice_features.supports_unknown_bits()); } #[test] fn set_feature_bits() { - let mut features = InvoiceFeatures::empty(); + let mut features = Bolt11InvoiceFeatures::empty(); features.set_basic_mpp_optional(); features.set_payment_secret_required(); assert!(features.supports_basic_mpp()); assert!(!features.requires_basic_mpp()); assert!(features.requires_payment_secret()); assert!(features.supports_payment_secret()); + + // Set flags manually + let mut features = NodeFeatures::empty(); + assert!(features.set_optional_feature_bit(55).is_ok()); + assert!(features.supports_keysend()); + assert!(features.set_optional_feature_bit(255).is_ok()); + assert!(features.set_required_feature_bit(256).is_err()); } #[test] fn set_custom_bits() { - let mut features = InvoiceFeatures::empty(); + let mut features = Bolt11InvoiceFeatures::empty(); features.set_variable_length_onion_optional(); assert_eq!(features.flags[1], 0b00000010); @@ -1089,19 +1173,19 @@ mod tests { assert_eq!(features.flags[31], 0b00000000); assert_eq!(features.flags[32], 0b00000101); - let known_bit = ::EVEN_BIT; - let byte_offset = ::BYTE_OFFSET; + let known_bit = ::EVEN_BIT; + let byte_offset = ::BYTE_OFFSET; assert_eq!(byte_offset, 1); assert_eq!(features.flags[byte_offset], 0b00000010); assert!(features.set_required_custom_bit(known_bit).is_err()); assert_eq!(features.flags[byte_offset], 0b00000010); - let mut features = InvoiceFeatures::empty(); + let mut features = Bolt11InvoiceFeatures::empty(); assert!(features.set_optional_custom_bit(256).is_ok()); assert!(features.set_optional_custom_bit(259).is_ok()); assert_eq!(features.flags[32], 0b00001010); - let mut features = InvoiceFeatures::empty(); + let mut features = Bolt11InvoiceFeatures::empty(); assert!(features.set_required_custom_bit(257).is_ok()); assert!(features.set_required_custom_bit(258).is_ok()); assert_eq!(features.flags[32], 0b00000101); @@ -1138,7 +1222,7 @@ mod tests { u5::try_from_u8(16).unwrap(), u5::try_from_u8(1).unwrap(), ]; - let features = InvoiceFeatures::from_le_bytes(vec![1, 2, 3, 4, 5, 42, 100, 101]); + let features = Bolt11InvoiceFeatures::from_le_bytes(vec![1, 2, 3, 4, 5, 42, 100, 101]); // Test length calculation. assert_eq!(features.base32_len(), 13); @@ -1148,13 +1232,13 @@ mod tests { assert_eq!(features_as_u5s, features_serialized); // Test deserialization. - let features_deserialized = InvoiceFeatures::from_base32(&features_as_u5s).unwrap(); + let features_deserialized = Bolt11InvoiceFeatures::from_base32(&features_as_u5s).unwrap(); assert_eq!(features, features_deserialized); } #[test] fn test_channel_type_mapping() { - // If we map an InvoiceFeatures with StaticRemoteKey optional, it should map into a + // If we map an Bolt11InvoiceFeatures with StaticRemoteKey optional, it should map into a // required-StaticRemoteKey ChannelTypeFeatures. let mut init_features = InitFeatures::empty(); init_features.set_static_remote_key_optional(); @@ -1163,4 +1247,26 @@ mod tests { assert!(!converted_features.supports_any_optional_bits()); assert!(converted_features.requires_static_remote_key()); } + + #[test] + #[cfg(feature = "std")] + fn test_excess_zero_bytes_ignored() { + // Checks that `Hash` and `PartialEq` ignore excess zero bytes, which may appear due to + // feature conversion or because a peer serialized their feature poorly. + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut zerod_features = InitFeatures::empty(); + zerod_features.flags = vec![0]; + let empty_features = InitFeatures::empty(); + assert!(empty_features.flags.is_empty()); + + assert_eq!(zerod_features, empty_features); + + let mut zerod_hash = DefaultHasher::new(); + zerod_features.hash(&mut zerod_hash); + let mut empty_hash = DefaultHasher::new(); + empty_features.hash(&mut empty_hash); + assert_eq!(zerod_hash.finish(), empty_hash.finish()); + } }