• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / catalyst-libs / 19153566703

06 Nov 2025 11:58PM UTC coverage: 68.421% (-0.02%) from 68.442%
19153566703

Pull #628

github

web-flow
Merge fdbbd67b6 into b703f0134
Pull Request #628: chore(general): Migrate to the 2024 rust edition

321 of 463 new or added lines in 46 files covered. (69.33%)

11 existing lines in 5 files now uncovered.

13921 of 20346 relevant lines covered (68.42%)

2897.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.06
/rust/immutable-ledger/src/serialize.rs
1
//! Block structure
2

3
//! Block structure
4

5
use anyhow::{Ok, anyhow, bail};
6
use blake2b_simd::{self, Params};
7
use uuid::Uuid;
8

9
/// Genesis block MUST have 0 value height.
10
const GENESIS_BLOCK: i64 = 0;
11

12
/// Block header size
13
#[derive(Debug, Clone, PartialEq)]
14
pub struct BlockHeaderSize(usize);
15

16
/// Signatures
17
#[derive(Debug, Clone, PartialEq)]
18
pub struct Signatures(Vec<Vec<u8>>);
19

20
/// Decoded block
21
pub type DecodedBlock = (BlockHeader, BlockData, Signatures);
22

23
/// Encoded genesis Block contents as cbor, used for hash validation
24
#[derive(Debug, Clone, PartialEq)]
25
pub struct EncodedGenesisBlockContents(pub Vec<u8>);
26

27
/// Choice of hash function:
28
/// must be the same as the hash of the previous block.
29
#[derive(Debug, Clone, PartialEq)]
30
pub enum HashFunction {
31
    /// BLAKE3 is based on an optimized instance of the established hash function BLAKE2
32
    /// and on the original Bao tree mode
33
    Blake3,
34
    /// BLAKE2b-512 produces digest side of 512 bits.
35
    Blake2b,
36
}
37

38
/// Kid (The key identifier) size in bytes
39
const KID_BYTES: usize = 16;
40

41
/// Key ID - Blake2b-128 hash of the Role 0 Certificate defining the Session public key.
42
/// BLAKE2b-128 produces digest side of 16 bytes.
43
#[derive(Debug, Clone, Copy, PartialEq)]
44
pub struct Kid(pub [u8; KID_BYTES]);
45

46
/// Encoded whole block including block header, cbor encoded block data and signatures.
47
pub type EncodedBlock = Vec<u8>;
48

49
/// Produce BLAKE3 hash
50
pub(crate) fn blake3(value: &[u8]) -> anyhow::Result<[u8; 32]> {
×
51
    Ok(*blake3::hash(value).as_bytes())
×
52
}
×
53

54
/// BLAKE2b-512 produces digest side of 512 bits.
55
pub(crate) fn blake2b_512(value: &[u8]) -> anyhow::Result<[u8; 64]> {
1,536✔
56
    let h = Params::new().hash_length(64).hash(value);
1,536✔
57
    let b = h.as_bytes();
1,536✔
58
    b.try_into()
1,536✔
59
        .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_512, expected 64 got {}", b.len()))
1,536✔
60
}
1,536✔
61

62
/// Block data
63
#[derive(Debug, Clone, PartialEq)]
64
pub struct BlockData(Vec<u8>);
65

66
/// CBOR tag for timestamp
67
const TIMESTAMP_CBOR_TAG: u64 = 1;
68

69
/// CBOR tag for UUID
70
const UUID_CBOR_TAG: u64 = 37;
71

72
// CBOR tags for BLAKE2 and BLAKE3 hash functions
73
// `https://github.com/input-output-hk/catalyst-voices/blob/main/docs/src/catalyst-standards/cbor_tags/blake.md`
74

75
/// CBOR tag for UUID
76
const BLAKE3_CBOR_TAG: u64 = 32781;
77

78
/// CBOR tag for blake2b
79
const BLAKE_2B_CBOR_TAG: u64 = 32782;
80

81
/// Block
82
pub struct Block {
83
    /// Block header
84
    pub block_header: BlockHeader,
85
    /// cbor encoded block data
86
    pub block_data: BlockData,
87
    /// Validators signatures
88
    pub validator_sigs: Signatures,
89
}
90

