Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions node/rustchain_p2p_gossip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
54 changes: 54 additions & 0 deletions node/tests/test_get_state_arity.py
Original file line number Diff line number Diff line change
@@ -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()
Loading