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
2 changes: 1 addition & 1 deletion miners/windows/rustchain_miner_setup.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===
Expand Down
61 changes: 47 additions & 14 deletions miners/windows/rustchain_windows_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions setup_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
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",
"sha256": "e50cea51a24c8c0337e340287a05e6431f6d95883ab913a1a79c19e99bc03dd8",
},
"Windows": {
"url": "https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/windows/rustchain_windows_miner.py",
"sha256": "51fe431cbee3c5b81218a738c221d45e675dafa5d67f9aff716d4ea11f304662",
"sha256": "6ec3aaefb068ea2bb5adff7a5c279848fd4b2df01de390411c62e954719c22d3",
},
}

Expand Down
202 changes: 202 additions & 0 deletions tests/test_windows_attestation_diagnostics.py
Original file line number Diff line number Diff line change
@@ -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",
}
Loading