diff --git a/miners/windows/rustchain_miner_setup.bat b/miners/windows/rustchain_miner_setup.bat index 44e2624f3..f96acd5cd 100755 --- a/miners/windows/rustchain_miner_setup.bat +++ b/miners/windows/rustchain_miner_setup.bat @@ -6,7 +6,7 @@ set "PYTHON_URL=https://www.python.org/ftp/python/3.11.5/python-3.11.5-amd64.exe set "PYTHON_INSTALLER=%SCRIPT_DIR%python-3.11.5-amd64.exe" set "MINER_URL=https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/windows/rustchain_windows_miner.py" set "MINER_SCRIPT=%SCRIPT_DIR%rustchain_windows_miner.py" -set "MINER_SHA256=51fe431cbee3c5b81218a738c221d45e675dafa5d67f9aff716d4ea11f304662" +set "MINER_SHA256=6ec3aaefb068ea2bb5adff7a5c279848fd4b2df01de390411c62e954719c22d3" echo. echo === RustChain Windows Miner Bootstrap === diff --git a/miners/windows/rustchain_windows_miner.py b/miners/windows/rustchain_windows_miner.py index 0c480af04..9e067b0f9 100644 --- a/miners/windows/rustchain_windows_miner.py +++ b/miners/windows/rustchain_windows_miner.py @@ -114,6 +114,7 @@ def __init__(self, wallet_address): self.enrolled = False self.hw_info = self._get_hw_info() self.last_entropy = {} + self.last_attestation_error = "" # Zephyr dual-mining state — detected once per attest() cycle self._pow_proof = None @@ -281,7 +282,10 @@ def _ensure_ready(self, callback): if now >= self.attestation_valid_until - 60: if not self.attest(): if callback: - callback({"type": "error", "message": "Attestation failed"}) + message = "Attestation failed" + if self.last_attestation_error: + message = f"{message}: {self.last_attestation_error}" + callback({"type": "error", "message": message}) return False if (now - self.last_enroll) > 3600 or not self.enrolled: @@ -352,6 +356,26 @@ def _collect_entropy(self, cycles=48, inner=30000): "samples_preview": samples[:12], } + def _response_diagnostic(self, resp): + """Return a compact HTTP failure description for operator logs.""" + parts = [f"HTTP {getattr(resp, 'status_code', 'unknown')}"] + try: + payload = resp.json() + except Exception: + payload = None + + if isinstance(payload, dict): + for key in ("code", "error", "message"): + value = payload.get(key) + if value: + parts.append(f"{key}={value}") + else: + text = (getattr(resp, "text", "") or "").strip() + if text: + parts.append(f"body={text[:240]}") + + return " ".join(parts) + def attest(self): """ Perform hardware attestation for PoA. @@ -362,11 +386,23 @@ def attest(self): apply the PoW bonus multiplier to this miner's RTC rewards. """ try: - challenge = requests.post( + challenge_resp = requests.post( f"{self.node_url}/attest/challenge", json={}, timeout=10 - ).json() - nonce = challenge.get("nonce") - except Exception: + ) + if challenge_resp.status_code != 200: + self.last_attestation_error = ( + f"challenge rejected: {self._response_diagnostic(challenge_resp)}" + ) + return False + challenge = challenge_resp.json() + nonce = challenge.get("nonce") if isinstance(challenge, dict) else None + if not nonce: + self.last_attestation_error = ( + f"challenge rejected: {self._response_diagnostic(challenge_resp)}" + ) + return False + except Exception as e: + self.last_attestation_error = f"challenge request failed: {e}" return False entropy = self._collect_entropy() @@ -412,9 +448,11 @@ def attest(self): ) if resp.status_code == 200 and resp.json().get("ok"): self.attestation_valid_until = time.time() + 580 + self.last_attestation_error = "" return True - except Exception: - pass + self.last_attestation_error = f"submit rejected: {self._response_diagnostic(resp)}" + except Exception as e: + self.last_attestation_error = f"submit request failed: {e}" return False def enroll(self): @@ -580,11 +618,8 @@ def run(self): def run_headless(wallet_address: str, node_url: str) -> int: - wallet = RustChainWallet() - if wallet_address: - wallet.wallet_data["address"] = wallet_address - wallet.save_wallet(wallet.wallet_data) - miner = RustChainMiner(wallet.wallet_data["address"]) + active_wallet = wallet_address or RustChainWallet().wallet_data["address"] + miner = RustChainMiner(active_wallet) miner.node_url = node_url def cb(evt): @@ -634,8 +669,6 @@ def main(argv=None): app = RustChainGUI() app.miner.node_url = args.node if args.wallet: - app.wallet.wallet_data["address"] = args.wallet - app.wallet.save_wallet(app.wallet.wallet_data) app.miner.wallet_address = args.wallet app.miner.miner_id = f"windows_{hashlib.md5(args.wallet.encode()).hexdigest()[:8]}" app.run() diff --git a/setup_miner.py b/setup_miner.py index efef69fea..9075d01bd 100644 --- a/setup_miner.py +++ b/setup_miner.py @@ -19,7 +19,7 @@ MINER_ARTIFACTS = { "Linux": { "url": "https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/linux/rustchain_linux_miner.py", - "sha256": "9475fe15d149ef7b3824c0009453c55e17fb6d1d411ea37e9f24f58c6313871c", + "sha256": "91815ecf25042cfea1c60817c8b6e701c4324b60ceeb433da068243920344c0a", }, "Darwin": { "url": "https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/macos/rustchain_mac_miner_v2.5.py", @@ -27,7 +27,7 @@ }, "Windows": { "url": "https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/windows/rustchain_windows_miner.py", - "sha256": "51fe431cbee3c5b81218a738c221d45e675dafa5d67f9aff716d4ea11f304662", + "sha256": "6ec3aaefb068ea2bb5adff7a5c279848fd4b2df01de390411c62e954719c22d3", }, } diff --git a/tests/test_windows_attestation_diagnostics.py b/tests/test_windows_attestation_diagnostics.py new file mode 100644 index 000000000..51e1a76e5 --- /dev/null +++ b/tests/test_windows_attestation_diagnostics.py @@ -0,0 +1,202 @@ +# SPDX-License-Identifier: MIT +import importlib.util +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MINER_PATH = ROOT / "miners" / "windows" / "rustchain_windows_miner.py" + + +def _load_windows_miner(): + spec = importlib.util.spec_from_file_location("windows_miner_under_test", MINER_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class _Response: + def __init__(self, status_code, payload=None, text=""): + self.status_code = status_code + self._payload = payload + self.text = text + + def json(self): + if isinstance(self._payload, Exception): + raise self._payload + return self._payload + + +def test_attest_records_submit_rejection_details(monkeypatch): + module = _load_windows_miner() + miner = module.RustChainMiner("RTC877021895fd29d034f35c87e1b37af8534703792") + miner.node_url = "http://node.example" + + def fake_post(url, **kwargs): + if url.endswith("/attest/challenge"): + return _Response(200, {"nonce": "nonce-1"}) + if url.endswith("/attest/submit"): + return _Response( + 409, + { + "ok": False, + "code": "DUPLICATE_HARDWARE", + "error": "hardware_already_bound", + "message": "This hardware is already registered to wallet 6445fe3349a52537cf50...", + }, + ) + raise AssertionError(url) + + monkeypatch.setattr(module.requests, "post", fake_post) + monkeypatch.setattr(miner, "_collect_entropy", lambda: {"variance_ns": 1.0}) + monkeypatch.setattr(miner, "_build_pow_proof", lambda: None) + + assert miner.attest() is False + assert miner.last_attestation_error == ( + "submit rejected: HTTP 409 code=DUPLICATE_HARDWARE " + "error=hardware_already_bound " + "message=This hardware is already registered to wallet 6445fe3349a52537cf50..." + ) + + +def test_attest_records_challenge_exception(monkeypatch): + module = _load_windows_miner() + miner = module.RustChainMiner("RTC877021895fd29d034f35c87e1b37af8534703792") + + def fake_post(url, **kwargs): + raise TimeoutError("challenge timed out") + + monkeypatch.setattr(module.requests, "post", fake_post) + + assert miner.attest() is False + assert miner.last_attestation_error == "challenge request failed: challenge timed out" + + +def test_attest_records_challenge_rejection_details(monkeypatch): + module = _load_windows_miner() + miner = module.RustChainMiner("RTC877021895fd29d034f35c87e1b37af8534703792") + miner.node_url = "http://node.example" + + def fake_post(url, **kwargs): + if url.endswith("/attest/challenge"): + return _Response( + 503, + { + "code": "ATTEST_BUSY", + "error": "try later", + "message": "challenge unavailable", + }, + ) + raise AssertionError(url) + + monkeypatch.setattr(module.requests, "post", fake_post) + + assert miner.attest() is False + assert miner.last_attestation_error == ( + "challenge rejected: HTTP 503 code=ATTEST_BUSY " + "error=try later message=challenge unavailable" + ) + + +def test_attest_records_non_json_challenge_http_error(monkeypatch): + module = _load_windows_miner() + miner = module.RustChainMiner("RTC877021895fd29d034f35c87e1b37af8534703792") + miner.node_url = "http://node.example" + + def fake_post(url, **kwargs): + if url.endswith("/attest/challenge"): + return _Response(503, ValueError("not json"), text="temporarily unavailable") + raise AssertionError(url) + + monkeypatch.setattr(module.requests, "post", fake_post) + + assert miner.attest() is False + assert miner.last_attestation_error == ( + "challenge rejected: HTTP 503 body=temporarily unavailable" + ) + + +def test_ensure_ready_prints_last_attestation_error(): + module = _load_windows_miner() + miner = module.RustChainMiner("RTC877021895fd29d034f35c87e1b37af8534703792") + miner.last_attestation_error = "submit rejected: HTTP 409 code=DUPLICATE_HARDWARE" + miner.attest = lambda: False + events = [] + + assert miner._ensure_ready(events.append) is False + assert events == [ + { + "type": "error", + "message": "Attestation failed: submit rejected: HTTP 409 code=DUPLICATE_HARDWARE", + } + ] + + +def test_headless_wallet_override_does_not_overwrite_saved_wallet(monkeypatch, tmp_path): + module = _load_windows_miner() + saved_wallet = { + "address": "6445fe3349a52537cf50ORIGINAL", + "balance": 0.0, + "created": "2026-05-17T00:00:00", + "transactions": [], + } + wallet_file = tmp_path / "wallet.json" + wallet_file.write_text(json.dumps(saved_wallet), encoding="utf-8") + + monkeypatch.setattr(module, "WALLET_DIR", tmp_path) + monkeypatch.setattr(module, "WALLET_FILE", wallet_file) + monkeypatch.setattr(module.time, "sleep", lambda _: (_ for _ in ()).throw(KeyboardInterrupt())) + + started = {} + + def fake_start(self, callback=None): + started["wallet_address"] = self.wallet_address + started["saved_wallet"] = module.WALLET_FILE.read_text(encoding="utf-8") + + monkeypatch.setattr(module.RustChainMiner, "start_mining", fake_start) + monkeypatch.setattr(module.RustChainMiner, "stop_mining", lambda self: None) + + assert module.run_headless("RTC877021895fd29d034f35c87e1b37af8534703792", "http://node") == 0 + assert started["wallet_address"] == "RTC877021895fd29d034f35c87e1b37af8534703792" + assert json.loads(started["saved_wallet"]) == saved_wallet + assert json.loads(wallet_file.read_text(encoding="utf-8")) == saved_wallet + + +def test_gui_wallet_override_does_not_save_over_existing_wallet(monkeypatch): + module = _load_windows_miner() + saved_calls = [] + miner_state = {} + + class FakeWallet: + def __init__(self): + self.wallet_data = {"address": "6445fe3349a52537cf50ORIGINAL"} + + def save_wallet(self, wallet_data=None): + saved_calls.append(wallet_data) + + class FakeMiner: + def __init__(self): + self.wallet_address = "6445fe3349a52537cf50ORIGINAL" + self.miner_id = "windows_original" + self.node_url = module.RUSTCHAIN_API + + class FakeGUI: + def __init__(self): + self.wallet = FakeWallet() + self.miner = FakeMiner() + + def run(self): + miner_state["wallet_address"] = self.miner.wallet_address + miner_state["miner_id"] = self.miner.miner_id + miner_state["saved_wallet_address"] = self.wallet.wallet_data["address"] + + monkeypatch.setattr(module, "TK_AVAILABLE", True) + monkeypatch.setattr(module, "RustChainGUI", FakeGUI) + + assert module.main(["--wallet", "RTC877021895fd29d034f35c87e1b37af8534703792"]) == 0 + assert saved_calls == [] + assert miner_state == { + "wallet_address": "RTC877021895fd29d034f35c87e1b37af8534703792", + "miner_id": "windows_20f7ca20", + "saved_wallet_address": "6445fe3349a52537cf50ORIGINAL", + }