Skip to main content

blockle_core/
block.rs

1//! Block and block header.
2//!
3//! The header is deliberately Zcash-shaped so Equihash ASIC/pool tooling can
4//! consume it: a 140-byte fixed region (version, prev hash, merkle root, a
5//! 32-byte reserved/state root, time, bits, 32-byte nonce) followed by the
6//! compactsize-prefixed Equihash solution. The Equihash puzzle input is the
7//! first 108 bytes; the nonce is the puzzle's second input; the block hash is
8//! double-SHA256 of the full serialization.
9
10use serde::{Deserialize, Serialize};
11
12use crate::hash::{sha256d, Hash32};
13use crate::merkle::merkle_root;
14use crate::transaction::Transaction;
15
16#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
17pub struct BlockHeader {
18    pub version: u32,
19    pub prev_hash: Hash32,
20    pub merkle_root: Hash32,
21    /// Commitment to chain state (UTXO/note/contract roots). Zero until the
22    /// state-commitment milestone; consensus requires zero for now. Occupies
23    /// the reserved-hash slot of the Zcash header layout.
24    pub state_root: Hash32,
25    pub time: u32,
26    /// Compact difficulty target (Bitcoin nBits encoding).
27    pub bits: u32,
28    pub nonce: [u8; 32],
29    /// Equihash solution: big-endian bitstream of (c+1)-bit indices
30    /// (1344 bytes on mainnet, like Zcash).
31    pub solution: Vec<u8>,
32}
33
34/// Bitcoin-style compactsize length prefix.
35fn put_compact_size(out: &mut Vec<u8>, n: u64) {
36    match n {
37        0..=0xfc => out.push(n as u8),
38        0xfd..=0xffff => {
39            out.push(0xfd);
40            out.extend_from_slice(&(n as u16).to_le_bytes());
41        }
42        _ => {
43            out.push(0xfe);
44            out.extend_from_slice(&(n as u32).to_le_bytes());
45        }
46    }
47}
48
49impl BlockHeader {
50    /// The 108-byte Equihash puzzle input: every header field before the
51    /// nonce and solution.
52    pub fn equihash_input(&self) -> [u8; 108] {
53        let mut out = [0u8; 108];
54        out[0..4].copy_from_slice(&self.version.to_le_bytes());
55        out[4..36].copy_from_slice(&self.prev_hash);
56        out[36..68].copy_from_slice(&self.merkle_root);
57        out[68..100].copy_from_slice(&self.state_root);
58        out[100..104].copy_from_slice(&self.time.to_le_bytes());
59        out[104..108].copy_from_slice(&self.bits.to_le_bytes());
60        out
61    }
62
63    /// Full wire serialization: 140-byte fixed region + solution.
64    pub fn serialize(&self) -> Vec<u8> {
65        let mut out = Vec::with_capacity(140 + 3 + self.solution.len());
66        out.extend_from_slice(&self.equihash_input());
67        out.extend_from_slice(&self.nonce);
68        put_compact_size(&mut out, self.solution.len() as u64);
69        out.extend_from_slice(&self.solution);
70        out
71    }
72
73    /// Block id: double-SHA256 of the serialized header, compared to the
74    /// difficulty target as a little-endian integer.
75    pub fn hash(&self) -> Hash32 {
76        sha256d(&self.serialize())
77    }
78}
79
80#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
81pub struct Block {
82    pub header: BlockHeader,
83    /// Merged-mining proof: present when this block's PoW is a parent
84    /// chain's (any registered ASIC algorithm) rather than a native
85    /// solution. `default` keeps pre-auxpow JSON (e.g. the embedded
86    /// genesis) readable; no `skip_serializing_if` — bincode (the p2p wire
87    /// format) is not self-describing and must always see the Option tag.
88    #[serde(default)]
89    pub aux_pow: Option<crate::auxpow::AuxPow>,
90    pub transactions: Vec<Transaction>,
91}
92
93impl Block {
94    pub fn compute_merkle_root(&self) -> Hash32 {
95        let txids: Vec<Hash32> = self.transactions.iter().map(|t| t.txid()).collect();
96        merkle_root(&txids)
97    }
98
99    pub fn serialized_size(&self) -> usize {
100        self.header.serialize().len()
101            + self.aux_pow.as_ref().map(|a| a.serialized_size()).unwrap_or(0)
102            + self.transactions.iter().map(|t| t.serialized_size()).sum::<usize>()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn header_layout_is_zcash_shaped() {
112        let header = BlockHeader {
113            version: 4,
114            prev_hash: [1; 32],
115            merkle_root: [2; 32],
116            state_root: [0; 32],
117            time: 1_790_000_000,
118            bits: 0x1f07ffff,
119            nonce: [3; 32],
120            solution: vec![0xaa; 1344],
121        };
122        assert_eq!(header.equihash_input().len(), 108);
123        // 140 fixed + 3-byte compactsize (0xfd + u16) + 1344 solution.
124        assert_eq!(header.serialize().len(), 140 + 3 + 1344);
125        let mut tampered = header.clone();
126        tampered.nonce[0] ^= 1;
127        assert_ne!(header.hash(), tampered.hash());
128    }
129}