91
impl Block {
92
    /// New block
93
    #[must_use]
94
    pub fn new(
1,536✔
95
        block_header: BlockHeader,
1,536✔
96
        block_data: BlockData,
1,536✔
97
        validator_sigs: Signatures,
1,536✔
98
    ) -> Self {
1,536✔
99
        Self {
1,536✔
100
            block_header,
1,536✔
101
            block_data,
1,536✔
102
            validator_sigs,
1,536✔
103
        }
1,536✔
104
    }
1,536✔
105

106
    /// Encode block
107
    /// ## Errors
108
    ///
109
    /// Returns an error if encoding fails.
110
    pub fn to_bytes(&self) -> anyhow::Result<Vec<u8>> {
1,024✔
111
        // Enforce block data to be cbor encoded in the form of CBOR byte strings
112
        // which are just (ordered) series of bytes without further interpretation
113
        let _ = minicbor::Decoder::new(&self.block_data.0).bytes()?;
1,024✔
114

115
        // cbor encode block hdr
116
        let encoded_block_hdr = self.block_header.to_bytes()?;
768✔
117

118
        let out: Vec<u8> = Vec::new();
768✔
119
        let mut encoder = minicbor::Encoder::new(out);
768✔
120
        let signatures = &self.validator_sigs;
768✔
121
        encoder.array(signatures.0.len().try_into()?)?;
768✔
122
        for sig in signatures.0.clone() {
1,536✔
123
            encoder.bytes(&sig)?;
1,536✔
124
        }
125

126
        let signatures = encoder.writer().clone();
768✔
127

128
        let block_encoding = [
768✔
129
            [encoded_block_hdr, self.block_data.0.clone()].concat(),
768✔
130
            signatures,
768✔
131
        ]
768✔
132
        .concat();
768✔
133

134
        Ok(block_encoding)
768✔
135
    }
1,024✔
136

137
    /// Decode block
138
    /// ## Errors
139
    ///
140
    /// Returns an error if decoding fails.
141
    pub fn from_bytes(
256✔
142
        encoded_block: &[u8]
256✔
143
    ) -> anyhow::Result<(BlockHeader, BlockData, Signatures)> {
256✔
144
        // Decoded block hdr
145
        let (block_hdr, block_hdr_size, _) = BlockHeader::from_bytes(encoded_block)?;
256✔
146

147
        // Init decoder
148
        let mut cbor_decoder = minicbor::Decoder::new(encoded_block);
256✔
149

150
        // Decode remaining block, set position after block hdr data.
151
        cbor_decoder.set_position(block_hdr_size.0);
256✔
152

153
        // Block data
154
        let block_data = cbor_decoder
256✔
155
            .bytes()
256✔
156
            .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for block data : {e}")))?;
256✔
157

158
        // Extract signatures
159
        let number_of_sigs = cbor_decoder
256✔
160
            .array()?
256✔
161
            .ok_or(anyhow::anyhow!(format!("Invalid signature.")))?;
256✔
162

163
        let mut sigs = Vec::new();
256✔
164
        for _sig in 0..number_of_sigs {
512✔
165
            let sig = cbor_decoder
512✔
166
                .bytes()
512✔
167
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor signature : {e}")))?;
512✔
168
            sigs.push(sig.to_owned());
512✔
169
        }
170

171
        Ok((block_hdr, BlockData(block_data.to_vec()), Signatures(sigs)))
256✔
172
    }
256✔
173

174
    /// Validate block against previous block or validate itself if genesis block.
175
    /// ## Errors
176
    ///
177
    /// Returns an error if validation fails.
178
    pub fn validate(
768✔
179
        &self,
768✔
180
        previous_block: Option<Block>,
768✔
181
    ) -> anyhow::Result<()> {
768✔
182
        if let Some(previous_block) = previous_block {
768✔
183
            // Standard block
184
            let hashed_previous_block = match self.block_header.previous_block_hash.0 {
256✔
185
                HashFunction::Blake3 => {
186
                    (
187
                        HashFunction::Blake3,
×
188
                        blake3(&previous_block.to_bytes()?)?.to_vec(),
×
189
                    )
190
                },
191
                HashFunction::Blake2b => {
192
                    (
193
                        HashFunction::Blake2b,
256✔
194
                        blake2b_512(&previous_block.to_bytes()?)?.to_vec(),
256✔
195
                    )
196
                },
197
            };
198

199
            // chain_id MUST be the same as for the previous block (except for genesis).
200
            if self.block_header.chain_id != previous_block.block_header.chain_id {
256✔
201
                return Err(anyhow::anyhow!(
×
NEW
202
                    "Module: Immutable ledger,  Message: Chain_id MUST be the same as for the previous block {:?} {:?}",
×
NEW
203
                    self.block_header,
×
NEW
204
                    previous_block.block_header
×
NEW
205
                ));
×
206
            }
256✔
207

208
            // height MUST be incremented by 1 from the previous block height value (except for
209
            // genesis and final block). Genesis block MUST have 0 value. Final block MUST hash be
210
            // incremented by 1 from the previous block height and changed the sign to negative.
211
            // E.g. previous block height is 9 and the Final block height is -10.
212
            let Some(block_height) = previous_block.block_header.height.checked_add(1) else {
256✔
213
                return Err(anyhow!(
×
214
                    "Module: Immutable ledger, Message: block height overflow: {}",
×
215
                    previous_block.block_header.height
×
216
                ));
×
217
            };
218

219
            if self.block_header.height != block_height {
256✔
220
                return Err(anyhow!(
×
221
                    "Module: Immutable ledger, Message: height validation failed: {:?} {:?}",
×
222
                    self.block_header,
×
223
                    previous_block.block_header
×
224
                ));
×
225
            }
256✔
226

227
            // timestamp MUST be greater or equals than the timestamp of the previous block (except
228
            // for genesis)
229
            if self.block_header.block_time_stamp <= previous_block.block_header.block_time_stamp {
256✔
230
                return Err(anyhow::anyhow!(
×
231
                    "Module: Immutable ledger,  Message: timestamp validation failed: {:?} {:?}",
×
232
                    self.block_header,
×
233
                    previous_block.block_header
×
234
                ));
×
235
            }
256✔
236

237
            // prev_block_id MUST be a hash of the previous block bytes (except for genesis).
238
            if self.block_header.previous_block_hash != (hashed_previous_block) {
256✔
239
                return Err(anyhow::anyhow!(
×
240
                    "Module: Immutable ledger,  Message: previous hash validation failed: {:?} {:?}",
×
241
                    self.block_header,
×
242
                    previous_block.block_header
×
243
                ));
×
244
            }
256✔
245

246
            // ledger_type MUST be the same as for the previous block if present (except for
247
            // genesis).
248
            if self.block_header.ledger_type != previous_block.block_header.ledger_type {
256✔
249
                return Err(anyhow::anyhow!(
×
250
                    "Module: Immutable ledger,  Message: ledger type validation failed: {:?} {:?}",
×
251
                    self.block_header,
×
252
                    previous_block.block_header
×
253
                ));
×
254
            }
256✔
255

256
            // purpose_id MUST be the same as for the previous block if present (except for
257
            // genesis).
258
            if self.block_header.purpose_id != previous_block.block_header.purpose_id {
256✔
259
                return Err(anyhow::anyhow!(
×
260
                    "Module: Immutable ledger,  Message: purpose id validation failed: {:?} {:?}",
×
261
                    self.block_header,
×
262
                    previous_block.block_header
×
263
                ));
×
264
            }
256✔
265

266
            // validator MUST be the same as for the previous block if present (except for genesis)
267
            if self.block_header.validator != previous_block.block_header.validator {
256✔
268
                return Err(anyhow::anyhow!(
×
269
                    "Module: Immutable ledger,  Message: validator validation failed: {:?} {:?}",
×
270
                    self.block_header,
×
271
                    previous_block.block_header
×
272
                ));
×
273
            }
256✔
274
        } else if self.block_header.height == GENESIS_BLOCK {
512✔
275
            // Validate genesis block
276
            {
277
                let genesis_to_prev_hash = GenesisPreviousHash::new(
512✔
278
                    self.block_header.chain_id,
512✔
279
                    self.block_header.block_time_stamp,
512✔
280
                    self.block_header.ledger_type,
512✔
281
                    self.block_header.purpose_id,
512✔
282
                    self.block_header.validator.clone(),
512✔
283
                )
512✔
284
                .hash(&self.block_header.previous_block_hash.0)?;
512✔
285

286
                if self.block_header.previous_block_hash.1 != genesis_to_prev_hash {
512✔
287
                    return Err(anyhow::anyhow!(
256✔
288
                        "Module: Immutable ledger,  Message: Genesis block prev hash is invalid {:?}",
256✔
289
                        self.block_header,
256✔
290
                    ));
256✔
291
                }
256✔
292
            }
293
        }
×
294

295
        Ok(())
512✔
296
    }
768✔
297
}
298

