diff --git a/node/rustchain_p2p_gossip.py b/node/rustchain_p2p_gossip.py index 268526f50..90cdc9013 100644 --- a/node/rustchain_p2p_gossip.py +++ b/node/rustchain_p2p_gossip.py @@ -375,6 +375,7 @@ def _sign_message(self, content: str) -> Tuple[str, int]: hmac_sig: Optional[str] = None ed25519_sig: Optional[str] = None + key_version = 1 if mode in ("hmac", "dual"): hmac_sig = hmac.new( @@ -383,9 +384,10 @@ def _sign_message(self, content: str) -> Tuple[str, int]: if mode in ("dual", "ed25519", "strict") and self._keypair is not None: ed25519_sig = self._keypair.sign(message.encode()) + key_version = self._keypair.key_version from p2p_identity import pack_signature - return pack_signature(hmac_sig, ed25519_sig), timestamp + return pack_signature(hmac_sig, ed25519_sig, key_version), timestamp def _verify_signature(self, content: str, signature: str, timestamp: int) -> bool: """Verify a message signature. @@ -480,13 +482,23 @@ def verify_message(self, msg: GossipMessage) -> bool: mode = self._signing_mode from p2p_identity import unpack_signature, verify_ed25519 - hmac_sig, ed25519_sig = unpack_signature(msg.signature) + hmac_sig, ed25519_sig, incoming_version = unpack_signature(msg.signature) # 1) Try Ed25519 if available AND peer is registered. if ed25519_sig and self._peer_registry is not None: - pubkey = self._peer_registry.get_pubkey(msg.sender_id) - if pubkey and verify_ed25519(pubkey, ed25519_sig, message.encode()): - return True + entry = self._peer_registry.get_entry(msg.sender_id) + pubkey = entry.pubkey_hex if entry else None + if pubkey: + # Item A: Check key version matches registry + if incoming_version != entry.key_version: + logger.warning( + f"Peer {msg.sender_id} key version mismatch: " + f"registry expects v{entry.key_version}, got v{incoming_version}" + ) + return False + + if verify_ed25519(pubkey, ed25519_sig, message.encode()): + return True # In strict mode, Ed25519 must succeed — no fallback. if mode == "strict": return False @@ -880,16 +892,20 @@ def _handle_get_state(self, msg: GossipMessage) -> Dict: "balances": self.balance_crdt.to_dict() } # Sign the state response so the requester can verify authenticity. - # Uses the Phase A signed-content shape (msg_type:sender_id:payload) - # so verify_message() on the requester side accepts it. + # Generate a synthetic msg_id for the response. payload = {"state": state_data} - content = self._signed_content(MessageType.STATE.value, self.node_id, payload) + state_msg_id = hashlib.sha256( + f"STATE:{self.node_id}:{json.dumps(payload, sort_keys=True)}:{time.time()}".encode() + ).hexdigest()[:24] + content = self._signed_content(MessageType.STATE.value, self.node_id, state_msg_id, 0, payload) signature, timestamp = self._sign_message(content) return { "status": "ok", "state": state_data, "signature": signature, "timestamp": timestamp, + "msg_id": state_msg_id, + "ttl": 0, "sender_id": self.node_id } diff --git a/node/tests/test_get_state_arity.py b/node/tests/test_get_state_arity.py new file mode 100644 index 000000000..20363cf13 --- /dev/null +++ b/node/tests/test_get_state_arity.py @@ -0,0 +1,54 @@ +import unittest +import json +import os +import tempfile +import sqlite3 +from node.rustchain_p2p_gossip import GossipLayer, MessageType, GossipMessage + +class TestGetStateArity(unittest.TestCase): + def setUp(self): + self.db_fd, self.db_path = tempfile.mkstemp() + self.gossip = GossipLayer(node_id="test_node", db_path=self.db_path, peers={}) + + def tearDown(self): + os.close(self.db_fd) + os.unlink(self.db_path) + + def test_handle_get_state_no_raise(self): + """Assert _handle_get_state runs without TypeError and returns verifiable signature""" + # Create a mock GET_STATE message + msg = GossipMessage( + msg_type=MessageType.GET_STATE.value, + msg_id="test_get_state", + sender_id="peer_node", + timestamp=1234567890, + ttl=1, + signature="mock_sig", + payload={"requester": "peer_node"} + ) + + # This should NOT raise TypeError now + response = self.gossip._handle_get_state(msg) + + self.assertEqual(response["status"], "ok") + self.assertIn("state", response) + self.assertIn("signature", response) + self.assertIn("msg_id", response) + self.assertIn("ttl", response) + + # Verify the signature end-to-end + # Reconstruction logic used by the requester + state_msg = GossipMessage( + msg_type=MessageType.STATE.value, + msg_id=response["msg_id"], + sender_id=response["sender_id"], + timestamp=response["timestamp"], + ttl=response["ttl"], + signature=response["signature"], + payload={"state": response["state"]} + ) + + self.assertTrue(self.gossip.verify_message(state_msg), "Requester failed to verify state response signature") + +if __name__ == "__main__": + unittest.main()