Clean up block commitment enum and parsing (#1978)

* Rename RootHash to Commitment based on ZIP-244

Interactive replace using:
```sh
fastmod RootHash Commitment
fastmod root_hash commitment
fastmod root_bytes commitment_bytes
git mv zebra-chain/src/block/root_hash.rs zebra-chain/src/block/commitment.rs
```

All replacements were accepted.

* rustfmt

* Comment and format cleanups after interactive replace

* Distinguish Sapling tree roots from other tree roots

* Add the NU5 BlockCommitmentsHash variant to block::Commitment

This change parses the hash, but does not perform validation.

* Validate reserved values in Block::commitment

- change Block::commitment to return a Result rather than an Option
- enforce the all-zeroes reserved value consensus rules
- change `PreSaplingReserved([u8; 32])` to `PreSaplingReserved`
- change `ChainHistoryActivationReserved([u8; 32])` to `ChainHistoryActivationReserved`
- update the function comments to describe when each variant is verified

* Fix comment whitespace
This commit is contained in:
teor 2021-04-06 20:19:28 +10:00 committed by GitHub
parent 0daaf582e2
commit 05b60db993
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 180 additions and 49 deletions

View File

@ -16,10 +16,9 @@ mod tests;
use std::fmt; use std::fmt;
pub use commitment::Commitment; pub use commitment::{Commitment, CommitmentError};
pub use hash::Hash; pub use hash::Hash;
pub use header::BlockTimeError; pub use header::{BlockTimeError, CountedHeader, Header};
pub use header::{CountedHeader, Header};
pub use height::Height; pub use height::Height;
pub use serialize::MAX_BLOCK_BYTES; pub use serialize::MAX_BLOCK_BYTES;
@ -70,15 +69,20 @@ impl Block {
Hash::from(self) Hash::from(self)
} }
/// Get the parsed root hash for this block. /// Get the parsed block [`Commitment`] for this block.
/// ///
/// The interpretation of the root hash depends on the /// The interpretation of the commitment depends on the
/// configured `network`, and this block's height. /// configured `network`, and this block's height.
/// ///
/// Returns None if this block does not have a block height. /// Returns an error if this block does not have a block height,
pub fn commitment(&self, network: Network) -> Option<Commitment> { /// or if the commitment value is structurally invalid.
self.coinbase_height() pub fn commitment(&self, network: Network) -> Result<Commitment, CommitmentError> {
.map(|height| Commitment::from_bytes(self.header.commitment_bytes, network, height)) match self.coinbase_height() {
None => Err(CommitmentError::MissingBlockHeight {
block_hash: self.hash(),
}),
Some(height) => Commitment::from_bytes(self.header.commitment_bytes, network, height),
}
} }
} }

View File

