1use tendermint::block::{Commit, CommitSig, Id};
4use tendermint::signature::SIGNATURE_LENGTH;
5use tendermint::{Vote, chain, vote};
6
7use crate::block::GENESIS_HEIGHT;
8use crate::hash::Hash;
9use crate::{Error, Result, ValidateBasic, ValidationError, bail_validation};
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
57pub trait CommitExt {
61 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 #[derive(Clone, Debug)]
130 #[wasm_bindgen(getter_with_clone, js_name = "Commit")]
131 pub struct JsCommit {
132 pub height: u64,
134 pub round: u32,
136 pub block_id: JsBlockId,
138 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 #[derive(Clone, Debug)]
156 #[wasm_bindgen(getter_with_clone, js_name = "CommitSig")]
157 pub struct JsCommitSig {
158 pub vote_type: JsCommitVoteType,
160 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)] pub enum JsCommitVoteType {
203 BlockIdFlagAbsent,
205 BlockIdFlagCommit,
207 BlockIdFlagNil,
209 }
210
211 #[derive(Clone, Debug)]
213 #[wasm_bindgen(getter_with_clone, js_name = "CommitVote")]
214 pub struct JsCommitVote {
215 pub validator_address: String,
217 pub timestamp: String,
219 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 #[derive(Record)]
238 pub struct Commit {
239 pub height: u64,
241 pub round: u32,
243 pub block_id: BlockId,
245 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 #[derive(Enum)]
298 #[allow(clippy::enum_variant_names)] pub enum CommitSig {
300 BlockIdFlagAbsent,
302 BlockIdFlagCommit {
304 validator_address: UniffiAccountId,
306 timestamp: Time,
308 signature: Option<Signature>,
310 },
311 BlockIdFlagNil {
313 validator_address: UniffiAccountId,
315 timestamp: Time,
317 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}