Skip to main content

miden_protocol/block/
signed_block.rs

1use miden_core::Word;
2
3use crate::block::header::ParentValidationError;
4use crate::block::{BlockBody, BlockHeader, BlockNumber, BlockSignatures};
5use crate::utils::serde::{
6    ByteReader,
7    ByteWriter,
8    Deserializable,
9    DeserializationError,
10    Serializable,
11};
12
13// SIGNED BLOCK ERROR
14// ================================================================================================
15
16#[derive(Debug, thiserror::Error)]
17pub enum SignedBlockError {
18    #[error(
19        "signed block has {actual} signatures but its parent's validator set has {expected} keys"
20    )]
21    SignatureCountMismatch { expected: usize, actual: usize },
22    #[error(
23        "signed block signature at position {position} does not verify against the parent's validator key at that position"
24    )]
25    InvalidSignatureAtPosition { position: usize },
26    #[error(
27        "header tx commitment ({header_tx_commitment}) does not match body tx commitment ({body_tx_commitment})"
28    )]
29    TxCommitmentMismatch {
30        header_tx_commitment: Word,
31        body_tx_commitment: Word,
32    },
33    #[error(
34        "signed block previous block commitment ({expected}) does not match expected parent's block commitment ({parent})"
35    )]
36    ParentCommitmentMismatch { expected: Word, parent: Word },
37    #[error("parent block number ({parent}) is not signed block number - 1 ({expected})")]
38    ParentNumberMismatch {
39        expected: BlockNumber,
40        parent: BlockNumber,
41    },
42    #[error(
43        "signed block header note root ({header_root}) does not match the corresponding body's note root ({body_root})"
44    )]
45    NoteRootMismatch { header_root: Word, body_root: Word },
46    #[error("supplied parent block ({parent}) cannot be parent to genesis block")]
47    GenesisBlockHasNoParent { parent: BlockNumber },
48}
49
50impl From<ParentValidationError> for SignedBlockError {
51    fn from(err: ParentValidationError) -> Self {
52        match err {
53            ParentValidationError::SignatureCountMismatch { expected, actual } => {
54                Self::SignatureCountMismatch { expected, actual }
55            },
56            ParentValidationError::InvalidSignatureAtPosition { position } => {
57                Self::InvalidSignatureAtPosition { position }
58            },
59            ParentValidationError::ParentNumberMismatch { expected, parent } => {
60                Self::ParentNumberMismatch { expected, parent }
61            },
62            ParentValidationError::ParentCommitmentMismatch { expected, parent } => {
63                Self::ParentCommitmentMismatch { expected, parent }
64            },
65            ParentValidationError::GenesisBlockHasNoParent { parent } => {
66                Self::GenesisBlockHasNoParent { parent }
67            },
68        }
69    }
70}
71
72// SIGNED BLOCK
73// ================================================================================================
74
75/// Represents a block in the Miden blockchain that has been signed by the validators.
76///
77/// Signed blocks are applied to the chain's state before they are proven.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct SignedBlock {
80    /// The header of the Signed block.
81    header: BlockHeader,
82
83    /// The body of the Signed block.
84    body: BlockBody,
85
86    /// The validators' positional signatures over the block header.
87    signatures: BlockSignatures,
88}
89
90impl SignedBlock {
91    /// Returns a new [`SignedBlock`] instantiated from the provided components.
92    ///
93    /// Validates that the header and body correspond by checking the transaction commitment and
94    /// note root. This does NOT verify the validator signatures, which can only be checked against
95    /// the parent block's validator keys; call [`Self::validate`] with the parent header to
96    /// authenticate the block.
97    ///
98    /// Involves non-trivial computation. Use [`Self::new_unchecked`] if the validation is not
99    /// necessary.
100    pub fn new(
101        header: BlockHeader,
102        body: BlockBody,
103        signatures: BlockSignatures,
104    ) -> Result<Self, SignedBlockError> {
105        let signed_block = Self { header, body, signatures };
106
107        signed_block.validate(None)?;
108
109        Ok(signed_block)
110    }
111
112    /// Returns a new [`SignedBlock`] instantiated from the provided components.
113    ///
114    /// # Warning
115    ///
116    /// This constructor does not do any validation as to whether the arguments correctly correspond
117    /// to each other, which could cause errors downstream.
118    pub fn new_unchecked(
119        header: BlockHeader,
120        body: BlockBody,
121        signatures: BlockSignatures,
122    ) -> Self {
123        Self { header, signatures, body }
124    }
125
126    /// Returns the header of the block.
127    pub fn header(&self) -> &BlockHeader {
128        &self.header
129    }
130
131    /// Returns the body of the block.
132    pub fn body(&self) -> &BlockBody {
133        &self.body
134    }
135
136    /// Returns the validators' positional signatures over the block header.
137    pub fn signatures(&self) -> &BlockSignatures {
138        &self.signatures
139    }
140
141    /// Destructures this signed block into individual parts.
142    pub fn into_parts(self) -> (BlockHeader, BlockBody, BlockSignatures) {
143        (self.header, self.body, self.signatures)
144    }
145
146    /// Validates that the header and body correspond by checking the transaction commitment and
147    /// note root, and -- when `parent` is provided -- authenticates the block against its parent.
148    ///
149    /// Pass `Some(parent)` to additionally authenticate the block against its parent; pass `None`
150    /// for the genesis block, which has no parent, or when only self-consistency is required.
151    ///
152    /// `parent` MUST come from already-trusted chain state. Because `prev_block_commitment` is
153    /// attacker-controlled, passing an untrusted parent would let a forged block self-authorize.
154    ///
155    /// # Errors
156    /// Returns an error if:
157    /// - the transaction commitment in the block header is inconsistent with the transactions
158    ///   included in the block body;
159    /// - the note root in the block header is inconsistent with the notes included in the block
160    ///   body; or
161    /// - a `parent` is provided and the block is not authorized by it: the block is the genesis
162    ///   block (which has no parent), the parent's number or commitment do not match, or the
163    ///   signatures do not verify against the parent's validator keys.
164    pub fn validate(&self, parent: Option<&BlockHeader>) -> Result<(), SignedBlockError> {
165        // Validate that header / body transaction commitments match.
166        self.validate_tx_commitment()?;
167
168        // Validate that header / body note roots match.
169        self.validate_note_root()?;
170
171        // When a trusted parent is provided, authenticate the block against it.
172        if let Some(parent) = parent {
173            self.header.validate_against_parent(parent, &self.signatures)?;
174        }
175
176        Ok(())
177    }
178
179    /// Validates that the transaction commitments between the header and body match for this signed
180    /// block.
181    ///
182    /// Involves non-trivial computation of the body's transaction commitment.
183    fn validate_tx_commitment(&self) -> Result<(), SignedBlockError> {
184        let header_tx_commitment = self.header.tx_commitment();
185        let body_tx_commitment = self.body.transactions().commitment();
186        if header_tx_commitment != body_tx_commitment {
187            Err(SignedBlockError::TxCommitmentMismatch { header_tx_commitment, body_tx_commitment })
188        } else {
189            Ok(())
190        }
191    }
192
193    /// Validates that the header's note tree root matches that of the body.
194    ///
195    /// Involves non-trivial computation of the body's note tree.
196    fn validate_note_root(&self) -> Result<(), SignedBlockError> {
197        let header_root = self.header.note_root();
198        let body_root = self.body.compute_block_note_tree().root();
199        if header_root != body_root {
200            Err(SignedBlockError::NoteRootMismatch { header_root, body_root })
201        } else {
202            Ok(())
203        }
204    }
205}
206
207// SERIALIZATION
208// ================================================================================================
209
210impl Serializable for SignedBlock {
211    fn write_into<W: ByteWriter>(&self, target: &mut W) {
212        self.header.write_into(target);
213        self.body.write_into(target);
214        self.signatures.write_into(target);
215    }
216}
217
218impl Deserializable for SignedBlock {
219    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
220        let block = Self {
221            header: BlockHeader::read_from(source)?,
222            body: BlockBody::read_from(source)?,
223            signatures: BlockSignatures::read_from(source)?,
224        };
225
226        Ok(block)
227    }
228}
229
230// TESTS
231// ================================================================================================
232
233#[cfg(test)]
234mod tests {
235    use alloc::vec::Vec;
236
237    use miden_crypto::dsa::ecdsa_k256_keccak::SigningKey;
238
239    use super::*;
240    use crate::Word;
241    use crate::block::ValidatorKeys;
242    use crate::testing::validator_keys::{random_validator_set as validator_set, sign_all};
243    use crate::transaction::OrderedTransactionHeaders;
244
245    fn empty_body() -> BlockBody {
246        BlockBody::new_unchecked(
247            Vec::new(),
248            Vec::new(),
249            Vec::new(),
250            OrderedTransactionHeaders::new_unchecked(Vec::new()),
251        )
252    }
253
254    fn block_one(
255        parent: &BlockHeader,
256        parent_keys: &ValidatorKeys,
257        signers: &[SigningKey],
258    ) -> SignedBlock {
259        let next_keys = validator_set(3).1;
260        let header = BlockHeader::new_dummy(1, parent.commitment(), next_keys);
261        let signatures = sign_all(parent_keys, signers, header.commitment());
262        SignedBlock::new_unchecked(header, empty_body(), signatures)
263    }
264
265    #[test]
266    fn validate_accepts_committed_signers() {
267        let (signers, keys) = validator_set(3);
268        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
269        block_one(&parent, &keys, &signers).validate(Some(&parent)).unwrap();
270    }
271
272    #[test]
273    fn validate_accepts_single_validator() {
274        let (signers, keys) = validator_set(1);
275        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
276        block_one(&parent, &keys, &signers).validate(Some(&parent)).unwrap();
277    }
278
279    #[test]
280    fn validate_rejects_uncommitted_signers() {
281        let (_, keys) = validator_set(3);
282        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
283        let next_keys = validator_set(3).1;
284        let header = BlockHeader::new_dummy(1, parent.commitment(), next_keys);
285
286        // The block is signed by a full, valid validator set of the same size the parent never
287        // committed.
288        let (impostor_signers, impostor_keys) = validator_set(3);
289        let signatures = sign_all(&impostor_keys, &impostor_signers, header.commitment());
290        let block = SignedBlock::new_unchecked(header, empty_body(), signatures);
291
292        let result = block.validate(Some(&parent));
293        assert!(matches!(result, Err(SignedBlockError::InvalidSignatureAtPosition { .. })));
294    }
295}