Skip to main content

sumchain_primitives/
block.rs

1//! Block and BlockHeader types for SUM Chain.
2//!
3//! Blocks contain a header with metadata and a list of transactions.
4//! The header includes the proposer's signature for PoA validation.
5
6use serde::{Deserialize, Serialize};
7use serde_big_array::BigArray;
8
9use crate::{BlockHeight, Hash, SignedTransaction, Timestamp};
10
11/// Block header containing metadata and proposer signature
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct BlockHeader {
14    /// Hash of the parent block (zero for genesis)
15    pub parent_hash: Hash,
16    /// Block height (0 for genesis)
17    pub height: BlockHeight,
18    /// Block creation timestamp (ms since epoch)
19    pub timestamp: Timestamp,
20    /// Merkle root of transactions
21    pub tx_root: Hash,
22    /// Root hash of state after applying this block
23    pub state_root: Hash,
24    /// Proposer's Ed25519 public key
25    pub proposer_pubkey: [u8; 32],
26    /// Proposer's signature over the header hash (excluding this field)
27    #[serde(with = "BigArray")]
28    pub proposer_sig: [u8; 64],
29}
30
31impl BlockHeader {
32    /// Create a new block header (signature must be added separately)
33    pub fn new(
34        parent_hash: Hash,
35        height: BlockHeight,
36        timestamp: Timestamp,
37        tx_root: Hash,
38        state_root: Hash,
39        proposer_pubkey: [u8; 32],
40    ) -> Self {
41        Self {
42            parent_hash,
43            height,
44            timestamp,
45            tx_root,
46            state_root,
47            proposer_pubkey,
48            proposer_sig: [0u8; 64], // Will be filled after signing
49        }
50    }
51
52    /// Compute the hash that the proposer signs
53    /// This excludes the signature field itself to avoid circular dependency
54    pub fn signing_hash(&self) -> Hash {
55        // Create a copy without signature for hashing
56        let signable = SignableHeader {
57            parent_hash: self.parent_hash,
58            height: self.height,
59            timestamp: self.timestamp,
60            tx_root: self.tx_root,
61            state_root: self.state_root,
62            proposer_pubkey: self.proposer_pubkey,
63        };
64        let bytes = bincode::serialize(&signable).expect("Header serialization should not fail");
65        Hash::hash(&bytes)
66    }
67
68    /// Compute the full block header hash (includes signature)
69    /// This is the hash used to reference the block
70    pub fn hash(&self) -> Hash {
71        let bytes = bincode::serialize(self).expect("Header serialization should not fail");
72        Hash::hash(&bytes)
73    }
74
75    /// Check if this is a genesis block
76    pub fn is_genesis(&self) -> bool {
77        self.height == 0 && self.parent_hash.is_zero()
78    }
79
80    /// Set the proposer signature
81    pub fn set_signature(&mut self, signature: [u8; 64]) {
82        self.proposer_sig = signature;
83    }
84}
85
86/// Header data that gets signed (excludes the signature itself)
87#[derive(Serialize)]
88struct SignableHeader {
89    parent_hash: Hash,
90    height: BlockHeight,
91    timestamp: Timestamp,
92    tx_root: Hash,
93    state_root: Hash,
94    proposer_pubkey: [u8; 32],
95}
96
97/// A complete block with header and transactions
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct Block {
100    /// Block header
101    pub header: BlockHeader,
102    /// List of transactions in this block
103    pub transactions: Vec<SignedTransaction>,
104}
105
106impl Block {
107    /// Create a new block
108    pub fn new(header: BlockHeader, transactions: Vec<SignedTransaction>) -> Self {
109        Self {
110            header,
111            transactions,
112        }
113    }
114
115    /// Create a genesis block
116    pub fn genesis(state_root: Hash, proposer_pubkey: [u8; 32], timestamp: Timestamp) -> Self {
117        let header = BlockHeader::new(
118            Hash::ZERO,
119            0,
120            timestamp,
121            Hash::ZERO, // No transactions in genesis
122            state_root,
123            proposer_pubkey,
124        );
125
126        Self {
127            header,
128            transactions: Vec::new(),
129        }
130    }
131
132    /// Get the block hash
133    pub fn hash(&self) -> Hash {
134        self.header.hash()
135    }
136
137    /// Get block height
138    pub fn height(&self) -> BlockHeight {
139        self.header.height
140    }
141
142    /// Compute the transaction root from the block's transactions
143    pub fn compute_tx_root(&self) -> Hash {
144        if self.transactions.is_empty() {
145            return Hash::ZERO;
146        }
147
148        let tx_hashes: Vec<Hash> = self.transactions.iter().map(|tx| tx.hash()).collect();
149        Hash::merkle_root(&tx_hashes)
150    }
151
152    /// Verify that tx_root matches the transactions
153    pub fn verify_tx_root(&self) -> bool {
154        self.header.tx_root == self.compute_tx_root()
155    }
156
157    /// Serialize to bytes
158    pub fn to_bytes(&self) -> Vec<u8> {
159        bincode::serialize(self).expect("Block serialization should not fail")
160    }
161
162    /// Deserialize from bytes
163    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
164        bincode::deserialize(bytes)
165    }
166
167    /// Get the number of transactions
168    pub fn tx_count(&self) -> usize {
169        self.transactions.len()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn sample_header() -> BlockHeader {
178        BlockHeader::new(
179            Hash::ZERO,
180            1,
181            1000,
182            Hash::ZERO,
183            Hash::hash(b"state"),
184            [0u8; 32],
185        )
186    }
187
188    #[test]
189    fn test_signing_hash_excludes_signature() {
190        let mut header1 = sample_header();
191        let mut header2 = sample_header();
192
193        header1.proposer_sig = [1u8; 64];
194        header2.proposer_sig = [2u8; 64];
195
196        // Signing hash should be the same regardless of signature
197        assert_eq!(header1.signing_hash(), header2.signing_hash());
198    }
199
200    #[test]
201    fn test_block_hash_includes_signature() {
202        let mut header1 = sample_header();
203        let mut header2 = sample_header();
204
205        header1.proposer_sig = [1u8; 64];
206        header2.proposer_sig = [2u8; 64];
207
208        // Full hash should differ with different signatures
209        assert_ne!(header1.hash(), header2.hash());
210    }
211
212    #[test]
213    fn test_genesis_block() {
214        let genesis = Block::genesis(Hash::hash(b"genesis_state"), [0u8; 32], 0);
215        assert!(genesis.header.is_genesis());
216        assert_eq!(genesis.height(), 0);
217        assert!(genesis.transactions.is_empty());
218    }
219
220    #[test]
221    fn test_tx_root_empty() {
222        let block = Block::new(sample_header(), vec![]);
223        assert_eq!(block.compute_tx_root(), Hash::ZERO);
224    }
225
226    #[test]
227    fn test_serialization_roundtrip() {
228        let block = Block::genesis(Hash::hash(b"state"), [42u8; 32], 12345);
229        let bytes = block.to_bytes();
230        let block2 = Block::from_bytes(&bytes).unwrap();
231        assert_eq!(block, block2);
232    }
233}