299
/// Block header
300
#[derive(Debug, Clone, PartialEq)]
301
pub struct BlockHeader {
302
    /// Unique identifier of the chain.
303
    pub chain_id: Uuid,
304
    /// Block height.
305
    pub height: i64,
306
    /// Block epoch-based date/time.
307
    pub block_time_stamp: i64,
308
    /// Previous Block hash.
309
    pub previous_block_hash: (HashFunction, Vec<u8>),
310
    /// unique identifier of the ledger type.
311
    /// In general, this is the way to strictly bound and specify `block_data` of the
312
    /// ledger for the specific `ledger_type`.
313
    pub ledger_type: Uuid,
314
    /// unique identifier of the purpose, each Ledger instance will have a strict time
315
    /// boundaries, so each of them will run for different purposes.
316
    pub purpose_id: Uuid,
317
    /// Identifier or identifiers of the entity who was produced and processed a block.
318
    pub validator: Vec<Kid>,
319
    /// Add arbitrary metadata to the block.
320
    pub metadata: Vec<u8>,
321
}
322

323
impl BlockHeader {
324
    /// Create new block
325
    #[must_use]
326
    #[allow(clippy::too_many_arguments)]
327
    pub fn new(
1,536✔
328
        chain_id: Uuid,
1,536✔
329
        height: i64,
1,536✔
330
        block_time_stamp: i64,
1,536✔
331
        previous_block_hash: (HashFunction, Vec<u8>),
1,536✔
332
        ledger_type: Uuid,
1,536✔
333
        purpose_id: Uuid,
1,536✔
334
        validator: Vec<Kid>,
1,536✔
335
        metadata: Vec<u8>,
1,536✔
336
    ) -> Self {
1,536✔
337
        Self {
1,536✔
338
            chain_id,
1,536✔
339
            height,
1,536✔
340
            block_time_stamp,
1,536✔
341
            previous_block_hash,
1,536✔
342
            ledger_type,
1,536✔
343
            purpose_id,
1,536✔
344
            validator,
1,536✔
345
            metadata,
1,536✔
346
        }
1,536✔
347
    }
1,536✔
348

349
    /// Encode block header
350
    /// ## Errors
351
    ///
352
    /// Returns an error encoding fails
353
    pub fn to_bytes(&self) -> anyhow::Result<Vec<u8>> {
1,280✔
354
        /// # of elements in block header
355
        const BLOCK_HEADER_SIZE: u64 = 8;
356

357
        let out: Vec<u8> = Vec::new();
1,280✔
358
        let mut encoder = minicbor::Encoder::new(out);
1,280✔
359

360
        encoder.array(BLOCK_HEADER_SIZE)?;
1,280✔
361

362
        // Chain id
363
        encoder.tag(minicbor::data::Tag::new(UUID_CBOR_TAG))?;
1,280✔
364
        encoder.bytes(self.chain_id.as_bytes())?;
1,280✔
365

366
        // Block height
367
        encoder.int(self.height.into())?;
1,280✔
368

369
        // Block timestamp
370
        encoder.tag(minicbor::data::Tag::new(TIMESTAMP_CBOR_TAG))?;
1,280✔
371
        encoder.int(self.block_time_stamp.into())?;
1,280✔
372

373
        let hash_function = self.previous_block_hash.0.clone();
1,280✔
374
        let cbor_hash_tag = match hash_function {
1,280✔
375
            HashFunction::Blake3 => BLAKE3_CBOR_TAG,
×
376
            HashFunction::Blake2b => BLAKE_2B_CBOR_TAG,
1,280✔
377
        };
378

379
        // Prev block hash
380
        encoder.tag(minicbor::data::Tag::new(cbor_hash_tag))?;
1,280✔
381
        encoder.bytes(&self.previous_block_hash.1)?;
1,280✔
382

383
        // Ledger type
384
        encoder.tag(minicbor::data::Tag::new(UUID_CBOR_TAG))?;
1,280✔
385
        encoder.bytes(self.ledger_type.as_bytes())?;
1,280✔
386

387
        // Purpose id
388
        encoder.tag(minicbor::data::Tag::new(UUID_CBOR_TAG))?;
1,280✔
389
        encoder.bytes(self.purpose_id.as_bytes())?;
1,280✔
390

391
        // Validators
392
        encoder.array(self.validator.len().try_into()?)?;
1,280✔
393
        for val in self.validator.clone() {
2,560✔
394
            encoder.tag(minicbor::data::Tag::new(cbor_hash_tag))?;
2,560✔
395
            encoder.bytes(&val.0)?;
2,560✔
396
        }
397

398
        // Metadata
399
        encoder.bytes(&self.metadata)?;
1,280✔
400

401
        Ok(encoder.writer().clone())
1,280✔
402
    }
1,280✔
403

404
    /// Decode block header
405
    /// ## Errors
406
    ///
407
    /// Returns an error decoding fails
408
    pub fn from_bytes(
512✔
409
        block: &[u8]
512✔
410
    ) -> anyhow::Result<(
512✔
411
        BlockHeader,
512✔
412
        BlockHeaderSize,
512✔
413
        Option<EncodedGenesisBlockContents>,
512✔
414
    )> {
512✔
415
        // Decode cbor to bytes
416
        let mut cbor_decoder = minicbor::Decoder::new(block);
512✔
417
        cbor_decoder.array()?;
512✔
418

419
        // Raw chain_id
420
        cbor_decoder.tag()?;
512✔
421
        let chain_id = Uuid::from_bytes(
512✔
422
            cbor_decoder
512✔
423
                .bytes()
512✔
424
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for chain id : {e}")))?
512✔
425
                .try_into()?,
512✔
426
        );
427

428
        // Raw Block height
429
        let block_height: i64 = cbor_decoder.int()?.try_into()?;
512✔
430

431
        // Raw time stamp
432
        cbor_decoder.tag()?;
512✔
433
        let ts: i64 = cbor_decoder.int()?.try_into()?;
512✔
434

435
        // Raw prev block hash
436
        let hash_function = cbor_decoder.tag()?;
512✔
437
        let prev_block_hash_type = match hash_function.as_u64() {
512✔
438
            BLAKE3_CBOR_TAG => HashFunction::Blake3,
×
439
            BLAKE_2B_CBOR_TAG => HashFunction::Blake2b,
512✔
440
            _ => bail!(format!("Invalid hash function type {hash_function:?}")),
×
441
        };
442

443
        let prev_block_hash = cbor_decoder
512✔
444
            .bytes()
512✔
445
            .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for prev block hash : {e}")))?
512✔
446
            .to_vec();
512✔
447

448
        // Raw ledger type
449
        cbor_decoder.tag()?;
512✔
450
        let ledger_type = Uuid::from_bytes(
512✔
451
            cbor_decoder
512✔
452
                .bytes()
512✔
453
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for ledger type : {e}")))?
512✔
454
                .try_into()?,
512✔
455
        );
456

457
        // Raw purpose id
458
        cbor_decoder.tag()?;
512✔
459
        let purpose_id = Uuid::from_bytes(
512✔
460
            cbor_decoder
512✔
461
                .bytes()
512✔
462
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for purpose id : {e}")))?
512✔
463
                .try_into()?,
512✔
464
        );
465

466
        // Validators
467
        let mut validators = Vec::new();
512✔
468
        let number_of_validators = cbor_decoder.array()?.ok_or(anyhow::anyhow!(format!(
512✔
469
            "Invalid amount of validators, should be at least two"
512✔
470
        )))?;
×
471

472
        for _validator in 0..number_of_validators {
1,024✔
473
            cbor_decoder.tag()?;
1,024✔
474
            let validator_kid: [u8; 16] = cbor_decoder
1,024✔
475
                .bytes()
1,024✔
476
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for validators : {e}")))?
1,024✔
477
                .try_into()?;
1,024✔
478

479
            validators.push(Kid(validator_kid));
1,024✔
480
        }
481

482
        let metadata = cbor_decoder
512✔
483
            .bytes()
512✔
484
            .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for metadata : {e}")))?
512✔
485
            .into();
512✔
486

487
        let block_header = BlockHeader {
512✔
488
            chain_id,
512✔
489
            height: block_height,
512✔
490
            block_time_stamp: ts,
512✔
491
            previous_block_hash: (prev_block_hash_type, prev_block_hash),
512✔
492
            ledger_type,
512✔
493
            purpose_id,
512✔
494
            validator: validators,
512✔
495
            metadata,
512✔
496
        };
512✔
497

498
        Ok((block_header, BlockHeaderSize(cbor_decoder.position()), None))
512✔
499
    }
512✔
500
}
501

