11import time
22import hashlib
3- from typing import List , Optional
3+ from typing import Optional # <-- Removed 'List' as requested
4+ from collections .abc import Sequence
5+
46from .transaction import Transaction
5- from .serialization import canonical_json_hash
7+ from .serialization import canonical_json_hash , canonical_json_bytes
8+
69
710def _sha256 (data : str ) -> str :
811 return hashlib .sha256 (data .encode ()).hexdigest ()
912
10-
11- def _calculate_merkle_root (transactions : List [Transaction ]) -> Optional [str ]:
13+ # <-- Updated to Sequence to accept the frozen tuple
14+ def _calculate_merkle_root (transactions : Sequence [Transaction ]) -> Optional [str ]:
1215 if not transactions :
1316 return None
1417
@@ -32,29 +35,27 @@ def _calculate_merkle_root(transactions: List[Transaction]) -> Optional[str]:
3235
3336 return tx_hashes [0 ]
3437
35-
3638class Block :
3739 def __init__ (
3840 self ,
3941 index : int ,
4042 previous_hash : str ,
41- transactions : Optional [List [Transaction ]] = None ,
43+ transactions : Optional [Sequence [Transaction ]] = None ,
4244 timestamp : Optional [float ] = None ,
4345 difficulty : Optional [int ] = None ,
44- state_root : Optional [str ] = None ,
45- miner : Optional [str ] = None ,
46+ miner : Optional [str ] = None
4647 ):
4748 self .index = index
4849 self .previous_hash = previous_hash
49- self .transactions : List [Transaction ] = transactions or []
50-
50+ # Freeze transactions into an immutable tuple to prevent header/body mismatch
51+ self .transactions = tuple (transactions ) if transactions else ()
52+ self .miner = miner
5153 # Deterministic timestamp (ms)
5254 self .timestamp : int = (
5355 round (time .time () * 1000 )
5456 if timestamp is None
5557 else int (timestamp )
5658 )
57-
5859 self .difficulty : Optional [int ] = difficulty
5960 self .nonce : int = 0
6061 self .hash : Optional [str ] = None
@@ -68,7 +69,7 @@ def __init__(
6869 # HEADER (used for mining)
6970 # -------------------------
7071 def to_header_dict (self ):
71- return {
72+ header = {
7273 "index" : self .index ,
7374 "previous_hash" : self .previous_hash ,
7475 "merkle_root" : self .merkle_root ,
@@ -78,7 +79,11 @@ def to_header_dict(self):
7879 "nonce" : self .nonce ,
7980 "miner" : self .miner ,
8081 }
81-
82+ # Include miner in header only when present (optional field) <-- Reworded comment
83+ if self .miner is not None :
84+ header ["miner" ] = self .miner
85+ return header
86+
8287 # -------------------------
8388 # BODY (transactions only)
8489 # -------------------------
@@ -93,11 +98,10 @@ def to_body_dict(self):
9398 # FULL BLOCK
9499 # -------------------------
95100 def to_dict (self ):
96- return {
97- ** self .to_header_dict (),
98- ** self .to_body_dict (),
99- "hash" : self .hash ,
100- }
101+ data = self .to_header_dict ()
102+ data .update (self .to_body_dict ()) # Reuses existing serialization logic
103+ data ["hash" ] = self .hash
104+ return data
101105
102106 # -------------------------
103107 # HASH CALCULATION
@@ -111,17 +115,43 @@ def from_dict(cls, payload: dict):
111115 Transaction .from_dict (tx_payload )
112116 for tx_payload in payload .get ("transactions" , [])
113117 ]
118+
119+ # Safely extract and cast difficulty if it exists
120+ raw_diff = payload .get ("difficulty" )
121+ parsed_diff = int (raw_diff ) if raw_diff is not None else None
122+
123+ # Safely extract and cast timestamp if it exists <-- Added explicit timestamp casting
124+ raw_ts = payload .get ("timestamp" )
125+ parsed_ts = int (raw_ts ) if raw_ts is not None else None
126+
114127 block = cls (
115- index = payload ["index" ],
128+ index = int ( payload ["index" ]),
116129 previous_hash = payload ["previous_hash" ],
117130 transactions = transactions ,
118- timestamp = payload .get ("timestamp" ),
119- difficulty = payload .get ("difficulty" ),
120- state_root = payload .get ("state_root" ),
131+ timestamp = parsed_ts , # <-- Passed the casted timestamp
132+ difficulty = parsed_diff ,
121133 miner = payload .get ("miner" ),
122134 )
123- block .nonce = payload .get ("nonce" , 0 )
135+ block .nonce = int ( payload .get ("nonce" , 0 ))
124136 block .hash = payload .get ("hash" )
125- if "merkle_root" in payload :
126- block .merkle_root = payload ["merkle_root" ]
137+
138+ # Verify the block hash
139+ expected_hash = block .compute_hash ()
140+ if block .hash is not None and block .hash != expected_hash :
141+ raise ValueError ("block hash does not match header" )
142+
143+ # Recalculate and verify the Merkle root!
144+ if "merkle_root" in payload and payload ["merkle_root" ] != block .merkle_root :
145+ raise ValueError ("merkle_root does not match transactions" )
127146 return block
147+
148+ @property
149+ def canonical_payload (self ) -> bytes :
150+ """Returns the full block (header + body) as canonical bytes for networking."""
151+ # Sanity checks to prevent broadcasting invalid blocks
152+ if self .hash is None :
153+ raise ValueError ("block hash is missing" )
154+ if self .hash != self .compute_hash ():
155+ raise ValueError ("block hash does not match header" )
156+
157+ return canonical_json_bytes (self .to_dict ())
0 commit comments