What changed, and why it matters
This commit adds support for a new Bitcoin peer-to-peer message type called FEATURE, defined in BIP 434. It is infrastructure for negotiating optional protocol features during the initial handshake, not a finished feature itself. The code bumps the protocol version to 70017, registers the new message, and adds basic rules: FEATURE messages must appear between VERSION and VERACK, the peer must have negotiated the new version, and the payload must contain a feature ID of at least 4 characters plus up to 512 bytes of feature data. Unknown feature IDs are ignored. There is no immediate security issue visible in the diff, but the change touches network message handling and version negotiation, which are sensitive parts of the node.
No immediate action required. Treat as a routine protocol-infrastructure change. Reviewers should verify that the new FEATURE handler cannot be used to partition the network or fingerprint nodes, and that the length limits and version checks are sufficient once concrete features begin using this infrastructure. When follow-up features are implemented, each should be reviewed for its own security implications.
Security signals we found
New network message type introduced (FEATURE)
Protocol version bumped to 70017
Peer disconnection logic added for malformed or out-of-sequence FEATURE messages
Input bounds enforced via LIMITED_STRING and LIMITED_VECTOR
BIP324 v2 short-id table expanded
No concrete feature negotiation implemented yet (infrastructure only)
Evidence from the diff
The patch implements BIP 434 ‘Peer Feature Negotiation’ scaffolding. Key changes: (1) PROTOCOL_VERSION and FEATURE_VERSION set to 70017 in src/node/protocol_version.h; (2) NetMsgType::FEATURE added in src/protocol.h with MAX_FEATUREID_LENGTH=80 and MAX_FEATUREDATA_LENGTH=512; (3) BIP324 v2 short-id table extended to include FEATURE at short-id 37 (BIP324_SHORTIDS_IMPLEMENTED raised to 38); (4) net_processing.cpp adds MakeAndPushFeature helper and a handler that disconnects peers sending FEATURE after VERACK or with a common version below 70017, deserializes a limited string feature_id and limited vector feature_data, disconnects on invalid payload, and ignores unknown feature IDs; (5) test framework classes for FEATURE added. The actual feature announcement block is commented out, and no concrete NetMsgFeature IDs are defined, confirming this is infrastructure only.
Changed components
src/net.cpp (BIP324 v2 short-id table)src/net_processing.cpp (FEATURE message handler and sending helper)src/node/protocol_version.h (protocol version constants)src/protocol.h (message type and length limits)src/bip324.h (short-id count constant)src/test/net_tests.cpp (v2 transport test update)test/functional/test_framework/messages.pytest/functional/test_framework/p2p.pytest/functional/test_framework/v2_p2p.pyInspect captured patch +105 / −8
diff --git a/doc/bips.md b/doc/bips.md
index ebf6b8fc..6f9b322e 100644
--- a/doc/bips.md
+++ b/doc/bips.md
@@ -79,3 +79,4 @@ BIPs that are implemented by Bitcoin Core:
* [`BIP 390`](https://github.com/bitcoin/bips/blob/master/bip-0390.mediawiki): MuSig2 Descriptor parsing is implemented in **v30.0** ([PR 31244](https://github.com/bitcoin/bitcoin/pull/31244)) and signing in **v31.0** ([PR 29675](https://github.com/bitcoin/bitcoin/pull/29675))
* [`BIP 431`](https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki): transactions with nVersion=3 are standard and treated as Topologically Restricted Until Confirmation as of **v28.0** ([PR 29496](https://github.com/bitcoin/bitcoin/pull/29496)).
* [`BIP 433`](https://github.com/bitcoin/bips/blob/master/bip-0433.mediawiki): Spending of Pay to Anchor (P2A) outputs is standard as of **v28.0** ([PR 30352](https://github.com/bitcoin/bitcoin/pull/30352)).
+* [`BIP 434`](https://github.com/bitcoin/bips/blob/master/bip-0434.md): Peer Feature Negotiation as of **v32.0** ([PR 35221](https://github.com/bitcoin/bitcoin/pull/35221)).
diff --git a/src/bip324.h b/src/bip324.h
index 396a28a4..821cc3f7 100644
--- a/src/bip324.h
+++ b/src/bip324.h
@@ -15,6 +15,8 @@
#include <pubkey.h>
#include <span.h>
+static constexpr unsigned BIP324_SHORTIDS_IMPLEMENTED{38};
+
/** The BIP324 packet cipher, encapsulating its key derivation, stream cipher, and AEAD. */
class BIP324Cipher
{
diff --git a/src/net.cpp b/src/net.cpp
index 6fb54118..df5848a6 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -921,7 +921,7 @@ namespace {
* Only message types that are actually implemented in this codebase need to be listed, as other
* messages get ignored anyway - whether we know how to decode them or not.
*/
-const std::array<std::string, 33> V2_MESSAGE_IDS = {
+const std::array<std::string, BIP324_SHORTIDS_IMPLEMENTED> V2_MESSAGE_IDS = {
"", // 12 bytes follow encoding the message type like in V1
NetMsgType::ADDR,
NetMsgType::BLOCK,
@@ -951,11 +951,10 @@ const std::array<std::string, 33> V2_MESSAGE_IDS = {
NetMsgType::GETCFCHECKPT,
NetMsgType::CFCHECKPT,
NetMsgType::ADDRV2,
- // Unimplemented message types that are assigned in BIP324:
- "",
- "",
- "",
- ""
+ "", "", "", // Unimplemented message types 29-31
+ "", "", "", "", // Unimplemented message types 32-35
+ "", // Unimplemented message type 36
+ NetMsgType::FEATURE,
};
class V2MessageMap
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index def72704..2e82d7f8 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -722,6 +722,15 @@ private:
{
m_connman.PushMessage(&node, NetMsg::Make(std::move(msg_type), std::forward<Args>(args)...));
}
+ template <typename... Args>
+ void MakeAndPushFeature(CNode& node, std::string_view feature_id, Args&&... args) const
+ {
+ if (!Assume(feature_id.size() >= 4 && feature_id.size() <= MAX_FEATUREID_LENGTH)) return;
+ std::vector<unsigned char> feature_data;
+ VectorWriter{feature_data, 0, std::forward<Args>(args)...};
+ if (!Assume(feature_data.size() <= MAX_FEATUREDATA_LENGTH)) return;
+ MakeAndPushMessage(node, NetMsgType::FEATURE, feature_id, std::move(feature_data));
+ }
/** Send a version message to a peer */
void PushNodeVersion(CNode& pnode, const Peer& peer);
@@ -3738,6 +3747,11 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string
}
}
+ if (greatest_common_version >= FEATURE_VERSION) {
+ // announce supported features
+ // MakeAndPushFeature(pfrom, NetMsgFeature::FOO, uint32_t{1});
+ }
+
MakeAndPushMessage(pfrom, NetMsgType::VERACK);
// Potentially mark this peer as a preferred download peer.
@@ -3953,6 +3967,45 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string
return;
}
+ if (msg_type == NetMsgType::FEATURE) {
+ if (pfrom.fSuccessfullyConnected) {
+ // Disconnect peers that send a FEATURE message after VERACK.
+ LogDebug(BCLog::NET, "feature received after verack, %s", pfrom.DisconnectMsg());
+ pfrom.fDisconnect = true;
+ return;
+ } else if (pfrom.GetCommonVersion() < FEATURE_VERSION) {
+ // Disconnect peers that send a FEATURE message without valid version negotiation.
+ LogDebug(BCLog::NET, "feature received with incompatible version %d, %s", pfrom.GetCommonVersion(), pfrom.DisconnectMsg());
+ pfrom.fDisconnect = true;
+ return;
+ }
+
+ std::string feature_id;
+ DataStream feature_data;
+ try {
+ vRecv >> LIMITED_STRING(feature_id, MAX_FEATUREID_LENGTH);
+ std::vector<unsigned char> feature_data_vec;
+ vRecv >> LIMITED_VECTOR(feature_data_vec, MAX_FEATUREDATA_LENGTH);
+ feature_data = DataStream(feature_data_vec);
+ } catch (const std::exception&) {
+ feature_id.clear(); // use empty feature_id as error indicator
+ }
+ if (feature_id.size() < 4 || !vRecv.empty()) {
+ LogDebug(BCLog::NET, "invalid feature payload, %s", pfrom.DisconnectMsg());
+ pfrom.fDisconnect = true;
+ return;
+ }
+
+ // if (feature_id == NetMsgFeature::FOO) {
+ // ...
+ // return;
+ // }
+
+ // ignore unknown feature_id
+ LogDebug(BCLog::NET, "unknown feature advertised: %s", SanitizeString(feature_id));
+ return;
+ }
+
// Received from a peer demonstrating readiness to announce transactions via reconciliations.
// This feature negotiation must happen between VERSION and VERACK to avoid relay problems
// from switching announcement protocols after the connection is up.
diff --git a/src/node/protocol_version.h b/src/node/protocol_version.h
index 7904086f..a72ac777 100644
--- a/src/node/protocol_version.h
+++ b/src/node/protocol_version.h
@@ -9,7 +9,7 @@
* network protocol versioning
*/
-static const int PROTOCOL_VERSION = 70016;
+static const int PROTOCOL_VERSION = 70017;
//! initial proto version, to be increased after version/verack negotiation
static const int INIT_PROTO_VERSION = 209;
@@ -35,4 +35,7 @@ static const int INVALID_CB_NO_BAN_VERSION = 70015;
//! "wtxidrelay" message type for wtxid-based relay starts with this version
static const int WTXID_RELAY_VERSION = 70016;
+//! "feature" message type for feature negotiation starts with this version
+static const int FEATURE_VERSION = 70017;
+
#endif // BITCOIN_NODE_PROTOCOL_VERSION_H
diff --git a/src/protocol.h b/src/protocol.h
index 8ed90dd3..24851e2f 100644
--- a/src/protocol.h
+++ b/src/protocol.h
@@ -264,6 +264,10 @@ inline constexpr const char* WTXIDRELAY{"wtxidrelay"};
* txreconciliation, as described by BIP 330.
*/
inline constexpr const char* SENDTXRCNCL{"sendtxrcncl"};
+/**
+ * BIP 434 Peer feature negotiation
+ */
+inline constexpr const char* FEATURE{"feature"};
}; // namespace NetMsgType
/** All known message types (see above). Keep this in the same order as the list of messages above. */
@@ -303,8 +307,16 @@ inline const std::array ALL_NET_MESSAGE_TYPES{std::to_array<std::string>({
NetMsgType::CFCHECKPT,
NetMsgType::WTXIDRELAY,
NetMsgType::SENDTXRCNCL,
+ NetMsgType::FEATURE,
})};
+static constexpr size_t MAX_FEATUREID_LENGTH{80};
+static constexpr size_t MAX_FEATUREDATA_LENGTH{512};
+
+namespace NetMsgFeature {
+//inline constexpr std::string_view FOO{"BIP-FOO"};
+}
+
/** nServices flags */
enum ServiceFlags : uint64_t {
// NOTE: When adding here, be sure to update serviceFlagToStr too
diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp
index 32801d97..77d7b31b 100644
--- a/src/test/net_tests.cpp
+++ b/src/test/net_tests.cpp
@@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <addrman.h>
+#include <bip324.h>
#include <chainparams.h>
#include <clientversion.h>
#include <common/args.h>
@@ -1533,7 +1534,7 @@ BOOST_AUTO_TEST_CASE(v2transport_test)
tester.CompareSessionIDs();
auto msg_data_1 = m_rng.randbytes<uint8_t>(4000000); // test that receiving 4M payload works
auto msg_data_2 = m_rng.randbytes<uint8_t>(4000000); // test that sending 4M payload works
- tester.SendMessage(uint8_t(m_rng.randrange(223) + 33), {}); // unknown short id
+ tester.SendMessage(uint8_t(m_rng.randrange(256 - BIP324_SHORTIDS_IMPLEMENTED) + BIP324_SHORTIDS_IMPLEMENTED), {}); // unknown short id
tester.SendMessage(uint8_t(2), msg_data_1); // "block" short id
tester.AddMessage("blocktxn", msg_data_2); // schedule blocktxn to be sent to us
ret = tester.Interact();
diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py
index a0f2a174..07abd5b6 100755
--- a/test/functional/test_framework/messages.py
+++ b/test/functional/test_framework/messages.py
@@ -1928,6 +1928,28 @@ class msg_sendtxrcncl:
return "msg_sendtxrcncl(version=%lu, salt=%lu)" %\
(self.version, self.salt)
+class msg_feature:
+ """FEATURE message for negotiating optional features."""
+ __slots__ = ("feature_id", "feature_data")
+ msgtype = b"feature"
+
+ def __init__(self, feature_id="", feature_data=b""):
+ self.feature_id = feature_id
+ self.feature_data = feature_data
+
+ def deserialize(self, f):
+ self.feature_id = deser_string(f).decode()
+ self.feature_data = deser_string(f)
+
+ def serialize(self):
+ r = ser_string(self.feature_id.encode())
+ r += ser_string(self.feature_data)
+ return r
+
+ def __repr__(self):
+ return f"msg_feature(feature_id={self.feature_id}, data={self.feature_data.hex()})"
+
+
class TestFrameworkScript(unittest.TestCase):
def test_addrv2_encode_decode(self):
def check_addrv2(ip, net):
diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py
index 4d812ce3..c6e3280d 100755
--- a/test/functional/test_framework/p2p.py
+++ b/test/functional/test_framework/p2p.py
@@ -43,6 +43,7 @@ from test_framework.messages import (
msg_cfheaders,
msg_cfilter,
msg_cmpctblock,
+ msg_feature,
msg_feefilter,
msg_filteradd,
msg_filterclear,
@@ -124,6 +125,7 @@ MESSAGEMAP = {
b"cfheaders": msg_cfheaders,
b"cfilter": msg_cfilter,
b"cmpctblock": msg_cmpctblock,
+ b"feature": msg_feature,
b"feefilter": msg_feefilter,
b"filteradd": msg_filteradd,
b"filterclear": msg_filterclear,
@@ -543,6 +545,7 @@ class P2PInterface(P2PConnection):
def on_cfheaders(self, message): pass
def on_cfilter(self, message): pass
def on_cmpctblock(self, message): pass
+ def on_feature(self, message): pass
def on_feefilter(self, message): pass
def on_filteradd(self, message): pass
def on_filterclear(self, message): pass
diff --git a/test/functional/test_framework/v2_p2p.py b/test/functional/test_framework/v2_p2p.py
index 087b885a..4057fd9a 100644
--- a/test/functional/test_framework/v2_p2p.py
+++ b/test/functional/test_framework/v2_p2p.py
@@ -50,6 +50,7 @@ SHORTID = {
26: b"getcfcheckpt",
27: b"cfcheckpt",
28: b"addrv2",
+ 37: b"feature",
}
# Dictionary which contains short message type ID for the P2P message
Why this scored 23/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.