44from collections .abc import Sequence
55
66from .transaction import Transaction
7+ from .receipt import Receipt
78from .serialization import canonical_json_hash , canonical_json_bytes
89
9-
1010def _sha256 (data : str ) -> str :
1111 return hashlib .sha256 (data .encode ()).hexdigest ()
1212
13- # <-- Updated to Sequence to accept the frozen tuple
14- def _calculate_merkle_root (transactions : Sequence [Transaction ]) -> Optional [str ]:
15- if not transactions :
13+ def _calculate_merkle_tree (hashes : Sequence [str ]) -> Optional [str ]:
14+ if not hashes :
1615 return None
17-
18- # Hash each transaction deterministically
19- tx_hashes = [
20- tx .tx_id
21- for tx in transactions
22- ]
23-
24- # Build Merkle tree
25- while len (tx_hashes ) > 1 :
26- if len (tx_hashes ) % 2 != 0 :
27- tx_hashes .append (tx_hashes [- 1 ]) # duplicate last if odd
28-
16+ hashes_list = list (hashes )
17+ while len (hashes_list ) > 1 :
18+ if len (hashes_list ) % 2 != 0 :
19+ hashes_list .append (hashes_list [- 1 ])
2920 new_level = []
30- for i in range (0 , len (tx_hashes ), 2 ):
31- combined = tx_hashes [i ] + tx_hashes [i + 1 ]
21+ for i in range (0 , len (hashes_list ), 2 ):
22+ combined = hashes_list [i ] + hashes_list [i + 1 ]
3223 new_level .append (_sha256 (combined ))
24+ hashes_list = new_level
25+ return hashes_list [0 ]
3326
34- tx_hashes = new_level
27+ # <-- Updated to Sequence to accept the frozen tuple
28+ def _calculate_merkle_root (transactions : Sequence [Transaction ]) -> Optional [str ]:
29+ if not transactions :
30+ return None
31+ return _calculate_merkle_tree ([tx .tx_id for tx in transactions ])
3532
36- return tx_hashes [0 ]
33+ def calculate_receipt_root (receipts : Sequence [Receipt ]) -> Optional [str ]:
34+ if not receipts :
35+ return None
36+ return _calculate_merkle_tree ([canonical_json_hash (r .to_dict ()) for r in receipts ])
3737
3838class Block :
3939 def __init__ (
@@ -43,12 +43,16 @@ def __init__(
4343 transactions : Optional [Sequence [Transaction ]] = None ,
4444 timestamp : Optional [float ] = None ,
4545 difficulty : Optional [int ] = None ,
46- miner : Optional [str ] = None
46+ state_root : Optional [str ] = None ,
47+ receipt_root : Optional [str ] = None ,
48+ receipts : Optional [Sequence [Receipt ]] = None ,
49+ miner : Optional [str ] = None ,
4750 ):
4851 self .index = index
4952 self .previous_hash = previous_hash
5053 # Freeze transactions into an immutable tuple to prevent header/body mismatch
5154 self .transactions = tuple (transactions ) if transactions else ()
55+ self .receipts = tuple (receipts ) if receipts else ()
5256 self .miner = miner
5357 # Deterministic timestamp (ms)
5458 self .timestamp : int = (
@@ -60,10 +64,15 @@ def __init__(
6064 self .nonce : int = 0
6165 self .hash : Optional [str ] = None
6266 self .state_root : Optional [str ] = state_root
67+ self .receipt_root : Optional [str ] = receipt_root
6368 self .miner : Optional [str ] = miner
6469
65- # NEW: compute merkle root once
70+ # NEW: compute merkle roots once
6671 self .merkle_root : Optional [str ] = _calculate_merkle_root (self .transactions )
72+
73+ # If receipt_root is missing but we have receipts, calculate it.
74+ if self .receipt_root is None and self .receipts :
75+ self .receipt_root = calculate_receipt_root (self .receipts )
6776
6877 # -------------------------
6978 # HEADER (used for mining)
@@ -74,10 +83,10 @@ def to_header_dict(self):
7483 "previous_hash" : self .previous_hash ,
7584 "merkle_root" : self .merkle_root ,
7685 "state_root" : self .state_root ,
86+ "receipt_root" : self .receipt_root ,
7787 "timestamp" : self .timestamp ,
7888 "difficulty" : self .difficulty ,
7989 "nonce" : self .nonce ,
80- "miner" : self .miner ,
8190 }
8291 # Include miner in header only when present (optional field) <-- Reworded comment
8392 if self .miner is not None :
@@ -91,6 +100,9 @@ def to_body_dict(self):
91100 return {
92101 "transactions" : [
93102 tx .to_dict () for tx in self .transactions
103+ ],
104+ "receipts" : [
105+ r .to_dict () for r in self .receipts
94106 ]
95107 }
96108
@@ -115,6 +127,10 @@ def from_dict(cls, payload: dict):
115127 Transaction .from_dict (tx_payload )
116128 for tx_payload in payload .get ("transactions" , [])
117129 ]
130+ receipts = [
131+ Receipt .from_dict (r_payload )
132+ for r_payload in payload .get ("receipts" , [])
133+ ]
118134
119135 # Safely extract and cast difficulty if it exists
120136 raw_diff = payload .get ("difficulty" )
@@ -123,13 +139,15 @@ def from_dict(cls, payload: dict):
123139 # Safely extract and cast timestamp if it exists <-- Added explicit timestamp casting
124140 raw_ts = payload .get ("timestamp" )
125141 parsed_ts = int (raw_ts ) if raw_ts is not None else None
126-
127142 block = cls (
128143 index = int (payload ["index" ]),
129144 previous_hash = payload ["previous_hash" ],
130145 transactions = transactions ,
131146 timestamp = parsed_ts , # <-- Passed the casted timestamp
132147 difficulty = parsed_diff ,
148+ state_root = payload .get ("state_root" ),
149+ receipt_root = payload .get ("receipt_root" ),
150+ receipts = receipts ,
133151 miner = payload .get ("miner" ),
134152 )
135153 block .nonce = int (payload .get ("nonce" , 0 ))
@@ -143,6 +161,12 @@ def from_dict(cls, payload: dict):
143161 # Recalculate and verify the Merkle root!
144162 if "merkle_root" in payload and payload ["merkle_root" ] != block .merkle_root :
145163 raise ValueError ("merkle_root does not match transactions" )
164+
165+ if "receipt_root" in payload :
166+ expected_receipt_root = calculate_receipt_root (block .receipts )
167+ if payload ["receipt_root" ] != expected_receipt_root :
168+ raise ValueError ("receipt_root does not match receipts" )
169+
146170 return block
147171
148172 @property
0 commit comments