Skip to content
Open
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
6 changes: 6 additions & 0 deletions node/rustchain_p2p_gossip.py
Original file line number Diff line number Diff line change
Expand Up @@ -1818,6 +1818,12 @@ def _require_p2p_read_auth():
@app.route('/p2p/gossip', methods=['POST'])
def receive_gossip():
"""Receive and process gossip message"""
# Auth: every other P2P endpoint requires X-P2P-Key. The gossip
# POST feeds CRDT merges, so it needs the same gate.
auth_error = _require_p2p_read_auth()
if auth_error:
return auth_error

# FIX(#2867 M5): per-IP rate limit BEFORE expensive verify+CRDT work.
remote_ip = request.headers.get('X-Forwarded-For', request.remote_addr or 'unknown').split(',')[0].strip()
if not _gossip_rate_check(remote_ip):
Expand Down
27 changes: 24 additions & 3 deletions node/tests/test_p2p_gossip_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_p2p_gossip_requires_json_object():
app, node = _app_and_node()

with app.test_client() as client:
resp = client.post("/p2p/gossip", json=["not", "an", "object"])
resp = client.post("/p2p/gossip", json=["not", "an", "object"], headers={"X-P2P-Key": "a" * 64})

assert resp.status_code == 400
assert resp.get_json()["error"] == "JSON object required"
Expand All @@ -66,7 +66,7 @@ def test_p2p_gossip_forwards_valid_object_body():
payload = {"msg_type": "ping"}

with app.test_client() as client:
resp = client.post("/p2p/gossip", json=payload)
resp = client.post("/p2p/gossip", json=payload, headers={"X-P2P-Key": "a" * 64})

assert resp.status_code == 200
assert resp.get_json()["status"] == "ok"
Expand All @@ -89,7 +89,7 @@ def test_p2p_gossip_rejects_oversized_payload_before_handler():
}

with app.test_client() as client:
resp = client.post("/p2p/gossip", json=payload)
resp = client.post("/p2p/gossip", json=payload, headers={"X-P2P-Key": "a" * 64})

assert resp.status_code == 400
assert "too many keys" in resp.get_json()["error"]
Expand All @@ -112,3 +112,24 @@ def test_gossip_message_rejects_payload_that_exceeds_serialized_cap():

with pytest.raises(ValueError, match="maximum serialized size"):
GossipMessage.from_dict(payload)


def test_p2p_gossip_requires_auth_header():
"""Without X-P2P-Key the gossip POST should be rejected."""
app, node = _app_and_node()
with app.test_client() as client:
resp = client.post("/p2p/gossip", json={"msg_type": "ping"})
assert resp.status_code == 401
assert node.handled == []


def test_p2p_gossip_accepts_valid_auth():
"""With the correct X-P2P-Key the gossip POST should succeed."""
app, node = _app_and_node()
payload = {"msg_type": "ping"}
with app.test_client() as client:
resp = client.post(
"/p2p/gossip", json=payload, headers={"X-P2P-Key": "a" * 64}
)
assert resp.status_code == 200
assert node.handled == [payload]
2 changes: 1 addition & 1 deletion node/utxo_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,7 @@ def compute_state_root(self) -> str:
return hashlib.sha256(b"empty").hexdigest()

# Mix element count into leaf hashes to bind tree to cardinality
count_bytes = len(rows).to_bytes(8, 'little')
count_bytes = len(rows).to_bytes(8, 'big')
hashes = []
for row in rows:
leaf = {
Expand Down