diff --git a/miners/checksums.sha256 b/miners/checksums.sha256 index c00c67935..806031ba7 100644 --- a/miners/checksums.sha256 +++ b/miners/checksums.sha256 @@ -1,4 +1,4 @@ -3253afafbe67ed527a5f474bc26d0f3ce6974ded3c3e172925efc97452a71146 linux/rustchain_linux_miner.py +06acede1758b1310be421dad163e56fcaadd417cb471b11f2532125faffdb4fe linux/rustchain_linux_miner.py cdfca6e63ecd24f53b30140dd44df42415a3254c68aad95b1fca3c1557e15f7b linux/fingerprint_checks.py 603d9a3b3ebfe1a0ca56a60988db4b5d4a80ab57cb5feb1c0b563a1d4020fcd7 macos/rustchain_mac_miner_v2.4.py 42bb69840bc8723108ee13157cf1771d3af8c17f2664cbcd79c5b3077376736b macos/rustchain_mac_miner_v2.5.py diff --git a/miners/linux/rustchain_linux_miner.py b/miners/linux/rustchain_linux_miner.py index ea9fb5e13..acf335bf1 100755 --- a/miners/linux/rustchain_linux_miner.py +++ b/miners/linux/rustchain_linux_miner.py @@ -98,6 +98,130 @@ def _miner_id_from_hw(hw_info): return f"{arch}-{hostname}" +VIRTUAL_INTERFACE_PREFIXES = ( + "br-", + "cni", + "docker", + "flannel", + "kube", + "lxc", + "lxd", + "podman", + "tailscale", + "tap", + "tun", + "vboxnet", + "veth", + "virbr", + "vmnet", + "wg", + "zt", +) + +VIRTUAL_MAC_PREFIXES = ( + "00:15:5d", # Hyper-V + "00:16:3e", # Xen + "02:42", # Docker bridge/veth + "08:00:27", # VirtualBox + "52:54:00", # QEMU/KVM +) + + +def _is_virtual_interface(name): + iface = str(name or "").split("@", 1)[0].strip().lower() + raw = str(name or "").strip().lower() + if not iface or iface == "lo" or "@" in raw: + return True + return any(iface.startswith(prefix) for prefix in VIRTUAL_INTERFACE_PREFIXES) + + +def _is_usable_mac(mac): + value = str(mac or "").strip().lower() + if not re.fullmatch(r"[0-9a-f]{2}(?::[0-9a-f]{2}){5}", value): + return False + if value in {"00:00:00:00:00:00", "ff:ff:ff:ff:ff:ff"}: + return False + first_octet = int(value.split(":", 1)[0], 16) + if first_octet & 1: + return False + if first_octet & 0x02: + return False + return not any(value.startswith(prefix) for prefix in VIRTUAL_MAC_PREFIXES) + + +def _add_unique_mac(macs, seen, iface, mac): + mac = str(mac or "").lower() + if _is_virtual_interface(iface) or not _is_usable_mac(mac) or mac in seen: + return + macs.append(mac) + seen.add(mac) + + +def _coerce_float(value): + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _derive_instruction_jitter_cv(data): + cvs = [] + for prefix in ("int", "fp", "branch"): + avg = _coerce_float(data.get(f"{prefix}_avg_ns")) + stdev = _coerce_float(data.get(f"{prefix}_stdev")) + if avg and avg > 0 and stdev is not None and stdev >= 0: + cvs.append(stdev / avg) + if not cvs: + return None + return round(statistics.mean(cvs), 6) + + +def _normalize_fingerprint_for_binding(fingerprint): + """Add node hardware-binding aliases while preserving raw check payloads.""" + if not isinstance(fingerprint, dict): + return fingerprint + checks = fingerprint.get("checks") + if not isinstance(checks, dict): + return fingerprint + + normalized = dict(fingerprint) + normalized_checks = dict(checks) + normalized["checks"] = normalized_checks + + cache = normalized_checks.get("cache_timing") + if isinstance(cache, dict) and isinstance(cache.get("data"), dict): + cache_data = dict(cache["data"]) + if "L1" not in cache_data and "l1_ns" in cache_data: + cache_data["L1"] = cache_data["l1_ns"] + if "L2" not in cache_data and "l2_ns" in cache_data: + cache_data["L2"] = cache_data["l2_ns"] + cache_entry = dict(cache) + cache_entry["data"] = cache_data + normalized_checks["cache_timing"] = cache_entry + + thermal = normalized_checks.get("thermal_drift") + if isinstance(thermal, dict) and isinstance(thermal.get("data"), dict): + thermal_data = dict(thermal["data"]) + if "ratio" not in thermal_data and "drift_ratio" in thermal_data: + thermal_data["ratio"] = thermal_data["drift_ratio"] + thermal_entry = dict(thermal) + thermal_entry["data"] = thermal_data + normalized_checks["thermal_drift"] = thermal_entry + + jitter = normalized_checks.get("instruction_jitter") + if isinstance(jitter, dict) and isinstance(jitter.get("data"), dict): + jitter_data = dict(jitter["data"]) + if "cv" not in jitter_data: + cv = _derive_instruction_jitter_cv(jitter_data) + if cv is not None: + jitter_data["cv"] = cv + jitter_entry = dict(jitter) + jitter_entry["data"] = jitter_data + normalized_checks["instruction_jitter"] = jitter_entry + + return normalized + + def _request_with_network_retry(method, url, action, retries=NETWORK_RETRY_ATTEMPTS, base_delay=NETWORK_RETRY_BASE_DELAY, sleep_func=None, **kwargs): @@ -227,7 +351,7 @@ def _run_fingerprint_checks(self): try: passed, results = validate_all_checks() self.fingerprint_passed = passed - self.fingerprint_data = {"checks": results, "all_passed": passed} + self.fingerprint_data = _normalize_fingerprint_for_binding({"checks": results, "all_passed": passed}) if passed: print("[FINGERPRINT] All checks PASSED - eligible for full rewards") else: @@ -256,6 +380,7 @@ def _run_cmd(self, args): def _get_mac_addresses(self): """Return list of real MAC addresses present on the system.""" macs = [] + seen = set() # Try `ip -o link` try: output = subprocess.run( @@ -266,11 +391,13 @@ def _get_mac_addresses(self): timeout=5, ).stdout.splitlines() for line in output: - m = re.search(r"link/(?:ether|loopback)\s+([0-9a-f:]{17})", line, re.IGNORECASE) + m = re.match( + r"\d+:\s+([^:]+):.*\blink/(?:ether|loopback)\s+([0-9a-f:]{17})", + line, + re.IGNORECASE, + ) if m: - mac = m.group(1).lower() - if mac != "00:00:00:00:00:00": - macs.append(mac) + _add_unique_mac(macs, seen, m.group(1), m.group(2)) except Exception: pass @@ -284,12 +411,13 @@ def _get_mac_addresses(self): text=True, timeout=5, ).stdout.splitlines() + iface = "" for line in output: + if line and not line[0].isspace(): + iface = line.split(":", 1)[0].split()[0] m = re.search(r"(?:ether|HWaddr)\s+([0-9a-f:]{17})", line, re.IGNORECASE) if m: - mac = m.group(1).lower() - if mac != "00:00:00:00:00:00": - macs.append(mac) + _add_unique_mac(macs, seen, iface, m.group(1)) except Exception: pass @@ -455,6 +583,7 @@ def attest(self): self._run_fingerprint_checks() # Submit attestation with fingerprint data + fingerprint_payload = _normalize_fingerprint_for_binding(self.fingerprint_data) attestation = { "miner": self.wallet, "miner_id": self._miner_id(), @@ -482,7 +611,7 @@ def attest(self): "hostname": self.hw_info["hostname"] }, # RIP-PoA hardware fingerprint attestation - "fingerprint": self.fingerprint_data, + "fingerprint": fingerprint_payload, # Warthog dual-mining proof (None if sidecar not active) "warthog": self.warthog.collect_proof() if self.warthog else None } diff --git a/setup_miner.py b/setup_miner.py index 5b0d2af4d..c9ff29298 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": "3253afafbe67ed527a5f474bc26d0f3ce6974ded3c3e172925efc97452a71146", + "sha256": "06acede1758b1310be421dad163e56fcaadd417cb471b11f2532125faffdb4fe", }, "Darwin": { "url": "https://raw.githubusercontent.com/Scottcjn/Rustchain/main/miners/macos/rustchain_mac_miner_v2.5.py", diff --git a/tests/test_linux_miner_identity.py b/tests/test_linux_miner_identity.py index 719ded3a8..af5b377bf 100644 --- a/tests/test_linux_miner_identity.py +++ b/tests/test_linux_miner_identity.py @@ -29,6 +29,54 @@ def test_miner_id_uses_detected_arch_and_hostname(): assert "ryzen" not in miner_id +def test_linux_miner_adds_binding_aliases_to_fingerprint(): + miner = load_miner_module() + from node.hardware_binding_v2 import extract_entropy_profile + + fingerprint = { + "checks": { + "cache_timing": { + "passed": True, + "data": {"l1_ns": 4.2, "l2_ns": 8.4, "l3_ns": 22.0}, + }, + "thermal_drift": { + "passed": True, + "data": {"drift_ratio": 1.034}, + }, + "instruction_jitter": { + "passed": True, + "data": { + "int_avg_ns": 1000, + "int_stdev": 50, + "fp_avg_ns": 2000, + "fp_stdev": 160, + "branch_avg_ns": 1500, + "branch_stdev": 90, + }, + }, + } + } + + normalized = miner._normalize_fingerprint_for_binding(fingerprint) + + cache = normalized["checks"]["cache_timing"]["data"] + thermal = normalized["checks"]["thermal_drift"]["data"] + jitter = normalized["checks"]["instruction_jitter"]["data"] + + assert cache["L1"] == 4.2 + assert cache["L2"] == 8.4 + assert thermal["ratio"] == 1.034 + assert jitter["cv"] > 0 + assert "l1_ns" in cache + assert "drift_ratio" in thermal + + profile = extract_entropy_profile(normalized) + assert profile["cache_l1"] == 4.2 + assert profile["cache_l2"] == 8.4 + assert profile["thermal_ratio"] == 1.034 + assert profile["jitter_cv"] > 0 + + def test_linux_miner_source_does_not_hardcode_victus_identity(): source = MINER_PATH.read_text(encoding="utf-8") diff --git a/tests/test_miner_hardware_probes.py b/tests/test_miner_hardware_probes.py index b0caa5a16..f18254e90 100644 --- a/tests/test_miner_hardware_probes.py +++ b/tests/test_miner_hardware_probes.py @@ -47,6 +47,36 @@ def fake_run(args, **kwargs): assert calls == [(["nproc"], {"stdout": miner.subprocess.PIPE, "stderr": miner.subprocess.PIPE, "text": True, "timeout": 10})] +def test_linux_miner_filters_virtual_macs_from_ip_link(monkeypatch): + miner = load_module(Path("miners/linux/rustchain_linux_miner.py"), "rustchain_linux_miner_mac_filter") + instance = object.__new__(miner.LocalMiner) + ip_link = "\n".join( + [ + "1: lo: mtu 65536 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00", + "2: docker0: mtu 1500 link/ether 02:42:00:12:34:56 brd ff:ff:ff:ff:ff:ff", + "3: veth9@if2: mtu 1500 link/ether 4a:24:1f:22:33:44 brd ff:ff:ff:ff:ff:ff", + "4: tailscale0: mtu 1280 link/ether 66:55:44:33:22:11 brd ff:ff:ff:ff:ff:ff", + "5: enp3s0: mtu 1500 link/ether 10:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff", + "6: wlan0: mtu 1500 link/ether 10:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff", + "7: eth0: mtu 1500 link/ether 02:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff", + "8: enp4s0: mtu 1500 link/ether 06:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff", + ] + ) + calls = [] + + class Result: + stdout = ip_link + + def fake_run(args, **kwargs): + calls.append(args) + return Result() + + monkeypatch.setattr(miner.subprocess, "run", fake_run) + + assert instance._get_mac_addresses() == ["10:22:33:44:55:66"] + assert calls == [["ip", "-o", "link"]] + + def test_linux_miner_collects_darwin_hardware_with_sysctl(monkeypatch): miner = load_module(Path("miners/linux/rustchain_linux_miner.py"), "rustchain_linux_miner_darwin_hw") instance = object.__new__(miner.LocalMiner)