What changed, and why it matters
This commit adds support in the rust-bitcoin P2P library for a new Bitcoin network message type called `feature`, defined by BIP 434. It also bumps the default protocol version from 70016 to 70017. This is a feature addition rather than a bug fix; there is no direct evidence in the commit that it fixes a security vulnerability. The main security consideration is that any new network message decoder could introduce parsing bugs, but the diff itself only wires up an existing `Feature` type and does not show the decoder implementation.
Review the implementation of `crate::bip434::{Feature, FeatureDecoder, FeatureData}` separately to confirm the decoder handles malformed input safely, enforces length limits, and cannot panic or allocate unbounded memory. Run the newly added fuzz targets for these types.
Security signals we found
New network message decoder added to P2P message dispatch
Protocol version bumped to 70017
Fuzz coverage expanded for new BIP434 types
No security-relevant description or CVE reference in commit message
Evidence from the diff
The change extends NetworkMessage with a Feature(Feature) variant, adds the feature command string, assigns it short ID 37 in v2 transport framing, and integrates it into the streaming encoder/decoder dispatch. It defines FEATURE_VERSION = 70017 and documents that this version supports feature messages between version and verack. Fuzzing and encodable-coverage scripts are updated to include Feature, FeatureId, and FeatureData. The actual BIP434 types (Feature, FeatureDecoder, FeatureData) are imported but not shown in the diff, so the correctness and robustness of their parsing cannot be reviewed here.
Changed components
p2p/src/message.rsp2p/src/lib.rsfuzz/generate-encoding-roundtrip.shcontrib/check-encodable-coverage.shInspect captured patch +29 / −1
diff --git a/contrib/check-encodable-coverage.sh b/contrib/check-encodable-coverage.sh
index 8375bf53..ebac25d8 100755
--- a/contrib/check-encodable-coverage.sh
+++ b/contrib/check-encodable-coverage.sh
@@ -19,7 +19,8 @@ TRAIT_IMPL_JS="$REPO_DIR/target/doc/trait.impl/bitcoin_consensus_encoding/encode
# - InventoryPayload has no type in 0.32 bitcoin. Vec<Inventory> fails special case consideration.
# - FeeFilter is a FeeRate newtype. FeeRate has no old Encodable/Decodable and is just a u64 le encoding in FeeFilter.
# - SendTxRcnCl is a new type that does not have a comparison.
-EXCLUSIONS="CommandString HeadersMessage InventoryPayload FeeFilter NetworkMessage Script Validation V2NetworkMessage V1MessageHeader SendTxRcnCl"
+# - Feature is a new type that does not have a comparison.
+EXCLUSIONS="CommandString HeadersMessage InventoryPayload FeeFilter NetworkMessage Script Validation V2NetworkMessage V1MessageHeader SendTxRcnCl Feature FeatureId FeatureData"
main() {
check_required_commands
diff --git a/fuzz/generate-encoding-roundtrip.sh b/fuzz/generate-encoding-roundtrip.sh
index 8251aadb..ff929d37 100755
--- a/fuzz/generate-encoding-roundtrip.sh
+++ b/fuzz/generate-encoding-roundtrip.sh
@@ -80,6 +80,9 @@ ROUNDTRIP_TYPES=(
"p2p::message_network::UserAgent"
"p2p::message_network::VersionMessage"
"p2p::message_erlay::SendTxRcnCl"
+ "p2p::bip434::Feature"
+ "p2p::bip434::FeatureId"
+ "p2p::bip434::FeatureData"
)
# Types tested with check_script_roundtrip (Buf types that Deref to their Encodable target).
diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs
index 6744952b..9ffd13ad 100644
--- a/p2p/src/lib.rs
+++ b/p2p/src/lib.rs
@@ -77,6 +77,7 @@ pub use self::error::{
/// may lead to you incorrectly banning other peers or other peers banning you.
///
/// These are the features required for each version:
+/// 70017 - Support `feature` messages between `version` and `verack` message
/// 70016 - Support receiving `wtxidrelay` message between `version` and `verack` message
/// 70015 - Support receiving invalid compact blocks from a peer without banning them
/// 70014 - Support compact block messages `sendcmpct`, `cmpctblock`, `getblocktxn` and `blocktxn`
@@ -91,6 +92,8 @@ pub use self::error::{
pub struct ProtocolVersion(u32);
impl ProtocolVersion {
+ /// Support `feature` messages between `version` and `verack` message
+ pub const FEATURE_VERSION: Self = Self(70017);
/// Support receiving `wtxidrelay` message between `version` and `verack` message
pub const WTXID_RELAY_VERSION: Self = Self(70016);
/// Support receiving invalid compact blocks from a peer without banning them
diff --git a/p2p/src/message.rs b/p2p/src/message.rs
index a1d9e967..e0666c95 100644
--- a/p2p/src/message.rs
+++ b/p2p/src/message.rs
@@ -23,6 +23,7 @@ use units::{Amount, FeeRate};
use self::error::V1NetworkMessageDecoderErrorInner;
use crate::address::{AddrV1Message, AddrV2Message, AddrV2MessageDecoderError};
+use crate::bip434::{Feature, FeatureDecoder};
use crate::merkle_tree::MerkleBlock;
use crate::message_erlay::{SendTxRcnCl, SendTxRcnClDecoder};
use crate::{
@@ -662,6 +663,8 @@ pub enum NetworkMessage {
SendAddrV2,
/// `sendtxrcncl`
SendTxRcnCl(SendTxRcnCl),
+ /// `feature`
+ Feature(Feature),
/// Any other message.
Unknown {
/// The command of this message.
@@ -716,6 +719,7 @@ impl NetworkMessage {
Self::AddrV2(_) => "addrv2",
Self::SendAddrV2 => "sendaddrv2",
Self::SendTxRcnCl(_) => "sendtxrcncl",
+ Self::Feature(_) => "feature",
Self::Unknown { .. } => "unknown",
}
}
@@ -863,6 +867,8 @@ pub enum NetworkMessageEncoder<'e> {
AddrV2(<AddrV2Payload as encoding::Encode>::Encoder<'e>),
/// Encodes [`NetworkMessage::SendTxRcnCl`].
SendTxRcnCl(<SendTxRcnCl as encoding::Encode>::Encoder<'e>),
+ /// Encodes [`NetworkMessage::Feature`].
+ Feature(<Feature as encoding::Encode>::Encoder<'e>),
/// Encodes zero-payload messages: verack, mempool, sendheaders, getaddr, wtxidrelay,
/// filterclear, sendaddrv2.
Empty,
@@ -904,6 +910,7 @@ impl<'e> NetworkMessageEncoder<'e> {
NetworkMessage::FeeFilter(dat) => Self::FeeFilter(dat.encoder()),
NetworkMessage::AddrV2(dat) => Self::AddrV2(dat.encoder()),
NetworkMessage::SendTxRcnCl(dat) => Self::SendTxRcnCl(dat.encoder()),
+ NetworkMessage::Feature(dat) => Self::Feature(dat.encoder()),
NetworkMessage::Verack
| NetworkMessage::SendHeaders
| NetworkMessage::MemPool
@@ -949,6 +956,7 @@ impl encoding::Encoder for NetworkMessageEncoder<'_> {
Self::FeeFilter(e) => e.current_chunk(),
Self::AddrV2(e) => e.current_chunk(),
Self::SendTxRcnCl(e) => e.current_chunk(),
+ Self::Feature(e) => e.current_chunk(),
Self::Empty => &[],
Self::Unknown(e) => e.current_chunk(),
}
@@ -984,6 +992,7 @@ impl encoding::Encoder for NetworkMessageEncoder<'_> {
Self::FeeFilter(e) => e.advance(),
Self::AddrV2(e) => e.advance(),
Self::SendTxRcnCl(e) => e.advance(),
+ Self::Feature(e) => e.advance(),
Self::Empty => EncoderStatus::Finished,
Self::Unknown(e) => e.advance(),
}
@@ -1044,6 +1053,7 @@ enum NetworkMessageDecoderInner {
FeeFilter(FeeFilterDecoder),
AddrV2(AddrV2PayloadDecoder),
SendTxRcnCl(SendTxRcnClDecoder),
+ Feature(FeatureDecoder),
/// Zero-payload messages: verack, mempool, sendheaders, getaddr, wtxidrelay,
/// filterclear, sendaddrv2.
Empty(CommandString),
@@ -1091,6 +1101,7 @@ impl NetworkMessageDecoderInner {
"feefilter" => Self::FeeFilter(FeeFilter::decoder()),
"sendtxrcncl" => Self::SendTxRcnCl(SendTxRcnCl::decoder()),
"addrv2" => Self::AddrV2(AddrV2Payload::decoder()),
+ "feature" => Self::Feature(Feature::decoder()),
_ => Self::Unknown {
command,
remaining: payload_len,
@@ -1137,6 +1148,7 @@ impl encoding::Decoder for NetworkMessageDecoderInner {
Self::FeeFilter(d) => d.push_bytes(bytes).map_err(|_| err),
Self::AddrV2(d) => d.push_bytes(bytes).map_err(|_| err),
Self::SendTxRcnCl(d) => d.push_bytes(bytes).map_err(|_| err),
+ Self::Feature(d) => d.push_bytes(bytes).map_err(|_| err),
Self::Empty(_) => Ok(encoding::DecoderStatus::Ready),
Self::Unknown { remaining, buffer, .. } => {
let copy_len = bytes.len().min(*remaining);
@@ -1187,6 +1199,7 @@ impl encoding::Decoder for NetworkMessageDecoderInner {
Self::FeeFilter(d) => Ok(NetworkMessage::FeeFilter(d.end().map_err(|_| err)?)),
Self::AddrV2(d) => Ok(NetworkMessage::AddrV2(d.end().map_err(|_| err)?)),
Self::SendTxRcnCl(d) => Ok(NetworkMessage::SendTxRcnCl(d.end().map_err(|_| err)?)),
+ Self::Feature(d) => Ok(NetworkMessage::Feature(d.end().map_err(|_| err)?)),
Self::Empty(cmd) => match cmd.as_ref() {
"verack" => Ok(NetworkMessage::Verack),
"mempool" => Ok(NetworkMessage::MemPool),
@@ -1237,6 +1250,7 @@ impl encoding::Decoder for NetworkMessageDecoderInner {
Self::FeeFilter(d) => d.read_limit(),
Self::AddrV2(d) => d.read_limit(),
Self::SendTxRcnCl(d) => d.read_limit(),
+ Self::Feature(d) => d.read_limit(),
Self::Empty(_) => 0,
Self::Unknown { remaining, .. } => *remaining,
}
@@ -1508,6 +1522,7 @@ fn v2_command_byte(payload: &NetworkMessage) -> (u8, Option<CommandString>) {
NetworkMessage::GetCFCheckpt(_) => (26u8, None),
NetworkMessage::CFCheckpt(_) => (27u8, None),
NetworkMessage::AddrV2(_) => (28u8, None),
+ NetworkMessage::Feature(_) => (37u8, None),
NetworkMessage::Version(_)
| NetworkMessage::Verack
| NetworkMessage::SendHeaders
@@ -1695,6 +1710,7 @@ impl V2NetworkMessageDecoder {
26u8 => Ok(E::GetCFCheckpt(message_filter::GetCFCheckpt::decoder())),
27u8 => Ok(E::CFCheckpt(message_filter::CFCheckpt::decoder())),
28u8 => Ok(E::AddrV2(AddrV2Payload::decoder())),
+ 37u8 => Ok(E::Feature(Feature::decoder())),
id => Err(V2NetworkMessageDecoderError::UnknownShortId(id)),
})
.map(NetworkMessageDecoder::from_inner)
@@ -2329,6 +2345,7 @@ mod test {
use super::*;
use crate::address::{AddrV2, Address};
use crate::bip152::BlockTransactionsRequest;
+ use crate::bip434::FeatureData;
use crate::message_blockdata::{BlockLocator, GetBlocksMessage, GetHeadersMessage, Inventory};
use crate::message_bloom::{BloomFlags, FilterAdd, FilterLoad};
use crate::message_compact_blocks::SendCmpct;
@@ -2482,6 +2499,10 @@ mod test {
NetworkMessage::BlockTxn(blocktxn),
NetworkMessage::SendCmpct(SendCmpct { send_compact: true, version: 8333 }),
NetworkMessage::SendTxRcnCl(SendTxRcnCl::from_salt(224)),
+ NetworkMessage::Feature(Feature {
+ feature_id: "featureid".parse().unwrap(),
+ feature_data: FeatureData::new(Vec::new()).unwrap(),
+ }),
];
for msg in &msgs {
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.