Skip to content

fix(utxo): register dual-write transfer outputs as account-mirror provenance (danaher #2819 receiver residual) - #8388

Merged
Scottcjn merged 5 commits into
mainfrom
fix/utxo-dualwrite-receiver-provenance
Sep 19, 2026
Merged

Scottcjn merged 5 commits into
mainfrom
fix/utxo-dualwrite-receiver-provenance

Conversation

@Scottcjn

Copy link
Copy Markdown
Owner

Closes the receiver-provenance residual of the danaher-j cross-model double-spend (#2819). Found by a GPT-6 Astra audit of the UTXO transfer path and source-verified against the full mirror model.

The model (verified)

Under UTXO_DUAL_WRITE=1, balances (account) is the primary/total ledger (every epoch reward credits it); UTXO boxes are its shadow. account_mirror_boxes records which UTXO boxes back account value, with the consensus invariant mirror ≤ balance. For the two models not to double-count, every UTXO box representing account value must be mirror-tagged — so it can't be spent via the UTXO path independently of the account balance.

The residual

The mirror-input exclusion (spending an existing mirror box via UTXO) was already fixed and made unconditional. But a /utxo/transfer under dual-write:

  • credits the receiver's account balance (balances += amount), and
  • creates a spendable UTXO output box for the receiver via apply_transaction,

…and never registered that output as mirror provenance. So the receiver held amount in both models — spendable via /utxo/transfer (the untagged box) and via /wallet/transfer/signed (the account credit) = the same value spent twice. A total-only integrity check stays models_agree=True throughout.

Latent today (prod runs UTXO_DUAL_WRITE=0); arms on any dual-write enablement.

Fix

  • utxo_db.apply_transaction exposes the authoritative tx_id on the caller's tx dict (tx['tx_id']) so the endpoint can find the boxes it created.
  • The dual-write branch of /utxo/transfer registers every output box of the transfer (receiver idx 0, change idx 1) into account_mirror_boxes → the unconditional mirror-input exclusion then blocks re-spending them via UTXO; the value moves via the account path (which consumes the mirror on settle).
  • Fail-closed per-wallet mirror ≤ balance assertion (compared in nRTC: value_nrtc vs amount_i64 × (UNIT//ACCOUNT_UNIT) — mirror boxes are nano-RTC, account is micro-RTC); a violation rolls back rather than committing money that exists twice.

Tests (node/test_utxo_dualwrite_receiver_provenance.py)

  • receiver output is registered as mirror provenance;
  • receiver box is excluded from UTXO spendable candidates (double-spend closed);
  • mirror ≤ balance holds after transfer.

All 3 fail on origin/main and pass with this change. Existing UTXO suites: 154 passed, no new failures (the 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 bug class, same latency behind UTXO_DUAL_WRITE=0. Keep UTXO_DUAL_WRITE=0 on prod until both are resolved. Recommend danaher-j review; bounty for #2819 remains held pending payout destination.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbyXP4eiiRYEa8GsQtQPhR

@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 size/L PR: 201-500 lines labels Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

⚠️ BCOS v2 Scan Results

Metric Value
Trust Score 36/100
Certificate ID BCOS-2576f7f7
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

Crypteaucajun and others added 5 commits September 19, 2026 13:41
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>
…nance (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
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>
…venance (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
…; 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
Scottcjn force-pushed the fix/utxo-dualwrite-receiver-provenance branch from f623815 to c727426 Compare September 19, 2026 18:57
@Scottcjn

Copy link
Copy Markdown
Owner Author

Updated (rebased + tests repaired).

Stack order: #8481#8394 → this PR. Their commits show here until they merge, so merge them in that order.

⚠️ The design assumption, stated plainly: registering change as a mirror assumes a wallet's UTXO value equals its account balance. That holds when all UTXO value comes from genesis, reward mints (#8394 + #8481) and dual-write transfers. A wallet holding native, non-mirrored UTXO beyond its account balance would be blocked, and it fails closed, so nothing is double-spent. Keep this in mind before any path creates non-mirrored boxes while dual-write is on.

Regression check against the #8394 base: 0 regressions (4199 → 4202).

@github-actions github-actions Bot added tests Test suite changes size/XL PR: 500+ lines and removed size/L PR: 201-500 lines labels Sep 19, 2026
Scottcjn added a commit that referenced this pull request Sep 19, 2026
…nance (#8394)

* 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>

---------

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>
@Scottcjn
Scottcjn merged commit d930f06 into main Sep 19, 2026
14 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

RTC Reward

This merged PR earned 5 RTC — sent to Scottcjn.

RustChain Bounty Program

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