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
12 changes: 9 additions & 3 deletions agent_reputation.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,14 @@ def check_eligibility():
Returns whether an agent is eligible to claim a job of given value.
"""
agent_id = request.args.get("agent_id", "").strip()
job_value = float(request.args.get("job_value", 0))

if not agent_id:
return jsonify({"error": "agent_id required"}), 400

try:
job_value = float(request.args.get("job_value", 0))
except (ValueError, TypeError):
return jsonify({"error": "job_value must be a number"}), 400

rep = _engine.get(agent_id)
max_val = rep["max_job_value_rtc"]
level = rep["level"]
Expand Down Expand Up @@ -364,7 +367,10 @@ def leaderboard():
GET /agent/reputation/leaderboard?limit=20
Returns top agents by reputation (from cache).
"""
limit = min(int(request.args.get("limit", 20)), 100)
try:
limit = min(int(request.args.get("limit", 20)), 100)
except (ValueError, TypeError):
return jsonify({"error": "limit must be an integer"}), 400
with _engine._lock:
entries = [(w, d["reputation_score"]) for w, (d, _) in _engine._cache.items()]
entries.sort(key=lambda x: x[1], reverse=True)
Expand Down
11 changes: 8 additions & 3 deletions explorer/explorer_websocket_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

Integration:
from explorer_websocket_server import socketio, app, start_explorer_poller
socketio.init_app(app, cors_allowed_origins="*", async_mode="threading")
socketio.init_app(app, cors_allowed_origins=os.environ.get("WS_ALLOWED_ORIGINS", "http://localhost,http://127.0.0.1").split(","), async_mode="threading")
start_explorer_poller()

Author: RustChain Team
Expand Down Expand Up @@ -286,14 +286,19 @@ def start_explorer_poller():

# ─── Flask App ──────────────────────────────────────────────────────────────── #
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'rustchain-explorer-secret')
secret_key = os.environ.get('SECRET_KEY')
if not secret_key:
import secrets
secret_key = secrets.token_hex(32)
print(f"[WARNING] Using auto-generated SECRET_KEY. Set SECRET_KEY env var for production.")
app.config['SECRET_KEY'] = secret_key

# ─── Flask Blueprint ────────────────────────────────────────────────────────── #
ws_bp = Blueprint("explorer_ws", __name__)

if HAVE_SOCKETIO:
socketio = SocketIO(
cors_allowed_origins="*",
cors_allowed_origins=os.environ.get("WS_ALLOWED_ORIGINS", "http://localhost,http://127.0.0.1").split(","),
async_mode="threading",
ping_timeout=HEARTBEAT_S,
ping_interval=HEARTBEAT_S,
Expand Down
9 changes: 7 additions & 2 deletions explorer/realtime_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@

# Flask app with SocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'rustchain-explorer-secret')
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
secret_key = os.environ.get('SECRET_KEY')
if not secret_key:
import secrets
secret_key = secrets.token_hex(32)
print(f"[WARNING] Using auto-generated SECRET_KEY. Set SECRET_KEY env var for production.")
app.config['SECRET_KEY'] = secret_key
socketio = SocketIO(app, cors_allowed_origins=os.environ.get("WS_ALLOWED_ORIGINS", "http://localhost,http://127.0.0.1").split(","), async_mode='threading')

# State tracking
class ExplorerState:
Expand Down
9 changes: 7 additions & 2 deletions explorer/ws_explorer_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
PORT = int(os.environ.get("WS_EXPLORER_PORT", "8060"))

app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "rustchain-ws-explorer")
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
secret_key = os.environ.get("SECRET_KEY")
if not secret_key:
import secrets
secret_key = secrets.token_hex(32)
print(f"[WARNING] Using auto-generated SECRET_KEY. Set SECRET_KEY env var for production.")
app.config["SECRET_KEY"] = secret_key
socketio = SocketIO(app, cors_allowed_origins=os.environ.get("WS_ALLOWED_ORIGINS", "http://localhost,http://127.0.0.1").split(","), async_mode="threading")

# ── State ─────────────────────────────────────────────────────────
state = {
Expand Down
5 changes: 4 additions & 1 deletion faucet.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,10 @@ def drip():
Response:
{"ok": true, "amount": 0.5, "next_available": "2026-03-08T12:00:00Z"}
"""
data = request.get_json()
try:
data = request.get_json(silent=True)
except Exception:
return jsonify({'ok': False, 'error': 'Invalid JSON'}), 400

if not data or 'wallet' not in data:
return jsonify({'ok': False, 'error': 'Wallet address required'}), 400
Expand Down
24 changes: 24 additions & 0 deletions issue2307_boot_chime/boot_chime_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from flask import Flask, request, jsonify, send_file
from werkzeug.utils import secure_filename
from flask_cors import CORS
import json
import os
Expand Down Expand Up @@ -158,6 +159,15 @@ def submit_proof():
audio_data = None
if 'audio' in request.files:
audio_file = request.files['audio']
# Security fix: validate filename and file type
filename = secure_filename(audio_file.filename or '')
if not filename.lower().endswith(('.wav', '.mp3', '.flac', '.ogg')):
return jsonify({'error': 'Invalid file type. Only audio files allowed.'}), 400
# Security fix: limit file size (max 10MB)
audio_file.seek(0, 2)
if audio_file.tell() > 10 * 1024 * 1024:
return jsonify({'error': 'File too large. Max size: 10MB.'}), 413
audio_file.seek(0)
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp:
audio_file.save(tmp)
tmp_path = tmp.name
Expand Down Expand Up @@ -238,6 +248,13 @@ def enroll_miner():
audio_file = None
if 'audio' in request.files:
audio = request.files['audio']
filename = secure_filename(audio.filename or '')
if not filename.lower().endswith(('.wav', '.mp3', '.flac', '.ogg')):
return jsonify({'error': 'Invalid file type. Only audio files allowed.'}), 400
audio.seek(0, 2)
if audio.tell() > 10 * 1024 * 1024:
return jsonify({'error': 'File too large. Max size: 10MB.'}), 413
audio.seek(0)
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp:
audio.save(tmp)
audio_file = tmp.name
Expand Down Expand Up @@ -413,6 +430,13 @@ def analyze_audio():
return jsonify({'error': 'audio file required'}), 400

audio_file = request.files['audio']
filename = secure_filename(audio_file.filename or '')
if not filename.lower().endswith(('.wav', '.mp3', '.flac', '.ogg')):
return jsonify({'error': 'Invalid file type. Only audio files allowed.'}), 400
audio_file.seek(0, 2)
if audio_file.tell() > 10 * 1024 * 1024:
return jsonify({'error': 'File too large. Max size: 10MB.'}), 413
audio_file.seek(0)

with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp:
audio_file.save(tmp)
Expand Down
4 changes: 2 additions & 2 deletions issue2307_boot_chime/src/proof_of_iron.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ def _save_features(self, features_hash: str,
conn = sqlite3.connect(self.db_path)
c = conn.cursor()

features_data = pickle.dumps({
features_data = json.dumps({
'mfcc_mean': features.mfcc_mean.tolist(),
'mfcc_std': features.mfcc_std.tolist(),
'spectral_centroid': features.spectral_centroid,
Expand Down Expand Up @@ -536,7 +536,7 @@ def _load_features(self, features_hash: str) -> Optional[FingerprintFeatures]:
conn.close()

if row:
data = pickle.loads(row[0])
data = json.loads(row[0])
return FingerprintFeatures(
mfcc_mean=np.array(data['mfcc_mean']),
mfcc_std=np.array(data['mfcc_std']),
Expand Down
3 changes: 2 additions & 1 deletion node/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"""

import hashlib
import hmac
import json
import logging
import sqlite3
Expand Down Expand Up @@ -553,7 +554,7 @@ def founder_veto(proposal_id: int):
# Admin key is validated via environment variable (not hardcoded)
import os
expected_key = os.environ.get("RUSTCHAIN_ADMIN_KEY", "")
if not expected_key or admin_key != expected_key:
if not expected_key or not hmac.compare_digest(admin_key, expected_key):
return jsonify({"error": "invalid admin_key"}), 403

try:
Expand Down
158 changes: 85 additions & 73 deletions node/hardware_binding_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DB_PATH = os.environ.get('RUSTCHAIN_DB_PATH') or os.environ.get('DB_PATH') or '/root/rustchain/rustchain_v2.db'
ENTROPY_TOLERANCE = 0.30 # 30% tolerance for entropy drift
MIN_COMPARABLE_FIELDS = 3 # require at least 3 non-zero entropy fields for quality
MIN_COLLISION_FIELDS = 2 # require at least 2 fields for collision detection (prevents bypass with sparse profiles)
CORE_ENTROPY_FIELDS = ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']

def init_hardware_bindings_v2():
Expand Down Expand Up @@ -145,7 +146,7 @@ def check_entropy_collision(entropy_profile: Dict, exclude_serial: str = None) -
# Count non-zero fields in current profile
nonzero_fields = _count_nonzero_fields(entropy_profile)

if nonzero_fields < MIN_COMPARABLE_FIELDS:
if nonzero_fields < MIN_COLLISION_FIELDS:
# Not enough entropy data to detect collisions reliably
return None

Expand Down Expand Up @@ -199,90 +200,101 @@ def bind_hardware_v2(
with sqlite3.connect(DB_PATH) as conn:
c = conn.cursor()

# Check existing binding
c.execute('SELECT bound_wallet, entropy_profile, macs_seen, attestation_count FROM hardware_bindings_v2 WHERE serial_hash = ?',
(serial_hash,))
row = c.fetchone()

if row is None:
# NEW HARDWARE - enforce entropy quality first
nonzero_fields = _count_nonzero_fields(entropy_profile)
if nonzero_fields < MIN_COMPARABLE_FIELDS:
return False, 'entropy_insufficient', {
'error': 'Entropy profile quality too low for secure binding',
'required_nonzero_fields': MIN_COMPARABLE_FIELDS,
'provided_nonzero_fields': nonzero_fields,
'action': 'submit a fuller fingerprint payload'
}
# FIX: Use BEGIN IMMEDIATE to acquire write lock early, preventing
# race conditions where concurrent requests both pass the SELECT check
# before either inserts. This ensures serializable isolation.
c.execute('BEGIN IMMEDIATE')
try:
# Check existing binding
c.execute('SELECT bound_wallet, entropy_profile, macs_seen, attestation_count FROM hardware_bindings_v2 WHERE serial_hash = ?',
(serial_hash,))
row = c.fetchone()

if row is None:
# NEW HARDWARE - enforce entropy quality first
nonzero_fields = _count_nonzero_fields(entropy_profile)
if nonzero_fields < MIN_COMPARABLE_FIELDS:
return False, 'entropy_insufficient', {
'error': 'Entropy profile quality too low for secure binding',
'required_nonzero_fields': MIN_COMPARABLE_FIELDS,
'provided_nonzero_fields': nonzero_fields,
'action': 'submit a fuller fingerprint payload'
}

# NEW HARDWARE - Check for entropy collision first
collision = check_entropy_collision(entropy_profile)
if collision:
return False, 'entropy_collision', {
'error': 'This hardware entropy matches an existing registration',
'collision_hash': collision[:16],
'suspected': 'serial_spoofing'
# NEW HARDWARE - Check for entropy collision first
# NOTE: check_entropy_collision uses MIN_COLLISION_FIELDS (lower threshold)
# to prevent bypass with sparse profiles
collision = check_entropy_collision(entropy_profile)
if collision:
return False, 'entropy_collision', {
'error': 'This hardware entropy matches an existing registration',
'collision_hash': collision[:16],
'suspected': 'serial_spoofing'
}

# Create new binding
c.execute('''
INSERT INTO hardware_bindings_v2
(serial_hash, serial_raw, bound_wallet, arch, cores, entropy_profile, macs_seen, first_seen, last_seen, attestation_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
''', (serial_hash, serial, wallet, arch, cores, json.dumps(entropy_profile), macs_str, now, now))
conn.commit()

return True, 'new_binding', {
'serial_hash': serial_hash[:16],
'wallet': wallet[:20],
'status': 'bound'
}

# EXISTING HARDWARE
bound_wallet, stored_entropy_json, stored_macs, attest_count = row

# Check wallet match
if bound_wallet != wallet:
return False, 'hardware_already_bound', {
'error': 'This hardware is permanently bound to another wallet',
'bound_to': bound_wallet[:20],
'attempted': wallet[:20]
}

# Validate entropy profile
stored_entropy = json.loads(stored_entropy_json) if stored_entropy_json else {}
is_valid, similarity, reason = compare_entropy_profiles(stored_entropy, entropy_profile)

if not is_valid:
return False, 'suspected_spoof', {
'error': 'Entropy profile does not match registered hardware',
'similarity': f'{similarity:.1%}',
'reason': reason,
'suspected': 'serial_spoofing_or_hardware_swap'
}

# Create new binding
# Update record
new_macs = stored_macs
if macs_str and macs_str not in (stored_macs or ''):
new_macs = f'{stored_macs},{macs_str}' if stored_macs else macs_str

flags = None
if 'drift' in reason:
flags = f'entropy_drift:{now}'

c.execute('''
INSERT INTO hardware_bindings_v2
(serial_hash, serial_raw, bound_wallet, arch, cores, entropy_profile, macs_seen, first_seen, last_seen, attestation_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
''', (serial_hash, serial, wallet, arch, cores, json.dumps(entropy_profile), macs_str, now, now))
UPDATE hardware_bindings_v2
SET last_seen = ?, attestation_count = attestation_count + 1, macs_seen = ?, flags = COALESCE(flags || ';' || ?, flags, ?)
WHERE serial_hash = ?
''', (now, new_macs, flags, flags, serial_hash))
conn.commit()

return True, 'new_binding', {
'serial_hash': serial_hash[:16],
'wallet': wallet[:20],
'status': 'bound'
}

# EXISTING HARDWARE
bound_wallet, stored_entropy_json, stored_macs, attest_count = row

# Check wallet match
if bound_wallet != wallet:
return False, 'hardware_already_bound', {
'error': 'This hardware is permanently bound to another wallet',
'bound_to': bound_wallet[:20],
'attempted': wallet[:20]
}

# Validate entropy profile
stored_entropy = json.loads(stored_entropy_json) if stored_entropy_json else {}
is_valid, similarity, reason = compare_entropy_profiles(stored_entropy, entropy_profile)

if not is_valid:
return False, 'suspected_spoof', {
'error': 'Entropy profile does not match registered hardware',
'similarity': f'{similarity:.1%}',
'reason': reason,
'suspected': 'serial_spoofing_or_hardware_swap'
}

# Update record
new_macs = stored_macs
if macs_str and macs_str not in (stored_macs or ''):
new_macs = f'{stored_macs},{macs_str}' if stored_macs else macs_str

flags = None
if 'drift' in reason:
flags = f'entropy_drift:{now}'

c.execute('''
UPDATE hardware_bindings_v2
SET last_seen = ?, attestation_count = attestation_count + 1, macs_seen = ?, flags = COALESCE(flags || ';' || ?, flags, ?)
WHERE serial_hash = ?
''', (now, new_macs, flags, flags, serial_hash))
conn.commit()
except Exception:
conn.rollback()
raise

return True, 'authorized', {
'serial_hash': serial_hash[:16],
'similarity': f'{similarity:.1%}',
'attestations': attest_count + 1
}


# Initialize on import.
# If DB path is explicitly configured and init fails, fail fast (safer for prod).
# If using the default Linux path on non-Linux / local dev, don't crash the whole node.
Expand Down
2 changes: 1 addition & 1 deletion node/rustchain_sync_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def require_admin(f):
@wraps(f)
def decorated(*args, **kwargs):
key = request.headers.get("X-Admin-Key") or request.headers.get("X-API-Key")
if not key or key != admin_key:
if not key or not hmac.compare_digest(key, admin_key):
return jsonify({"error": "Unauthorized"}), 401
return f(*args, **kwargs)

Expand Down
Loading
Loading