Skip to content

Commit 2359fe4

Browse files
committed
refactor: implement canonical p2p broadcasting and cross-instance tests
1 parent 0153620 commit 2359fe4

2 files changed

Lines changed: 39 additions & 29 deletions

File tree

minichain/p2p.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import json
1010
import logging
1111

12-
from .serialization import canonical_json_hash
12+
from .serialization import canonical_json_hash, canonical_json_dumps
1313
from .validators import is_valid_receiver
1414

1515
logger = logging.getLogger(__name__)
@@ -46,10 +46,8 @@ def register_handler(self, handler_callback):
4646
async def start(self, port: int = 9000):
4747
"""Start listening for incoming peer connections on the given port."""
4848
self._port = port
49-
self._server = await asyncio.start_server(
50-
self._handle_incoming, host, port
51-
)
52-
logger.info("Network: Listening on %s:%d", host, port)
49+
self._server = await asyncio.start_server(self._handle_incoming, "0.0.0.0", port)
50+
logger.info("Network: Listening on 0.0.0.0:%d", port)
5351

5452
async def stop(self):
5553
"""Gracefully shut down the server and disconnect all peers."""
@@ -204,9 +202,7 @@ def _validate_block_payload(self, payload):
204202
)
205203

206204
def _validate_message(self, message):
207-
if not isinstance(message, dict):
208-
return False
209-
if set(message) != {"type", "data"}:
205+
if not {"type", "data"}.issubset(set(message)):
210206
return False
211207

212208
msg_type = message.get("type")
@@ -303,6 +299,7 @@ async def _listen_to_peer(
303299
async def _broadcast_raw(self, payload: dict):
304300
"""Send a JSON message to every connected peer."""
305301
line = (json.dumps(payload) + "\n").encode()
302+
line = (canonical_json_dumps(payload) + "\n").encode()
306303
disconnected = []
307304
for reader, writer in self._peers:
308305
try:
@@ -333,10 +330,16 @@ async def broadcast_transaction(self, tx):
333330

334331
async def broadcast_block(self, block, miner=None):
335332
logger.info("Network: Broadcasting Block #%d", block.index)
336-
block_payload = block.to_dict()
337-
if miner is not None:
338-
block_payload["miner"] = miner
339-
payload = {"type": "block", "data": block_payload}
333+
334+
# 1. Convert block to a dict (deterministic via serialization.py)
335+
block_data = json.loads(block.canonical_payload.decode('utf-8'))
336+
337+
# 2. Wrap it in an envelope where 'miner' is OUTSIDE the 'data'
338+
payload = {
339+
"type": "block",
340+
"data": block_data,
341+
"miner": miner
342+
}
340343
self._mark_seen("block", payload["data"])
341344
await self._broadcast_raw(payload)
342345

tests/test_serialization.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,31 @@ def test_transaction_id_stability():
2929
assert first_id == second_id
3030
print("Success: Transaction ID is stable and deterministic!\n")
3131

32-
def test_block_hash_consistency():
33-
print("--- Testing Block Hash Consistency ---")
34-
# Create a block with one transaction
35-
tx = Transaction(sender="A", receiver="B", amount=10, nonce=5)
36-
block = Block(index=1, previous_hash="0"*64, transactions=[tx], difficulty=2)
32+
def test_block_serialization_determinism():
33+
print("--- Testing Block Serialization & Cross-Instance Determinism ---")
34+
tx_params = {"sender": "A", "receiver": "B", "amount": 10, "nonce": 5}
3735

38-
initial_hash = block.compute_hash()
39-
print(f"Initial Block Hash: {initial_hash}")
36+
# Create two SEPARATE block instances with the exact same data
37+
tx1 = Transaction(**tx_params)
38+
block1 = Block(index=1, previous_hash="0"*64, transactions=[tx1], difficulty=2)
4039

41-
# Manually re-computing to ensure it's identical
42-
assert block.compute_hash() == initial_hash
43-
print("Success: Block hash is consistent!\n")
40+
tx2 = Transaction(**tx_params)
41+
block2 = Block(index=1, previous_hash="0"*64, transactions=[tx2], difficulty=2)
42+
43+
# 1. Test stable bytes on one instance (same object, twice)
44+
assert block1.canonical_payload == block1.canonical_payload
45+
46+
# 2. Test cross-instance determinism (different objects, same data)
47+
assert block1.canonical_payload == block2.canonical_payload, "Identical blocks must have identical payloads"
48+
49+
# 3. Test hash consistency
50+
assert block1.compute_hash() == block2.compute_hash()
51+
52+
print("✅ Success: Block serialization is cross-instance deterministic!\n")
4453

4554
if __name__ == "__main__":
46-
try:
47-
test_raw_data_determinism()
48-
test_transaction_id_stability()
49-
test_block_hash_consistency()
50-
print("ALL CANONICAL TESTS PASSED!")
51-
except AssertionError as e:
52-
print("TEST FAILED: Serialization is not deterministic!")
55+
# Removed try/except so that AssertionErrors 'bubble up' to the test runner
56+
test_raw_data_determinism()
57+
test_transaction_id_stability()
58+
test_block_serialization_determinism()
59+
print("🚀 ALL CANONICAL TESTS PASSED!")

0 commit comments

Comments
 (0)