Skip to content

fix(windows-miner): KeyError on attestation[nonce] posts every attestation unsigned, blocking enrollment#7979

Open
Vyacheslav-Tomashevskiy wants to merge 2 commits into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/windows-miner-nonce-keyerror
Open

fix(windows-miner): KeyError on attestation[nonce] posts every attestation unsigned, blocking enrollment#7979
Vyacheslav-Tomashevskiy wants to merge 2 commits into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/windows-miner-nonce-keyerror

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

Problem

attest() builds the attestation with the nonce under report — there is no top-level copy:

attestation = {
    "miner":    self.wallet_address,
    "miner_id": self.miner_id,
    "report":   report_payload,      # {"nonce": nonce, "commitment": ..., ...}
    "device":   {...},
    "signals":  {...},
}

The signing fallback then reads a key that does not exist:

sign_msg = "{}|{}|{}|{}".format(
    attestation["miner_id"],
    attestation["miner"],
    attestation["nonce"],            # KeyError: 'nonce'
    attestation["report"]["commitment"],
).encode("utf-8")

The except below it swallows the KeyError and falls through unsigned:

WARNING rustchain_windows_miner.py:595 attestation signing failed; falling through unsigned: 'nonce'

Because the raise happens inside if CRYPTO_AVAILABLE and self.keypair:, the else: legacy-sha512 fallback never runs either — so the POST carries no signature, no public_key, no signature_type at all.

Why this is worse than a missing signature

The unsigned attest is accepted, but record_attestation_success(..., signing_pubkey=pubkey_hex or None, ...) then stores nothing. Enrollment, meanwhile, signs fine (it never touches attestation["nonce"]) and sends signature + public_key — but the server looks up the stored pubkey, finds none, and with ENROLL_ALLOW_UNSIGNED_LEGACY defaulting to "0" fails closed:

412 ENROLLMENT_SIGNING_KEY_REQUIRED — "Re-attest with signature/public_key before enrolling"

which is precisely what this miner structurally cannot do. The miner requires status_code == 200, returns False, and never enrolls. A Windows miner in this state mines nothing, forever.

Why only Windows, and why CI didn't catch it

The Linux miner sets a top-level "nonce": nonce alongside report["nonce"] (miners/linux/rustchain_linux_miner.py:618), and macOS v2.5 sets both. That asymmetry is also why build_pipe_sign_message reads report.get("nonce") or attestation["nonce"] — it tolerates either spelling, so the helper branch masks the bug. Only the Windows fallback hard-codes the spelling Windows doesn't use.

And the fallback branch is the one a real install takes. signing_helpers.py lives at miners/signing_helpers.py; rustchain_miner_setup.bat:7,10 downloads only rustchain_windows_miner.py and miner_crypto.py into a flat directory, and the PyInstaller spec ships hiddenimports=[]. So both imports fail, _SIGNING_HELPERS = False, and the broken branch runs. From a dev checkout from miners.signing_helpers import ... resolves via implicit namespace packages, so it looks fine locally.

The existing test_windows_miner_signs_report_nonce does not catch it because it sets miner_mod._SIGNING_HELPERS = True — it pins the masked path. requirements-miner.txt pins PyNaCl>=1.5.0 and the .bat installs it, so CRYPTO_AVAILABLE and self.keypair is true on a default install: everyone lands in the broken branch.

Fix

One line — read the nonce where this attestation actually keeps it:

attestation["report"]["nonce"]

This makes the fallback produce byte-identical output to build_pipe_sign_message for the same attestation (both emit miner_id|miner|nonce|commitment), so the two branches can no longer drift. I did not add a top-level nonce to match Linux: that would change the posted payload shape, and this keeps the diff to the defect. Happy to align the miners on one shape instead if you'd rather fix it at the source.

Bootstrap hash

rustchain_miner_setup.bat pins MINER_SHA256 of this exact file, verified both by check_bootstrap_hashes.py in CI and by the installer itself at line 91 — an un-updated pin would make real installs refuse the fixed file. Re-pinned to 1c25b32e…5d42; check_bootstrap_hashes.py reports "Windows bootstrap hashes are in sync." The .bat diff is that one line, CRLF preserved.

Tests

  • test_windows_miner_signs_when_signing_helpers_unavailable — the real-install branch (_SIGNING_HELPERS = False), asserting signature / public_key / signature_type are present and that the bytes match the node's reconstruction. Fails on main with the 'nonce' warning above.
  • test_windows_fallback_message_matches_shared_helper — pins the two branches to identical bytes on a Windows-shaped attestation.

tests/test_attestation_signing_6798.py: 12 passed (was 11 + 1 failing).

Severity, honestly

High impact but narrow scope: Windows miners only, and only against a node running the default ENROLL_ALLOW_UNSIGNED_LEGACY=0. Linux/macOS/POWER8 are unaffected. The GUI installer copy under miners/windows/installer/src/ is a separate, older copy without the signing refactor, so it is unaffected too — the blast radius is the .bat / build_windows_miner.ps1 channel, which is live and CI-guarded. I have not tested against a running node; the enrollment chain above is read from the node source, so please sanity-check that half.

RTC: RTCd1554f0f35576faf01d386a6be1c947f560dd0b7

…nonce

The no-helpers signing branch read attestation["nonce"], but the Windows
attestation dict only carries the nonce under report. That KeyError is caught
by the enclosing except, so the miner posts an attestation with no signature,
no public_key and no signature_type - and because the raise happens inside the
CRYPTO_AVAILABLE branch, the legacy sha512 fallback never runs either.

The node then stores no signing_pubkey for that miner, and enrollment fails
closed with ENROLLMENT_SIGNING_KEY_REQUIRED (ENROLL_ALLOW_UNSIGNED_LEGACY
defaults to 0), asking the miner to re-attest with a signature it cannot
produce. The Linux miner carries a top-level nonce, which is why the shared
helper tolerates either spelling and why only Windows is affected.

Reading report[nonce] makes the fallback produce byte-identical output to
build_pipe_sign_message. MINER_SHA256 in the bootstrap .bat is re-pinned to
match the edited file.
@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor Author

/claim

RTC: RTCd1554f0f35576faf01d386a6be1c947f560dd0b7

@github-actions

Copy link
Copy Markdown
Contributor

Welcome to RustChain! Thanks for your first pull request.

Before we review, please make sure:

  • Non-doc PRs have a BCOS-L1 or BCOS-L2 label
  • Doc-only PRs are exempt from BCOS tier labels when they only touch docs/**, *.md, or common image/PDF files
  • New code files include an SPDX license header
  • You've tested your changes against the live node

Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150)

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) tests Test suite changes size/M PR: 51-200 lines labels Jul 15, 2026
setup_miner.py is the second place that pins the Windows miner digest
(the bootstrap .bat is the first, already re-pinned in this branch).
The nonce fix changed rustchain_windows_miner.py, so the setup_miner
pin went stale: download_miner() fetches the file from main and
verifies it against this constant, so a Windows install would abort on
a checksum mismatch. tests/test_setup_miner_downloads.py caught it.

Linux/Darwin pins are unaffected and left untouched.
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) size/M PR: 51-200 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant