Skip to main content

miden_protocol/block/
proven_block.rs

1use alloc::string::ToString;
2
3use miden_core::Word;
4
5use crate::MIN_PROOF_SECURITY_LEVEL;
6use crate::block::header::ParentValidationError;
7use crate::block::{BlockBody, BlockHeader, BlockNumber, BlockSignatures};
8use crate::utils::serde::{
9    ByteReader,
10    ByteWriter,
11    Deserializable,
12    DeserializationError,
13    Serializable,
14};
15use crate::vm::ExecutionProof;
16
17// PROVEN BLOCK ERROR
18// ================================================================================================
19
20#[derive(Debug, thiserror::Error)]
21pub enum ProvenBlockError {
22    #[error("block proof contains precompiles")]
23    BlockProofContainsPrecompiles,
24    #[error(
25        "proven block has {actual} signatures but its parent's validator set has {expected} keys"
26    )]
27    SignatureCountMismatch { expected: usize, actual: usize },
28    #[error(
29        "proven block signature at position {position} does not verify against the parent's validator key at that position"
30    )]
31    InvalidSignatureAtPosition { position: usize },
32    #[error(
33        "header tx commitment ({header_tx_commitment}) does not match body tx commitment ({body_tx_commitment})"
34    )]
35    TxCommitmentMismatch {
36        header_tx_commitment: Word,
37        body_tx_commitment: Word,
38    },
39    #[error(
40        "proven block header note root ({header_root}) does not match the corresponding body's note root ({body_root})"
41    )]
42    NoteRootMismatch { header_root: Word, body_root: Word },
43    #[error(
44        "proven block previous block commitment ({expected}) does not match expected parent's block commitment ({parent})"
45    )]
46    ParentCommitmentMismatch { expected: Word, parent: Word },
47    #[error("parent block number ({parent}) is not proven block number - 1 ({expected})")]
48    ParentNumberMismatch {
49        expected: BlockNumber,
50        parent: BlockNumber,
51    },
52    #[error("supplied parent block ({parent}) cannot be parent to genesis block")]
53    GenesisBlockHasNoParent { parent: BlockNumber },
54}
55
56impl From<ParentValidationError> for ProvenBlockError {
57    fn from(err: ParentValidationError) -> Self {
58        match err {
59            ParentValidationError::SignatureCountMismatch { expected, actual } => {
60                Self::SignatureCountMismatch { expected, actual }
61            },
62            ParentValidationError::InvalidSignatureAtPosition { position } => {
63                Self::InvalidSignatureAtPosition { position }
64            },
65            ParentValidationError::ParentNumberMismatch { expected, parent } => {
66                Self::ParentNumberMismatch { expected, parent }
67            },
68            ParentValidationError::ParentCommitmentMismatch { expected, parent } => {
69                Self::ParentCommitmentMismatch { expected, parent }
70            },
71            ParentValidationError::GenesisBlockHasNoParent { parent } => {
72                Self::GenesisBlockHasNoParent { parent }
73            },
74        }
75    }
76}
77
78// PROVEN BLOCK
79// ================================================================================================
80
81/// Represents a block in the Miden blockchain that has been signed and proven.
82///
83/// Blocks transition through proposed, signed, and proven states. This struct represents the final,
84/// proven state of a block.
85///
86/// Proven blocks are the final, canonical blocks in the chain.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ProvenBlock {
89    /// The header of the proven block.
90    header: BlockHeader,
91
92    /// The body of the proven block.
93    body: BlockBody,
94
95    /// The validators' positional signatures over the block header.
96    signatures: BlockSignatures,
97
98    /// The execution proof of the block kernel over this block.
99    // TODO: The block kernel takes BATCHES_COMMITMENT as a public input, but this struct carries
100    // neither that commitment nor the `BatchId`s it is built from, so the claim the proof attests
101    // to cannot be reconstructed here. Recomputing the batch IDs additionally needs the per-batch
102    // grouping of `body.transactions()`, which is flattened away. Add the batch IDs and that
103    // grouping to this struct or to `BlockBody` so the proof can be tied back to the block's data:
104    // https://github.com/0xMiden/protocol/issues/1706
105    proof: ExecutionProof,
106}
107
108impl ProvenBlock {
109    /// Returns a new [`ProvenBlock`] instantiated from the provided components.
110    ///
111    /// Validates that the header and body correspond by checking the transaction commitment and
112    /// note root. This does NOT verify the validator signatures, which can only be checked against
113    /// the parent block's validator keys; call [`Self::validate`] with the parent header to
114    /// authenticate the block.
115    ///
116    /// Involves non-trivial computation. Use [`Self::new_unchecked`] if the validation is not
117    /// necessary.
118    ///
119    /// Note: this does not fully validate the consistency of provided components. Specifically,
120    /// we cannot validate that:
121    /// - That applying the account updates in the block body to the account tree represented by the
122    ///   root from the previous block header would actually result in the account root in the
123    ///   provided header.
124    /// - That inserting the created nullifiers in the block body to the nullifier tree represented
125    ///   by the root from the previous block header would actually result in the nullifier root in
126    ///   the provided header.
127    ///
128    /// # Errors
129    /// Returns an error if:
130    /// - If the execution proof contains precompiles.
131    /// - If the transaction commitment in the block header is inconsistent with the transactions
132    ///   included in the block body.
133    /// - If the note root in the block header is inconsistent with the notes included in the block
134    ///   body.
135    pub fn new(
136        header: BlockHeader,
137        body: BlockBody,
138        signatures: BlockSignatures,
139        proof: ExecutionProof,
140    ) -> Result<Self, ProvenBlockError> {
141        let proven_block = Self { header, signatures, body, proof };
142
143        proven_block.validate(None)?;
144
145        Ok(proven_block)
146    }
147
148    /// Returns a new [`ProvenBlock`] instantiated from the provided components.
149    ///
150    /// # Warning
151    ///
152    /// This constructor does not do any validation as to whether the arguments correctly correspond
153    /// to each other, which could cause errors downstream.
154    pub fn new_unchecked(
155        header: BlockHeader,
156        body: BlockBody,
157        signatures: BlockSignatures,
158        proof: ExecutionProof,
159    ) -> Self {
160        Self { header, signatures, body, proof }
161    }
162
163    /// Validates that the components of the proven block correspond by checking the transaction
164    /// commitment and note root, and -- when `parent` is provided -- authenticates the block
165    /// against its parent.
166    ///
167    /// Pass `Some(parent)` to additionally authenticate the block against its parent; pass `None`
168    /// for the genesis block, which has no parent, or when only self-consistency is required.
169    ///
170    /// `parent` MUST come from already-trusted chain state. Because `prev_block_commitment` is
171    /// attacker-controlled, passing an untrusted parent would let a forged block self-authorize.
172    ///
173    /// Validation involves non-trivial computation, and depending on the size of the block may
174    /// take non-negligible amount of time.
175    ///
176    /// Note: this does not fully validate the consistency of internal components. Specifically,
177    /// we cannot validate that:
178    /// - That applying the account updates in the block body to the account tree represented by the
179    ///   root from the previous block header would actually result in the account root in the
180    ///   provided header.
181    /// - That inserting the created nullifiers in the block body to the nullifier tree represented
182    ///   by the root from the previous block header would actually result in the nullifier root in
183    ///   the provided header.
184    ///
185    /// # Errors
186    /// Returns an error if:
187    /// - the execution proof contains precompiles;
188    /// - the transaction commitment in the block header is inconsistent with the transactions
189    ///   included in the block body;
190    /// - the note root in the block header is inconsistent with the notes included in the block
191    ///   body; or
192    /// - a `parent` is provided and the block is not authorized by it: the block is the genesis
193    ///   block (which has no parent), the parent's number or commitment do not match, or the
194    ///   signatures do not verify against the parent's validator keys.
195    pub fn validate(&self, parent: Option<&BlockHeader>) -> Result<(), ProvenBlockError> {
196        self.validate_proof()?;
197
198        // Validate that header / body transaction commitments match.
199        self.validate_tx_commitment()?;
200
201        // Validate that header / body note roots match.
202        self.validate_note_root()?;
203
204        // When a trusted parent is provided, authenticate the block against it.
205        if let Some(parent) = parent {
206            self.header.validate_against_parent(parent, &self.signatures)?;
207        }
208
209        Ok(())
210    }
211
212    /// Returns the proof security level of the block.
213    pub fn proof_security_level(&self) -> u32 {
214        MIN_PROOF_SECURITY_LEVEL
215    }
216
217    /// Returns the header of the block.
218    pub fn header(&self) -> &BlockHeader {
219        &self.header
220    }
221
222    /// Returns the body of the block.
223    pub fn body(&self) -> &BlockBody {
224        &self.body
225    }
226
227    /// Returns the validators' positional signatures over the block header.
228    pub fn signatures(&self) -> &BlockSignatures {
229        &self.signatures
230    }
231
232    /// Returns the execution proof attached to this block.
233    pub fn proof(&self) -> &ExecutionProof {
234        &self.proof
235    }
236
237    /// Destructures this proven block into individual parts.
238    pub fn into_parts(self) -> (BlockHeader, BlockBody, BlockSignatures, ExecutionProof) {
239        (self.header, self.body, self.signatures, self.proof)
240    }
241
242    // HELPER METHODS
243    // --------------------------------------------------------------------------------------------
244
245    /// Validates that the block proof has no outstanding or settled precompile work.
246    fn validate_proof(&self) -> Result<(), ProvenBlockError> {
247        if self.proof.has_precompiles() {
248            Err(ProvenBlockError::BlockProofContainsPrecompiles)
249        } else {
250            Ok(())
251        }
252    }
253
254    /// Validates that the transaction commitments between the header and body match for this proven
255    /// block.
256    ///
257    /// Involves non-trivial computation of the body's transaction commitment.
258    fn validate_tx_commitment(&self) -> Result<(), ProvenBlockError> {
259        let header_tx_commitment = self.header.tx_commitment();
260        let body_tx_commitment = self.body.transactions().commitment();
261        if header_tx_commitment != body_tx_commitment {
262            Err(ProvenBlockError::TxCommitmentMismatch { header_tx_commitment, body_tx_commitment })
263        } else {
264            Ok(())
265        }
266    }
267
268    /// Validates that the header's note tree root matches that of the body.
269    ///
270    /// Involves non-trivial computation of the body's note tree.
271    fn validate_note_root(&self) -> Result<(), ProvenBlockError> {
272        let header_root = self.header.note_root();
273        let body_root = self.body.compute_block_note_tree().root();
274        if header_root != body_root {
275            Err(ProvenBlockError::NoteRootMismatch { header_root, body_root })
276        } else {
277            Ok(())
278        }
279    }
280}
281
282// SERIALIZATION
283// ================================================================================================
284
285impl Serializable for ProvenBlock {
286    fn write_into<W: ByteWriter>(&self, target: &mut W) {
287        self.header.write_into(target);
288        self.body.write_into(target);
289        self.signatures.write_into(target);
290        self.proof.write_into(target);
291    }
292}
293
294impl Deserializable for ProvenBlock {
295    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
296        let block = Self {
297            header: BlockHeader::read_from(source)?,
298            body: BlockBody::read_from(source)?,
299            signatures: BlockSignatures::read_from(source)?,
300            proof: ExecutionProof::read_from(source)?,
301        };
302
303        block
304            .validate_proof()
305            .map_err(|error| DeserializationError::InvalidValue(error.to_string()))?;
306
307        Ok(block)
308    }
309}
310
311// TESTS
312// ================================================================================================
313
314#[cfg(test)]
315mod tests {
316    use alloc::vec::Vec;
317
318    use miden_crypto::dsa::ecdsa_k256_keccak::SigningKey;
319
320    use super::*;
321    use crate::Word;
322    use crate::block::ValidatorConfig;
323    use crate::transaction::OrderedTransactionHeaders;
324
325    fn empty_body() -> BlockBody {
326        BlockBody::new_unchecked(
327            Vec::new(),
328            Vec::new(),
329            Vec::new(),
330            OrderedTransactionHeaders::new_unchecked(Vec::new()),
331        )
332    }
333
334    /// Builds block 1 linked to `parent` and signed by `signers` over the validator set
335    /// `parent_keys` committed to by the parent. Here we only confirm `ProvenBlock::validate`
336    /// wires the signatures and parent header through to the shared check.
337    fn block_one(
338        parent: &BlockHeader,
339        parent_keys: &ValidatorConfig,
340        signers: &[SigningKey],
341    ) -> ProvenBlock {
342        let next_keys = ValidatorConfig::random_with_signers(3).1;
343        let header = BlockHeader::new_dummy(1, parent.commitment(), next_keys);
344        let signatures = parent_keys.sign_all(signers, header.commitment());
345        ProvenBlock::new_unchecked(
346            header,
347            empty_body(),
348            signatures,
349            crate::testing::dummy_execution_proof(),
350        )
351    }
352
353    #[test]
354    fn validate_accepts_committed_signers() {
355        let (signers, keys) = ValidatorConfig::random_with_signers(3);
356        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
357        block_one(&parent, &keys, &signers).validate(Some(&parent)).unwrap();
358    }
359
360    #[test]
361    fn validate_accepts_single_validator() {
362        let (signers, keys) = ValidatorConfig::random_with_signers(1);
363        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
364        block_one(&parent, &keys, &signers).validate(Some(&parent)).unwrap();
365    }
366
367    #[test]
368    fn rejects_proofs_with_precompiles() {
369        let (signers, keys) = ValidatorConfig::random_with_signers(1);
370        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
371        let block = block_one(&parent, &keys, &signers);
372        let (header, body, signatures, _) = block.into_parts();
373
374        for proof in [
375            crate::testing::dummy_deferred_execution_proof(),
376            crate::testing::dummy_precompile_execution_proof(),
377        ] {
378            let error =
379                ProvenBlock::new(header.clone(), body.clone(), signatures.clone(), proof.clone())
380                    .unwrap_err();
381            assert!(matches!(error, ProvenBlockError::BlockProofContainsPrecompiles));
382
383            let block =
384                ProvenBlock::new_unchecked(header.clone(), body.clone(), signatures.clone(), proof);
385            let error = ProvenBlock::read_from_bytes(&block.to_bytes()).unwrap_err();
386            assert!(matches!(error, DeserializationError::InvalidValue(_)));
387        }
388    }
389
390    #[test]
391    fn validate_rejects_uncommitted_signers() {
392        let (_, keys) = ValidatorConfig::random_with_signers(3);
393        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
394        let next_keys = ValidatorConfig::random_with_signers(3).1;
395        let header = BlockHeader::new_dummy(1, parent.commitment(), next_keys);
396
397        // The block is signed by a full, valid validator set of the same size the parent never
398        // committed.
399        let (impostor_signers, impostor_keys) = ValidatorConfig::random_with_signers(3);
400        let signatures = impostor_keys.sign_all(&impostor_signers, header.commitment());
401        let block = ProvenBlock::new_unchecked(
402            header,
403            empty_body(),
404            signatures,
405            crate::testing::dummy_execution_proof(),
406        );
407
408        let result = block.validate(Some(&parent));
409        assert!(matches!(result, Err(ProvenBlockError::InvalidSignatureAtPosition { .. })));
410    }
411}