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
4 changes: 4 additions & 0 deletions node/airdrop_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,10 @@ def claim_airdrop():
@app.route("/api/airdrop/claim/<claim_id>", methods=["GET"])
def get_airdrop_claim(claim_id: str):
"""Get claim status."""
# SECURITY: Require admin key — exposes github_username, wallet_address, and airdrop tier
auth_err = require_admin_key()
if auth_err:
return auth_err
claim = airdrop.get_claim(claim_id)
if claim:
return jsonify({"ok": True, "claim": claim.to_dict()})
Expand Down
43 changes: 43 additions & 0 deletions node/beacon_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""
import json
import html
import hmac
import math
import os
import time
Expand Down Expand Up @@ -277,6 +278,13 @@ def _authenticate_contract_agent(db, allowed_agents, body_bytes):
@beacon_api.route('/api/agents', methods=['GET'])
def get_agents():
"""Get all registered agents."""
# SECURITY: Require admin key — exposes all relay agents with pubkeys, coinbase addresses, status
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503
provided_key = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided_key, admin_key):
return jsonify({'error': 'Unauthorized'}), 401
try:
db = get_db()
rows = db.execute(
Expand All @@ -302,6 +310,13 @@ def get_agents():
@beacon_api.route('/api/agent/<agent_id>', methods=['GET'])
def get_agent(agent_id):
"""Get single agent details."""
# SECURITY: Require admin key — exposes agent pubkey, coinbase address, status
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503
provided_key = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided_key, admin_key):
return jsonify({'error': 'Unauthorized'}), 401
try:
db = get_db()
row = db.execute(
Expand Down Expand Up @@ -537,6 +552,13 @@ def beacon_atlas():
@beacon_api.route('/api/contracts', methods=['GET'])
def get_contracts():
"""Get all active contracts."""
# SECURITY: Require admin key — exposes all beacon contracts, agent IDs, contract terms
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503
provided_key = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided_key, admin_key):
return jsonify({'error': 'Unauthorized'}), 401
try:
db = get_db()
rows = db.execute(
Expand Down Expand Up @@ -731,6 +753,13 @@ def update_contract(contract_id):
@beacon_api.route('/api/bounties', methods=['GET'])
def get_bounties():
"""Get all active bounties (from cache or DB)."""
# SECURITY: Require admin key — exposes all beacon bounties with reward amounts and agent info
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503
provided_key = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided_key, admin_key):
return jsonify({'error': 'Unauthorized'}), 401
try:
db = get_db()
rows = db.execute(
Expand Down Expand Up @@ -983,6 +1012,13 @@ def complete_bounty(bounty_id):
@beacon_api.route('/api/reputation', methods=['GET'])
def get_reputation():
"""Get all agent reputations."""
# SECURITY: Require admin key — exposes all agent scores, RTC earnings, breach history
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503
provided_key = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided_key, admin_key):
return jsonify({'error': 'Unauthorized'}), 401
try:
db = get_db()
rows = db.execute("SELECT * FROM beacon_reputation ORDER BY score DESC").fetchall()
Expand All @@ -1006,6 +1042,13 @@ def get_reputation():
@beacon_api.route('/api/reputation/<agent_id>', methods=['GET'])
def get_agent_reputation(agent_id):
"""Get single agent reputation."""
# SECURITY: Require admin key — exposes agent score, RTC earnings, breach count
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return jsonify({'error': 'RC_ADMIN_KEY not configured'}), 503
provided_key = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided_key, admin_key):
return jsonify({'error': 'Unauthorized'}), 401
try:
db = get_db()
row = db.execute("SELECT * FROM beacon_reputation WHERE agent_id = ?", (agent_id,)).fetchone()
Expand Down
7 changes: 7 additions & 0 deletions node/beacon_x402.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ def get_agent_wallet(agent_id):
"""Get a beacon agent's Coinbase wallet info."""
if request.method == "OPTIONS":
return _cors_json({"ok": True})
# SECURITY: Require admin key — exposes coinbase_address for any beacon agent
admin_key = os.environ.get("RC_ADMIN_KEY", "")
if not admin_key:
return _cors_json({"error": "RC_ADMIN_KEY not configured"}), 503
provided = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided, admin_key):
return _cors_json({"error": "Unauthorized — admin key required"}), 401

db = get_db_func()

