diff --git a/.gitattributes b/.gitattributes index 7d2e1ec28..df3c78725 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/miners/gpu_fingerprint.py b/miners/gpu_fingerprint.py index 133ad81ae..12da2f0af 100644 --- a/miners/gpu_fingerprint.py +++ b/miners/gpu_fingerprint.py @@ -24,6 +24,8 @@ Author: Elyan Labs (RIP-0308: Proof of Physical AI) """ +from __future__ import annotations + import argparse import json import hashlib @@ -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.") # --------------------------------------------------------------------------- @@ -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 @@ -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) diff --git a/node/beacon_api.py b/node/beacon_api.py index 6bbd12dc9..412279476 100644 --- a/node/beacon_api.py +++ b/node/beacon_api.py @@ -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( @@ -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( @@ -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() @@ -1099,13 +1078,6 @@ def get_reputation(): @beacon_api.route('/api/reputation/', 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() diff --git a/node/gpu_render_protocol.py b/node/gpu_render_protocol.py index 57532a9ae..0e26fba20 100644 --- a/node/gpu_render_protocol.py +++ b/node/gpu_render_protocol.py @@ -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) diff --git a/node/lock_ledger.py b/node/lock_ledger.py index 25b210fc9..7bd8a927c 100644 --- a/node/lock_ledger.py +++ b/node/lock_ledger.py @@ -724,15 +724,7 @@ def parse_optional_string(data, name: str, default: Optional[str] = None): @app.route('/api/lock/miner/', 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: @@ -768,15 +760,7 @@ def get_miner_locks(miner_id: str): @app.route('/api/lock/', 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) @@ -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 diff --git a/node/rustchain_tx_handler.py b/node/rustchain_tx_handler.py index 98affd0e9..d712baf3b 100644 --- a/node/rustchain_tx_handler.py +++ b/node/rustchain_tx_handler.py @@ -754,10 +754,6 @@ def submit_transaction(): @app.route('/tx/status/', 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) @@ -797,10 +793,6 @@ def list_pending(): @app.route('/wallet/
/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) @@ -820,10 +812,6 @@ def get_wallet_balance(address: str): @app.route('/wallet/
/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) @@ -845,10 +833,6 @@ def get_wallet_nonce(address: str): @app.route('/wallet/
/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') diff --git a/node/rustchain_v2_integrated_v2.2.1_rip200.py b/node/rustchain_v2_integrated_v2.2.1_rip200.py index 1ad0e68bd..327bfbb62 100644 --- a/node/rustchain_v2_integrated_v2.2.1_rip200.py +++ b/node/rustchain_v2_integrated_v2.2.1_rip200.py @@ -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: diff --git a/tests/test_api.py b/tests/test_api.py index dd4e4c7e2..018eaf23a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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 @@ -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): diff --git a/tests/test_beacon_atlas_behavior.py b/tests/test_beacon_atlas_behavior.py index 17fe91f0c..64772187d 100644 --- a/tests/test_beacon_atlas_behavior.py +++ b/tests/test_beacon_atlas_behavior.py @@ -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') @@ -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.""" diff --git a/tests/test_bridge_lock_ledger.py b/tests/test_bridge_lock_ledger.py index 6ebdcf929..f98a68c5b 100644 --- a/tests/test_bridge_lock_ledger.py +++ b/tests/test_bridge_lock_ledger.py @@ -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): @@ -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): diff --git a/tests/test_governance_api.py b/tests/test_governance_api.py index 658d892c6..d188078ac 100644 --- a/tests/test_governance_api.py +++ b/tests/test_governance_api.py @@ -37,6 +37,17 @@ def _vote_payload(proposal_id: int, wallet: str, vote: str, nonce: str): return payload +def _proposal_payload(pub_hex: str, title: str, description: str, nonce: str): + return { + "wallet": integrated_node.address_from_pubkey(pub_hex), + "title": title, + "description": description, + "nonce": nonce, + "signature": "ab" * 64, + "public_key": pub_hex, + } + + def test_governance_propose_requires_gt_10_rtc_balance(): with _temporary_directory() as td: db_path = str(Path(td) / "gov.db") @@ -44,16 +55,17 @@ def test_governance_propose_requires_gt_10_rtc_balance(): integrated_node.app.config["DB_PATH"] = db_path integrated_node.init_db() + pub_hex = "22" * 32 + payload = _proposal_payload(pub_hex, "No", "insufficient", "proposal-low-1") + with sqlite3.connect(db_path) as c: - c.execute("INSERT INTO balances(miner_pk, balance_rtc) VALUES(?, ?)", ("RTC-low", 10.0)) + c.execute("INSERT INTO balances(miner_pk, balance_rtc) VALUES(?, ?)", (payload["wallet"], 10.0)) c.commit() integrated_node.app.config["TESTING"] = True with integrated_node.app.test_client() as client: - resp = client.post( - "/governance/propose", - json={"wallet": "RTC-low", "title": "No", "description": "insufficient"}, - ) + with patch("integrated_node.verify_rtc_signature", return_value=True): + resp = client.post("/governance/propose", json=payload) assert resp.status_code == 403 assert resp.get_json()["error"] == "insufficient_balance_to_propose" @@ -70,7 +82,10 @@ def test_governance_propose_rejects_non_object_json(): def test_governance_vote_rejects_non_object_json(): integrated_node.app.config["TESTING"] = True with integrated_node.app.test_client() as client: - resp = client.post("/governance/vote", json=["not", "an", "object"]) + resp = client.post( + "/governance/vote", + json=["not", "an", "object"], + ) assert resp.status_code == 400 assert resp.get_json()["error"] == "JSON object required" @@ -136,10 +151,14 @@ def test_governance_vote_flow_and_lifecycle_finalization(): integrated_node.app.config["TESTING"] = True with integrated_node.app.test_client() as client: # Create proposal - r1 = client.post( - "/governance/propose", - json={"wallet": wallet, "title": "Raise testnet fee", "description": "for anti-spam"}, + proposal_payload = _proposal_payload( + pub_hex, + "Raise testnet fee", + "for anti-spam", + "proposal-flow-1", ) + with patch("integrated_node.verify_rtc_signature", return_value=True): + r1 = client.post("/governance/propose", json=proposal_payload) assert r1.status_code == 201 proposal_id = r1.get_json()["proposal"]["id"] diff --git a/tests/test_gpu_fingerprint_import.py b/tests/test_gpu_fingerprint_import.py new file mode 100644 index 000000000..35f6c850b --- /dev/null +++ b/tests/test_gpu_fingerprint_import.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: MIT +"""Regression coverage for importing the GPU fingerprint helper on CPU CI.""" + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +def test_gpu_fingerprint_import_without_torch_does_not_exit(monkeypatch): + monkeypatch.setitem(sys.modules, "torch", None) + monkeypatch.setitem(sys.modules, "torch.cuda", None) + + module_path = Path(__file__).resolve().parents[1] / "miners" / "gpu_fingerprint.py" + spec = importlib.util.spec_from_file_location("gpu_fingerprint_without_torch", module_path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + monkeypatch.setitem(sys.modules, spec.name, module) + + spec.loader.exec_module(module) + + assert module.HAS_TORCH is False + with pytest.raises(RuntimeError, match="PyTorch with CUDA support required"): + module.check_requirements() diff --git a/tests/test_miner_headerkey_schema.py b/tests/test_miner_headerkey_schema.py index 306969e4d..c9d168bfa 100644 --- a/tests/test_miner_headerkey_schema.py +++ b/tests/test_miner_headerkey_schema.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT """Regression tests for miner header key schema initialisation.""" +import os import sys @@ -18,7 +19,7 @@ def test_init_db_creates_miner_header_keys_table(tmp_path): with node.app.test_client() as client: response = client.post( "/miner/headerkey", - headers={"X-API-Key": "0" * 32}, + headers={"X-API-Key": os.environ["RC_ADMIN_KEY"]}, json={"miner_id": "miner-one", "pubkey_hex": "a" * 64}, ) diff --git a/tests/test_otc_bridge_htlc_preimage.py b/tests/test_otc_bridge_htlc_preimage.py index 60e0022ad..c588c12f8 100644 --- a/tests/test_otc_bridge_htlc_preimage.py +++ b/tests/test_otc_bridge_htlc_preimage.py @@ -182,7 +182,13 @@ def test_buy_order_defers_htlc_secret_to_matching_seller(tmp_path): assert "htlc_secret" not in public_order with patch.object(module.requests, "post") as mock_post: - mock_post.return_value = MagicMock(ok=True, text='{"ok": true}') + mock_response = MagicMock(ok=True, text='{"ok": true}', status_code=200) + mock_response.json.return_value = { + "ok": True, + "phase": "pending", + "pending_id": "payout-1", + } + mock_post.return_value = mock_response confirm_response = client.post( f"/api/orders/{order['order_id']}/confirm", json={ diff --git a/tests/test_rustchain_monitor_cli.py b/tests/test_rustchain_monitor_cli.py index bbbcbf733..be30d2ba4 100644 --- a/tests/test_rustchain_monitor_cli.py +++ b/tests/test_rustchain_monitor_cli.py @@ -148,6 +148,31 @@ def test_print_miners_renders_paginated_api_envelope(capsys): assert "Unexpected response" not in output +def test_miners_cli_smoke_renders_paginated_api_envelope(capsys): + module = load_module() + + with patch.object(sys, "argv", ["rustchain-monitor", "--miners"]), patch.object( + module, + "get_miners", + return_value={ + "miners": [ + { + "miner": "smoke-miner", + "hardware_type": "ARM", + "antiquity_multiplier": 1.0, + "last_attest": 0, + } + ], + "pagination": {"page": 1, "total": 1}, + }, + ): + module.main() + + output = capsys.readouterr().out + assert "Active miners: 1" in output + assert "smoke-miner" in output + + def test_print_epoch_renders_success_and_error(capsys): module = load_module() diff --git a/tests/test_server_proxy_path.py b/tests/test_server_proxy_path.py index c6d0cec30..751316d1d 100644 --- a/tests/test_server_proxy_path.py +++ b/tests/test_server_proxy_path.py @@ -92,8 +92,9 @@ class FakeResponse: text = "ok" headers = {"Content-Type": "text/plain"} - def fake_get(url, timeout): + def fake_get(url, headers, timeout): captured["url"] = url + captured["headers"] = headers captured["timeout"] = timeout return FakeResponse() @@ -103,7 +104,11 @@ def fake_get(url, timeout): assert response.status_code == 200 assert response.get_data(as_text=True) == "ok" - assert captured == {"url": "http://localhost:8088/api/stats", "timeout": 10} + assert captured == { + "url": "http://localhost:8088/api/stats", + "headers": {}, + "timeout": 10, + } def test_proxy_forwards_allowed_post_json(monkeypatch): diff --git a/tests/test_signed_transfer_replay.py b/tests/test_signed_transfer_replay.py index 395d0dd37..94b861112 100644 --- a/tests/test_signed_transfer_replay.py +++ b/tests/test_signed_transfer_replay.py @@ -572,9 +572,7 @@ def test_pending_confirm_keeps_transfer_pending_on_unsupported_balance_schema(mo body = response.get_json() assert body["confirmed_count"] == 0 assert body["confirmed_ids"] == [] - assert body["errors"] == [ - {"id": 1, "error": "unsupported balances schema for wallet transfer"} - ] + assert body["errors"] == [{"id": 1, "error": "internal_error"}] with closing(sqlite3.connect(db_path)) as conn: (status, voided_reason, confirmed_at) = conn.execute( diff --git a/tests/test_tx_handler_error_redaction.py b/tests/test_tx_handler_error_redaction.py index e8590280d..8d7732090 100644 --- a/tests/test_tx_handler_error_redaction.py +++ b/tests/test_tx_handler_error_redaction.py @@ -10,6 +10,7 @@ LEAKY_ERROR = "no such table: pending_transactions at /srv/rustchain/prod.db" +ADMIN_KEY = "test-admin-key-0123456789abcdef" class ExplodingPool: @@ -40,13 +41,18 @@ def _get_pending_nonces(self, address): self._boom() -def _client_for_exploding_pool(): +def _client_for_exploding_pool(monkeypatch): + monkeypatch.setenv("RC_ADMIN_KEY", ADMIN_KEY) app = Flask(__name__) app.config["TESTING"] = True create_tx_api_routes(app, ExplodingPool()) return app.test_client() +def _admin_headers(): + return {"X-Admin-Key": ADMIN_KEY} + + def _assert_redacted(response): assert response.status_code == 500 assert response.get_json() == {"error": "internal_error"} @@ -54,23 +60,23 @@ def _assert_redacted(response): assert b"/srv/rustchain/prod.db" not in response.data -def test_tx_status_redacts_internal_exception_details(): - with _client_for_exploding_pool() as client: +def test_tx_status_redacts_internal_exception_details(monkeypatch): + with _client_for_exploding_pool(monkeypatch) as client: _assert_redacted(client.get("/tx/status/hash_1")) -def test_tx_pending_redacts_internal_exception_details(): - with _client_for_exploding_pool() as client: - _assert_redacted(client.get("/tx/pending")) +def test_tx_pending_redacts_internal_exception_details(monkeypatch): + with _client_for_exploding_pool(monkeypatch) as client: + _assert_redacted(client.get("/tx/pending", headers=_admin_headers())) -def test_wallet_balance_redacts_internal_exception_details(): - with _client_for_exploding_pool() as client: +def test_wallet_balance_redacts_internal_exception_details(monkeypatch): + with _client_for_exploding_pool(monkeypatch) as client: _assert_redacted(client.get("/wallet/alice/balance")) -def test_wallet_nonce_redacts_internal_exception_details(): - with _client_for_exploding_pool() as client: +def test_wallet_nonce_redacts_internal_exception_details(monkeypatch): + with _client_for_exploding_pool(monkeypatch) as client: _assert_redacted(client.get("/wallet/alice/nonce")) @@ -82,5 +88,5 @@ def raise_connect_error(*args, **kwargs): monkeypatch.setattr(tx_handler.sqlite3, "connect", raise_connect_error) - with _client_for_exploding_pool() as client: + with _client_for_exploding_pool(monkeypatch) as client: _assert_redacted(client.get("/wallet/alice/history")) diff --git a/tests/test_tx_handler_limits.py b/tests/test_tx_handler_limits.py index 8566b03ae..afa8433b7 100644 --- a/tests/test_tx_handler_limits.py +++ b/tests/test_tx_handler_limits.py @@ -8,9 +8,11 @@ """ import os +import gc import json import sqlite3 import tempfile +import time import pytest from flask import Flask from node.rustchain_tx_handler import TransactionPool, create_tx_api_routes @@ -27,6 +29,7 @@ def app_context(monkeypatch): monkeypatch.setenv("RC_ADMIN_KEY", admin_key) db_fd, db_path = tempfile.mkstemp() + os.close(db_fd) app = Flask(__name__) app.config['TESTING'] = True @@ -50,8 +53,18 @@ def app_context(monkeypatch): client.environ_base['HTTP_X_ADMIN_KEY'] = admin_key yield client - os.close(db_fd) - os.unlink(db_path) + del client + del pool + gc.collect() + + for attempt in range(5): + try: + os.unlink(db_path) + break + except PermissionError: + if attempt == 4: + raise + time.sleep(0.1) def test_pending_default_limit(app_context): """Scenario: Default parameters (no query string) - Expect 100 (from logic)""" diff --git a/tools/rustchain-monitor/rustchain_monitor.py b/tools/rustchain-monitor/rustchain_monitor.py index e4c4a270c..78ddb3792 100755 --- a/tools/rustchain-monitor/rustchain_monitor.py +++ b/tools/rustchain-monitor/rustchain_monitor.py @@ -77,13 +77,6 @@ def get_epoch(): except Exception as e: return {"error": str(e)} -def normalize_miners_payload(data): - if isinstance(data, list): - return data - if isinstance(data, dict) and isinstance(data.get("miners"), list): - return data["miners"] - return None - def print_health(data): if "error" in data: print(f"❌ Health check failed: {data['error']}") @@ -112,7 +105,7 @@ def print_miners(data): print(f"❌ Failed to fetch miners: {data['error']}") return miners = normalize_miners_payload(data) - if miners is None: + if not isinstance(miners, list): print(f"⚠ Unexpected response: {data}") return print(f"📊 Active miners: {len(miners)}")