Summary
compute_entropy_profile_hash() (node/hardware_fingerprint_replay.py:177) reads seven of its nine entropy fields under key names the shipping fingerprint_checks.py does not emit, so they silently fall back to 0/''. The profile that gates check_entropy_collision — a live blocking path (node/rustchain_v2_integrated_v2.2.1_rip200.py:4708) — is effectively built from clock_cv plus an arch-level SIMD hash.
I am not opening a PR for this, because the obvious fix (mirror the key tolerance) looks like it would make one thing worse. Details below; I'd rather you pick the direction.
The repo already documents this exact skew
node/hardware_binding_v2.py:60-71 calls it out and adapts to it:
Tolerates two naming conventions across fingerprint_checks.py versions: Legacy: cache fields 'L1'/'L2', thermal 'ratio', jitter 'cv'. v3 (current): cache 'l1_ns'/'l2_ns', thermal 'drift_ratio' [...] Without this tolerance, every miner running v3 fingerprint_checks.py reports only 1 of 5 entropy fields (clock_cv — the one key that matches across formats) [...] Verified against IBM ThinkPad T40 Pentium M on 2026-05-27.
hardware_fingerprint_replay.py still reads the legacy names and never got that treatment.
Reproduced against the real shipping payload
Ran miners/linux/fingerprint_checks.py::validate_all_checks() (the real producer) and fed {"checks": results} — the exact shape rustchain_linux_miner.py:345 sends — into compute_entropy_profile_hash():
clock_cv = 2.230503 live
clock_drift_hash = '' <-- dead (producer emits no 'drift_hash')
cache_hash = '' <-- dead (producer emits no 'cache_hash')
cache_l1 = 0 <-- dead (producer emits 'l1_ns')
cache_l2 = 0 <-- dead (producer emits 'l2_ns')
thermal_ratio = 0 <-- dead (producer emits 'drift_ratio')
jitter_cv = 0 <-- dead (producer emits int_avg_ns/int_stdev)
jitter_map_hash = absent <-- dead (producer emits no 'jitter_map')
Real data keys per check, for reference:
clock_drift → cv, drift_stdev, mean_ns, stdev_ns
cache_timing → l1_ns, l2_ns, l3_ns, l2_l1_ratio, l3_l2_ratio
thermal_drift → cold_avg_ns, hot_avg_ns, cold_stdev, hot_stdev, drift_ratio
instruction_jitter → int_avg_ns, int_stdev, fp_avg_ns, fp_stdev, branch_avg_ns, branch_stdev
Only simd_identity survives besides clock_cv, and it hashes {arch, has_avx, has_sse, has_neon, has_altivec, simd_flags_count, sample_flags} — categorical, identical across every machine of the same arch class.
Why I think the naive fix is wrong
The profile is only consumed by check_entropy_collision, which matches hashes for exact equality across wallets (:341). Two observations:
-
It cannot detect hardware sharing either way. rustchain_linux_miner.py:611 computes the fingerprint once per process and caches it (:642 reuses it for every attestation). Two wallets on one machine = two processes = two independent clock_cv measurements = different hashes. Restoring cache_l1_ns/drift_ratio/jitter does not help — those are per-process measurements too. Same machine, 3 runs here: clock_cv 1.88328 / 1.955935 / 2.093544 → three different profile hashes.
-
The dead fields are currently load-bearing for the one case it does catch. Cross-wallet theft of a fingerprint blob collides today because thermal/cache/jitter are ignored — a thief who perturbs those numbers changes compute_fingerprint_hash (evading the layer-1 replay check) but not the entropy hash, so layer 2 still blocks. Fix the key names and perturbing exactly those fields evades layer 2 as well.
So renaming keys makes the profile more faithful to intent, and simultaneously makes the only working detection cheaper to evade. That trade shouldn't be made silently in a "fix the key names" PR.
What I think it actually needs
For the profile to do its stated job it has to be stable per machine and distinct across machines, which means quantizing/bucketing the timing values (e.g. bucket l1_ns/drift_ratio to a coarse grid) rather than hashing raw floats, and only then reading the v3 keys. That's a design change to a security control with a false-positive blast radius (a wrong bucket width blocks honest miners), so I didn't want to unilaterally ship it.
Happy to implement whichever you prefer:
- (a) key tolerance only, mirroring
hardware_binding_v2.extract_entropy_profile — accepting the evasion trade above;
- (b) quantized profile + v3 keys, with tests pinning the bucket widths;
- (c) nothing in code, and this issue stands as documentation that the layer is inert by design.
Reported by @Vyacheslav-Tomashevskiy. RTC: RTCd1554f0f35576faf01d386a6be1c947f560dd0b7
Summary
compute_entropy_profile_hash()(node/hardware_fingerprint_replay.py:177) reads seven of its nine entropy fields under key names the shippingfingerprint_checks.pydoes not emit, so they silently fall back to0/''. The profile that gatescheck_entropy_collision— a live blocking path (node/rustchain_v2_integrated_v2.2.1_rip200.py:4708) — is effectively built fromclock_cvplus an arch-level SIMD hash.I am not opening a PR for this, because the obvious fix (mirror the key tolerance) looks like it would make one thing worse. Details below; I'd rather you pick the direction.
The repo already documents this exact skew
node/hardware_binding_v2.py:60-71calls it out and adapts to it:hardware_fingerprint_replay.pystill reads the legacy names and never got that treatment.Reproduced against the real shipping payload
Ran
miners/linux/fingerprint_checks.py::validate_all_checks()(the real producer) and fed{"checks": results}— the exact shaperustchain_linux_miner.py:345sends — intocompute_entropy_profile_hash():Real
datakeys per check, for reference:clock_drift→cv, drift_stdev, mean_ns, stdev_nscache_timing→l1_ns, l2_ns, l3_ns, l2_l1_ratio, l3_l2_ratiothermal_drift→cold_avg_ns, hot_avg_ns, cold_stdev, hot_stdev, drift_ratioinstruction_jitter→int_avg_ns, int_stdev, fp_avg_ns, fp_stdev, branch_avg_ns, branch_stdevOnly
simd_identitysurvives besidesclock_cv, and it hashes{arch, has_avx, has_sse, has_neon, has_altivec, simd_flags_count, sample_flags}— categorical, identical across every machine of the same arch class.Why I think the naive fix is wrong
The profile is only consumed by
check_entropy_collision, which matches hashes for exact equality across wallets (:341). Two observations:It cannot detect hardware sharing either way.
rustchain_linux_miner.py:611computes the fingerprint once per process and caches it (:642reuses it for every attestation). Two wallets on one machine = two processes = two independentclock_cvmeasurements = different hashes. Restoringcache_l1_ns/drift_ratio/jitter does not help — those are per-process measurements too. Same machine, 3 runs here:clock_cv1.88328 / 1.955935 / 2.093544 → three different profile hashes.The dead fields are currently load-bearing for the one case it does catch. Cross-wallet theft of a fingerprint blob collides today because thermal/cache/jitter are ignored — a thief who perturbs those numbers changes
compute_fingerprint_hash(evading the layer-1 replay check) but not the entropy hash, so layer 2 still blocks. Fix the key names and perturbing exactly those fields evades layer 2 as well.So renaming keys makes the profile more faithful to intent, and simultaneously makes the only working detection cheaper to evade. That trade shouldn't be made silently in a "fix the key names" PR.
What I think it actually needs
For the profile to do its stated job it has to be stable per machine and distinct across machines, which means quantizing/bucketing the timing values (e.g. bucket
l1_ns/drift_ratioto a coarse grid) rather than hashing raw floats, and only then reading the v3 keys. That's a design change to a security control with a false-positive blast radius (a wrong bucket width blocks honest miners), so I didn't want to unilaterally ship it.Happy to implement whichever you prefer:
hardware_binding_v2.extract_entropy_profile— accepting the evasion trade above;Reported by @Vyacheslav-Tomashevskiy. RTC: RTCd1554f0f35576faf01d386a6be1c947f560dd0b7