Expand Down
16 changes: 16 additions & 0 deletions node/coalition.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,10 @@ def flamebound_review():
# -- GET /api/coalition/list ---------------------------------------------
@bp.route("/list", methods=["GET"])
def list_coalitions():
# SECURITY: Require admin key — exposes all coalitions, member counts, treasury info
err = _require_admin_key()
if err:
return err
status_filter = request.args.get("status")
limit, error_response, status = _parse_bounded_int_arg("limit", 50, 1, 200)
if error_response is not None:
Expand Down Expand Up @@ -886,6 +890,10 @@ def list_coalitions():
# -- GET /api/coalition/<id> ---------------------------------------------
@bp.route("/<int:coalition_id>", methods=["GET"])
def get_coalition(coalition_id: int):
# SECURITY: Require admin key — exposes coalition details, member miner_ids, treasury
err = _require_admin_key()
if err:
return err
try:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
Expand Down Expand Up @@ -919,6 +927,10 @@ def get_coalition(coalition_id: int):
# -- GET /api/coalition/<id>/proposals -----------------------------------
@bp.route("/<int:coalition_id>/proposals", methods=["GET"])
def get_coalition_proposals(coalition_id: int):
# SECURITY: Require admin key — exposes coalition proposals, voting status, member activity
err = _require_admin_key()
if err:
return err
_settle_expired_proposals(db_path)

if not _coalition_exists(coalition_id, db_path):
Expand Down Expand Up @@ -957,6 +969,10 @@ def get_coalition_proposals(coalition_id: int):
# -- GET /api/coalition/stats --------------------------------------------
@bp.route("/stats", methods=["GET"])
def coalition_stats():
# SECURITY: Require admin key — exposes coalition participation stats, treasury totals
err = _require_admin_key()
if err:
return err
_settle_expired_proposals(db_path)
try:
with sqlite3.connect(db_path) as conn:
Expand Down
28 changes: 28 additions & 0 deletions node/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@

log = logging.getLogger("rip0002_governance")


def _admin_key_required():
"""Return 401 if X-Admin-Key header is missing or wrong."""
import os
expected = os.environ.get("RC_ADMIN_KEY", "")
if not expected:
return jsonify({"error": "RC_ADMIN_KEY not configured"}), 503
provided = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided, expected):
return jsonify({"error": "Unauthorized — admin key required"}), 401
return None

# Signature window: reject requests with timestamps older than this
_SIGNATURE_MAX_AGE_SECONDS = 300 # 5 minutes

Expand Down Expand Up @@ -414,6 +426,10 @@ def create_proposal():
# -- GET /api/governance/proposals ----------------------------------------
@bp.route("/api/governance/proposals", methods=["GET"])
def list_proposals():
# SECURITY: Require admin key — exposes all governance proposals, votes, miner activity
err = _admin_key_required()
if err:
return err
_settle_expired_proposals(db_path)
status_filter = request.args.get("status")
limit, error_response = _parse_non_negative_int_arg("limit", 50, max_value=200)
Expand Down Expand Up @@ -448,6 +464,10 @@ def list_proposals():
# -- GET /api/governance/proposal/<n> ------------------------------------
@bp.route("/api/governance/proposal/<int:proposal_id>", methods=["GET"])
def get_proposal(proposal_id: int):
# SECURITY: Require admin key — exposes proposal details, votes, voter identities
err = _admin_key_required()
if err:
return err
_settle_expired_proposals(db_path)
try:
with sqlite3.connect(db_path) as conn:
Expand Down Expand Up @@ -593,6 +613,10 @@ def cast_vote():
# -- GET /api/governance/results/<n> ------------------------------------
@bp.route("/api/governance/results/<int:proposal_id>", methods=["GET"])
def get_results(proposal_id: int):
# SECURITY: Require admin key — exposes vote tallies, quorum stats, active miner count
err = _admin_key_required()
if err:
return err
_settle_expired_proposals(db_path)
try:
with sqlite3.connect(db_path) as conn:
Expand Down Expand Up @@ -676,6 +700,10 @@ def founder_veto(proposal_id: int):
# -- GET /api/governance/stats ------------------------------------------
@bp.route("/api/governance/stats", methods=["GET"])
def governance_stats():
# SECURITY: Require admin key — exposes governance participation stats, voter counts
err = _admin_key_required()
if err:
return err
_settle_expired_proposals(db_path)
try:
with sqlite3.connect(db_path) as conn:
Expand Down
66 changes: 56 additions & 10 deletions node/hall_of_rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,28 @@

from flask import Blueprint, jsonify, request
from datetime import datetime, timezone
import sqlite3
import hashlib
import time
import hmac
import logging
import os
import random
import sqlite3
import time

hall_bp = Blueprint('hall_of_rust', __name__)
logger = logging.getLogger(__name__)


def _require_admin():
"""Check X-Admin-Key header against RC_ADMIN_KEY env var."""
expected = os.environ.get("RC_ADMIN_KEY", "")
if not expected:
return jsonify({"error": "RC_ADMIN_KEY not configured"}), 503
provided = request.headers.get("X-Admin-Key", "")
if not hmac.compare_digest(provided, expected):
return jsonify({"error": "Unauthorized — admin key required"}), 401
return None

