1use 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
20pub type Height = tendermint::block::Height;
22
23#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
31#[serde(try_from = "RawBlock", into = "RawBlock")]
32pub struct Block {
33 pub header: Header,
35
36 pub data: Data,
38
39 pub evidence: evidence::List,
41
42 pub last_commit: Option<Commit>,
44}
45
46impl Block {
47 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 pub fn header(&self) -> &Header {
64 &self.header
65 }
66
67 pub fn data(&self) -> &Data {
69 &self.data
70 }
71
72 pub fn evidence(&self) -> &evidence::List {
74 &self.evidence
75 }
76
77 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 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 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
213pub trait CommitExt {
217 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}