celestia_types/
block.rs

1//! Blocks within the chains of a Tendermint network
2
3use celestia_proto::tendermint_celestia_mods::types::Block as RawBlock;
4use serde::{Deserialize, Serialize};
5use tendermint::block::{Commit, CommitSig, Header, Id};
6use tendermint::signature::SIGNATURE_LENGTH;
7use tendermint::{chain, evidence, vote, Vote};
8use tendermint_proto::Protobuf;
9
10use crate::consts::{genesis::MAX_CHAIN_ID_LEN, version};
11use crate::hash::Hash;
12use crate::{bail_validation, Error, Result, ValidateBasic, ValidationError};
13
14mod data;
15
16pub use data::Data;
17
18pub(crate) const GENESIS_HEIGHT: u64 = 1;
19
20/// The height of the block in Celestia network.
21pub type Height = tendermint::block::Height;
22
23/// Blocks consist of a header, transactions, votes (the commit), and a list of
24/// evidence of malfeasance (i.e. signing conflicting votes).
25///
26/// This is a modified version of [`tendermint::block::Block`] which contains
27/// [modifications](data-mod) that Celestia introduced.
28///
29/// [data-mod]: https://github.com/celestiaorg/celestia-core/blob/a1268f7ae3e688144a613c8a439dd31818aae07d/proto/tendermint/types/types.proto#L84-L104
30#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
31#[serde(try_from = "RawBlock", into = "RawBlock")]
32pub struct Block {
33    /// Block header
34    pub header: Header,
35
36    /// Transaction data
37    pub data: Data,
38
39    /// Evidence of malfeasance
40    pub evidence: evidence::List,
41
42    /// Last commit, should be `None` for the initial block.
43    pub last_commit: Option<Commit>,
44}
45
46impl Block {
47    /// Builds a new [`Block`], based on the given [`Header`], [`Data`], evidence, and last commit.
48    pub fn new(
49        header: Header,
50        data: Data,
51        evidence: evidence::List,
52        last_commit: Option<Commit>,
53    ) -> Self {
54        Block {
55            header,
56            data,
57            evidence,
58            last_commit,
59        }
60    }
61
62    /// Get header
63    pub fn header(&self) -> &Header {
64        &self.header
65    }
66
67    /// Get data
68    pub fn data(&self) -> &Data {
69        &self.data
70    }
71
72    /// Get evidence
73    pub fn evidence(&self) -> &evidence::List {
74        &self.evidence
75    }
76
77    /// Get last commit
78    pub fn last_commit(&self) -> &Option<Commit> {
79        &self.last_commit
80    }
81}
82
83impl Protobuf<RawBlock> for Block {}
84
85impl TryFrom<RawBlock> for Block {
86    type Error = Error;
87
88    fn try_from(value: RawBlock) -> Result<Self, Self::Error> {
89        let header: Header = value
90            .header
91            .ok_or_else(tendermint::Error::missing_header)?
92            .try_into()?;
93
94        // If last_commit is the default Commit, it is considered nil by Go.
95        let last_commit = value
96            .last_commit
97            .map(TryInto::try_into)
98            .transpose()?
99            .filter(|c| c != &Commit::default());
100
101        Ok(Block::new(
102            header,
103            value
104                .data
105                .ok_or_else(tendermint::Error::missing_data)?
106                .try_into()?,
107            value
108                .evidence
109                .map(TryInto::try_into)
110                .transpose()?
111                .unwrap_or_default(),
112            last_commit,
113        ))
114    }
115}
116
117impl From<Block> for RawBlock {
118    fn from(value: Block) -> Self {
119        RawBlock {
120            header: Some(value.header.into()),
121            data: Some(value.data.into()),
122            evidence: Some(value.evidence.into()),
123            last_commit: value.last_commit.map(Into::into),
124        }
125    }
126}
127
128impl ValidateBasic for Header {
129    fn validate_basic(&self) -> Result<(), ValidationError> {
130        if self.version.block != version::BLOCK_PROTOCOL {
131            bail_validation!(
132                "version block ({}) != block protocol ({})",
133                self.version.block,
134                version::BLOCK_PROTOCOL,
135            )
136        }
137
138        if self.chain_id.as_str().len() > MAX_CHAIN_ID_LEN {
139            bail_validation!(
140                "chain id ({}) len > maximum ({})",
141                self.chain_id,
142                MAX_CHAIN_ID_LEN
143            )
144        }
145
146        if self.height.value() == 0 {
147            bail_validation!("height == 0")
148        }
149
150        if self.height.value() == GENESIS_HEIGHT && self.last_block_id.is_some() {
151            bail_validation!("last_block_id == Some() at height {GENESIS_HEIGHT}");
152        }
153
154        if self.height.value() != GENESIS_HEIGHT && self.last_block_id.is_none() {
155            bail_validation!("last_block_id == None at height {}", self.height)
156        }
157
158        // NOTE: We do not validate `Hash` fields because they are type safe.
159        // In Go implementation the validation passes if their length is 0 or 32.
160        //
161        // NOTE: We do not validate `app_hash` because if can be anything
162
163        Ok(())
164    }
165}
166
167impl ValidateBasic for Commit {
168    fn validate_basic(&self) -> Result<(), ValidationError> {
169        if self.height.value() >= GENESIS_HEIGHT {
170            if is_zero(&self.block_id) {
171                bail_validation!("block_id is zero")
172            }
173
174            if self.signatures.is_empty() {
175                bail_validation!("no signatures in commit")
176            }
177
178            for commit_sig in &self.signatures {
179                commit_sig.validate_basic()?;
180            }
181        }
182        Ok(())
183    }
184}
185
186impl ValidateBasic for CommitSig {
187    fn validate_basic(&self) -> Result<(), ValidationError> {
188        match self {
189            CommitSig::BlockIdFlagAbsent => (),
190            CommitSig::BlockIdFlagCommit { signature, .. }
191            | CommitSig::BlockIdFlagNil { signature, .. } => {
192                if let Some(signature) = signature {
193                    if signature.as_bytes().is_empty() {
194                        bail_validation!("no signature in commit sig")
195                    }
196                    if signature.as_bytes().len() != SIGNATURE_LENGTH {
197                        bail_validation!(
198                            "signature ({:?}) length != required ({})",
199                            signature.as_bytes(),
200                            SIGNATURE_LENGTH
201                        )
202                    }
203                } else {
204                    bail_validation!("no signature in commit sig")
205                }
206            }
207        }
208
209        Ok(())
210    }
211}
212
213/// An extension trait for the [`Commit`] to perform additional actions.
214///
215/// [`Commit`]: tendermint::block::Commit
216pub trait CommitExt {
217    /// Get the signed [`Vote`] from the [`Commit`] at the given index.
218    ///
219    /// [`Commit`]: tendermint::block::Commit
220    /// [`Vote`]: tendermint::Vote
221    fn vote_sign_bytes(&self, chain_id: &chain::Id, signature_idx: usize) -> Result<Vec<u8>>;
222}
223
224impl CommitExt for Commit {
225    fn vote_sign_bytes(&self, chain_id: &chain::Id, signature_idx: usize) -> Result<Vec<u8>> {
226        let sig =
227            self.signatures
228                .get(signature_idx)
229                .cloned()
230                .ok_or(Error::InvalidSignatureIndex(
231                    signature_idx,
232                    self.height.value(),
233                ))?;
234
235        let (validator_address, timestamp, signature) = match sig {
236            CommitSig::BlockIdFlagCommit {
237                validator_address,
238                timestamp,
239                signature,
240            }
241            | CommitSig::BlockIdFlagNil {
242                validator_address,
243                timestamp,
244                signature,
245            } => (validator_address, timestamp, signature),
246            CommitSig::BlockIdFlagAbsent => return Err(Error::UnexpectedAbsentSignature),
247        };
248
249        let vote = Vote {
250            vote_type: vote::Type::Precommit,
251            height: self.height,
252            round: self.round,
253            block_id: Some(self.block_id),
254            timestamp: Some(timestamp),
255            validator_address,
256            validator_index: signature_idx.try_into()?,
257            signature,
258            extension: Vec::new(),
259            extension_signature: None,
260        };
261
262        Ok(vote.into_signable_vec(chain_id.clone()))
263    }
264}
265
266fn is_zero(id: &Id) -> bool {
267    matches!(id.hash, Hash::None)
268        && matches!(id.part_set_header.hash, Hash::None)
269        && id.part_set_header.total == 0
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::hash::HashExt;
276
277    #[cfg(target_arch = "wasm32")]
278    use wasm_bindgen_test::wasm_bindgen_test as test;
279
280    fn sample_commit() -> Commit {
281        serde_json::from_str(r#"{
282          "height": "1",
283          "round": 0,
284          "block_id": {
285            "hash": "17F7D5108753C39714DCA67E6A73CE855C6EA9B0071BBD4FFE5D2EF7F3973BFC",
286            "parts": {
287              "total": 1,
288              "hash": "BEEBB79CDA7D0574B65864D3459FAC7F718B82496BD7FE8B6288BF0A98C8EA22"
289            }
290          },
291          "signatures": [
292            {
293              "block_id_flag": 2,
294              "validator_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
295              "timestamp": "2023-06-23T10:40:48.769228056Z",
296              "signature": "HNn4c02eCt2+nGuBs55L8f3DAz9cgy9psLFuzhtg2XCWnlkt2V43TX2b54hQNi7C0fepBEteA3GC01aJM/JJCg=="
297            }
298          ]
299        }"#).unwrap()
300    }
301
302    fn sample_header() -> Header {
303        serde_json::from_str(r#"{
304          "version": {
305            "block": "11",
306            "app": "1"
307          },
308          "chain_id": "private",
309          "height": "1",
310          "time": "2023-06-23T10:40:48.410305119Z",
311          "last_block_id": {
312            "hash": "",
313            "parts": {
314              "total": 0,
315              "hash": ""
316            }
317          },
318          "last_commit_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
319          "data_hash": "3D96B7D238E7E0456F6AF8E7CDF0A67BD6CF9C2089ECB559C659DCAA1F880353",
320          "validators_hash": "64AEB6CA415A37540650FC04471974CE4FE88884CDD3300DF7BB27C1786871E9",
321          "next_validators_hash": "64AEB6CA415A37540650FC04471974CE4FE88884CDD3300DF7BB27C1786871E9",
322          "consensus_hash": "C0B6A634B72AE9687EA53B6D277A73ABA1386BA3CFC6D0F26963602F7F6FFCD6",
323          "app_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
324          "last_results_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
325          "evidence_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
326          "proposer_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53"
327        }"#).unwrap()
328    }
329
330    #[test]
331    fn block_id_is_zero() {
332        let mut block_id = sample_commit().block_id;
333        assert!(!is_zero(&block_id));
334
335        block_id.hash = Hash::None;
336        assert!(!is_zero(&block_id));
337
338        block_id.part_set_header.hash = Hash::None;
339        assert!(!is_zero(&block_id));
340
341        block_id.part_set_header.total = 0;
342        assert!(is_zero(&block_id));
343    }
344
345    #[test]
346    fn header_validate_basic() {
347        sample_header().validate_basic().unwrap();
348    }
349
350    #[test]
351    fn header_validate_invalid_block_version() {
352        let mut header = sample_header();
353        header.version.block = 1;
354
355        header.validate_basic().unwrap_err();
356    }
357
358    #[test]
359    fn header_validate_zero_height() {
360        let mut header = sample_header();
361        header.height = 0u32.into();
362
363        header.validate_basic().unwrap_err();
364    }
365
366    #[test]
367    fn header_validate_missing_last_block_id() {
368        let mut header = sample_header();
369        header.height = 2u32.into();
370
371        header.validate_basic().unwrap_err();
372    }
373
374    #[test]
375    fn header_validate_genesis_with_last_block_id() {
376        let mut header = sample_header();
377
378        header.last_block_id = Some(Id {
379            hash: Hash::default_sha256(),
380            ..Id::default()
381        });
382
383        header.validate_basic().unwrap_err();
384    }
385
386    #[test]
387    fn commit_validate_basic() {
388        sample_commit().validate_basic().unwrap();
389    }
390
391    #[test]
392    fn commit_validate_invalid_block_id() {
393        let mut commit = sample_commit();
394        commit.block_id.hash = Hash::None;
395        commit.block_id.part_set_header.hash = Hash::None;
396        commit.block_id.part_set_header.total = 0;
397
398        commit.validate_basic().unwrap_err();
399    }
400
401    #[test]
402    fn commit_validate_no_signatures() {
403        let mut commit = sample_commit();
404        commit.signatures = vec![];
405
406        commit.validate_basic().unwrap_err();
407    }
408
409    #[test]
410    fn commit_validate_absent() {
411        let mut commit = sample_commit();
412        commit.signatures[0] = CommitSig::BlockIdFlagAbsent;
413
414        commit.validate_basic().unwrap();
415    }
416
417    #[test]
418    fn commit_validate_no_signature_in_sig() {
419        let mut commit = sample_commit();
420        let CommitSig::BlockIdFlagCommit {
421            validator_address,
422            timestamp,
423            ..
424        } = commit.signatures[0].clone()
425        else {
426            unreachable!()
427        };
428        commit.signatures[0] = CommitSig::BlockIdFlagCommit {
429            signature: None,
430            timestamp,
431            validator_address,
432        };
433
434        commit.validate_basic().unwrap_err();
435    }
436
437    #[test]
438    fn vote_sign_bytes() {
439        let commit = sample_commit();
440
441        let signable_bytes = commit
442            .vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 0)
443            .unwrap();
444
445        assert_eq!(
446            signable_bytes,
447            vec![
448                108u8, 8, 2, 17, 1, 0, 0, 0, 0, 0, 0, 0, 34, 72, 10, 32, 23, 247, 213, 16, 135, 83,
449                195, 151, 20, 220, 166, 126, 106, 115, 206, 133, 92, 110, 169, 176, 7, 27, 189, 79,
450                254, 93, 46, 247, 243, 151, 59, 252, 18, 36, 8, 1, 18, 32, 190, 235, 183, 156, 218,
451                125, 5, 116, 182, 88, 100, 211, 69, 159, 172, 127, 113, 139, 130, 73, 107, 215,
452                254, 139, 98, 136, 191, 10, 152, 200, 234, 34, 42, 12, 8, 176, 237, 213, 164, 6,
453                16, 152, 250, 229, 238, 2, 50, 7, 112, 114, 105, 118, 97, 116, 101
454            ]
455        );
456    }
457
458    #[test]
459    fn vote_sign_bytes_absent_signature() {
460        let mut commit = sample_commit();
461        commit.signatures[0] = CommitSig::BlockIdFlagAbsent;
462
463        let res = commit.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 0);
464
465        assert!(matches!(res, Err(Error::UnexpectedAbsentSignature)));
466    }
467
468    #[test]
469    fn vote_sign_bytes_non_existent_signature() {
470        let commit = sample_commit();
471
472        let res = commit.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 3);
473
474        assert!(matches!(res, Err(Error::InvalidSignatureIndex(..))));
475    }
476}