Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 213 additions & 23 deletions chain/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,61 @@ where
digest: Digest,
}

struct DigestInput<'a, Tx, Payload> {
struct DigestInput<'a, Payload> {
context: &'a Context,
parent: &'a Digest,
height: Height,
timestamp: u64,
transactions: &'a [Tx],
transaction_root: &'a Digest,
reshare_log: &'a Option<DealerLog>,
extension: &'a Payload,
state: &'a StateCommitment,
}

/// Domain separator for a block's transaction commitment.
const TRANSACTION_ROOT_DOMAIN: &[u8] = b"nunchi/chain/transaction_root/v1";

/// A domain-separated commitment over a block's ordered transaction list.
///
/// This is a sequential-hash *commitment*, not a Merkle tree: it authenticates the exact ordered
/// set of transactions (count, order, and encoded bytes all matter), but it supports only equality
/// checks, not per-transaction inclusion proofs. Committing the block digest over this root instead
/// of inlining every transaction lets a compact [`BlockHeader`] reproduce the digest without
/// carrying the transactions.
fn transaction_root<Tx>(transactions: &[Tx]) -> Digest
where
Tx: EncodeSize + Write,
{
let mut hasher = Sha256::new();
hasher.update(TRANSACTION_ROOT_DOMAIN);
hasher.update(&(transactions.len() as u64).to_be_bytes());
for transaction in transactions {
hasher.update(&transaction.encode());
}
hasher.finalize()
}