502
/// Genesis block previous identifier type i.e hash of itself
503
pub struct GenesisPreviousHash {
504
    /// Unique identifier of the chain.
505
    pub chain_id: Uuid,
506
    /// Block epoch-based date/time.
507
    pub block_time_stamp: i64,
508
    /// unique identifier of the ledger type.
509
    /// In general, this is the way to strictly bound and specify `block_data` of the
510
    /// ledger for the specific `ledger_type`.
511
    pub ledger_type: Uuid,
512
    /// unique identifier of the purpose, each Ledger instance will have a strict time
513
    /// boundaries, so each of them will run for different purposes.
514
    pub purpose_id: Uuid,
515
    /// Identifier or identifiers of the entity who was produced and processed a block.
516
    pub validator: Vec<Kid>,
517
}
518

519
impl GenesisPreviousHash {
520
    /// Create previous block id
521
    #[must_use]
522
    pub fn new(
768✔
523
        chain_id: Uuid,
768✔
524
        block_time_stamp: i64,
768✔
525
        ledger_type: Uuid,
768✔
526
        purpose_id: Uuid,
768✔
527
        validator: Vec<Kid>,
768✔
528
    ) -> Self {
768✔
529
        Self {
768✔
530
            chain_id,
768✔
531
            block_time_stamp,
768✔
532
            ledger_type,
768✔
533
            purpose_id,
768✔
534
            validator,
768✔
535
        }
768✔
536
    }
768✔
537

538
    /// Encode genesis previous hash to cbor
539
    /// ## Errors
540
    ///
541
    /// Returns an error encoding fails
542
    pub fn to_bytes(
768✔
543
        &self,
768✔
544
        hasher: &HashFunction,
768✔
545
    ) -> anyhow::Result<Vec<u8>> {
768✔
546
        /// # of elements in genesis to prev hash
547
        const GENESIS_TO_PREV_HASH_SIZE: u64 = 5;
548

549
        let out: Vec<u8> = Vec::new();
768✔
550
        let mut encoder = minicbor::Encoder::new(out);
768✔
551
        encoder.array(GENESIS_TO_PREV_HASH_SIZE)?;
768✔
552

553
        // Chain id
554
        encoder.tag(minicbor::data::Tag::new(UUID_CBOR_TAG))?;
768✔
555
        encoder.bytes(self.chain_id.as_bytes())?;
768✔
556

557
        // Block timestamp
558
        encoder.tag(minicbor::data::Tag::new(TIMESTAMP_CBOR_TAG))?;
768✔
559
        encoder.int(self.block_time_stamp.into())?;
768✔
560

561
        let cbor_hash_tag = match hasher {
768✔
562
            HashFunction::Blake3 => BLAKE3_CBOR_TAG,
×
563
            HashFunction::Blake2b => BLAKE_2B_CBOR_TAG,
768✔
564
        };
565

566
        // Ledger type
567
        encoder.tag(minicbor::data::Tag::new(UUID_CBOR_TAG))?;
768✔
568
        encoder.bytes(self.ledger_type.as_bytes())?;
768✔
569

570
        // Purpose id
571
        encoder.tag(minicbor::data::Tag::new(UUID_CBOR_TAG))?;
768✔
572
        encoder.bytes(self.purpose_id.as_bytes())?;
768✔
573

574
        // Validators
575
        encoder.array(self.validator.len().try_into()?)?;
768✔
576
        for val in self.validator.clone() {
1,536✔
577
            encoder.tag(minicbor::data::Tag::new(cbor_hash_tag))?;
1,536✔
578
            encoder.bytes(&val.0)?;
1,536✔
579
        }
580

581
        Ok(encoder.writer().clone())
768✔
582
    }
768✔
583

584
    /// Generate hash of cbor encoded self
585
    /// ## Errors
586
    ///
587
    /// Returns an error if hashing fails
588
    pub fn hash(
768✔
589
        &self,
768✔
590
        hasher: &HashFunction,
768✔
591
    ) -> anyhow::Result<Vec<u8>> {
768✔
592
        let encoding = self.to_bytes(hasher)?;
768✔
593

594
        // get hash of genesis_to_prev_hash
595
        let genesis_prev_hash = match hasher {
768✔
596
            HashFunction::Blake3 => blake3(&encoding)?.to_vec(),
×
597
            HashFunction::Blake2b => blake2b_512(&encoding)?.to_vec(),
768✔
598
        };
599

600
        Ok(genesis_prev_hash)
768✔
601
    }
768✔
602
}
603

