From 6ee452c210921efcb430ec30dd4f6f5ae27ebb58 Mon Sep 17 00:00:00 2001 From: Scott Boudreaux Date: Mon, 1 Jun 2026 14:41:16 -0500 Subject: [PATCH] security(block-producer): allowlist device_info on public /block/producers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Scott's call, /block/slot, /block/producers, and /api/wallet stay PUBLIC (proof-of-antiquity consensus transparency the explorer/dashboard depend on; no secrets/IPs/keys are exposed). The only hardening kept after tri-brain review: expose device_info through an explicit field allowlist (arch/family/model/year/enroll_weight) so a future column added to device_info (e.g. an IP/hostname) can never leak through this unauthenticated endpoint. Output is unchanged for current data; a non-dict/None row degrades to {} instead of raising. is_my_turn and the balance summary are intentionally left as-is to avoid breaking the public API contract. Supersedes #6715 (which admin-gated these public endpoints and would have broken the explorer). Tri-brain reviewed (Codex/Grok; GPT-OSS offline) — earlier over-broad changes (removing is_my_turn, capping balance) were reverted as regressions. Co-Authored-By: Claude Opus 4.8 (1M context) --- node/rustchain_block_producer.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/node/rustchain_block_producer.py b/node/rustchain_block_producer.py index d208f87af..974497a11 100644 --- a/node/rustchain_block_producer.py +++ b/node/rustchain_block_producer.py @@ -55,6 +55,10 @@ GENESIS_TIMESTAMP = 1764706927 # Production chain launch (Dec 2, 2025) BLOCK_TIME = 600 # 10 minutes (600 seconds) + +# Public allowlist for /block/producers device_info (prevents future +# fields leaking through this unauthenticated endpoint). +_DEVICE_PUBLIC_FIELDS = ("arch", "family", "model", "year", "enroll_weight") MAX_TXS_PER_BLOCK = 1000 ATTESTATION_TTL = 600 # 10 minutes MAX_BATCH_BLOCKS = 100 @@ -1093,6 +1097,11 @@ def list_producers(): current_ts = int(time.time()) miners = producer.get_attested_miners(current_ts) + # Intentionally PUBLIC consensus transparency. device_info is exposed via + # an explicit field allowlist so a future column added to it (e.g. an + # IP/hostname) can never leak through this unauthenticated endpoint. + # Behaviour for current data is unchanged (these are the only fields + # device_info carries); a non-dict/None row degrades to {} instead of 500. return jsonify({ "count": len(miners), "balance": producer.get_producer_balance_summary( @@ -1104,7 +1113,9 @@ def list_producers(): "wallet": m[0], "arch": m[1], "selection_weight": producer._miner_selection_weight(m), - "device_info": m[2] + "device_info": { + k: m[2].get(k) for k in _DEVICE_PUBLIC_FIELDS + } if isinstance(m[2], dict) else {}, } for m in miners ]