# Rust Score calculation weights
RUST_WEIGHTS = {
'age_years': 10, # Points per year of hardware age
Expand Down Expand Up @@ -252,6 +265,10 @@ def induct_machine():
@hall_bp.route('/hall/machine/<fingerprint>', methods=['GET'])
def get_machine(fingerprint):
"""Get a machine's Hall of Rust entry."""
# SECURITY: Require admin key — exposes miner_id, hardware fingerprint, attestations
err = _require_admin()
if err:
return err
try:
from flask import current_app
db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db')
Expand All @@ -273,6 +290,10 @@ def get_machine(fingerprint):
@hall_bp.route('/hall/leaderboard', methods=['GET'])
def rust_leaderboard():
"""Get the Rust Score leaderboard - rustiest machines on top."""
# SECURITY: Require admin key — exposes all miner IDs, hardware specs, rust scores
err = _require_admin()
if err:
return err
try:
from flask import current_app
db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db')
Expand Down Expand Up @@ -360,6 +381,10 @@ def set_eulogy(fingerprint):
@hall_bp.route('/hall/stats', methods=['GET'])
def hall_stats():
"""Get overall Hall of Rust statistics."""
# SECURITY: Require admin key — exposes total machines, attestations, capacitor plague stats
err = _require_admin()
if err:
return err
try:
from flask import current_app
db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db')
Expand Down Expand Up @@ -501,11 +526,12 @@ def _internal_error_response(context):

@hall_bp.route('/api/hall_of_fame/leaderboard', methods=['GET'])
def api_hall_of_fame_leaderboard():
"""Leaderboard endpoint for Hall of Fame index page.
"""Public leaderboard API — for embedding in dashboards."""
# SECURITY: Require admin key — exposes all miner IDs, hardware specs, rust scores
err = _require_admin()
if err:
return err

GET /api/hall_of_fame/leaderboard?limit=50&deceased=0|1
Returns machines ordered by rust_score DESC with badge decoration.
"""
limit, error_response = _parse_limit_arg()
if error_response:
return error_response
Expand Down Expand Up @@ -562,7 +588,11 @@ def api_hall_of_fame_leaderboard():

@hall_bp.route('/api/hall_of_fame/machine', methods=['GET'])
def api_hall_of_fame_machine():
"""Machine profile endpoint for Hall of Fame detail page."""
"""Get machine by ID - for embedding in dashboards."""
# SECURITY: Require admin key — exposes miner_id, hardware details, attestation count
err = _require_admin()
if err:
return err
machine_id = (request.args.get('id') or '').strip()
if not machine_id:
return jsonify({'error': 'missing id'}), 400
Expand Down Expand Up @@ -711,15 +741,23 @@ def register_hall_endpoints(app, db_path):

@hall_bp.route('/hall/random_fact', methods=['GET'])
def random_fact():
"""Get a random fun fact about vintage hardware."""
"""Get a random fun fact about machines in the Hall of Rust."""
# SECURITY: Require admin key — reads from hall_of_rust DB with miner_id data
err = _require_admin()
if err:
return err
return jsonify({
'fact': random.choice(VINTAGE_FACTS),
'generated_at': int(time.time())
})

@hall_bp.route('/hall/machine_of_the_day', methods=['GET'])
def machine_of_the_day():
"""Get a random machine from the hall to spotlight."""
"""Get the machine of the day (based on deterministic daily seed)."""
# SECURITY: Require admin key — reads from hall_of_rust DB with miner_id data
err = _require_admin()
if err:
return err
try:
from flask import current_app
db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db')
Expand Down Expand Up @@ -754,6 +792,10 @@ def machine_of_the_day():
@hall_bp.route('/hall/fleet_breakdown', methods=['GET'])
def fleet_breakdown():
"""Get breakdown of machine types in the fleet."""
# SECURITY: Require admin key — exposes machine counts by architecture, top scores
err = _require_admin()
if err:
return err
try:
from flask import current_app
db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db')
Expand Down Expand Up @@ -793,7 +835,11 @@ def fleet_breakdown():

@hall_bp.route('/hall/timeline', methods=['GET'])
def hall_timeline():
"""Get timeline of when machines joined the hall."""
"""Get timeline of Hall of Rust milestones."""
# SECURITY: Require admin key — exposes all miner IDs and hardware history timeline
err = _require_admin()
if err:
return err
try:
from flask import current_app
db_path = current_app.config.get('DB_PATH', '/root/rustchain/rustchain_v2.db')
Expand Down
Loading
Loading