Skip to content

Fingerprint validation reads evidence content; RIP-309 rotation unified to one algorithm - #8065

Open
Scottcjn wants to merge 2 commits into
mainfrom
fix/rip309-rotation-and-fingerprint-evidence-content
Open

Fingerprint validation reads evidence content; RIP-309 rotation unified to one algorithm#8065
Scottcjn wants to merge 2 commits into
mainfrom
fix/rip309-rotation-and-fingerprint-evidence-content

Conversation

@Scottcjn

Copy link
Copy Markdown
Owner

Two contained security fixes in the node's fingerprint validation and RIP-309 rotation. Nothing here is deployed. No production server was touched. This is code plus tests only, and the rollout risk for Fix B is spelled out below so whoever deploys knows what changes on the live chain.

BCOS Checklist (Required For Non-Doc PRs)

  • Tier: BCOS-L2 (consensus-adjacent validation and rotation logic)
  • New test files carry # SPDX-License-Identifier: MIT
  • Test evidence below (commands and real counts)

What Changed

Fix A: fingerprint validation now reads the evidence, not just its presence

validate_fingerprint_data() in node/rustchain_v2_integrated_v2.2.1_rip200.py. The 2026-02-02 hardening required evidence to EXIST but never looked at what it said. This payload was accepted:

{"passed": true, "data": {"vm_indicators": ["qemu", "hypervisor"]}}

has_evidence was satisfied because vm_indicators was present, and passed was not literally False, so the server held the string "qemu" and never looked at it.

Verified line numbers in the pre-fix file:

  • anti-emulation dict branch: lines 3518-3536, with has_evidence at 3523-3529 and the == False verdict check at 3534
  • clock_drift == False at 3562, rom_fingerprint == False at 3613
  • the bool-format C miner branches to preserve: elif isinstance(anti_emu_check, bool) at 3537 and elif isinstance(clock_check, bool) at 3572
  • the all_passed == False Phase 4 gate at 3619

Changes:

  1. Non-empty vm_indicators now rejects with vm_self_reported:<indicators> regardless of the claimed verdict. is_likely_vm: true likewise. Honest-miner impact is none, and I verified that instead of assuming it: node/fingerprint_checks.py lines 629-638 always emit vm_indicators and set it to an empty list on real hardware (valid = len(vm_indicators) == 0).
  2. The three == False comparisons became is not True (anti_emulation, clock_drift, rom_fingerprint). Omitting the passed key used to skip both the pass branch and the fail branch. Distinct reasons: anti_emulation_no_verdict, clock_drift_no_verdict, rom_check_no_verdict. The bool-format branches at 3537/3572 are untouched and covered by tests, so the C-payload miners (Apple II, i386) keep working. Note: this is a strict identity check, so a client that sends "passed": 1 instead of JSON true would now be rejected; JSON booleans decode to Python bools, so spec-compliant clients are unaffected.
  3. Same pattern in the other checks, found and fixed:
    • Every check's data can carry fail_reason (cache_timing, simd_identity, thermal_drift, instruction_jitter, rom, device_age_oracle, pico bridge fail_reasons). A check claiming pass while its own data records a failure is now rejected: check_self_reported_failure:<name>:<reason>. Honest clients only set fail_reason when the check failed.
    • ROM: non-empty detection_details with a forged emulator_detected: false is now rejected. The client only records details when it matched a known emulator ROM.
    • Phase 4 was gated on all_passed == False, so a payload claiming all_passed: true (or omitting it) while embedding individually failed hard checks (thermal_drift, instruction_jitter, simd_identity) was returned as "valid". The per-check scan now runs regardless of the client's overall claim. cache_timing stays soft, clock_drift stays soft for vintage archs, exactly as before.
    • all_passed: false with no failed check shown is rejected as internally inconsistent (all_passed_false_unattributed).

Fix B: one RIP-309 rotation algorithm instead of three

