celestia_types/block/
commit.rs

1//! Commitment types for blocks
2
3use tendermint::block::{Commit, CommitSig, Id};
4use tendermint::signature::SIGNATURE_LENGTH;
5use tendermint::{chain, vote, Vote};
6
7use crate::block::GENESIS_HEIGHT;
8use crate::hash::Hash;
9use crate::{bail_validation, Error, Result, ValidateBasic, ValidationError};
10
11impl ValidateBasic for Commit {
12    fn validate_basic(&self) -> Result<(), ValidationError> {
13        if self.height.value() >= GENESIS_HEIGHT {
14            if is_zero(&self.block_id) {
15                bail_validation!("block_id is zero")
16            }
17
18            if self.signatures.is_empty() {
19                bail_validation!("no signatures in commit")
20            }
21
22            for commit_sig in &self.signatures {
23                commit_sig.validate_basic()?;
24            }
25        }
26        Ok(())
27    }
28}
29
30impl ValidateBasic for CommitSig {
31    fn validate_basic(&self) -> Result<(), ValidationError> {
32        match self {
33            CommitSig::BlockIdFlagAbsent => (),
34            CommitSig::BlockIdFlagCommit { signature, .. }
35            | CommitSig::BlockIdFlagNil { signature, .. } => {
36                if let Some(signature) = signature {
37                    if signature.as_bytes().is_empty() {
38                        bail_validation!("no signature in commit sig")
39                    }
40                    if signature.as_bytes().len() != SIGNATURE_LENGTH {
41                        bail_validation!(
42                            "signature ({:?}) length != required ({})",
43                            signature.as_bytes(),
44                            SIGNATURE_LENGTH
45                        )
46                    }
47                } else {
48                    bail_validation!("no signature in commit sig")
49                }
50            }
51        }
52
53        Ok(())
54    }
55}
56
57/// An extension trait for the [`Commit`] to perform additional actions.
58///
59/// [`Commit`]: tendermint::block::Commit
60pub trait CommitExt {
61    /// Get the signed [`Vote`] from the [`Commit`] at the given index.
62    ///
63    /// [`Commit`]: tendermint::block::Commit
64    /// [`Vote`]: tendermint::Vote
65    fn vote_sign_bytes(&self, chain_id: &chain::Id, signature_idx: usize) -> Result<Vec<u8>>;
66}
67
68impl CommitExt for Commit {
69    fn vote_sign_bytes(&self, chain_id: &chain::Id, signature_idx: usize) -> Result<Vec<u8>> {
70        let sig =
71            self.signatures
72                .get(signature_idx)
73                .cloned()
74                .ok_or(Error::InvalidSignatureIndex(
75                    signature_idx,
76                    self.height.value(),
77                ))?;
78
79        let (validator_address, timestamp, signature) = match sig {
80            CommitSig::BlockIdFlagCommit {
81                validator_address,
82                timestamp,
83                signature,
84            }
85            | CommitSig::BlockIdFlagNil {
86                validator_address,
87                timestamp,
88                signature,
89            } => (validator_address, timestamp, signature),
90            CommitSig::BlockIdFlagAbsent => return Err(Error::UnexpectedAbsentSignature),
91        };
92
93        let vote = Vote {
94            vote_type: vote::Type::Precommit,
95            height: self.height,
96            round: self.round,
97            block_id: Some(self.block_id),
98            timestamp: Some(timestamp),
99            validator_address,
100            validator_index: signature_idx.try_into()?,
101            signature,
102            extension: Vec::new(),
103            extension_signature: None,
104        };
105
106        Ok(vote.into_signable_vec(chain_id.clone()))
107    }
108}
109
110fn is_zero(id: &Id) -> bool {
111    matches!(id.hash, Hash::None)
112        && matches!(id.part_set_header.hash, Hash::None)
113        && id.part_set_header.total == 0
114}
115
116#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
117pub use wbg::*;
118
119#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
120mod wbg {
121    use tendermint::block::{Commit, CommitSig};
122    use wasm_bindgen::prelude::*;
123
124    use crate::block::JsBlockId;
125    use crate::signature::JsSignature;
126
127    /// Commit contains the justification (ie. a set of signatures) that a block was
128    /// committed by a set of validators.
129    #[derive(Clone, Debug)]
130    #[wasm_bindgen(getter_with_clone, js_name = "Commit")]
131    pub struct JsCommit {
132        /// Block height
133        pub height: u64,
134        /// Round
135        pub round: u32,
136        /// Block ID
137        pub block_id: JsBlockId,
138        /// Signatures
139        pub signatures: Vec<JsCommitSig>,
140    }
141
142    impl From<Commit> for JsCommit {
143        fn from(value: Commit) -> Self {
144            JsCommit {
145                height: value.height.into(),
146                round: value.round.into(),
147                block_id: value.block_id.into(),
148                signatures: value.signatures.into_iter().map(Into::into).collect(),
149            }
150        }
151    }
152
153    /// CommitSig represents a signature of a validator. It’s a part of the Commit and can
154    /// be used to reconstruct the vote set given the validator set.
155    #[derive(Clone, Debug)]
156    #[wasm_bindgen(getter_with_clone, js_name = "CommitSig")]
157    pub struct JsCommitSig {
158        /// vote type of a validator
159        pub vote_type: JsCommitVoteType,
160        /// vote, if received
161        pub vote: Option<JsCommitVote>,
162    }
163
164    impl From<CommitSig> for JsCommitSig {
165        fn from(value: CommitSig) -> Self {
166            match value {
167                CommitSig::BlockIdFlagAbsent => JsCommitSig {
168                    vote_type: JsCommitVoteType::BlockIdFlagAbsent,
169                    vote: None,
170                },
171                CommitSig::BlockIdFlagCommit {
172                    validator_address,
173                    timestamp,
174                    signature,
175                } => JsCommitSig {
176                    vote_type: JsCommitVoteType::BlockIdFlagCommit,
177                    vote: Some(JsCommitVote {
178                        validator_address: validator_address.to_string(),
179                        timestamp: timestamp.to_rfc3339(),
180                        signature: signature.map(Into::into),
181                    }),
182                },
183                CommitSig::BlockIdFlagNil {
184                    validator_address,
185                    timestamp,
186                    signature,
187                } => JsCommitSig {
188                    vote_type: JsCommitVoteType::BlockIdFlagNil,
189                    vote: Some(JsCommitVote {
190                        validator_address: validator_address.to_string(),
191                        timestamp: timestamp.to_rfc3339(),
192                        signature: signature.map(Into::into),
193                    }),
194                },
195            }
196        }
197    }
198
199    #[derive(Clone, Copy, Debug)]
200    #[wasm_bindgen(js_name = "CommitVoteType")]
201    #[allow(clippy::enum_variant_names)] // keep the tendermint names
202    pub enum JsCommitVoteType {
203        /// no vote was received from a validator.
204        BlockIdFlagAbsent,
205        /// voted for the Commit.BlockID.
206        BlockIdFlagCommit,
207        /// voted for nil
208        BlockIdFlagNil,
209    }
210
211    /// Value of the validator vote
212    #[derive(Clone, Debug)]
213    #[wasm_bindgen(getter_with_clone, js_name = "CommitVote")]
214    pub struct JsCommitVote {
215        /// Address of the voting validator
216        pub validator_address: String,
217        /// Timestamp
218        pub timestamp: String,
219        /// Signature
220        pub signature: Option<JsSignature>,
221    }
222}
223
224#[cfg(feature = "uniffi")]
225pub mod uniffi_types {
226    use tendermint::block::{Commit as TendermintCommit, CommitSig as TendermintCommitSig};
227    use uniffi::{Enum, Record};
228
229    use crate::block::uniffi_types::BlockId;
230    use crate::error::UniffiConversionError;
231    use crate::signature::uniffi_types::Signature;
232    use crate::state::UniffiAccountId;
233    use crate::uniffi_types::Time;
234
235    /// Commit contains the justification (ie. a set of signatures) that a block was
236    /// committed by a set of validators.
237    #[derive(Record)]
238    pub struct Commit {
239        /// Block height
240        pub height: u64,
241        /// Round
242        pub round: u32,
243        /// Block ID
244        pub block_id: BlockId,
245        /// Signatures
246        pub signatures: Vec<CommitSig>,
247    }
248
249    impl TryFrom<TendermintCommit> for Commit {
250        type Error = UniffiConversionError;
251
252        fn try_from(value: TendermintCommit) -> Result<Self, Self::Error> {
253            Ok(Commit {
254                height: value.height.value(),
255                round: value.round.value(),
256                block_id: value.block_id.into(),
257                signatures: value
258                    .signatures
259                    .into_iter()
260                    .map(|s| s.try_into())
261                    .collect::<Result<Vec<_>, _>>()?,
262            })
263        }
264    }
265
266    impl TryFrom<Commit> for TendermintCommit {
267        type Error = UniffiConversionError;
268
269        fn try_from(value: Commit) -> Result<Self, Self::Error> {
270            Ok(TendermintCommit {
271                height: value
272                    .height
273                    .try_into()
274                    .map_err(|_| UniffiConversionError::HeaderHeightOutOfRange)?,
275                round: value
276                    .round
277                    .try_into()
278                    .map_err(|_| UniffiConversionError::InvalidRoundIndex)?,
279                block_id: value.block_id.try_into()?,
280                signatures: value
281                    .signatures
282                    .into_iter()
283                    .map(|s| s.try_into())
284                    .collect::<Result<_, _>>()?,
285            })
286        }
287    }
288
289    uniffi::custom_type!(TendermintCommit, Commit, {
290        remote,
291        try_lift: |value| Ok(value.try_into()?),
292        lower: |value| value.try_into().expect("valid tendermint timestamp")
293    });
294
295    /// CommitSig represents a signature of a validator. It’s a part of the Commit and can
296    /// be used to reconstruct the vote set given the validator set.
297    #[derive(Enum)]
298    #[allow(clippy::enum_variant_names)] // keep the tendermint names
299    pub enum CommitSig {
300        /// no vote was received from a validator.
301        BlockIdFlagAbsent,
302        /// voted for the Commit.BlockID.
303        BlockIdFlagCommit {
304            /// Validator address
305            validator_address: UniffiAccountId,
306            /// Timestamp
307            timestamp: Time,
308            /// Signature of vote
309            signature: Option<Signature>,
310        },
311        /// voted for nil
312        BlockIdFlagNil {
313            /// Validator address
314            validator_address: UniffiAccountId,
315            /// Timestamp
316            timestamp: Time,
317            /// Signature of vote
318            signature: Option<Signature>,
319        },
320    }
321
322    impl TryFrom<TendermintCommitSig> for CommitSig {
323        type Error = UniffiConversionError;
324
325        fn try_from(value: TendermintCommitSig) -> Result<Self, Self::Error> {
326            Ok(match value {
327                TendermintCommitSig::BlockIdFlagAbsent => CommitSig::BlockIdFlagAbsent,
328                TendermintCommitSig::BlockIdFlagCommit {
329                    validator_address,
330                    timestamp,
331                    signature,
332                } => CommitSig::BlockIdFlagCommit {
333                    validator_address: validator_address.into(),
334                    timestamp: timestamp.try_into()?,
335                    signature: signature.map(Into::into),
336                },
337                TendermintCommitSig::BlockIdFlagNil {
338                    validator_address,
339                    timestamp,
340                    signature,
341                } => CommitSig::BlockIdFlagNil {
342                    validator_address: validator_address.into(),
343                    timestamp: timestamp.try_into()?,
344                    signature: signature.map(Into::into),
345                },
346            })
347        }
348    }
349
350    impl TryFrom<CommitSig> for TendermintCommitSig {
351        type Error = UniffiConversionError;
352
353        fn try_from(value: CommitSig) -> Result<Self, Self::Error> {
354            Ok(match value {
355                CommitSig::BlockIdFlagAbsent => TendermintCommitSig::BlockIdFlagAbsent,
356                CommitSig::BlockIdFlagCommit {
357                    validator_address,
358                    timestamp,
359                    signature,
360                } => TendermintCommitSig::BlockIdFlagCommit {
361                    validator_address: validator_address.try_into()?,
362                    timestamp: timestamp.try_into()?,
363                    signature: signature.map(TryInto::try_into).transpose()?,
364                },
365                CommitSig::BlockIdFlagNil {
366                    validator_address,
367                    timestamp,
368                    signature,
369                } => TendermintCommitSig::BlockIdFlagNil {
370                    validator_address: validator_address.try_into()?,
371                    timestamp: timestamp.try_into()?,
372                    signature: signature.map(TryInto::try_into).transpose()?,
373                },
374            })
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[cfg(target_arch = "wasm32")]
384    use wasm_bindgen_test::wasm_bindgen_test as test;
385
386    fn sample_commit() -> Commit {
387        serde_json::from_str(r#"{
388          "height": "1",
389          "round": 0,
390          "block_id": {
391            "hash": "17F7D5108753C39714DCA67E6A73CE855C6EA9B0071BBD4FFE5D2EF7F3973BFC",
392            "parts": {
393              "total": 1,
394              "hash": "BEEBB79CDA7D0574B65864D3459FAC7F718B82496BD7FE8B6288BF0A98C8EA22"
395            }
396          },
397          "signatures": [
398            {
399              "block_id_flag": 2,
400              "validator_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
401              "timestamp": "2023-06-23T10:40:48.769228056Z",
402              "signature": "HNn4c02eCt2+nGuBs55L8f3DAz9cgy9psLFuzhtg2XCWnlkt2V43TX2b54hQNi7C0fepBEteA3GC01aJM/JJCg=="
403            }
404          ]
405        }"#).unwrap()
406    }
407
408    #[test]
409    fn block_id_is_zero() {
410        let mut block_id = sample_commit().block_id;
411        assert!(!is_zero(&block_id));
412
413        block_id.hash = Hash::None;
414        assert!(!is_zero(&block_id));
415
416        block_id.part_set_header.hash = Hash::None;
417        assert!(!is_zero(&block_id));
418
419        block_id.part_set_header.total = 0;
420        assert!(is_zero(&block_id));
421    }
422
423    #[test]
424    fn commit_validate_basic() {
425        sample_commit().validate_basic().unwrap();
426    }
427
428    #[test]
429    fn commit_validate_invalid_block_id() {
430        let mut commit = sample_commit();
431        commit.block_id.hash = Hash::None;
432        commit.block_id.part_set_header.hash = Hash::None;
433        commit.block_id.part_set_header.total = 0;
434
435        commit.validate_basic().unwrap_err();
436    }
437
438    #[test]
439    fn commit_validate_no_signatures() {
440        let mut commit = sample_commit();
441        commit.signatures = vec![];
442
443        commit.validate_basic().unwrap_err();
444    }
445
446    #[test]
447    fn commit_validate_absent() {
448        let mut commit = sample_commit();
449        commit.signatures[0] = CommitSig::BlockIdFlagAbsent;
450
451        commit.validate_basic().unwrap();
452    }
453
454    #[test]
455    fn commit_validate_no_signature_in_sig() {
456        let mut commit = sample_commit();
457        let CommitSig::BlockIdFlagCommit {
458            validator_address,
459            timestamp,
460            ..
461        } = commit.signatures[0].clone()
462        else {
463            unreachable!()
464        };
465        commit.signatures[0] = CommitSig::BlockIdFlagCommit {
466            signature: None,
467            timestamp,
468            validator_address,
469        };
470
471        commit.validate_basic().unwrap_err();
472    }
473
474    #[test]
475    fn vote_sign_bytes() {
476        let commit = sample_commit();
477
478        let signable_bytes = commit
479            .vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 0)
480            .unwrap();
481
482        assert_eq!(
483            signable_bytes,
484            vec![
485                108u8, 8, 2, 17, 1, 0, 0, 0, 0, 0, 0, 0, 34, 72, 10, 32, 23, 247, 213, 16, 135, 83,
486                195, 151, 20, 220, 166, 126, 106, 115, 206, 133, 92, 110, 169, 176, 7, 27, 189, 79,
487                254, 93, 46, 247, 243, 151, 59, 252, 18, 36, 8, 1, 18, 32, 190, 235, 183, 156, 218,
488                125, 5, 116, 182, 88, 100, 211, 69, 159, 172, 127, 113, 139, 130, 73, 107, 215,
489                254, 139, 98, 136, 191, 10, 152, 200, 234, 34, 42, 12, 8, 176, 237, 213, 164, 6,
490                16, 152, 250, 229, 238, 2, 50, 7, 112, 114, 105, 118, 97, 116, 101
491            ]
492        );
493    }
494
495    #[test]
496    fn vote_sign_bytes_absent_signature() {
497        let mut commit = sample_commit();
498        commit.signatures[0] = CommitSig::BlockIdFlagAbsent;
499
500        let res = commit.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 0);
501
502        assert!(matches!(res, Err(Error::UnexpectedAbsentSignature)));
503    }
504
505    #[test]
506    fn vote_sign_bytes_non_existent_signature() {
507        let commit = sample_commit();
508
509        let res = commit.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 3);
510
511        assert!(matches!(res, Err(Error::InvalidSignatureIndex(..))));
512    }
513}