fix(chain): Validate header versions when serializing blocks (#6475)
* checks block header version when serializing * fixes test panic * adds docs * test block header (de)serialization
This commit is contained in:
parent
2908acf75a
commit
403c074502
|
@ -22,9 +22,49 @@ use crate::{
|
||||||
/// transaction in the chain is approximately 1.5 kB smaller.)
|
/// transaction in the chain is approximately 1.5 kB smaller.)
|
||||||
pub const MAX_BLOCK_BYTES: u64 = 2_000_000;
|
pub const MAX_BLOCK_BYTES: u64 = 2_000_000;
|
||||||
|
|
||||||
|
/// Checks if a block header version is valid.
|
||||||
|
///
|
||||||
|
/// Zebra could encounter a [`Header`] with an invalid version when serializing a block header constructed
|
||||||
|
/// in memory with the wrong version in tests or the getblocktemplate RPC.
|
||||||
|
///
|
||||||
|
/// The getblocktemplate RPC generates a template with version 4. The miner generates the actual block,
|
||||||
|
/// and then we deserialize it and do this check.
|
||||||
|
///
|
||||||
|
/// All other blocks are deserialized when we receive them, and never modified,
|
||||||
|
/// so the deserialisation would pick up any errors.
|
||||||
|
fn check_version(version: u32) -> Result<(), &'static str> {
|
||||||
|
match version {
|
||||||
|
// The Zcash specification says that:
|
||||||
|
// "The current and only defined block version number for Zcash is 4."
|
||||||
|
// but this is not actually part of the consensus rules, and in fact
|
||||||
|
// broken mining software created blocks that do not have version 4.
|
||||||
|
// There are approximately 4,000 blocks with version 536870912; this
|
||||||
|
// is the bit-reversal of the value 4, indicating that that mining pool
|
||||||
|
// reversed bit-ordering of the version field. Because the version field
|
||||||
|
// was not properly validated, these blocks were added to the chain.
|
||||||
|
//
|
||||||
|
// The only possible way to work around this is to do a similar hack
|
||||||
|
// as the overwintered field in transaction parsing, which we do here:
|
||||||
|
// treat the high bit (which zcashd interprets as a sign bit) as an
|
||||||
|
// indicator that the version field is meaningful.
|
||||||
|
version if version >> 31 != 0 => Err("high bit was set in version field"),
|
||||||
|
|
||||||
|
// # Consensus
|
||||||
|
//
|
||||||
|
// > The block version number MUST be greater than or equal to 4.
|
||||||
|
//
|
||||||
|
// https://zips.z.cash/protocol/protocol.pdf#blockheader
|
||||||
|
version if version < ZCASH_BLOCK_VERSION => Err("version must be at least 4"),
|
||||||
|
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ZcashSerialize for Header {
|
impl ZcashSerialize for Header {
|
||||||
#[allow(clippy::unwrap_in_result)]
|
#[allow(clippy::unwrap_in_result)]
|
||||||
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
|
||||||
|
check_version(self.version).map_err(|msg| io::Error::new(io::ErrorKind::Other, msg))?;
|
||||||
|
|
||||||
writer.write_u32::<LittleEndian>(self.version)?;
|
writer.write_u32::<LittleEndian>(self.version)?;
|
||||||
self.previous_block_hash.zcash_serialize(&mut writer)?;
|
self.previous_block_hash.zcash_serialize(&mut writer)?;
|
||||||
writer.write_all(&self.merkle_root.0[..])?;
|
writer.write_all(&self.merkle_root.0[..])?;
|
||||||
|
@ -44,41 +84,8 @@ impl ZcashSerialize for Header {
|
||||||
|
|
||||||
impl ZcashDeserialize for Header {
|
impl ZcashDeserialize for Header {
|
||||||
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
|
||||||
// The Zcash specification says that
|
let version = reader.read_u32::<LittleEndian>()?;
|
||||||
// "The current and only defined block version number for Zcash is 4."
|
check_version(version).map_err(SerializationError::Parse)?;
|
||||||
// but this is not actually part of the consensus rules, and in fact
|
|
||||||
// broken mining software created blocks that do not have version 4.
|
|
||||||
// There are approximately 4,000 blocks with version 536870912; this
|
|
||||||
// is the bit-reversal of the value 4, indicating that that mining pool
|
|
||||||
// reversed bit-ordering of the version field. Because the version field
|
|
||||||
// was not properly validated, these blocks were added to the chain.
|
|
||||||
//
|
|
||||||
// The only possible way to work around this is to do a similar hack
|
|
||||||
// as the overwintered field in transaction parsing, which we do here:
|
|
||||||
// treat the high bit (which zcashd interprets as a sign bit) as an
|
|
||||||
// indicator that the version field is meaningful.
|
|
||||||
//
|
|
||||||
//
|
|
||||||
let (version, future_version_flag) = {
|
|
||||||
const LOW_31_BITS: u32 = (1 << 31) - 1;
|
|
||||||
let raw_version = reader.read_u32::<LittleEndian>()?;
|
|
||||||
(raw_version & LOW_31_BITS, raw_version >> 31 != 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if future_version_flag {
|
|
||||||
return Err(SerializationError::Parse(
|
|
||||||
"high bit was set in version field",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// # Consensus
|
|
||||||
//
|
|
||||||
// > The block version number MUST be greater than or equal to 4.
|
|
||||||
//
|
|
||||||
// https://zips.z.cash/protocol/protocol.pdf#blockheader
|
|
||||||
if version < ZCASH_BLOCK_VERSION {
|
|
||||||
return Err(SerializationError::Parse("version must be at least 4"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Header {
|
Ok(Header {
|
||||||
version,
|
version,
|
||||||
|
|
|
@ -13,7 +13,9 @@ use crate::{
|
||||||
Network::{self, *},
|
Network::{self, *},
|
||||||
NetworkUpgrade::*,
|
NetworkUpgrade::*,
|
||||||
},
|
},
|
||||||
serialization::{sha256d, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
|
serialization::{
|
||||||
|
sha256d, SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
|
||||||
|
},
|
||||||
transaction::LockTime,
|
transaction::LockTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ fn blockheaderhash_from_blockheader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_blockheader() {
|
fn blockheader_serialization() {
|
||||||
let _init_guard = zebra_test::init();
|
let _init_guard = zebra_test::init();
|
||||||
|
|
||||||
// Includes the 32-byte nonce and 3-byte equihash length field.
|
// Includes the 32-byte nonce and 3-byte equihash length field.
|
||||||
|
@ -73,11 +75,65 @@ fn deserialize_blockheader() {
|
||||||
+ crate::work::equihash::SOLUTION_SIZE;
|
+ crate::work::equihash::SOLUTION_SIZE;
|
||||||
|
|
||||||
for block in zebra_test::vectors::BLOCKS.iter() {
|
for block in zebra_test::vectors::BLOCKS.iter() {
|
||||||
|
// successful deserialization
|
||||||
|
|
||||||
let header_bytes = &block[..BLOCK_HEADER_LENGTH];
|
let header_bytes = &block[..BLOCK_HEADER_LENGTH];
|
||||||
|
|
||||||
let _header = header_bytes
|
let mut header = header_bytes
|
||||||
.zcash_deserialize_into::<Header>()
|
.zcash_deserialize_into::<Header>()
|
||||||
.expect("blockheader test vector should deserialize");
|
.expect("blockheader test vector should deserialize");
|
||||||
|
|
||||||
|
// successful serialization
|
||||||
|
|
||||||
|
let _serialized_header = header
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect("blockheader test vector should serialize");
|
||||||
|
|
||||||
|
// deserialiation errors
|
||||||
|
|
||||||
|
let header_bytes = [&[255; 4], &header_bytes[4..]].concat();
|
||||||
|
|
||||||
|
let deserialization_err = header_bytes
|
||||||
|
.zcash_deserialize_into::<Header>()
|
||||||
|
.expect_err("blockheader test vector should fail to deserialize");
|
||||||
|
|
||||||
|
let SerializationError::Parse(err_msg) = deserialization_err else {
|
||||||
|
panic!("SerializationError variant should be Parse")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(err_msg, "high bit was set in version field");
|
||||||
|
|
||||||
|
let header_bytes = [&[0; 4], &header_bytes[4..]].concat();
|
||||||
|
|
||||||
|
let deserialization_err = header_bytes
|
||||||
|
.zcash_deserialize_into::<Header>()
|
||||||
|
.expect_err("blockheader test vector should fail to deserialize");
|
||||||
|
|
||||||
|
let SerializationError::Parse(err_msg) = deserialization_err else {
|
||||||
|
panic!("SerializationError variant should be Parse")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(err_msg, "version must be at least 4");
|
||||||
|
|
||||||
|
// serialiation errors
|
||||||
|
|
||||||
|
header.version = u32::MAX;
|
||||||
|
|
||||||
|
let serialization_err = header
|
||||||
|
.zcash_serialize_to_vec()
|
||||||
|
.expect_err("blockheader test vector with modified version should fail to serialize");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
serialization_err.kind(),
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"error kind should be Other"
|
||||||
|
);
|
||||||
|
|
||||||
|
let err_msg = serialization_err
|
||||||
|
.into_inner()
|
||||||
|
.expect("there should be an inner error");
|
||||||
|
|
||||||
|
assert_eq!(err_msg.to_string(), "high bit was set in version field");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -504,7 +504,7 @@ async fn wrong_checkpoint_hash_fail() -> Result<(), Report> {
|
||||||
// Change the header hash
|
// Change the header hash
|
||||||
let mut bad_block0 = good_block0.clone();
|
let mut bad_block0 = good_block0.clone();
|
||||||
let bad_block0_mut = Arc::make_mut(&mut bad_block0);
|
let bad_block0_mut = Arc::make_mut(&mut bad_block0);
|
||||||
Arc::make_mut(&mut bad_block0_mut.header).version = 0;
|
Arc::make_mut(&mut bad_block0_mut.header).version = 5;
|
||||||
|
|
||||||
// Make a checkpoint list containing the genesis block checkpoint
|
// Make a checkpoint list containing the genesis block checkpoint
|
||||||
let genesis_checkpoint_list: BTreeMap<block::Height, block::Hash> =
|
let genesis_checkpoint_list: BTreeMap<block::Height, block::Hash> =
|
||||||
|
|
Loading…
Reference in New Issue