@ -53,7 +53,16 @@ impl Arbitrary for Commitment {
fn arbitrary_with(_args: ()) -> Self::Strategy { fn arbitrary_with(_args: ()) -> Self::Strategy {
(any::<[u8; 32]>(), any::<Network>(), any::<Height>()) (any::<[u8; 32]>(), any::<Network>(), any::<Height>())
.prop_map(|(commitment_bytes, network, block_height)| { .prop_map(|(commitment_bytes, network, block_height)| {
Commitment::from_bytes(commitment_bytes, network, block_height) match Commitment::from_bytes(commitment_bytes, network, block_height) {
Ok(commitment) => commitment,
// just fix up the reserved values when they fail
Err(_) => Commitment::from_bytes(
super::commitment::RESERVED_BYTES,
network,
block_height,
)
.expect("from_bytes only fails due to reserved bytes"),
}
}) })
.boxed() .boxed()
} }

View File

@ -1,36 +1,55 @@
//! The Commitment enum, used for the corresponding block header field. //! The Commitment enum, used for the corresponding block header field.
use thiserror::Error;
use crate::parameters::{Network, NetworkUpgrade, NetworkUpgrade::*}; use crate::parameters::{Network, NetworkUpgrade, NetworkUpgrade::*};
use crate::sapling::tree::Root; use crate::sapling;
use super::Height; use super::super::block;
/// Zcash blocks contain different kinds of root hashes, depending on the network upgrade. /// Zcash blocks contain different kinds of commitments to their contents,
/// depending on the network and height.
/// ///
/// The `BlockHeader.commitment_bytes` field is interpreted differently, /// The `Header.commitment_bytes` field is interpreted differently, based on the
/// based on the current block height. The interpretation changes at or after /// network and height. The interpretation changes in the network upgrade
/// network upgrades. /// activation block, or in the block immediately after network upgrade
/// activation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Commitment { pub enum Commitment {
/// [Pre-Sapling] Reserved field. /// [Pre-Sapling] Reserved field.
/// ///
/// All zeroes. /// The value of this field MUST be all zeroes.
PreSaplingReserved([u8; 32]), ///
/// This field is verified in `Commitment::from_bytes`.
PreSaplingReserved,
/// [Sapling and Blossom] The final Sapling treestate of this block. /// [Sapling and Blossom] The final Sapling treestate of this block.
/// ///
/// The root LEBS2OSP256(rt) of the Sapling note commitment tree /// The root LEBS2OSP256(rt) of the Sapling note commitment tree
/// corresponding to the final Sapling treestate of this block. /// corresponding to the final Sapling treestate of this block.
FinalSaplingRoot(Root), ///
/// Subsequent `Commitment` variants also commit to the `FinalSaplingRoot`,
/// via their `EarliestSaplingRoot` and `LatestSaplingRoot` fields.
///
/// TODO: this field is verified during semantic verification
///
/// Since Zebra checkpoints on Canopy, we don't need to validate this
/// field, but since it's included in the ChainHistoryRoot, we are
/// already calculating it, so we might as well validate it.
FinalSaplingRoot(sapling::tree::Root),
/// [Heartwood activation block] Reserved field. /// [Heartwood activation block] Reserved field.
/// ///
/// All zeroes. This MUST NOT be interpreted as a root hash. /// The value of this field MUST be all zeroes.
///
/// This MUST NOT be interpreted as a root hash.
/// See ZIP-221 for details. /// See ZIP-221 for details.
ChainHistoryActivationReserved([u8; 32]), ///
/// This field is verified in `Commitment::from_bytes`.
ChainHistoryActivationReserved,
/// [After Heartwood activation block] The root of a Merkle Mountain /// [(Heartwood activation block + 1) to Canopy] The root of a Merkle
/// Range chain history tree. /// Mountain Range chain history tree.
/// ///
/// This root hash commits to various features of the chain's history, /// This root hash commits to various features of the chain's history,
/// including the Sapling commitment tree. This commitment supports the /// including the Sapling commitment tree. This commitment supports the
@ -39,23 +58,65 @@ pub enum Commitment {
/// The commitment in each block covers the chain history from the most /// The commitment in each block covers the chain history from the most
/// recent network upgrade, through to the previous block. In particular, /// recent network upgrade, through to the previous block. In particular,
/// an activation block commits to the entire previous network upgrade, and /// an activation block commits to the entire previous network upgrade, and
/// the block after activation commits only to the activation block. /// the block after activation commits only to the activation block. (And
/// therefore transitively to all previous network upgrades covered by a
/// chain history hash in their activation block, via the previous block
/// hash field.)
///
/// TODO: this field is verified during semantic verification
ChainHistoryRoot(ChainHistoryMmrRootHash), ChainHistoryRoot(ChainHistoryMmrRootHash),
/// [NU5 activation onwards] A commitment to:
/// - the chain history Merkle Mountain Range tree, and
/// - the auth data merkle tree covering this block.
///
/// The chain history Merkle Mountain Range tree commits to the previous
/// block and all ancestors in the current network upgrade. The auth data
/// merkle tree commits to this block.
///
/// This commitment supports the FlyClient protocol and non-malleable
/// transaction IDs. See ZIP-221 and ZIP-244 for details.
///
/// See also the [`ChainHistoryRoot`] variant.
///
/// TODO: this field is verified during semantic verification
//
// TODO: Do block commitments activate at NU5 activation, or (NU5 + 1)?
// https://github.com/zcash/zips/pull/474
BlockCommitments(BlockCommitmentsHash),
} }
/// The required value of reserved `Commitment`s.
pub(crate) const RESERVED_BYTES: [u8; 32] = [0; 32];
impl Commitment { impl Commitment {
/// Returns `bytes` as the Commitment variant for `network` and `height`. /// Returns `bytes` as the Commitment variant for `network` and `height`.
pub(super) fn from_bytes(bytes: [u8; 32], network: Network, height: Height) -> Commitment { pub(super) fn from_bytes(
bytes: [u8; 32],
network: Network,
height: block::Height,
) -> Result<Commitment, CommitmentError> {
use Commitment::*; use Commitment::*;
use CommitmentError::*;
match NetworkUpgrade::current(network, height) { match NetworkUpgrade::current(network, height) {
Genesis | BeforeOverwinter | Overwinter => PreSaplingReserved(bytes), Genesis | BeforeOverwinter | Overwinter => {
Sapling | Blossom => FinalSaplingRoot(Root(bytes)), if bytes == RESERVED_BYTES {
Heartwood if Some(height) == Heartwood.activation_height(network) => { Ok(PreSaplingReserved)
ChainHistoryActivationReserved(bytes) } else {
Err(InvalidPreSaplingReserved { actual: bytes })
}
} }
Heartwood | Canopy => ChainHistoryRoot(ChainHistoryMmrRootHash(bytes)), Sapling | Blossom => Ok(FinalSaplingRoot(sapling::tree::Root(bytes))),
Nu5 => unimplemented!("Nu5 uses hashAuthDataRoot as specified in ZIP-244"), Heartwood if Some(height) == Heartwood.activation_height(network) => {
if bytes == RESERVED_BYTES {
Ok(ChainHistoryActivationReserved)
} else {
Err(InvalidChainHistoryActivationReserved { actual: bytes })
}
}
Heartwood | Canopy => Ok(ChainHistoryRoot(ChainHistoryMmrRootHash(bytes))),
Nu5 => Ok(BlockCommitments(BlockCommitmentsHash(bytes))),
} }
} }
@ -65,10 +126,11 @@ impl Commitment {
use Commitment::*; use Commitment::*;
match self { match self {
PreSaplingReserved(b) => b, PreSaplingReserved => RESERVED_BYTES,
FinalSaplingRoot(v) => v.0, FinalSaplingRoot(hash) => hash.0,
ChainHistoryActivationReserved(b) => b, ChainHistoryActivationReserved => RESERVED_BYTES,
ChainHistoryRoot(v) => v.0, ChainHistoryRoot(hash) => hash.0,
BlockCommitments(hash) => hash.0,
} }
} }
} }
@ -76,7 +138,60 @@ impl Commitment {
/// The root hash of a Merkle Mountain Range chain history tree. /// The root hash of a Merkle Mountain Range chain history tree.
// TODO: // TODO:
// - add methods for maintaining the MMR peaks, and calculating the root // - add methods for maintaining the MMR peaks, and calculating the root
// hash from the current set of peaks. // hash from the current set of peaks
// - move to a separate file. // - move to a separate file
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ChainHistoryMmrRootHash([u8; 32]); pub struct ChainHistoryMmrRootHash([u8; 32]);
/// The Block Commitments for a block. As of NU5, these cover:
/// - the chain history tree for all ancestors in the current network upgrade,
/// and
/// - the transaction authorising data in this block.
//
// TODO:
// - add auth data type
// - add a method for hashing chain history and auth data together
// - move to a separate file
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct BlockCommitmentsHash([u8; 32]);
/// Errors that can occur when checking RootHash consensus rules.
///
/// Each error variant corresponds to a consensus rule, so enumerating
/// all possible verification failures enumerates the consensus rules we
/// implement, and ensures that we don't reject blocks or transactions
/// for a non-enumerated reason.
#[allow(dead_code)]
#[derive(Error, Debug, PartialEq)]
pub enum CommitmentError {
#[error("invalid pre-Sapling reserved committment: expected all zeroes, actual: {actual:?}")]
InvalidPreSaplingReserved {
// TODO: are these fields a security risk? If so, open a ticket to remove
// similar fields across Zebra
actual: [u8; 32],
},
#[error("invalid final sapling root: expected {expected:?}, actual: {actual:?}")]
InvalidFinalSaplingRoot {
expected: [u8; 32],
actual: [u8; 32],
},
#[error("invalid chain history activation reserved block committment: expected all zeroes, actual: {actual:?}")]
InvalidChainHistoryActivationReserved { actual: [u8; 32] },
#[error("invalid chain history root: expected {expected:?}, actual: {actual:?}")]
InvalidChainHistoryRoot {
expected: [u8; 32],
actual: [u8; 32],
},
#[error("invalid block commitment: expected {expected:?}, actual: {actual:?}")]
InvalidBlockCommitment {
expected: [u8; 32],
actual: [u8; 32],
},
#[error("missing required block height: block commitments can't be parsed without a block height, block hash: {block_hash:?}")]
MissingBlockHeight { block_hash: block::Hash },
}

View File

@ -45,12 +45,14 @@ pub struct Header {
/// valid. /// valid.
pub merkle_root: merkle::Root, pub merkle_root: merkle::Root,
/// Some kind of root hash. /// Zcash blocks contain different kinds of commitments to their contents,
/// depending on the network and height.
/// ///
/// Unfortunately, the interpretation of this field was changed without /// The interpretation of this field has been changed multiple times, without
/// incrementing the version, so it cannot be parsed without the block height /// incrementing the block [`version`]. Therefore, this field cannot be
/// and network. Use [`Block::commitment`](super::Block::commitment) to get the /// parsed without the network and height. Use
/// parsed [`Commitment`](super::Commitment). /// [`Block::commitment`](super::Block::commitment) to get the parsed
/// [`Commitment`](super::Commitment).
pub commitment_bytes: [u8; 32], pub commitment_bytes: [u8; 32],
/// The block timestamp is a Unix epoch time (UTC) when the miner /// The block timestamp is a Unix epoch time (UTC) when the miner

View File

@ -47,10 +47,13 @@ proptest! {
) { ) {
zebra_test::init(); zebra_test::init();
let commitment = Commitment::from_bytes(bytes, network, block_height); // just skip the test if the bytes don't parse, because there's nothing
let other_bytes = commitment.to_bytes(); // to compare with
if let Ok(commitment) = Commitment::from_bytes(bytes, network, block_height) {
let other_bytes = commitment.to_bytes();
prop_assert_eq![bytes, other_bytes]; prop_assert_eq![bytes, other_bytes];
}
} }
} }
@ -69,13 +72,11 @@ proptest! {
let bytes = block.zcash_serialize_to_vec()?; let bytes = block.zcash_serialize_to_vec()?;
let bytes = &mut bytes.as_slice(); let bytes = &mut bytes.as_slice();
// Check the root hash // Check the block commitment
let commitment = block.commitment(network); let commitment = block.commitment(network);
if let Some(commitment) = commitment { if let Ok(commitment) = commitment {
let commitment_bytes = commitment.to_bytes(); let commitment_bytes = commitment.to_bytes();
prop_assert_eq![block.header.commitment_bytes, commitment_bytes]; prop_assert_eq![block.header.commitment_bytes, commitment_bytes];
} else {
prop_assert_eq![block.coinbase_height(), None];
} }
// Check the block size limit // Check the block size limit