604
#[cfg(test)]
605
#[allow(clippy::zero_prefixed_literal)]
606
#[allow(clippy::items_after_statements)]
607
mod tests {
608

609
    use ed25519_dalek::{SECRET_KEY_LENGTH, Signature, Signer, SigningKey};
610
    use test_strategy::proptest;
611
    use uuid::Uuid;
612

613
    use super::{BlockHeader, Kid};
614
    use crate::serialize::{
615
        Block, BlockData, GenesisPreviousHash, HashFunction::Blake2b, Signatures, blake2b_512,
616
    };
617

618
    #[proptest]
619
    fn block_header_encoding(
620
        prev_block_hash: Vec<u8>,
621
        metadata: Vec<u8>,
622
        block_height: i64,
623
        block_timestamp: i64,
624
    ) {
625
        let kid_a: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
626
            .unwrap()
627
            .try_into()
628
            .unwrap();
629

630
        let kid_b: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
631
            .unwrap()
632
            .try_into()
633
            .unwrap();
634

635
        let block_hdr = BlockHeader::new(
636
            Uuid::now_v7(),
637
            block_height,
638
            block_timestamp,
639
            (Blake2b, prev_block_hash),
640
            Uuid::new_v4(),
641
            Uuid::now_v7(),
642
            vec![Kid(kid_a), Kid(kid_b)],
643
            metadata,
644
        );
645

646
        let encoded_block_hdr = block_hdr.to_bytes().unwrap();
647

648
        let (block_hdr_from_bytes, ..) = BlockHeader::from_bytes(&encoded_block_hdr).unwrap();
649
        assert_eq!(block_hdr_from_bytes.chain_id, block_hdr.chain_id);
650
        assert_eq!(block_hdr_from_bytes.height, block_hdr.height);
651
        assert_eq!(
652
            block_hdr_from_bytes.block_time_stamp,
653
            block_hdr.block_time_stamp
654
        );
655
        assert_eq!(
656
            block_hdr_from_bytes.previous_block_hash,
657
            block_hdr.previous_block_hash
658
        );
659
        assert_eq!(block_hdr_from_bytes.ledger_type, block_hdr.ledger_type);
660
        assert_eq!(block_hdr_from_bytes.purpose_id, block_hdr.purpose_id);
661
        assert_eq!(block_hdr_from_bytes.validator, block_hdr.validator);
662
        assert_eq!(block_hdr_from_bytes.metadata, block_hdr.metadata);
663
    }
664

665
    #[proptest]
666
    fn block_encoding(
667
        prev_block_hash: Vec<u8>,
668
        metadata: Vec<u8>,
669
        block_height: i64,
670
        block_timestamp: i64,
671
        block_data_bytes: Vec<u8>,
672
    ) {
673
        // validators
674
        let validator_secret_key_bytes: [u8; SECRET_KEY_LENGTH] = [
675
            157, 097, 177, 157, 239, 253, 090, 096, 186, 132, 074, 244, 146, 236, 044, 196, 068,
676
            073, 197, 105, 123, 050, 105, 025, 112, 059, 172, 003, 028, 174, 127, 096,
677
        ];
678

679
        let kid_a: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
680
            .unwrap()
681
            .try_into()
682
            .unwrap();
683

684
        let kid_b: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
685
            .unwrap()
686
            .try_into()
687
            .unwrap();
688

689
        let block_hdr = BlockHeader::new(
690
            Uuid::now_v7(),
691
            block_height,
692
            block_timestamp,
693
            (Blake2b, prev_block_hash),
694
            Uuid::new_v4(),
695
            Uuid::now_v7(),
696
            vec![Kid(kid_a), Kid(kid_b)],
697
            metadata,
698
        );
699

700
        let out: Vec<u8> = Vec::new();
701
        let mut block_data = minicbor::Encoder::new(out);
702

703
        block_data.bytes(&block_data_bytes).unwrap();
704
        let encoded_block_data = block_data.writer().clone();
705

706
        // validator_signature MUST be a signature of the hashed block_header bytes
707
        // and the block_data bytes
708
        let hashed_block_header = blake2b_512(&block_hdr.to_bytes().unwrap())
709
            .unwrap()
710
            .to_vec();
711

712
        let data_to_sign = [hashed_block_header, block_data_bytes.clone()].concat();
713

714
        // sign data with keys, block type is signature agnostic, test case uses ed25519
715
        let sk: SigningKey = SigningKey::from_bytes(&validator_secret_key_bytes);
716
        let signature_a = sk.sign(&data_to_sign).to_bytes();
717
        let signature_b = sk.sign(&data_to_sign).to_bytes();
718

719
        let block = Block::new(
720
            block_hdr.clone(),
721
            BlockData(encoded_block_data.clone()),
722
            Signatures(vec![signature_a.to_vec(), signature_b.to_vec()]),
723
        );
724

725
        let encoded_block = block.to_bytes().unwrap();
726

727
        // DECODE RAW BYTES BACK INTO BLOCK TYPE
728
        let (block_header, block_data, sigs) = Block::from_bytes(&encoded_block).unwrap();
729

730
        assert_eq!(block_header, block_hdr);
731

732
        // signatures are over encoded block data
733
        // block data is returned as plain bytes decoded from cbor
734
        assert_eq!(block_data.0, block_data_bytes);
735

736
        let verifying_key = SigningKey::from_bytes(&validator_secret_key_bytes);
737

738
        for sig in sigs.0 {
739
            let s: [u8; 64] = sig.try_into().unwrap();
740
            let signature = Signature::from_bytes(&s);
741
            verifying_key
742
                .verify_strict(&data_to_sign, &signature)
743
                .unwrap();
744
        }
745

746
        // ENCODING SHOULD FAIL with block data that is NOT cbor encoded
747
        let block = Block::new(
748
            block_hdr.clone(),
749
            BlockData(vec![7; 1024]),
750
            Signatures(vec![
751
                validator_secret_key_bytes.to_vec(),
752
                validator_secret_key_bytes.to_vec(),
753
            ]),
754
        );
755

756
        assert!(block.to_bytes().is_err());
757
    }
758

759
    #[proptest]
760
    #[allow(clippy::zero_prefixed_literal)]
761
    fn validate_block_test(
762
        prev_block_hash: Vec<u8>,
763
        metadata: Vec<u8>,
764
        block_data_bytes: Vec<u8>,
765
    ) {
766
        // PREVIOUS BLOCK
767
        //
768
        //
769
        // validators
770
        let validator_secret_key_bytes: [u8; SECRET_KEY_LENGTH] = [
771
            157, 097, 177, 157, 239, 253, 090, 096, 186, 132, 074, 244, 146, 236, 044, 196, 068,
772
            073, 197, 105, 123, 050, 105, 025, 112, 059, 172, 003, 028, 174, 127, 096,
773
        ];
774

775
        let kid_a: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
776
            .unwrap()
777
            .try_into()
778
            .unwrap();
779

780
        let kid_b: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
781
            .unwrap()
782
            .try_into()
783
            .unwrap();
784

785
        let chain_id = Uuid::now_v7();
786
        let ledger_type = Uuid::new_v4();
787
        let purpose_id = Uuid::now_v7();
788

789
        let block_hdr = BlockHeader::new(
790
            chain_id,
791
            5,
792
            1_728_474_515,
793
            (Blake2b, prev_block_hash),
794
            ledger_type,
795
            purpose_id,
796
            vec![Kid(kid_a), Kid(kid_b)],
797
            metadata.clone(),
798
        );
799

800
        let out: Vec<u8> = Vec::new();
801
        let mut block_data = minicbor::Encoder::new(out);
802

803
        block_data.bytes(&block_data_bytes).unwrap();
804
        let encoded_block_data = block_data.writer().clone();
805

806
        let previous_block = Block::new(
807
            block_hdr.clone(),
808
            BlockData(encoded_block_data.clone()),
809
            Signatures(vec![
810
                validator_secret_key_bytes.to_vec(),
811
                validator_secret_key_bytes.to_vec(),
812
            ]),
813
        );
814

815
        // CURRENT BLOCK
816

817
        let prev_block_hash = blake2b_512(&previous_block.to_bytes().unwrap()).unwrap();
818

819
        let block_hdr = BlockHeader::new(
820
            chain_id,
821
            6,
822
            1_728_474_516,
823
            (Blake2b, prev_block_hash.to_vec()),
824
            ledger_type,
825
            purpose_id,
826
            vec![Kid(kid_a), Kid(kid_b)],
827
            metadata,
828
        );
829

830
        let out: Vec<u8> = Vec::new();
831
        let mut block_data = minicbor::Encoder::new(out);
832

833
        block_data.bytes(&block_data_bytes).unwrap();
834
        let encoded_block_data = block_data.writer().clone();
835

836
        let current_block = Block::new(
837
            block_hdr.clone(),
838
            BlockData(encoded_block_data.clone()),
839
            Signatures(vec![
840
                validator_secret_key_bytes.to_vec(),
841
                validator_secret_key_bytes.to_vec(),
842
            ]),
843
        );
844

845
        assert!(current_block.validate(Some(previous_block),).is_ok());
846
    }
847

848
    #[proptest]
849
    fn genesis_encoding_and_validation(
850
        invalid_prev_block_hash: Vec<u8>,
851
        metadata: Vec<u8>,
852
        block_data_bytes: Vec<u8>,
853
    ) {
854
        // validators
855
        let validator_secret_key_bytes: [u8; SECRET_KEY_LENGTH] = [
856
            157, 097, 177, 157, 239, 253, 090, 096, 186, 132, 074, 244, 146, 236, 044, 196, 068,
857
            073, 197, 105, 123, 050, 105, 025, 112, 059, 172, 003, 028, 174, 127, 096,
858
        ];
859

860
        let chain_id = Uuid::now_v7();
861
        let ledger_type = Uuid::new_v4();
862
        let purpose_id = Uuid::now_v7();
863
        let block_time_stamp = 1_728_474_515;
864

865
        let kid_a: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
866
            .unwrap()
867
            .try_into()
868
            .unwrap();
869

870
        let kid_b: [u8; 16] = hex::decode("00112233445566778899aabbccddeeff")
871
            .unwrap()
872
            .try_into()
873
            .unwrap();
874

875
        let validator = vec![Kid(kid_a), Kid(kid_b)];
876

877
        let genesis_to_prev_hash = GenesisPreviousHash::new(
878
            chain_id,
879
            block_time_stamp,
880
            ledger_type,
881
            purpose_id,
882
            validator.clone(),
883
        );
884

885
        let block_hdr = BlockHeader::new(
886
            chain_id,
887
            0,
888
            block_time_stamp,
889
            (Blake2b, genesis_to_prev_hash.hash(&Blake2b).unwrap()),
890
            ledger_type,
891
            purpose_id,
892
            validator.clone(),
893
            metadata.clone(),
894
        );
895

896
        let out: Vec<u8> = Vec::new();
897
        let mut block_data = minicbor::Encoder::new(out);
898

899
        block_data.bytes(&block_data_bytes).unwrap();
900
        let encoded_block_data = block_data.writer().clone();
901

902
        let block = Block::new(
903
            block_hdr.clone(),
904
            BlockData(encoded_block_data.clone()),
905
            Signatures(vec![
906
                validator_secret_key_bytes.to_vec(),
907
                validator_secret_key_bytes.to_vec(),
908
            ]),
909
        );
910

911
        assert!(block.validate(None).is_ok());
912

913
        // SHOULD FAIL as previous block hash for genesis is invalid, it should be a hash of
914
        // itself like above.
915
        let block_hdr = BlockHeader::new(
916
            chain_id,
917
            0,
918
            block_time_stamp,
919
            (Blake2b, invalid_prev_block_hash),
920
            ledger_type,
921
            purpose_id,
922
            validator,
923
            metadata,
924
        );
925

926
        let block = Block::new(
927
            block_hdr.clone(),
928
            BlockData(encoded_block_data.clone()),
929
            Signatures(vec![
930
                validator_secret_key_bytes.to_vec(),
931
                validator_secret_key_bytes.to_vec(),
932
            ]),
933
        );
934

935
        assert!(block.validate(None).is_err());
936
    }
937
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc