p2p: calculate v1 checksum on original payload
What changed, and why it matters
This commit fixes a bug in how Bitcoin peer-to-peer network messages are validated. The software was checking message checksums against a re-encoded version of the message payload rather than the original bytes received over the network. Because the protocol allows some wiggle room in encoding, a re-encoded payload can have a different checksum than the original raw bytes. This meant a malformed message could pass checksum validation when it should have been rejected. The fix computes the checksum directly from the raw bytes as they arrive, removing that ambiguity.
Review whether any other protocol versions or message decoders in the crate compute checksums over re-encoded payloads and apply the same raw-byte hashing pattern. Consider adding regression tests from the differential fuzz corpus and verify that the new checksum engine correctly handles partial/chunked payload reads.
Security signals we found
checksum validation bypass due to canonicalization/re-encoding
differential fuzz test finding
peer-to-peer protocol message integrity issue
hash computed over re-encoded data instead of raw input
Evidence from the diff
In rust-bitcoin’s p2p message decoder, the v1 checksum was previously computed by calling sha2_checksum() on the decoded-and-re-encoded payload. The patch changes V1NetworkMessageDecoder to maintain a sha256d::HashEngine while reading payload bytes and feed it the original bytes as they are consumed. The final checksum is then derived from the running hash engine rather than from a re-encoded payload. This closes a differential fuzz finding where a malformed v1 message passed checksum validation because the re-encoded payload produced a different checksum than the raw wire bytes.
Changed components
p2p/src/message.rsV1NetworkMessageDecoderDecoderState::ReadingPayloadInspect captured patch +22 / −4
diff --git a/p2p/src/message.rs b/p2p/src/message.rs
index 53431339..7c7e57ed 100644
--- a/p2p/src/message.rs
+++ b/p2p/src/message.rs
@@ -1386,6 +1386,8 @@ enum DecoderState {
length: u32,
checksum: [u8; 4],
payload_decoder: NetworkMessageDecoder,
+ // Hash engine to compute checksum over raw payload bytes as they arrive.
+ checksum_engine: sha256d::HashEngine,
},
}
@@ -1446,6 +1448,7 @@ impl encoding::Decoder for V1NetworkMessageDecoder {
length: header.length,
checksum: header.checksum,
payload_decoder,
+ checksum_engine: sha256d::HashEngine::new(),
};
// Continue with any remaining bytes.
@@ -1454,8 +1457,13 @@ impl encoding::Decoder for V1NetworkMessageDecoder {
Ok(need_more)
}
- DecoderState::ReadingPayload { payload_decoder, .. } =>
- payload_decoder.push_bytes(bytes),
+ DecoderState::ReadingPayload { payload_decoder, checksum_engine, .. } => {
+ let original_bytes = *bytes;
+ let result = payload_decoder.push_bytes(bytes)?;
+ checksum_engine.input(&original_bytes[..original_bytes.len() - bytes.len()]);
+
+ Ok(result)
+ }
}
}
@@ -1466,9 +1474,19 @@ impl encoding::Decoder for V1NetworkMessageDecoder {
.map_err(V1NetworkMessageDecoderErrorInner::Header)
.map_err(V1NetworkMessageDecoderError)
.expect_err("push_bytes() moves to ReadingPayload on header_decoder completion")),
- DecoderState::ReadingPayload { magic, length, checksum, payload_decoder, .. } => {
+ DecoderState::ReadingPayload {
+ magic,
+ length,
+ checksum,
+ payload_decoder,
+ checksum_engine,
+ } => {
let payload = payload_decoder.end()?;
- let (_, expected_checksum) = sha2_checksum(&payload);
+
+ let hash_bytes = checksum_engine.finalize().to_byte_array();
+ let expected_checksum =
+ [hash_bytes[0], hash_bytes[1], hash_bytes[2], hash_bytes[3]];
+
if checksum != expected_checksum {
return Err(V1NetworkMessageDecoderError(
V1NetworkMessageDecoderErrorInner::InvalidChecksum {
Why this scored 62/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.