Skip to content
Open
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/checksums.sha256
Original file line number Diff line number Diff line change
@@ -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
Expand Down
147 changes: 138 additions & 9 deletions miners/linux/rustchain_linux_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion setup_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions tests/test_linux_miner_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
30 changes: 30 additions & 0 deletions tests/test_miner_hardware_probes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: <LOOPBACK,UP> mtu 65536 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00",
"2: docker0: <BROADCAST,MULTICAST> mtu 1500 link/ether 02:42:00:12:34:56 brd ff:ff:ff:ff:ff:ff",
"3: veth9@if2: <BROADCAST,MULTICAST> mtu 1500 link/ether 4a:24:1f:22:33:44 brd ff:ff:ff:ff:ff:ff",
"4: tailscale0: <POINTOPOINT> mtu 1280 link/ether 66:55:44:33:22:11 brd ff:ff:ff:ff:ff:ff",
"5: enp3s0: <BROADCAST,MULTICAST,UP> mtu 1500 link/ether 10:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff",
"6: wlan0: <BROADCAST,MULTICAST,UP> mtu 1500 link/ether 10:22:33:44:55:66 brd ff:ff:ff:ff:ff:ff",
"7: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 link/ether 02:11:22:33:44:55 brd ff:ff:ff:ff:ff:ff",
"8: enp4s0: <BROADCAST,MULTICAST,UP> 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)
Expand Down
Loading