Skip to content

fix: sign macOS miner headers with wallet key#6546

Open
jonathanpopham wants to merge 1 commit into
Scottcjn:mainfrom
jonathanpopham:codex/mac-miner-ed25519-wallet-signing
Open

fix: sign macOS miner headers with wallet key#6546
jonathanpopham wants to merge 1 commit into
Scottcjn:mainfrom
jonathanpopham:codex/mac-miner-ed25519-wallet-signing

Conversation

@jonathanpopham
Copy link
Copy Markdown
Contributor

Summary

Fixes #6545.

The macOS miner v2.5 could attest successfully, but production header submission still used a SHA-512 placeholder signature and sent the RTC wallet address as pubkey. Production /headers/ingest_signed has mock signatures and inline pubkeys disabled, so the server expects a registered Ed25519 header key and a real Ed25519 signature.

This PR makes the macOS miner production-signing capable:

  • auto-loads a matching RTC wallet JSON from ~/.rustchain/wallets/*.json, RUSTCHAIN_WALLET_FILE, or --wallet-file
  • supports raw 32-byte hex seeds and the existing PKCS#8 DER/PEM wallet format
  • verifies the loaded public key maps to the configured RTC address before use
  • signs /attest/submit binding messages and includes public_key, allowing the node to register miner_header_keys
  • signs /headers/ingest_signed messages with Ed25519 instead of the legacy SHA-512 placeholder
  • keeps the old placeholder path only as a fallback for non-RTC/legacy deployments without a loaded signing key

Verification

  • PYTHONPATH not required: /tmp/rustchain6537-py311-venv/bin/python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
  • /tmp/rustchain6537-py311-venv/bin/python -m unittest miners.macos.test_rustchain_mac_miner_signing -> 2 tests OK
  • git diff --cached --check was clean before commit

Notes

I also live-tested the local client path with my own wallet: the miner loaded the Ed25519 wallet key, submitted a signed attestation, and the node accepted it with the hardware fingerprint passing. No private key material is logged or included here.

Payout address if accepted: RTC4730a64f821a590972e8b37781fae5d568b5865c.

@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) size/L PR: 201-500 lines labels May 28, 2026
@jonathanpopham
Copy link
Copy Markdown
Contributor Author

CI/status note for reviewers:

  • PR-scoped checks are green: Review Tier Label Gate, BCOS Trust Score/Scan, BCOS v2 Engine Scan, BCOS Tier Gate, PoC checks, welcome, label/size-label.
  • The remaining red hosted test job is the same broad repo baseline failure currently present on the other open PRs from this branch series.
  • Local focused verification for this PR passed:
    • /tmp/rustchain6537-py311-venv/bin/python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
    • /tmp/rustchain6537-py311-venv/bin/python -m unittest miners.macos.test_rustchain_mac_miner_signing -> 2 tests OK

@jonathanpopham jonathanpopham force-pushed the codex/mac-miner-ed25519-wallet-signing branch 2 times, most recently from 41332cc to 98bcdd0 Compare May 28, 2026 23:30
Copy link
Copy Markdown
Contributor

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head 41332cc04f143ab361c41cb36c14e697f499735d.

Verdict: request changes.

The code direction is good: the macOS miner now tries to load an Ed25519 wallet key, signs /attest/submit, submits signed /epoch/enroll, and sends real Ed25519 signatures to /headers/ingest_signed instead of the old SHA-512 placeholder. I also checked the server-side message formats:

  • attestation signs miner_id|wallet|nonce|commitment, matching /attest/submit verification
  • enrollment signs miner_pubkey|miner_id|epoch, matching /epoch/enroll verification
  • header signing uses the message bytes sent to /headers/ingest_signed

Local validation:

git diff --check origin/main...HEAD
# clean

git merge-tree --write-tree origin/main HEAD
# 27000e0965ee6adbc4871415820bec9d61055867

python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
# passed

Blocking issue: the focused test suite fails on Windows once pynacl is installed:

python -m unittest miners.macos.test_rustchain_mac_miner_signing

ERROR: test_loads_pkcs8_wallet_file_and_signs_messages
ValueError: wallet file does not contain a supported private key

FAIL: test_rejects_wallet_file_for_different_configured_address
AssertionError: "configured wallet address" does not match "wallet file does not contain a supported private key"

Ran 3 tests in 0.683s
FAILED (failures=1, errors=1)

There are two related problems to fix:

  1. The tests use tempfile.NamedTemporaryFile(...) while the file is still open. On Windows, that file cannot reliably be reopened by _load_wallet_signing_key(). Use TemporaryDirectory / Path.write_text() or close the temp file before loading it.
  2. _find_wallet_file() keeps explicit_path / env_path truthy for every later candidate. If the explicit file cannot be opened, the loop can fall through to ~/.rustchain/wallets/*.json and then accept the first wallet file regardless of data.address, because if explicit_path or env_path or data.get("address") == wallet: remains true. That can silently load the wrong wallet after an unreadable/missing explicit path. Track candidate source separately, or fail/return for an unreadable explicit path, and only use the address match for glob-discovered wallets.

Once those are fixed and the focused unittest passes with PyNaCl installed, this should be straightforward to re-review.

@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Update after follow-up on header-key registration:

  • Latest head 98bcdd0a now makes signed macOS attestations use the wallet address as miner_id, while preserving the hardware-derived ID as client_miner_id. This matches the ID used later by /headers/ingest_signed, so the server auto-enroll path registers miner_header_keys under the header-submission miner ID.
  • I avoided using explicit /epoch/enroll for this because the live node can reject that path with enrollment gates such as mac_churn; the attestation auto-enroll path is the one the miner already uses successfully.
  • Local focused verification now passes with 4 tests:
    • /tmp/rustchain6537-py311-venv/bin/python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
    • /tmp/rustchain6537-py311-venv/bin/python -m unittest miners.macos.test_rustchain_mac_miner_signing -> 4 tests OK
  • PR-scoped checks are green again; the remaining red hosted test job is the known repo-wide baseline failure.

@eliasx45
Copy link
Copy Markdown
Contributor

Quick correction/recheck after the force-push to 98bcdd0aea85ca5183d75fb5bc782eb93692c520: GitHub attached my review to the new head while I was submitting it, so I reran the focused checks on the actual current head.

Current-head validation:

git diff --check origin/main...HEAD
# clean

git merge-tree --write-tree origin/main HEAD
# 893c6741eb7cb6ef40b59d8518db75bb75aeab1f

python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
# passed

python -m unittest miners.macos.test_rustchain_mac_miner_signing
# Ran 4 tests in 0.642s
# FAILED (failures=1, errors=1)

The same Windows/PyNaCl blocker remains on current head: the two wallet-file tests still fail because the open NamedTemporaryFile cannot be reliably reopened on Windows, and _find_wallet_file() can still fall through to unrelated ~/.rustchain/wallets/*.json entries while explicit_path remains truthy for every candidate.

So the changes-requested review is still current for 98bcdd0a; the only correction is that the current suite has 4 tests, not 3.

@jonathanpopham jonathanpopham force-pushed the codex/mac-miner-ed25519-wallet-signing branch from 98bcdd0 to 47f485e Compare May 28, 2026 23:37
@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Updated in 47f485ed66591bbc2bdfe143a2258c9bb55bde1a to address the Windows focused-test blocker.

Changes:

  • Replaced the open NamedTemporaryFile tests with TemporaryDirectory + Path.write_text(), so Windows can reopen the wallet JSON during _load_wallet_signing_key().
  • Changed _find_wallet_file() so explicit --wallet-file and RUSTCHAIN_WALLET_FILE paths are loaded directly and fail fast if unreadable, instead of falling through to unrelated ~/.rustchain/wallets/*.json files.
  • Added coverage that glob-discovered wallet files require an address match before loading.

Current local focused verification:

/tmp/rustchain6537-py311-venv/bin/python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
# passed

/tmp/rustchain6537-py311-venv/bin/python -m unittest miners.macos.test_rustchain_mac_miner_signing
# Ran 6 tests in 0.037s
# OK

Current PR-scoped checks are green again; the remaining red hosted test job is still the known repo-wide baseline failure.

Copy link
Copy Markdown
Contributor

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed current head 47f485ed66591bbc2bdfe143a2258c9bb55bde1a after the Windows-test follow-up.

Verdict: request changes remains, but the blocker is narrower now.

Current validation:

git diff --check origin/main...HEAD
# clean

git merge-tree --write-tree origin/main HEAD
# ee01906fe7680663bae9f37b8f945b86b8404e62

python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
# passed

python -m unittest miners.macos.test_rustchain_mac_miner_signing
# Ran 6 tests in 0.784s
# FAILED (errors=1)

The previous open-NamedTemporaryFile failures are fixed. The remaining Windows failure is in test_glob_wallet_discovery_requires_address_match:

ERROR: test_glob_wallet_discovery_requires_address_match
AttributeError: 'NoneType' object has no attribute 'endswith'

Cause: the test writes the fake wallet under <tmp>/.rustchain/wallets and sets module.os.environ["HOME"] = tmpdir, but on Windows os.path.expanduser("~") does not necessarily use HOME; it commonly uses USERPROFILE / HOMEDRIVE + HOMEPATH. As a result _find_wallet_file(address) does not see the temporary wallet directory here and returns (None, None).

Fix direction: make the test control the lookup path in a cross-platform way. For example, temporarily set/restore USERPROFILE on Windows too, or monkeypatch the glob.glob(...) call / wallet directory helper if you add one. After that, rerun the same unittest with PyNaCl installed on Windows.

@jonathanpopham jonathanpopham force-pushed the codex/mac-miner-ed25519-wallet-signing branch from 47f485e to 070510f Compare May 28, 2026 23:45
@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Updated in 070510f6ad5761e1a414f60b3da10271da9e611e to address the remaining Windows-focused test blocker.

Change made:

  • test_glob_wallet_discovery_requires_address_match now monkeypatches module.glob.glob directly to return the temporary wallet path instead of relying on HOME / expanduser("~"), which differs between Unix and Windows (USERPROFILE, HOMEDRIVE, HOMEPATH).

Current local focused verification:

/tmp/rustchain6537-py311-venv/bin/python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
# passed

/tmp/rustchain6537-py311-venv/bin/python -m unittest miners.macos.test_rustchain_mac_miner_signing
# Ran 6 tests in 0.060s
# OK

Current PR-scoped checks are green again; only the hosted repo-wide test baseline is red.

Copy link
Copy Markdown
Contributor

@eliasx45 eliasx45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed current head 070510f6ad5761e1a414f60b3da10271da9e611e.

Verdict: approve.

The Windows/PyNaCl blocker from my previous reviews is resolved on this head. Local validation on this checkout:

git diff --check origin/main...HEAD
# clean

git merge-tree --write-tree origin/main HEAD
# 1b77d3914f5a1b0efaa812bee3bf3d42ebe23ed2

python -m compileall -q miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py
# passed

python -m unittest miners.macos.test_rustchain_mac_miner_signing
# Ran 6 tests in 0.812s
# OK

What I rechecked:

  • The macOS miner now loads Ed25519 wallet keys from explicit/env/glob-discovered wallet files without the earlier unsafe fallback behavior.
  • The tests no longer rely on an open NamedTemporaryFile, and the glob-discovery test now passes on Windows.
  • The signed attestation path uses the wallet address as the attestation/header-key miner id when signing is available, which aligns the server's stored miner_header_keys entry with the later /headers/ingest_signed miner id.
  • Header submissions sign the exact message bytes sent in the request and send the Ed25519 public key rather than the old wallet-string placeholder.

Hosted note: the broad test job is still red, but the focused suite for this PR passes locally on Windows and the other PR-scoped checks shown on GitHub are green.

@Scottcjn
Copy link
Copy Markdown
Owner

Thanks @jonathanpopham — the production macOS signing bug is real, but the patch shape introduces a different problem, so closing with NEEDS_FIX instead of merging.

Codex authoritative review summary:

  • Real bug (yes): macOS miner can't submit production signed headers.
  • Patch issue: introduces miner-identity drift — depending on which OS code path runs, the same physical miner can end up signing as different identities. That undermines the producer-eligibility guarantee [CONSENSUS-BUG] Enforce header producer eligibility #6541 just hardened.
  • Scope creep: scope is broader than the title suggests.

Required change: keep miner identity stable across OS code paths for a given physical machine + wallet key combination. The signing path should derive identity deterministically from a single source-of-truth, not from whichever OS-specific helper got called. Then verify with a test that the same wallet key on macOS and Linux produces identical signed-header identity.

Re-submit and ping — happy to re-review and pay on a clean fix.

Copy link
Copy Markdown
Contributor

@crystal-tensor crystal-tensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Code review approved by @cx95zz (QClaw automated review agent).

Reviewed for: correctness, security, test coverage, and code quality.

No issues found - APPROVED.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution! I've reviewed the changes and everything looks good.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! 🎉

Reviewing the changes...

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! 🎉

Reviewing the changes...

Copy link
Copy Markdown

@aisoh877 aisoh877 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the macOS miner wallet-signing change. This looks safe to merge from the focused checks I ran.

What I checked:

  • wallet key loading supports explicit wallet files, env/glob discovery, PKCS#8 DER/PEM seed extraction, and rejects mismatched configured addresses/public keys
  • signed attestations use the wallet address as miner_id while preserving the hardware id in client_miner_id; unsigned attestations preserve the existing hardware miner id behavior
  • header submission now uses the Ed25519 wallet signature/pubkey when a wallet key is loaded, with the old hash fallback preserved when no signing key is available

Verification:

  • Python 3.13 venv: PYTHONPATH=miners/macos python -m unittest miners/macos/test_rustchain_mac_miner_signing.py -v -> 6 tests OK
  • python -m py_compile miners/macos/rustchain_mac_miner_v2.5.py miners/macos/test_rustchain_mac_miner_signing.py -> OK
  • git diff --check origin/main...HEAD -> clean
  • git merge-tree --write-tree origin/main HEAD -> clean

@Scottcjn
Copy link
Copy Markdown
Owner

Tri-brain review (Codex 5.5) found two blocking issues before merge:

  1. Fails open → forgeable signatures. Missing PyNaCl / unreadable --wallet-file / malformed JSON / key mismatch are swallowed and mining continues, then submit_header silently falls back to sha512(message + wallet)forgeable by anyone who knows the public wallet address. On a node accepting mock sigs, that forged sig is accepted. RTC wallets + an explicitly-configured --wallet-file must fail closed; the legacy SHA-512 fallback should be narrowly gated (only when no wallet was ever configured).
  2. client_miner_id not signature-covered. Identity switches to the wallet address and the hardware id moves to client_miner_id, but that field isn't in the signed message — so it's tamperable on proxy paths and can bypass hardware-uniqueness. Bind the hardware id into the signature, paired with the matching server-side change.

The signing direction is exactly right and very welcome — these two just need fixing first. Holding. 🙏

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/L PR: 201-500 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] macOS miner cannot submit production signed headers

6 participants