Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

- [BREAKING] Renamed `AccountStorageDelta` to `AccountStoragePatch` ([#3002](https://github.com/0xMiden/protocol/pull/3002)).
- [BREAKING] Replaced the per-tree account and nullifier backend traits with shared `SmtBackend` and `SmtBackendReader` traits, split into read-only and read-write capabilities, enabling read-only `LargeSmt`-backed tree views via `reader()` ([#2755](https://github.com/0xMiden/protocol/pull/2755), [#3009](https://github.com/0xMiden/protocol/pull/3009)).
- [BREAKING] Block validator signatures are now verified against the validator key committed to by the parent block, enabling safe validator key rotation. `BlockHeader::validator_key` now denotes the signer of the *next* block, `ProvenBlock`/`SignedBlock` `new` no longer verify the signature (pass the parent header to `validate` to authenticate a block against its parent's validator key), and `ProposedBlock` serialization gained a trailing `next_validator_key` field ([#3030](https://github.com/0xMiden/protocol/pull/3030)).
- Added `active_note::is_public` and `active_note::is_private` MASM procedures for checking whether the active note is public or private ([#2988](https://github.com/0xMiden/protocol/pull/2988)).
- Added a `min_burn_amount` fungible faucet burn policy that rejects burns below a configurable, owner-gated minimum burn amount ([#3021](https://github.com/0xMiden/protocol/pull/3021)).
- Added the `active_account::has_storage_slot` MASM procedure for checking whether a storage slot exists on the active account without panicking ([#3037](https://github.com/0xMiden/protocol/pull/3037)).
Expand Down
7 changes: 5 additions & 2 deletions crates/miden-protocol/src/block/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use crate::{Felt, Hasher, Word, ZERO};
/// - `tx_commitment` is a commitment to the set of transaction IDs which affected accounts in the
/// block.
/// - `tx_kernel_commitment` a commitment to all transaction kernels supported by this block.
/// - `validator_key` is the public key of the validator that is expected to sign the block.
/// - `validator_key` is the public key of the validator authorized to sign the *next* block.
/// - `fee_parameters` are the parameters defining the base fees and the fee faucet ID, see
/// [`FeeParameters`] for more details.
/// - `timestamp` is the time when the block was created, in seconds since UNIX epoch. Current
Expand Down Expand Up @@ -171,7 +171,10 @@ impl BlockHeader {
self.note_root
}

/// Returns the public key of the block's validator.
/// Returns the public key of the validator authorized to sign the *next* block.
///
/// A block's signature is verified against the `validator_key` committed to by its parent
/// block, not against this field. See the [`BlockHeader`] docs for details.
pub fn validator_key(&self) -> &PublicKey {
&self.validator_key
}
Comment thread
bobbinth marked this conversation as resolved.
Expand Down
2 changes: 2 additions & 0 deletions crates/miden-protocol/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub use signed_block::SignedBlock;
mod proven_block;
pub use proven_block::ProvenBlock;

mod validation;

pub mod account_tree;
pub mod nullifier_tree;

Expand Down
38 changes: 34 additions & 4 deletions crates/miden-protocol/src/block/proposed_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::block::{
BlockNumber,
OutputNoteBatch,
};
use crate::crypto::dsa::ecdsa_k256_keccak::PublicKey;
use crate::errors::ProposedBlockError;
use crate::note::{NoteId, Nullifier};
use crate::transaction::{
Expand Down Expand Up @@ -73,6 +74,12 @@ pub struct ProposedBlock {
///
/// As part of proving the block, this header will be added to the next partial blockchain.
prev_block_header: BlockHeader,
/// The validator public key authorized to sign the *next* block, which is committed to in this
/// block's header.
///
/// Defaults to the previous block's `validator_key` (i.e. no rotation). Set a different key
/// via [`ProposedBlock::with_next_validator_key`] to rotate the validator key.
next_validator_key: PublicKey,
}

impl ProposedBlock {
Expand Down Expand Up @@ -239,6 +246,8 @@ impl ProposedBlock {
// Build proposed blocks from parts.
// --------------------------------------------------------------------------------------------

let next_validator_key = prev_block_header.validator_key().clone();

Ok(Self {
batches: OrderedBatches::new(batches),
timestamp,
Expand All @@ -247,6 +256,7 @@ impl ProposedBlock {
created_nullifiers: nullifier_witnesses,
partial_blockchain,
prev_block_header,
next_validator_key,
})
}

Expand Down Expand Up @@ -274,6 +284,21 @@ impl ProposedBlock {
Self::new_at(block_inputs, batches, timestamp)
}

// BUILDERS
// --------------------------------------------------------------------------------------------

/// Sets the validator key that this block commits to as the signer of the *next* block,
/// rotating away from the previous block's validator key.
///
/// The block this proposed block produces is still signed by the current validator (the key
/// committed to by the previous block); the provided key only takes effect for the following
/// block.
#[must_use]
pub fn with_next_validator_key(mut self, next_validator_key: PublicKey) -> Self {
self.next_validator_key = next_validator_key;
self
}

// ACCESSORS
// --------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -327,6 +352,11 @@ impl ProposedBlock {
self.timestamp
}

/// Returns the validator key committed to by this block as the signer of the next block.
pub fn next_validator_key(&self) -> &PublicKey {
&self.next_validator_key
}

// COMMITMENT COMPUTATIONS
// --------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -457,9 +487,6 @@ impl ProposedBlock {
/// - the transaction commitment; and
/// - the chain commitment.
///
/// The returned block header contains the same validator public key as the previous block, as
/// provided by the proposed block.
///
/// # Errors
///
/// Returns an error if any of the following conditions are met.
Expand All @@ -484,6 +511,7 @@ impl ProposedBlock {
let block_num = self.block_num();
let timestamp = self.timestamp();
let prev_block_header = self.prev_block_header().clone();
let next_validator_key = self.next_validator_key.clone();

// Insert the state commitments of updated accounts into the account tree to compute its new
// root.
Expand Down Expand Up @@ -528,7 +556,7 @@ impl ProposedBlock {
note_root,
tx_commitment,
tx_kernel_commitment,
prev_block_header.validator_key().clone(),
next_validator_key,
fee_parameters,
timestamp,
);
Expand Down Expand Up @@ -571,6 +599,7 @@ impl Serializable for ProposedBlock {
self.created_nullifiers.write_into(target);
self.partial_blockchain.write_into(target);
self.prev_block_header.write_into(target);
self.next_validator_key.write_into(target);
}
}

Expand All @@ -584,6 +613,7 @@ impl Deserializable for ProposedBlock {
created_nullifiers: <BTreeMap<Nullifier, NullifierWitness>>::read_from(source)?,
partial_blockchain: PartialBlockchain::read_from(source)?,
prev_block_header: BlockHeader::read_from(source)?,
next_validator_key: PublicKey::read_from(source)?,
};

Ok(block)
Expand Down
132 changes: 104 additions & 28 deletions crates/miden-protocol/src/block/proven_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use miden_core::Word;
use miden_crypto::dsa::ecdsa_k256_keccak::Signature;

use crate::MIN_PROOF_SECURITY_LEVEL;
use crate::block::{BlockBody, BlockHeader, BlockProof};
use crate::block::validation::ParentValidationError;
use crate::block::{BlockBody, BlockHeader, BlockNumber, BlockProof};
use crate::utils::serde::{
ByteReader,
ByteWriter,
Expand All @@ -17,7 +18,7 @@ use crate::utils::serde::{
#[derive(Debug, thiserror::Error)]
pub enum ProvenBlockError {
#[error(
"ECDSA signature verification failed based on the proven block's header commitment, validator public key and signature"
"ECDSA signature verification failed based on the proven block's header commitment, the parent block's validator public key and signature"
)]
InvalidSignature,
#[error(
Expand All @@ -31,6 +32,34 @@ pub enum ProvenBlockError {
"proven block header note root ({header_root}) does not match the corresponding body's note root ({body_root})"
)]
NoteRootMismatch { header_root: Word, body_root: Word },
#[error(
"proven block previous block commitment ({expected}) does not match expected parent's block commitment ({parent})"
)]
ParentCommitmentMismatch { expected: Word, parent: Word },
#[error("parent block number ({parent}) is not proven block number - 1 ({expected})")]
ParentNumberMismatch {
expected: BlockNumber,
parent: BlockNumber,
},
#[error("supplied parent block ({parent}) cannot be parent to genesis block")]
GenesisBlockHasNoParent { parent: BlockNumber },
}

impl From<ParentValidationError> for ProvenBlockError {
fn from(err: ParentValidationError) -> Self {
match err {
ParentValidationError::InvalidSignature => Self::InvalidSignature,
ParentValidationError::ParentNumberMismatch { expected, parent } => {
Self::ParentNumberMismatch { expected, parent }
},
ParentValidationError::ParentCommitmentMismatch { expected, parent } => {
Self::ParentCommitmentMismatch { expected, parent }
},
ParentValidationError::GenesisBlockHasNoParent { parent } => {
Self::GenesisBlockHasNoParent { parent }
},
}
}
}

// PROVEN BLOCK
Expand Down Expand Up @@ -60,8 +89,10 @@ pub struct ProvenBlock {
impl ProvenBlock {
/// Returns a new [`ProvenBlock`] instantiated from the provided components.
///
/// Validates that the provided components correspond to each other by verifying the signature,
/// and checking for matching transaction commitments and note roots.
/// Validates that the header and body correspond by checking the transaction commitment and
/// note root. This does NOT verify the validator signature, which can only be checked against
/// the parent block's validator key; call [`Self::validate`] with the parent header to
/// authenticate the block.
///
/// Involves non-trivial computation. Use [`Self::new_unchecked`] if the validation is not
/// necessary.
Expand All @@ -77,8 +108,6 @@ impl ProvenBlock {
///
/// # Errors
/// Returns an error if:
/// - If the validator signature does not verify against the block header commitment and the
/// validator key.
/// - If the transaction commitment in the block header is inconsistent with the transactions
/// included in the block body.
/// - If the note root in the block header is inconsistent with the notes included in the block
Expand All @@ -91,7 +120,7 @@ impl ProvenBlock {
) -> Result<Self, ProvenBlockError> {
let proven_block = Self { header, signature, body, proof };

proven_block.validate()?;
proven_block.validate(None)?;

Ok(proven_block)
}
Expand All @@ -111,8 +140,15 @@ impl ProvenBlock {
Self { header, signature, body, proof }
}

/// Validates that the components of the proven block correspond to each other by verifying the
/// signature, and checking for matching transaction commitments and note roots.
/// Validates that the components of the proven block correspond by checking the transaction
/// commitment and note root, and -- when `parent` is provided -- authenticates the block
/// against its parent.
///
/// Pass `Some(parent)` to additionally authenticate the block against its parent; pass `None`
/// for the genesis block, which has no parent, or when only self-consistency is required.
///
/// `parent` MUST come from already-trusted chain state. Because `prev_block_commitment` is
/// attacker-controlled, passing an untrusted parent would let a forged block self-authorize.
///
/// Validation involves non-trivial computation, and depending on the size of the block may
/// take non-negligible amount of time.
Expand All @@ -128,22 +164,25 @@ impl ProvenBlock {
///
/// # Errors
/// Returns an error if:
/// - If the validator signature does not verify against the block header commitment and the
/// validator key.
/// - If the transaction commitment in the block header is inconsistent with the transactions
/// included in the block body.
/// - If the note root in the block header is inconsistent with the notes included in the block
/// body.
pub fn validate(&self) -> Result<(), ProvenBlockError> {
// Verify signature.
self.validate_signature()?;

/// - the transaction commitment in the block header is inconsistent with the transactions
/// included in the block body;
/// - the note root in the block header is inconsistent with the notes included in the block
/// body; or
/// - a `parent` is provided and the block is not authorized by it: the block is the genesis
/// block (which has no parent), the parent's number or commitment do not match, or the
/// signature does not verify against the parent's validator key.
pub fn validate(&self, parent: Option<&BlockHeader>) -> Result<(), ProvenBlockError> {
// Validate that header / body transaction commitments match.
self.validate_tx_commitment()?;

// Validate that header / body note roots match.
self.validate_note_root()?;

// When a trusted parent is provided, authenticate the block against it.
if let Some(parent) = parent {
self.header.validate_against_parent(&self.signature, parent)?;
}

Ok(())
}

Expand Down Expand Up @@ -180,15 +219,6 @@ impl ProvenBlock {
// HELPER METHODS
// --------------------------------------------------------------------------------------------

/// Performs ECDSA signature verification against the header commitment and validator key.
fn validate_signature(&self) -> Result<(), ProvenBlockError> {
if !self.signature.verify(self.header.commitment(), self.header.validator_key()) {
Err(ProvenBlockError::InvalidSignature)
} else {
Ok(())
}
}

/// Validates that the transaction commitments between the header and body match for this proven
/// block.
///
Expand Down Expand Up @@ -241,3 +271,49 @@ impl Deserializable for ProvenBlock {
Ok(block)
}
}

// TESTS
// ================================================================================================

#[cfg(test)]
mod tests {
use alloc::vec::Vec;

use miden_crypto::dsa::ecdsa_k256_keccak::SigningKey;

use super::*;
use crate::Word;
use crate::block::validation::test_block_header;
use crate::testing::random_secret_key::random_secret_key;
use crate::transaction::OrderedTransactionHeaders;

/// Builds block 1 signed by `signer` and linked to `parent`. The exhaustive matrix of failure
/// modes lives in `block::validation`; here we only confirm `ProvenBlock::validate` wires the
/// signature and parent header through to the shared check.
fn block_one(parent: &BlockHeader, signer: &SigningKey) -> ProvenBlock {
let header = test_block_header(1, parent.commitment(), random_secret_key().public_key());
let signature = signer.sign(header.commitment());
let body = BlockBody::new_unchecked(
Vec::new(),
Vec::new(),
Vec::new(),
OrderedTransactionHeaders::new_unchecked(Vec::new()),
);
ProvenBlock::new_unchecked(header, body, signature, BlockProof::new_dummy())
}

#[test]
fn validate_accepts_committed_signer() {
let validator = random_secret_key();
let parent = test_block_header(0, Word::empty(), validator.public_key());
block_one(&parent, &validator).validate(Some(&parent)).unwrap();
}

#[test]
fn validate_rejects_uncommitted_signer() {
let parent = test_block_header(0, Word::empty(), random_secret_key().public_key());
let impostor = random_secret_key();
let result = block_one(&parent, &impostor).validate(Some(&parent));
assert!(matches!(result, Err(ProvenBlockError::InvalidSignature)));
}
}
Loading
Loading