Skip to content

entropy profile hash reads legacy key names -> 7 of 9 fields are dead defaults on the shipping payload (and the naive fix has a downside) #7991

Description

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_driftcv, drift_stdev, mean_ns, stdev_ns
  • cache_timingl1_ns, l2_ns, l3_ns, l2_l1_ratio, l3_l2_ratio
  • thermal_driftcold_avg_ns, hot_avg_ns, cold_stdev, hot_stdev, drift_ratio
  • instruction_jitterint_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:

  1. 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.

  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions