primitives: implement pull-based encoding for block::Header and its fields
What changed, and why it matters
This commit is a routine internal refactoring in the rust-bitcoin library. It adds a new 'pull-based' encoding system for block headers and related types, replacing or supplementing older serialization code. There is no indication in the commit that this fixes a security bug, prevents an attack, or changes behavior visible to users in a risky way.
No security action required. Treat as normal code-review/functional change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces Encodable implementations and encoder_newtype! wrappers for block::Header, Version, BlockHash, TxMerkleNode, CompactTarget, and BlockTime, using a new consensus-encoding crate. It wires the primitives and units crates to depend on consensus-encoding. The diff is purely additive refactoring of serialization machinery; no existing logic is removed and no security-sensitive bounds checks or cryptographic operations are altered.
Changed components
rust-bitcoin primitives craterust-bitcoin units crateconsensus_encoding crateInspect captured patch +134 / −1
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index c6138e6c..0c6a09c5 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -131,6 +131,7 @@ dependencies = [
"bitcoin-internals",
"bitcoin-units",
"bitcoin_hashes 0.16.0",
+ "consensus-encoding",
"hex-conservative 0.3.0",
"serde",
"serde_json",
@@ -143,6 +144,7 @@ dependencies = [
"arbitrary",
"bincode",
"bitcoin-internals",
+ "consensus-encoding",
"serde",
"serde_json",
"serde_test",
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 43147199..99135ba0 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -130,6 +130,7 @@ dependencies = [
"bitcoin-internals",
"bitcoin-units",
"bitcoin_hashes 0.16.0",
+ "consensus-encoding",
"hex-conservative 0.3.0",
"serde",
"serde_json",
@@ -142,6 +143,7 @@ dependencies = [
"arbitrary",
"bincode",
"bitcoin-internals",
+ "consensus-encoding",
"serde",
"serde_json",
"serde_test",
diff --git a/consensus_encoding/src/encode/encoders.rs b/consensus_encoding/src/encode/encoders.rs
index d1ec7b32..2b36279d 100644
--- a/consensus_encoding/src/encode/encoders.rs
+++ b/consensus_encoding/src/encode/encoders.rs
@@ -8,6 +8,8 @@
//! to your newtype. This avoids leaking encoding implementation details to the
//! users of your type.
//!
+//! For implementing these newtypes, we provide the [`encoder_newtype`] macro.
+//!
/// An encoder for a single byte slice.
use super::Encoder;
diff --git a/consensus_encoding/src/encode/mod.rs b/consensus_encoding/src/encode/mod.rs
index 1801bab0..901b03fb 100644
--- a/consensus_encoding/src/encode/mod.rs
+++ b/consensus_encoding/src/encode/mod.rs
@@ -38,3 +38,24 @@ pub trait Encoder<'e> {
/// and just call `current_chunk` to see if it works.
fn advance(&mut self) -> bool;
}
+
+/// Implements a newtype around an encoder which implements the
+/// [`Encoder`] trait by forwarding to the wrapped encoder.
+#[macro_export]
+macro_rules! encoder_newtype{
+ (
+ $(#[$($struct_attr:tt)*])*
+ pub struct $name:ident$(<$lt:lifetime>)?($encoder:ty);
+ ) => {
+ $(#[$($struct_attr)*])*
+ pub struct $name$(<$lt>)?($encoder);
+
+ impl<'e $(, $lt)?> $crate::Encoder<'e> for $name$(<$lt>)? {
+ #[inline]
+ fn current_chunk(&self) -> Option<&[u8]> { self.0.current_chunk() }
+
+ #[inline]
+ fn advance(&mut self) -> bool { self.0.advance() }
+ }
+ }
+}
diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml
index 4709c975..4d831b4c 100644
--- a/primitives/Cargo.toml
+++ b/primitives/Cargo.toml
@@ -23,9 +23,10 @@ arbitrary = ["dep:arbitrary", "units/arbitrary"]
hex = ["dep:hex", "hashes/hex", "internals/hex"]
[dependencies]
+encoding = { package = "consensus-encoding", path = "../consensus_encoding", default-features = false }
hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = false }
internals = { package = "bitcoin-internals", path = "../internals" }
-units = { package = "bitcoin-units", path = "../units", default-features = false }
+units = { package = "bitcoin-units", path = "../units", default-features = false, features = [ "encoding" ] }
arrayvec = { version = "0.7.2", default-features = false }
arbitrary = { version = "1.4.1", optional = true }
diff --git a/primitives/src/block.rs b/primitives/src/block.rs
index 0409c25d..6bad8f11 100644
--- a/primitives/src/block.rs
+++ b/primitives/src/block.rs
@@ -13,6 +13,7 @@ use core::marker::PhantomData;
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
+use encoding::Encodable;
use hashes::{sha256d, HashEngine as _};
use units::BlockTime;
@@ -247,6 +248,37 @@ impl fmt::Debug for Header {
}
}
+encoding::encoder_newtype! {
+ /// The encoder for the [`Header`] type.
+ pub struct HeaderEncoder(
+ encoding::Encoder6<
+ VersionEncoder,
+ BlockHashEncoder,
+ crate::merkle_tree::TxMerkleNodeEncoder,
+ crate::time::BlockTimeEncoder,
+ crate::pow::CompactTargetEncoder,
+ encoding::ArrayEncoder<4>,
+ >
+ );
+}
+
+impl Encodable for Header {
+ type Encoder<'e> = HeaderEncoder;
+
+ fn encoder(&self) -> Self::Encoder<'_> {
+ HeaderEncoder(
+ encoding::Encoder6::new(
+ self.version.encoder(),
+ self.prev_blockhash.encoder(),
+ self.merkle_root.encoder(),
+ self.time.encoder(),
+ self.bits.encoder(),
+ encoding::ArrayEncoder::without_length_prefix(self.nonce.to_le_bytes()),
+ )
+ )
+ }
+}
+
impl From<Header> for BlockHash {
#[inline]
fn from(header: Header) -> BlockHash { header.block_hash() }
@@ -336,6 +368,20 @@ hashes::hash_newtype! {
pub struct WitnessCommitment(sha256d::Hash);
}
+encoding::encoder_newtype! {
+ /// The encoder for the [`Version`] type.
+ pub struct VersionEncoder(encoding::ArrayEncoder<4>);
+}
+
+impl Encodable for Version {
+ type Encoder<'e> = VersionEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ VersionEncoder(
+ encoding::ArrayEncoder::without_length_prefix(self.to_consensus().to_le_bytes())
+ )
+ }
+}
+
#[cfg(feature = "hex")]
hashes::impl_hex_for_newtype!(BlockHash, WitnessCommitment);
#[cfg(not(feature = "hex"))]
@@ -348,6 +394,20 @@ impl BlockHash {
pub const GENESIS_PREVIOUS_BLOCK_HASH: Self = Self::from_byte_array([0; 32]);
}
+encoding::encoder_newtype! {
+ /// The encoder for the [`BlockHash`] type.
+ pub struct BlockHashEncoder(encoding::ArrayEncoder<32>);
+}
+
+impl Encodable for BlockHash {
+ type Encoder<'e> = BlockHashEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ BlockHashEncoder(
+ encoding::ArrayEncoder::without_length_prefix(self.to_byte_array())
+ )
+ }
+}
+
#[cfg(feature = "arbitrary")]
#[cfg(feature = "alloc")]
impl<'a> Arbitrary<'a> for Block {
diff --git a/primitives/src/merkle_tree.rs b/primitives/src/merkle_tree.rs
index 48c6972a..83efc1d4 100644
--- a/primitives/src/merkle_tree.rs
+++ b/primitives/src/merkle_tree.rs
@@ -20,6 +20,20 @@ hashes::impl_debug_only_for_newtype!(TxMerkleNode, WitnessMerkleNode);
#[cfg(feature = "serde")]
hashes::impl_serde_for_newtype!(TxMerkleNode, WitnessMerkleNode);
+encoding::encoder_newtype! {
+ /// The encoder for the [`TxMerkleNode`] type.
+ pub struct TxMerkleNodeEncoder(encoding::ArrayEncoder<32>);
+}
+
+impl encoding::Encodable for TxMerkleNode {
+ type Encoder<'e> = TxMerkleNodeEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ TxMerkleNodeEncoder(
+ encoding::ArrayEncoder::without_length_prefix(self.to_byte_array())
+ )
+ }
+}
+
#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for TxMerkleNode {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
diff --git a/primitives/src/pow.rs b/primitives/src/pow.rs
index 3cffa64c..e79bae20 100644
--- a/primitives/src/pow.rs
+++ b/primitives/src/pow.rs
@@ -48,6 +48,20 @@ impl fmt::UpperHex for CompactTarget {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) }
}
+encoding::encoder_newtype! {
+ /// The encoder for the [`TxMerkleNode`] type.
+ pub struct CompactTargetEncoder(encoding::ArrayEncoder<4>);
+}
+
+impl encoding::Encodable for CompactTarget {
+ type Encoder<'e> = CompactTargetEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ CompactTargetEncoder(
+ encoding::ArrayEncoder::without_length_prefix(self.to_consensus().to_le_bytes())
+ )
+ }
+}
+
#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
diff --git a/units/Cargo.toml b/units/Cargo.toml
index 428c6bf3..ec2b5a31 100644
--- a/units/Cargo.toml
+++ b/units/Cargo.toml
@@ -18,6 +18,7 @@ std = ["alloc", "internals/std"]
alloc = ["internals/alloc","serde?/alloc"]
[dependencies]
+encoding = { package = "consensus-encoding", path = "../consensus_encoding", optional = true }
internals = { package = "bitcoin-internals", path = "../internals", version = "0.4.0" }
serde = { version = "1.0.195", default-features = false, features = ["derive"], optional = true }
diff --git a/units/src/time.rs b/units/src/time.rs
index c58c3ae6..5516cf4d 100644
--- a/units/src/time.rs
+++ b/units/src/time.rs
@@ -73,6 +73,22 @@ impl<'de> Deserialize<'de> for BlockTime {
}
}
+#[cfg(feature = "encoding")]
+encoding::encoder_newtype! {
+ /// The encoder for the [`BlockTime`] type.
+ pub struct BlockTimeEncoder(encoding::ArrayEncoder<4>);
+}
+
+#[cfg(feature = "encoding")]
+impl encoding::Encodable for BlockTime {
+ type Encoder<'e> = BlockTimeEncoder;
+ fn encoder(&self) -> Self::Encoder<'_> {
+ BlockTimeEncoder(
+ encoding::ArrayEncoder::without_length_prefix(self.to_u32().to_le_bytes())
+ )
+ }
+}
+
#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for BlockTime {
#[inline]
Why this scored 18/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.