Skip to content

fix(enroll): bind reward weight to attested hardware, not unsigned request-body device#8013

Open
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/enroll-weight-device-spoof
Open

fix(enroll): bind reward weight to attested hardware, not unsigned request-body device#8013
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/enroll-weight-device-spoof

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

Reward-weight spoofing via unsigned device on /epoch/enroll

Bug

enroll_epoch() computes the HARDWARE_WEIGHTS reward multiplier from the request-body device:

family = device.get('family', 'x86')
arch   = device.get('arch', 'default')
hw_weight = HARDWARE_WEIGHTS.get(family, {}).get(arch, 1.0)

The enrollment signature covers only miner_pubkey|miner_id|epoch, so device is unsigned and fully caller-controlled. Nothing binds the enroll-claimed arch to the hardware the miner actually attested:

  • check_enrollment_requirements() validates only attestation recency, fingerprint_passed (VM/emulator realness) and MAC churn — never the device arch.
  • arch_cross_validation.validate_arch_consistency() — the module built specifically to catch claimed-arch-vs-fingerprint mismatch — has no callers in the server path (grep: only its own tests + an unwired rips/ module).

HARDWARE_WEIGHTS spans 0.0005 (aarch64 NAS) … 4.0 (ARM arm2), 2.8 (console nes_6502), 2.5 (x86 386) vs 0.8 for modern x86.

Impact (concrete trace)

  1. Attacker attests once from ordinary real hardware (modern x86). Fingerprint passes → fingerprint_passed=1, verified device stored as x86/modern (honest weight 0.8).
  2. Attacker signs the enrollment payload miner_pubkey|miner_id|epoch with their own attestation key — signature verifies.
  3. In the same body they set "device": {"family":"ARM","arch":"arm2"}hw_weight = 4.0.
  4. Settlement preserves the enroll-time weight snapshot (anti_double_mining.py: weight = enrolled_weights[miner_id]) and pays share = int((weight/total_weight) * total_reward_urtc) → attacker collects a ~5x inflated share of the fixed epoch pot (up to ~8000x versus an honest aarch64 miner), draining honest participants. fingerprint_passed stays 1 throughout because it only validates attestation-hardware realness, not the enroll-claimed arch.

Fix

resolve_enroll_weight_device() derives family/arch from the verified device the node already stored at attestation (miner_attest_recent.device_family/device_arch, written from derive_verified_device), mirroring the existing resolve_enroll_fingerprint pattern. The request-body device is consulted only as a legacy fallback when no verified device is stored (pre-migration rows), so deployed miners are not regressed. ~35 lines, localized to enroll_epoch + one helper.

Verification

  • New node/tests/test_enroll_weight_device_spoof.py drives the real /epoch/enroll endpoint with a valid Ed25519 signature:
    • test_spoofed_device_cannot_inflate_weight — attest verified as x86/modern, enroll body claims ARM/arm2 → hw_weight stays 0.8 (was 4.0). Mutation-checked: revert to the body-trusting line → test FAILS.
    • test_legacy_miner_without_verified_device_falls_back_to_body — no stored verified device → body device still honored (no regression).
  • Both pass on the fix; the spoof test fails on the pre-fix code.
  • The 8 pre-existing failures in test_enroll_signature_verification.py / test_block_producer_enroll_weight.py in my local env are identical with and without this change (missing optional modules like _epoch_eligible_miners/fleet_immune_system) — this PR introduces no new failures.

Claiming under rustchain-bounties#71. RTC address: RTCd1554f0f35576faf01d386a6be1c947f560dd0b7

…quest-body device

/epoch/enroll computed the HARDWARE_WEIGHTS reward multiplier from the
caller-supplied request-body `device` (family/arch). The enrollment
signature covers only (miner_pubkey|miner_id|epoch), so `device` is
unsigned and attacker-controlled: a miner that attests on ordinary x86
(verified x86/modern -> 0.8x) can enroll claiming ARM 'arm2' (4.0x) and
be recorded with a ~5x inflated epoch weight, draining the fixed
per-epoch reward pot from honest miners. check_enrollment_requirements
validates only recency/fingerprint_passed/MAC, never arch; the module
built to catch this (arch_cross_validation.validate_arch_consistency) has
no callers in the server path.

Fix: resolve_enroll_weight_device() derives family/arch from the verified
device stored at attestation (miner_attest_recent.device_family/arch,
written from derive_verified_device against the submitted fingerprint),
mirroring resolve_enroll_fingerprint. Request-body device is used only as
a legacy fallback when no verified device is stored, so deployed miners
are not regressed.

Regression test drives the real /epoch/enroll with a valid Ed25519
signature: fails on the old body-trusting code (weight 4.0), passes on
the fix (weight 0.8); a legacy-fallback case guards against regressing
pre-migration rows.
@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/L PR: 201-500 lines labels Jul 18, 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/L PR: 201-500 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant