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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ docs/media/*.mp3 -diff
*.env text eol=lf
*.txt text eol=lf
*.rst text eol=lf
*.sha256 text eol=lf

# Shell scripts should always use LF
*.sh text eol=lf
Expand Down
46 changes: 29 additions & 17 deletions miners/gpu_fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
Author: Elyan Labs (RIP-0308: Proof of Physical AI)
"""

from __future__ import annotations

import argparse
import json
import hashlib
Expand All @@ -40,12 +42,17 @@
import torch
import torch.cuda
except ImportError:
print("ERROR: PyTorch with CUDA support required. Install: pip install torch")
sys.exit(1)
torch = None
HAS_TORCH = False
else:
HAS_TORCH = True


if not torch.cuda.is_available():
print("ERROR: No CUDA-capable GPU detected.")
sys.exit(1)
def check_requirements():
if not HAS_TORCH or torch is None:
raise RuntimeError("PyTorch with CUDA support required. Install: pip install torch")
if not torch.cuda.is_available():
raise RuntimeError("No CUDA-capable GPU detected.")


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -789,6 +796,7 @@ def cross_validate_gpu(device: torch.device) -> ChannelResult:

def run_gpu_fingerprint(device_index: int = 0, samples: int = 200, epoch_salt: str = "") -> GPUFingerprint:
"""Run all GPU fingerprint channels and return results."""
check_requirements()
device = torch.device(f"cuda:{device_index}")

# GPU info
Expand Down Expand Up @@ -898,16 +906,20 @@ def run_gpu_fingerprint(device_index: int = 0, samples: int = 200, epoch_salt: s
help="Epoch salt for privacy (prevents cross-epoch correlation)")
args = parser.parse_args()

if args.json:
# Suppress banner output for clean JSON
import io, contextlib
with contextlib.redirect_stdout(io.StringIO()):
try:
if args.json:
# Suppress banner output for clean JSON
import io, contextlib
with contextlib.redirect_stdout(io.StringIO()):
fp = run_gpu_fingerprint(device_index=args.device, samples=args.samples, epoch_salt=args.epoch_salt)
print(json.dumps(fp.to_dict(), indent=2))
else:
fp = run_gpu_fingerprint(device_index=args.device, samples=args.samples, epoch_salt=args.epoch_salt)
print(json.dumps(fp.to_dict(), indent=2))
else:
fp = run_gpu_fingerprint(device_index=args.device, samples=args.samples, epoch_salt=args.epoch_salt)
# Print channel summary
print("Channel Details:")
for ch in fp.channels:
status = "PASS" if ch["passed"] else "FAIL"
print(f" [{status}] {ch['name']}: {ch['notes']}")
# Print channel summary
print("Channel Details:")
for ch in fp.channels:
status = "PASS" if ch["passed"] else "FAIL"
print(f" [{status}] {ch['name']}: {ch['notes']}")
except RuntimeError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)
28 changes: 0 additions & 28 deletions node/beacon_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,13 +581,6 @@ 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 @@ -810,13 +803,6 @@ 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 @@ -1069,13 +1055,6 @@ 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 @@ -1099,13 +1078,6 @@ 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
3 changes: 0 additions & 3 deletions node/gpu_render_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,9 +598,6 @@ def gpu_attest():

@app.route("/gpu/nodes", methods=["GET"])
def gpu_nodes():
err, status = _admin_key_required()
if err is not None:
return jsonify(err), status
job_type = request.args.get("job_type")
device_arch = request.args.get("device_arch")
nodes = protocol.list_gpu_nodes(job_type, device_arch)
Expand Down
30 changes: 3 additions & 27 deletions node/lock_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,15 +724,7 @@ def parse_optional_string(data, name: str, default: Optional[str] = None):

@app.route('/api/lock/miner/<miner_id>', methods=['GET'])
def get_miner_locks(miner_id: str):
"""Get locks for a specific miner. Requires admin key."""
# SECURITY: Exposes miner lock balances — admin key required
admin_key = request.headers.get("X-Admin-Key", "") or request.headers.get("X-API-Key", "")
expected_key = os.environ.get("RC_ADMIN_KEY", "")
if not expected_key:
return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503
if not hmac.compare_digest(admin_key, expected_key):
return jsonify({"error": "Unauthorized — admin key required"}), 401

"""Get locks for a specific miner."""
status = request.args.get("status")
limit, error_response = parse_bounded_int_arg("limit", 100, 1, 500)
if error_response is not None:
Expand Down Expand Up @@ -768,15 +760,7 @@ def get_miner_locks(miner_id: str):

@app.route('/api/lock/<int:lock_id>', methods=['GET'])
def get_lock(lock_id: int):
"""Get a specific lock by ID. Requires admin key."""
# SECURITY: Exposes detailed lock info including miner_id and amounts — admin key required
admin_key = request.headers.get("X-Admin-Key", "") or request.headers.get("X-API-Key", "")
expected_key = os.environ.get("RC_ADMIN_KEY", "")
if not expected_key:
return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503
if not hmac.compare_digest(admin_key, expected_key):
return jsonify({"error": "Unauthorized — admin key required"}), 401

"""Get a specific lock by ID."""
conn = sqlite3.connect(DB_PATH)
try:
lock = get_lock_by_id(conn, lock_id)
Expand All @@ -803,15 +787,7 @@ def get_lock(lock_id: int):

@app.route('/api/lock/pending-unlock', methods=['GET'])
def get_pending_unlocks_endpoint():
"""Get locks ready to be released. Requires admin key."""
# SECURITY: Exposes pending unlocks with miner IDs and amounts — admin key required
admin_key = request.headers.get("X-Admin-Key", "") or request.headers.get("X-API-Key", "")
expected_key = os.environ.get("RC_ADMIN_KEY", "")
if not expected_key:
return jsonify({"error": "RC_ADMIN_KEY not configured — endpoint disabled"}), 503
if not hmac.compare_digest(admin_key, expected_key):
return jsonify({"error": "Unauthorized — admin key required"}), 401

"""Get locks ready to be released."""
limit, error_response = parse_bounded_int_arg("limit", 100, 1, 500)
if error_response is not None:
return error_response
Expand Down
16 changes: 0 additions & 16 deletions node/rustchain_tx_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,10 +754,6 @@ def submit_transaction():
@app.route('/tx/status/<tx_hash>', methods=['GET'])
def get_tx_status(tx_hash: str):
"""Get transaction status"""
# SECURITY: Require admin key — prevents unauthorized transaction surveillance
auth_err = require_admin()
if auth_err:
return auth_err
try:
status = tx_pool.get_transaction_status(tx_hash)
return jsonify(status)
Expand Down Expand Up @@ -797,10 +793,6 @@ def list_pending():
@app.route('/wallet/<address>/balance', methods=['GET'])
def get_wallet_balance(address: str):
"""Get wallet balance"""
# SECURITY: Require admin key — exposes wallet balances without auth
auth_err = require_admin()
if auth_err:
return auth_err
try:
balance = tx_pool.get_balance(address)
available = tx_pool.get_available_balance(address)
Expand All @@ -820,10 +812,6 @@ def get_wallet_balance(address: str):
@app.route('/wallet/<address>/nonce', methods=['GET'])
def get_wallet_nonce(address: str):
"""Get wallet nonce (for transaction construction)"""
# SECURITY: Require admin key — exposes wallet nonces enabling nonce exhaustion attacks
auth_err = require_admin()
if auth_err:
return auth_err
try:
nonce = tx_pool.get_wallet_nonce(address)
pending_nonces = tx_pool._get_pending_nonces(address)
Expand All @@ -845,10 +833,6 @@ def get_wallet_nonce(address: str):
@app.route('/wallet/<address>/history', methods=['GET'])
def get_wallet_history(address: str):
"""Get transaction history for wallet"""
# SECURITY: Require admin key — exposes complete transaction history without auth
auth_err = require_admin()
if auth_err:
return auth_err
try:
limit_raw = request.args.get('limit')
offset_raw = request.args.get('offset')
Expand Down
1 change: 0 additions & 1 deletion node/rustchain_v2_integrated_v2.2.1_rip200.py
Original file line number Diff line number Diff line change
Expand Up @@ -6579,7 +6579,6 @@ def governance_proposal_detail(proposal_id: int):


@app.route('/governance/vote', methods=['POST'])
@admin_required
def governance_vote():
data = request.get_json(silent=True)
if data is None:
Expand Down
29 changes: 8 additions & 21 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import json
import sqlite3
from unittest.mock import patch, MagicMock
from unittest.mock import patch
import sys
from pathlib import Path
from types import SimpleNamespace
Expand Down Expand Up @@ -64,30 +64,17 @@ def test_api_epoch_admin_sees_full_payload(client):
assert data['enrolled_miners'] == 10


def test_api_miners_requires_auth(client):
def test_api_miners_requires_auth(client, monkeypatch, tmp_path):
"""Unauthenticated /api/miners endpoint should still return data (no auth required)."""
rate_info = {"limit": 100, "remaining": 99, "reset": 0, "retry_after": 0}
with patch('integrated_node.check_api_miners_rate_limit', return_value=(True, rate_info)), \
patch('sqlite3.connect') as mock_connect:
import sqlite3 as _sqlite3
mock_conn = mock_connect.return_value.__enter__.return_value
mock_conn.row_factory = _sqlite3.Row
mock_cursor = mock_conn.cursor.return_value
enrolled_conn = MagicMock()
enrolled_conn.execute.return_value.fetchone.return_value = [0]

# The endpoint calls c.execute() twice:
# 1. SELECT COUNT(*) ... -> fetchone() -> [0]
# 2. SELECT ... FROM miner_attest_recent ... -> fetchall() -> []
count_result = MagicMock()
count_result.fetchone.return_value = [0]
rows_result = MagicMock()
rows_result.fetchall.return_value = []
mock_cursor.execute.side_effect = [count_result, rows_result]
mock_connect.side_effect = [mock_connect.return_value, enrolled_conn]
db_path = tmp_path / "api_miners_public.db"
_init_api_miners_db(db_path)
monkeypatch.setattr(integrated_node, "DB_PATH", str(db_path))

rate_info = {"limit": 100, "remaining": 99, "reset": 0, "retry_after": 0}
with patch('integrated_node.check_api_miners_rate_limit', return_value=(True, rate_info)):
response = client.get('/api/miners')
assert response.status_code == 200
assert response.get_json()["miners"] == []


def _init_api_miners_db(path):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_beacon_atlas_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class TestBeaconAtlasAPIBehavior(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Set up test fixtures once for all tests."""
cls._previous_admin_key = os.environ.get('RC_ADMIN_KEY')
os.environ['RC_ADMIN_KEY'] = 'test-admin-key'

# Create temporary database for testing
cls.test_db_fd, cls.test_db_path = tempfile.mkstemp(suffix='.db')

Expand Down Expand Up @@ -79,6 +82,10 @@ def tearDownClass(cls):
gc.collect()
os.close(cls.test_db_fd)
os.unlink(cls.test_db_path)
if cls._previous_admin_key is None:
os.environ.pop('RC_ADMIN_KEY', None)
else:
os.environ['RC_ADMIN_KEY'] = cls._previous_admin_key

def setUp(self):
"""Reset database state before each test."""
Expand Down
7 changes: 4 additions & 3 deletions tests/test_bridge_lock_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,15 @@ def setup_test_db(tmp_path):
@pytest.fixture
def funded_miner(setup_test_db):
"""Create a miner with balance in the test database."""
miner_id = "RTC" + "1" * 40
conn = sqlite3.connect(setup_test_db['db_path'])
conn.execute(
"INSERT INTO balances (miner_id, amount_i64) VALUES (?, ?)",
("RTC_test_miner", 100 * 1000000) # 100 RTC
(miner_id, 100 * 1000000) # 100 RTC
)
conn.commit()
conn.close()
return "RTC_test_miner"
return miner_id


def assert_generic_database_error(result):
Expand Down Expand Up @@ -315,7 +316,7 @@ class TestAddressValidation:
def test_valid_rustchain_address(self, setup_test_db):
"""Test valid RustChain address."""
bridge_api = setup_test_db["bridge_api"]
valid, msg = bridge_api.validate_chain_address_format("rustchain", "RTC_test123abc")
valid, msg = bridge_api.validate_chain_address_format("rustchain", "RTC" + "a" * 40)
assert valid is True

def test_invalid_rustchain_address_prefix(self, setup_test_db):
Expand Down
Loading
Loading