/// Compute a block digest from its non-transaction fields and the transaction root.
///
/// Shared by [`Block`] (which folds its transactions into the root) and [`BlockHeader`] (which
/// carries the root directly), so both produce identical digests for the same block.
fn block_digest<Payload>(input: DigestInput<'_, Payload>) -> Digest
where
Payload: EncodeSize + Write,
{
let mut hasher = Sha256::new();
hasher.update(&input.context.encode());
hasher.update(input.parent);
hasher.update(&input.height.get().to_be_bytes());
hasher.update(&input.timestamp.to_be_bytes());
hasher.update(input.transaction_root);
hasher.update(&input.reshare_log.encode());
hasher.update(&input.extension.encode());
hasher.update(&input.state.root);
hasher.update(&input.state.range.encode());
hasher.finalize()
}

impl<Tx, Ext> Clone for Block<Tx, Ext>
where
Tx: Clone,
Expand Down Expand Up @@ -125,23 +169,6 @@ where
Tx: EncodeSize + Write,
Ext: BlockExtension,
{
fn compute_digest(input: DigestInput<'_, Tx, Ext::Payload>) -> Digest {
let mut hasher = Sha256::new();
hasher.update(&input.context.encode());
hasher.update(input.parent);
hasher.update(&input.height.get().to_be_bytes());
hasher.update(&input.timestamp.to_be_bytes());
hasher.update(&(input.transactions.len() as u64).to_be_bytes());
for transaction in input.transactions {
hasher.update(&transaction.encode());
}
hasher.update(&input.reshare_log.encode());
hasher.update(&input.extension.encode());
hasher.update(&input.state.root);
hasher.update(&input.state.range.encode());
hasher.finalize()
}

#[allow(clippy::too_many_arguments)]
pub fn new(
context: Context,
Expand All @@ -153,12 +180,12 @@ where
extension: Ext::Payload,
state: StateCommitment,
) -> Self {
let digest = Self::compute_digest(DigestInput {
let digest = block_digest(DigestInput {
context: &context,
parent: &parent,
height,
timestamp,
transactions: &transactions,
transaction_root: &transaction_root(&transactions),
reshare_log: &reshare_log,
extension: &extension,
state: &state,
Expand All @@ -176,6 +203,23 @@ where
digest,
}
}

/// The compact, digest-authenticated header of this block: every field that contributes to the
/// block digest except the transactions, which are folded into [`BlockHeader::transaction_root`].
/// [`BlockHeader::digest`] reproduces this block's digest exactly.
pub fn header(&self) -> BlockHeader<Ext> {
BlockHeader {
context: self.context.clone(),
parent: self.parent,
height: self.height,
timestamp: self.timestamp,
transaction_root: transaction_root(&self.transactions),
reshare_log: self.reshare_log.clone(),
extension: self.extension.clone(),
state_root: self.state_root,
state_range: self.state_range.clone(),
}
}
}

impl<Tx, Ext> Write for Block<Tx, Ext>
Expand Down Expand Up @@ -231,12 +275,12 @@ where
range: state_range,
};

let digest = Self::compute_digest(DigestInput {
let digest = block_digest(DigestInput {
context: &context,
parent: &parent,
height,
timestamp,
transactions: &transactions,
transaction_root: &transaction_root(&transactions),
reshare_log: &reshare_log,
extension: &extension,
state: &state,
Expand Down Expand Up @@ -303,6 +347,152 @@ where
}
}

/// The compact, digest-authenticated header of a [`Block`].
///
/// It carries every field that contributes to a block's digest except the transaction list, which
/// is folded into [`BlockHeader::transaction_root`]. This lets a verifier reproduce and check the
/// block digest (for example against a finalization certificate) without the full block, then read
/// the authenticated `state_root`/`state_range` out of it. See [`BlockHeader::digest`] and
/// [`Block::header`].
#[derive(Debug)]
pub struct BlockHeader<Ext: BlockExtension> {
/// The consensus context when the block was proposed.
pub context: Context,

/// The parent block's digest.
pub parent: Digest,

/// The height of the block in the blockchain.
pub height: Height,

/// The timestamp of the block (in milliseconds since the Unix epoch).
pub timestamp: u64,

/// Commitment over the block's ordered transaction list (count, order, and encoded bytes all
/// matter). A sequential-hash commitment, not a Merkle tree, so it supports equality checks but
/// not per-transaction inclusion proofs.
pub transaction_root: Digest,

/// Optional DKG resharing payload included outside ordinary runtime transactions.
pub reshare_log: Option<DealerLog>,

/// Additional consensus-side payload included outside ordinary runtime transactions.
pub extension: Ext::Payload,

/// Authenticated state root after executing the block's transactions.
pub state_root: Digest,

/// QMDB operation range that supports state sync to `state_root`.
pub state_range: NonEmptyRange<Location>,
}

impl<Ext: BlockExtension> BlockHeader<Ext> {
/// The block digest this header authenticates. Equal to the digest of the block it was taken
/// from (see [`Block::header`]).
pub fn digest(&self) -> Digest {
block_digest(DigestInput {
context: &self.context,
parent: &self.parent,
height: self.height,
timestamp: self.timestamp,
transaction_root: &self.transaction_root,
reshare_log: &self.reshare_log,
extension: &self.extension,
state: &StateCommitment {
root: self.state_root,
range: self.state_range.clone(),
},
})
}
}

impl<Ext: BlockExtension> Clone for BlockHeader<Ext> {
fn clone(&self) -> Self {
Self {
context: self.context.clone(),
parent: self.parent,
height: self.height,
timestamp: self.timestamp,
transaction_root: self.transaction_root,
reshare_log: self.reshare_log.clone(),
extension: self.extension.clone(),
state_root: self.state_root,
state_range: self.state_range.clone(),
}
}
}

impl<Ext: BlockExtension> PartialEq for BlockHeader<Ext> {
fn eq(&self, other: &Self) -> bool {
self.context == other.context
&& self.parent == other.parent
&& self.height == other.height
&& self.timestamp == other.timestamp
&& self.transaction_root == other.transaction_root
&& self.reshare_log.encode() == other.reshare_log.encode()
&& self.extension.encode() == other.extension.encode()
&& self.state_root == other.state_root
&& self.state_range == other.state_range
}
}

impl<Ext: BlockExtension> Eq for BlockHeader<Ext> {}

impl<Ext: BlockExtension> Write for BlockHeader<Ext> {
fn write(&self, writer: &mut impl BufMut) {
self.context.write(writer);
self.parent.write(writer);
self.height.write(writer);
UInt(self.timestamp).write(writer);
self.transaction_root.write(writer);
self.reshare_log.write(writer);
self.extension.write(writer);
self.state_root.write(writer);
self.state_range.write(writer);
}
}

impl<Ext: BlockExtension> Read for BlockHeader<Ext> {
type Cfg = (NonZeroU32, Ext::ReadCfg);

fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, Error> {
let context = Context::read(reader)?;
let parent = Digest::read(reader)?;
let height = Height::read(reader)?;
let timestamp = UInt::read(reader)?.0;
let transaction_root = Digest::read(reader)?;
let reshare_log = Option::<DealerLog>::read_cfg(reader, &cfg.0)?;
let extension = Ext::Payload::read_cfg(reader, &cfg.1)?;
let state_root = Digest::read(reader)?;
let state_range = NonEmptyRange::read(reader)?;
Ok(Self {
context,
parent,
height,
timestamp,
transaction_root,
reshare_log,
extension,
state_root,
state_range,
})
}
}

impl<Ext: BlockExtension> EncodeSize for BlockHeader<Ext> {
fn encode_size(&self) -> usize {
self.context.encode_size()
+ self.parent.encode_size()
+ self.height.encode_size()
+ UInt(self.timestamp).encode_size()
+ self.transaction_root.encode_size()
+ self.reshare_log.encode_size()
+ self.extension.encode_size()
+ self.state_root.encode_size()
+ self.state_range.encode_size()
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Notarized<Tx, Ext = NoConsensusExtension>
where
Expand Down
2 changes: 1 addition & 1 deletion chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod macros;
mod tests;

pub use application::{Application, SharedAppliedHeight};
pub use block::{Block, Finalized, Notarized, StateCommitment, MAX_TRANSACTIONS};
pub use block::{Block, BlockHeader, Finalized, Notarized, StateCommitment, MAX_TRANSACTIONS};
pub use consensus::{
dkg_reporters, BlockExtension, Composite, ConsensusExtension, DkgActor, DkgMailbox,
DkgReporters, NoConsensusExtension,
Expand Down
Loading
Loading