Verified the divergence at these pre-fix locations:

  • Enroll side (node/rustchain_v2_integrated_v2.2.1_rip200.py): derive_measurement_nonce at 2188-2191 computed sha256("rip-309:" + hash); select_active_fingerprint_checks at 2194-2211 ranked by sorted per-name hash over the tuple at 2176-2183, which contained simd_bias.
  • Settlement side (finalize_epoch, same file): inline at 3989-3996, sha256(hash_bytes + b"measurement_nonce") into random.Random(seed).sample(...) over names containing simd_identity.
  • Reward path (node/rip_200_round_robin_1cpu1vote.py line 635): already delegated to rip_309_measurement_rotation.get_reward_active_fingerprint_checks (same algorithm as settlement).
  • A third divergent copy the report did not mention: rip_200_round_robin_1cpu1vote.py lines 41-65 carried its own duplicate of the legacy sorted-hash selector.

So for the same epoch, enroll granted weight against one 4-of-6 subset and settlement vetoed against a different one. Measured agreement between the two algorithms over 10,000 random hashes: 673/10,000 = 6.7%, right at the expected 1/15 for independent 4-of-6 draws. They disagreed 93.3% of the time.

I verified the claim that get_reward_active_fingerprint_checks is the tested one before choosing it: node/tests/test_rip309_fingerprint_rotation.py golden-tests it against the inline settlement algorithm (line 127) and its empty-hash fallback (line 133). It is also what the live reward path already uses. So it wins, and everything else now delegates to it:

  • Integrated node: select_active_fingerprint_checks and derive_measurement_nonce delegate to rip_309_measurement_rotation. The T3.6 fail-closed behavior is preserved and slightly extended: empty, all-zeros, and now also non-hex previous hashes activate all six checks.
  • finalize_epoch: the inline copy is replaced by a call to the same helper. Bit-for-bit identical selection (test proves it), so settlement behavior does not change.
  • rip_200_round_robin_1cpu1vote.py: the duplicate legacy selector now delegates too.
  • Naming unified on simd_identity (what the miners actually emit, per fingerprint_checks.py line 866, and what rip_309_measurement_rotation.ALL_FP_CHECKS uses). The simd_bias alias from the RIP-309 issue text keeps working: the integrated node's _fingerprint_checks_map already mirrored both names, and the stored fingerprint_checks_json maps consulted in finalize_epoch (pre-fix line 4022) and calculate_epoch_rewards_time_aged now mirror simd_bias <-> simd_identity before evaluation. The alias map in rip_309_measurement_rotation.py lines 42-45 was confirmed present.
  • node/tests/test_rip309_integrated_simd_alias.py hard-coded the old simd_bias name in the epoch-0 rotation; updated to the canonical name and extended to cover the alias in both directions.

Rollout risk for Fix B (read before deploying)

  • Settlement and the reward path are unchanged. Byte-identical subset selection, proven by test_enroll_selector_matches_old_finalize_epoch_inline_algorithm.
  • The ENROLL-side active subset for the current epoch will almost certainly change on deploy: the old and new algorithms agree only ~1 in 15 times, and which subset is live depends on the actual previous epoch block hash in the production DB, which I cannot read from here (nothing was deployed and no server was touched). Assume it changes.
  • Who that affects: only miners whose fingerprints fail some checks. A miner passing all six checks has active_ratio 1.0 under either subset, so no weight change. A miner failing exactly the checks that rotate in or out will see enroll weight move.
  • Already-enrolled miners for the in-progress epoch keep their recorded weight: epoch_enroll uses INSERT OR IGNORE, first enrollment wins. The new subset applies to enrollments after deploy.
  • The point of the change is that the subset used to grant weight at enroll finally matches the subset used to veto at settlement. Until deploy, a miner can be paid weight for passing checks that settlement never verifies, and vetoed for checks enroll never told it were active.
  • Alias unification reward effect, quantified: it only bites a miner whose stored checks map is keyed simd_bias AND whose SIMD check failed AND with simd in the active set. Deployed miners emit simd_identity (fingerprint_checks.py line 866), so expected real-world impact is zero; a spoofing client renaming its failed check to simd_bias to dodge the veto now gets caught.
  • No other reward math was touched.

Testing / Evidence

New tests (29):

python3 -m pytest tests/test_fingerprint_evidence_content.py tests/test_rip309_rotation_unified.py -q
29 passed in 0.21s

That includes the headline pair: the QEMU-self-reporting payload with a forged passed: true is rejected (vm_self_reported:['qemu', 'hypervisor']), and a clean payload shaped like real fingerprint_checks.py output on honest hardware still returns valid.

