fix(security): canonical UTXO recipients, rollback mirror cleanup, serialized hw binding - #8465
Merged
Merged
Conversation
…rialized hw binding Three verified findings reported by Ondrej Nad (#2819 / #71): 1. /utxo/transfer accepted upper/mixed-case hex in to_address. Address derivation yields lower-case hex and ownership matching is case-sensitive, so such a transfer created a box no key could ever spend (permanent fund lock). The canonical regex is now RTC[0-9a-f]{40}; non-canonical case is rejected (400) before signature checks or state mutation. Rejecting rather than lower-casing avoids changing what the sender signed. 2. rollback_genesis() left account_mirror_boxes provenance rows behind (no FK/cascade), and re-migration over changed balances added new rows beside them. Inside the existing BEGIN IMMEDIATE, and only when a genesis existed (the guard then proves all UTXO state was genesis-only), delete every mirror row not backed by a live box. Live dual-write rows are left untouched when no genesis exists. 3. bind_hardware_v2() ran the cross-serial entropy-collision scan on a separate connection outside any transaction, so two concurrent first-time registrations of the same physical machine under different serials could both see "no collision" and both bind. Now BEGIN IMMEDIATE is taken before the existing-serial lookup and the scan reuses the same connection (check_entropy_collision gains an optional conn=). Busy timeout 30s; measured lock hold ~23 ms at 10x current prod rows (DB is WAL). Tests: each new regression test fails on origin/main and passes here (concurrency test reproduces the double-bind on old code; 5/5 stable). Per-file comparison of utxo/genesis/rollback/dual-write/hw-binding/attest suites: 0 regressions; the 18 remaining failures fail identically on main. Reported-by: Ondrej Nad (hsmmkdnw84-creator) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott <scottbphone12@gmail.com>
Contributor
|
| Metric | Value |
|---|---|
| Trust Score | 36/100 |
| Certificate ID | BCOS-96c00f3a |
| Tier | L2 (not met) |
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
BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs
Contributor
RTC RewardThis merged PR earned 5 RTC — sent to |
Scottcjn
pushed a commit
that referenced
this pull request
Sep 19, 2026
…; annotate bounded fetch - The tests used 'bob' as the recipient. /utxo/transfer format-checks to_address since #8396/#8465 (lower-case RTC + 40 hex), so the transfer returned 400 before reaching the code under test. Use a canonical address. - test_dual_write_exact_balance_goes_to_zero seeded 100 RTC of UTXO against a 10 RTC account. With receiver/change outputs registered as account mirrors, the 90 RTC change exceeds the (now 0) account balance and the MIRROR_EXCEEDS_BALANCE guard correctly fails closed. Seed the consistent dual-write state (UTXO == account) the invariant assumes. - Annotate the per-transfer output fetch for the fetchall guard (bounded by one transfer's outputs). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott <scottbphone12@gmail.com>
Scottcjn
added a commit
that referenced
this pull request
Sep 19, 2026
…venance (#8388) * fix(utxo): dual-write reward boxes exactly mirror the account credit Two ways finalize_epoch's UTXO_DUAL_WRITE mint diverged from the account model it is supposed to mirror (#2819, favoritegrandson-tech): 1. Precision split: the account credit truncated the share to 6 decimals (amount_i64) while the mint truncated the same Decimal to 8 decimals, so each reward box was up to 99 nRTC larger than the credit and /utxo/integrity reported the models disagreeing after every epoch with fractional shares. The mint is now derived from the truncated credit: amount_nrtc = amount_i64 * (UTXO_UNIT // ACCOUNT_UNIT). A module-level assert pins that ratio as an exact integer. 2. Ghost mint: a miner enrolled without a balances row gets no account credit (no-phantom invariant) but was still minted a UTXO box. The mint is now gated on the account row actually being credited. This is a prerequisite for #8394 (registering reward boxes as mirrors must not create mirrors larger than the balance they mirror). Tests (end-to-end finalize_epoch, dual-write on): the reward box equals the credit x100 for co-prime weights, a ghost gets nothing, and the credited miner still mirrors exactly while a ghost dilutes the weight. Both fail on main (45 nRTC drift; 0.75 RTC ghost mint) and pass here. tests/ + epoch/UTXO/dual-write suites: 0 regressions (4192 -> 4194). UTXO_DUAL_WRITE is off and prod has 0 UTXO rows, so there is no history to reconcile. Reported-by: favoritegrandson-tech Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott <scottbphone12@gmail.com> * fix(utxo): register epoch mining-reward boxes as account-mirror provenance (danaher #2819, reward path) Same cross-model double-spend class as the /utxo/transfer receiver residual, in the epoch settlement path. Under UTXO_DUAL_WRITE=1, finalize_epoch credits each miner's ACCOUNT balance (primary ledger) AND mints a UTXO reward box for them — but never registered those boxes in account_mirror_boxes. So each reward was spendable via BOTH models (UTXO box + account balance) = double spend; a total-only integrity check stays models_agree=True. Latent today (prod runs UTXO_DUAL_WRITE=0). Fix: - account_mirror_boxes is now part of the canonical UTXO SCHEMA_SQL, so init_tables() creates it for every dual-write writer. This avoids a CREATE TABLE inside finalize_epoch's open settlement transaction, which Python's sqlite3 would implicit-commit (splitting the atomic epoch settle). (Comment in SCHEMA_SQL is kept free of the ';' character — _execute_schema splits on it.) - finalize_epoch registers every reward box of each dual-write batch into account_mirror_boxes (located by the batch's creation_height; apply_transaction enforces one mining_reward per height, so all boxes at that height are exactly the batch's outputs). Pure INSERTs, same connection/transaction as the settle. The mirror-input exclusion then blocks re-spending these boxes via the UTXO path, forcing the reward to move through the account path (single spend). Tests (node/test_utxo_dualwrite_reward_provenance.py): init_tables creates the table; a registered reward box is excluded from UTXO spendable candidates (double-spend closed) while an unregistered one is not (control); finalize_epoch wires the registration inside the dual-write block (source/AST, matching test_epoch_utxo_dual_write_guard's convention). All fail on origin/main, pass here. UTXO + settlement suites: 165 passed, no new failures (2 pre-existing genesis-migration failures are identical on origin/main). Companion to PR #8388 (transfer receiver residual). Keep UTXO_DUAL_WRITE=0 on prod until both merge. danaher-j #2819 bounty remains held pending destination. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbyXP4eiiRYEa8GsQtQPhR * fix(utxo): register only this epoch's mint outputs as reward mirrors The reward-mirror registration selected every box at batch_height. The one-mining_reward-per-height guard only limits MINT transactions: an ordinary /utxo/transfer box can share that height (both use slot numbers), and would be tagged as an account mirror, locking the user's own funds (every spend -> 409 ACCOUNT_MIRROR_BOX_NOT_SPENDABLE). Join on the mining_reward tx so only this batch's mint outputs are registered. Materialize the rows (bounded by UTXO_MAX_OUTPUTS) because the INSERTs reuse the same cursor. End-to-end test (real finalize_epoch, dual-write on, a user transfer box at the batch height): the user box is not tagged, each reward box is registered once, and each mirror equals its account credit exactly (needs the precision fix underneath). Fails on the previous height-only query ('user-box' tagged) and on main (nothing registered). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott <scottbphone12@gmail.com> * fix(utxo): register dual-write transfer outputs as account-mirror provenance (danaher #2819 receiver residual) Under UTXO_DUAL_WRITE=1, `balances` (account) is the primary ledger and UTXO boxes are its shadow; `account_mirror_boxes` records which boxes back account value, with the consensus invariant `mirror <= balance`. The mirror-input exclusion already blocks spending a mirror box via the UTXO path (input side, fixed earlier). The RESIDUAL: a /utxo/transfer credits the receiver's account balance AND creates a spendable UTXO output box for the receiver, but never registered that output as mirror provenance — so the same value was spendable via BOTH models (UTXO output box + account credit) = double spend. A total-only integrity check stays models_agree=True throughout (danaher-j private report, #2819 residual; latent today: UTXO_DUAL_WRITE is off on prod). Fix: - utxo_db.apply_transaction now exposes the authoritative tx_id on the caller's tx dict (tx['tx_id']), so the endpoint can locate the boxes it created. - The dual-write branch of /utxo/transfer registers every output box of the transfer (receiver at index 0, change at index 1) into account_mirror_boxes, so the unconditional mirror-input exclusion blocks re-spending them via UTXO; the value must move via the account path (which consumes the mirror on settle). - Added a fail-closed per-wallet `mirror <= balance` assertion (compared in nRTC: mirror value_nrtc vs balance amount_i64 * (UNIT//ACCOUNT_UNIT)); a violation rolls the transfer back rather than committing money that exists twice. Tests: node/test_utxo_dualwrite_receiver_provenance.py (3 tests) — receiver output is mirror-registered, receiver box is excluded from UTXO spendable candidates (double-spend closed), invariant holds. All 3 fail on origin/main and pass with this change. Existing UTXO suites: 154 passed, no new failures (17 pre-existing POC/isolation failures are identical on origin/main). RELATED (not fixed here, flagged for follow-up): the epoch mining-reward dual-write path (rustchain_v2_integrated...:~5698) likewise creates UTXO reward boxes without account_mirror_boxes provenance — same class, same latency behind UTXO_DUAL_WRITE=0. Keep UTXO_DUAL_WRITE=0 on prod until both are resolved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbyXP4eiiRYEa8GsQtQPhR * test(utxo): update receiver-provenance tests for canonical recipients; annotate bounded fetch - The tests used 'bob' as the recipient. /utxo/transfer format-checks to_address since #8396/#8465 (lower-case RTC + 40 hex), so the transfer returned 400 before reaching the code under test. Use a canonical address. - test_dual_write_exact_balance_goes_to_zero seeded 100 RTC of UTXO against a 10 RTC account. With receiver/change outputs registered as account mirrors, the 90 RTC change exceeds the (now 0) account balance and the MIRROR_EXCEEDS_BALANCE guard correctly fails closed. Seed the consistent dual-write state (UTXO == account) the invariant assumes. - Annotate the per-transfer output fetch for the fetchall guard (bounded by one transfer's outputs). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott <scottbphone12@gmail.com> --------- Signed-off-by: Scott <scottbphone12@gmail.com> Co-authored-by: Scott <scottbphone12@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes three findings from Ondrej Nad (#2819 / #71). Each was checked against
origin/mainbefore any change was made./utxo/transferaccepted mixed-caseto_address. Derived addresses are lower-case and owner matching is case-sensitive, so the resulting box could never be spent.RTC[0-9a-f]{40}and non-canonical addresses get a 400. Rejecting rather than lower-casing avoids altering what the sender signed.rollback_genesis()left orphanaccount_mirror_boxesrows behind.BEGIN IMMEDIATE, and only when a genesis existed, delete every mirror row that no live box backs. Dual-write rows are never touched when there is no genesis.bind_hardware_v2()ran its collision check and insert without a shared transaction, so two concurrent registrations of the same machine under different serials could both bind.BEGIN IMMEDIATEis taken before the lookup, and the collision scan reuses the same connection.Verification
main.Review
This went through a tri-brain review, but a degraded one: Codex was unavailable (usage limit), so only the Grok regression lens ran. Its findings were applied: narrowed mirror cleanup, test isolation, and no duplicate test runs. Its "canonicalize instead of reject" and lock-duration concerns were declined for the reasons above. Please get a second pair of eyes before merging (wallet / anti-spoof code).
Related follow-up (not in this PR)
The account-model ledger has the same case problem.
balancesholds 236.26 RTC and 3 RTC under two mixed-case spellings ofRTC5800896ed658aa511d029361b6d7388ddb29248b, from 17founder_communitypayouts in May–June. Reconciling those balances needs a maintainer decision.🤖 Generated with Claude Code