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;
pub use commitment::Commitment;
pub use commitment::{Commitment, CommitmentError};
pub use hash::Hash;
pub use header::BlockTimeError;
pub use header::{CountedHeader, Header};
pub use header::{BlockTimeError, CountedHeader, Header};
pub use height::Height;
pub use serialize::MAX_BLOCK_BYTES;
@ -70,15 +69,20 @@ impl Block {
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.
///
/// Returns None if this block does not have a block height.
pub fn commitment(&self, network: Network) -> Option<Commitment> {
self.coinbase_height()
.map(|height| Commitment::from_bytes(self.header.commitment_bytes, network, height))
/// Returns an error if this block does not have a block height,
/// or if the commitment value is structurally invalid.
pub fn commitment(&self, network: Network) -> Result<Commitment, CommitmentError> {
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 {
(any::<[u8; 32]>(), any::<Network>(), any::<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()
}

View File

@ -1,36 +1,55 @@
//! The Commitment enum, used for the corresponding block header field.
use thiserror::Error;
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,
/// based on the current block height. The interpretation changes at or after
/// network upgrades.
/// The `Header.commitment_bytes` field is interpreted differently, based on the
/// network and height. The interpretation changes in the network upgrade
/// activation block, or in the block immediately after network upgrade
/// activation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Commitment {
/// [Pre-Sapling] Reserved field.
///
/// All zeroes.
PreSaplingReserved([u8; 32]),
/// The value of this field MUST be all zeroes.
///
/// This field is verified in `Commitment::from_bytes`.
PreSaplingReserved,
/// [Sapling and Blossom] The final Sapling treestate of this block.
///
/// The root LEBS2OSP256(rt) of the Sapling note commitment tree
/// 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.
///
/// 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.
ChainHistoryActivationReserved([u8; 32]),
///
/// This field is verified in `Commitment::from_bytes`.
ChainHistoryActivationReserved,
/// [After Heartwood activation block] The root of a Merkle Mountain
/// Range chain history tree.
/// [(Heartwood activation block + 1) to Canopy] The root of a Merkle
/// Mountain Range chain history tree.
///
/// This root hash commits to various features of the chain's history,
/// 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
/// recent network upgrade, through to the previous block. In particular,
/// 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),
/// [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 {
/// 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 CommitmentError::*;
match NetworkUpgrade::current(network, height) {
Genesis | BeforeOverwinter | Overwinter => PreSaplingReserved(bytes),
Sapling | Blossom => FinalSaplingRoot(Root(bytes)),
Heartwood if Some(height) == Heartwood.activation_height(network) => {
ChainHistoryActivationReserved(bytes)
Genesis | BeforeOverwinter | Overwinter => {
if bytes == RESERVED_BYTES {
Ok(PreSaplingReserved)
} else {
Err(InvalidPreSaplingReserved { actual: bytes })
}
Heartwood | Canopy => ChainHistoryRoot(ChainHistoryMmrRootHash(bytes)),
Nu5 => unimplemented!("Nu5 uses hashAuthDataRoot as specified in ZIP-244"),
}
Sapling | Blossom => Ok(FinalSaplingRoot(sapling::tree::Root(bytes))),
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::*;
match self {
PreSaplingReserved(b) => b,
FinalSaplingRoot(v) => v.0,
ChainHistoryActivationReserved(b) => b,
ChainHistoryRoot(v) => v.0,
PreSaplingReserved => RESERVED_BYTES,
FinalSaplingRoot(hash) => hash.0,
ChainHistoryActivationReserved => RESERVED_BYTES,
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.
// TODO:
// - add methods for maintaining the MMR peaks, and calculating the root
// hash from the current set of peaks.
// - move to a separate file.
// hash from the current set of peaks
// - move to a separate file
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
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.
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
/// incrementing the version, so it cannot be parsed without the block height
/// and network. Use [`Block::commitment`](super::Block::commitment) to get the
/// parsed [`Commitment`](super::Commitment).
/// The interpretation of this field has been changed multiple times, without
/// incrementing the block [`version`]. Therefore, this field cannot be
/// parsed without the network and height. Use
/// [`Block::commitment`](super::Block::commitment) to get the parsed
/// [`Commitment`](super::Commitment).
pub commitment_bytes: [u8; 32],
/// The block timestamp is a Unix epoch time (UTC) when the miner

View File

@ -47,11 +47,14 @@ proptest! {
) {
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
// 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];
}
}
}
proptest! {
@ -69,13 +72,11 @@ proptest! {
let bytes = block.zcash_serialize_to_vec()?;
let bytes = &mut bytes.as_slice();
// Check the root hash
// Check the block commitment
let commitment = block.commitment(network);
if let Some(commitment) = commitment {
if let Ok(commitment) = commitment {
let commitment_bytes = commitment.to_bytes();
prop_assert_eq![block.header.commitment_bytes, commitment_bytes];
} else {
prop_assert_eq![block.coinbase_height(), None];
}
// Check the block size limit