Full suites, compared against a clean checkout of main to separate my impact from pre-existing failures:

tests/  (minus test_device_classification_corpus.py, which fails to COLLECT on clean main too)
  3693 passed, 47 skipped, 4 failed, 100 subtests passed
  the 4 failures are all in test_epoch_settlement_formal.py and fail identically on clean main

node/tests/  (minus 11 files with pre-existing collection errors on clean main)
  1439 passed, 84 failed, 18 subtests passed
  the 84-test failure list is byte-identical to clean main (diff of sorted FAILED lists is empty)

python3 -m py_compile passes on both changed node files and all three test files.

Not deployed. The four production nodes are still running the old code until someone reviews this and rolls it out with the enroll-subset change above in mind.

…ation

Fix A: validate_fingerprint_data only checked that evidence EXISTED, never
what it said. A payload of {"passed": true, "data": {"vm_indicators":
["qemu", "hypervisor"]}} was accepted because vm_indicators was present and
the verdict was not literally False. Non-empty vm_indicators (or
is_likely_vm) now rejects regardless of the claimed verdict. The == False
verdict comparisons for anti_emulation, clock_drift and rom_fingerprint are
now "is not True" so an omitted verdict no longer skips both branches.
Bool-format C miner checks are untouched. Same pattern closed for the other
checks: a check claiming pass while its data records fail_reason(s) is
rejected, and the Phase 4 hard-failure scan no longer hides behind the
client-supplied all_passed flag.

Fix B: two divergent 4-of-6 rotation algorithms disagreed for the same
epoch. Enroll used sha256("rip-309:" + hash) with sorted per-name hashing
over a tuple containing simd_bias; settlement and rewards used
sha256(hash_bytes + b"measurement_nonce") with random.Random(seed).sample
over simd_identity names. Weight was granted against one subset and vetoed
against another. All selectors now delegate to the tested
rip_309_measurement_rotation.get_reward_active_fingerprint_checks, the
check-name tuples use the canonical simd_identity, and the stored checks
maps mirror simd_bias/simd_identity at evaluation. Settlement behavior is
bit-for-bit unchanged; the enroll-side subset changes to match it.

Not deployed anywhere. Tests: 29 new tests pass, full tests/ suite 3693
passed with only pre-existing failures, node/tests 1439 passed with the
identical pre-existing failure list as main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Scott <scottbphone12@gmail.com>
@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) node Node server related tests Test suite changes size/XL PR: 500+ lines labels Jul 25, 2026
@Scottcjn Scottcjn removed the BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) label Jul 25, 2026
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

⚠️ BCOS v2 Scan Results

Metric Value
Trust Score 49/100
Certificate ID BCOS-2aaf5a94
Tier L2 (not met)

BCOS Badge

What does this mean?

The BCOS (Beacon Certified Open Source) engine scans for:

  • SPDX license header compliance
  • Known CVE vulnerabilities (OSV database)
  • Static analysis findings (Semgrep)
  • SBOM completeness
  • Dependency freshness
  • Test infrastructure evidence
  • Review attestation tier

Full report | What is BCOS?


BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs

A Proxmox VM is attesting on production right now with fingerprint_passed=1,
earning 0.8x, by submitting six bare true values and no evidence at all. The
bool branch only rejected an explicit False, so true fell straight through.

The earlier commit on this branch hardened the dict path and said in a comment
that it deliberately left the bool path alone for C miners. That was the wrong
call: it left the live exploit open and wrote the reasoning down as if it were
intentional. Two independent reviews landed on the same point, that
anti_emulation is not another measurement channel like cache timing. It is the
VM gate, and the design says a VM earns nothing, so capable hardware must
affirmatively pass it rather than merely not fail it.

Capability is now decided by the server-derived device class rather than by
which keys the client omitted. No data cannot distinguish cannot-measure from
did-not-bother, and only the class can. The bare boolean stays valid for the
6502, i386, DOS and console-bridge classes, which genuinely cannot serialise a
nested object.

The old comment claimed a bare boolean would be flagged for reduced weight. No
reduced weight was applied anywhere.

Signed-off-by: Scott <scottbphone12@gmail.com>
@github-actions github-actions Bot added the BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) label Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) node Node server related size/XL PR: 500+ lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants