From 8177a98912e5cde65f77373c770cb871550e1c4a Mon Sep 17 00:00:00 2001 From: rebel117 <> Date: Sat, 8 Aug 2026 04:56:59 +0800 Subject: [PATCH] fix(p2p): require auth on gossip POST + fix state root endianness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /p2p/gossip POST endpoint — the write path that feeds CRDT merges and can inject attestation records or epoch state — had no auth gate, while every P2P GET endpoint required X-P2P-Key. Added the same _require_p2p_read_auth() check to the gossip handler so unauthenticated callers can't write to the CRDT. Also fixed the state root merkle tree count prefix from little-endian to big-endian to match the convention used by compute_box_id and the rest of the UTXO hashing code. --- node/rustchain_p2p_gossip.py | 6 ++++++ node/tests/test_p2p_gossip_routes.py | 27 ++++++++++++++++++++++++--- node/utxo_db.py | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/node/rustchain_p2p_gossip.py b/node/rustchain_p2p_gossip.py index 757babe51..019c26e7b 100644 --- a/node/rustchain_p2p_gossip.py +++ b/node/rustchain_p2p_gossip.py @@ -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): diff --git a/node/tests/test_p2p_gossip_routes.py b/node/tests/test_p2p_gossip_routes.py index 555163972..0d0c3a744 100644 --- a/node/tests/test_p2p_gossip_routes.py +++ b/node/tests/test_p2p_gossip_routes.py @@ -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" @@ -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" @@ -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"] @@ -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] diff --git a/node/utxo_db.py b/node/utxo_db.py index f423a6133..cf345288c 100644 --- a/node/utxo_db.py +++ b/node/utxo_db.py @@ -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 = {