Task
WalletBackend::refresh_tracked_coins admits hint-matched coins straight into coins with
asset_id = NULL, so a coin anybody can fabricate is counted as XCH and reaches the
spend-input selector. This is the same attack dig-node#380 closes on the peer ingest path, on the
coinset oracle path, and it is live on origin/main today with no peer involved.
Context
crates/dig-wallet/src/sage/rpc.rs, in refresh_tracked_coins:
let mut fetched = self.fallback.coin_records_by_puzzle_hashes(&phs).await?;
fetched.extend(self.fallback.coin_records_by_hints(&phs).await?);
let rows: Vec<CoinRow> = fetched.iter().map(fallback_coin_to_row).collect();
self.db.upsert_coins(&rows).await?;
coin_records_by_hints returns coins hinted to the wallet's p2 hashes. A hint is a memo: any
spend may attach any hint to any coin, so this set is attacker-controllable in full, needing only
the victim's public address.
fallback_coin_to_row produces asset_id = None, and asset_id IS NULL means XCH
(db.rs:1021). The row is therefore fed to unspent_coins(None) / unreserved_unspent_coins(None)
— the spend-input selector at rpc.rs:2933 and rpc.rs:3319.
singleton::reconstruct_all runs afterwards and attributes what it can, but it is
best-effort and non-subtractive: a coin it cannot reconstruct is left exactly as admitted, i.e.
as XCH. So the failure direction is a wrong figure, not incompleteness.
What it costs
The same three consequences #380 enumerates, reachable without touching a peer:
- a fabricated XCH balance;
- a send kill-switch — selection is largest-first, the fabricated coin is unspendable by
anyone, so it is chosen forever and never leaves the set;
- anything downstream that trusts
coins as the believed set.
Scope
Deliberately NOT fixed in #393, which closes the peer
ingest path only. Fixing it here would have widened a money-path PR that already carries a
measured acceptance bar, so it is logged rather than folded in.
The remedy is very likely the mechanism #393 builds rather than a new one: route hint-matched
arrivals through cat_admission_pending and let promotion decide, so this tier inherits the same
proof. Worth confirming that shape before implementing — a second, differently-shaped guard on the
oracle tier would be a rival implementation of the same distinction.
Evidence
Read on origin/main @ e094078. Not exploited; no coin was fabricated to demonstrate it.
Parent: #390
Task
WalletBackend::refresh_tracked_coinsadmits hint-matched coins straight intocoinswithasset_id = NULL, so a coin anybody can fabricate is counted as XCH and reaches thespend-input selector. This is the same attack dig-node#380 closes on the peer ingest path, on the
coinset oracle path, and it is live on
origin/maintoday with no peer involved.Context
crates/dig-wallet/src/sage/rpc.rs, inrefresh_tracked_coins:coin_records_by_hintsreturns coins hinted to the wallet's p2 hashes. A hint is a memo: anyspend may attach any hint to any coin, so this set is attacker-controllable in full, needing only
the victim's public address.
fallback_coin_to_rowproducesasset_id = None, andasset_id IS NULLmeans XCH(
db.rs:1021). The row is therefore fed tounspent_coins(None)/unreserved_unspent_coins(None)— the spend-input selector at
rpc.rs:2933andrpc.rs:3319.singleton::reconstruct_allruns afterwards and attributes what it can, but it isbest-effort and non-subtractive: a coin it cannot reconstruct is left exactly as admitted, i.e.
as XCH. So the failure direction is a wrong figure, not incompleteness.
What it costs
The same three consequences #380 enumerates, reachable without touching a peer:
anyone, so it is chosen forever and never leaves the set;
coinsas the believed set.Scope
Deliberately NOT fixed in #393, which closes the peer
ingest path only. Fixing it here would have widened a money-path PR that already carries a
measured acceptance bar, so it is logged rather than folded in.
The remedy is very likely the mechanism #393 builds rather than a new one: route hint-matched
arrivals through
cat_admission_pendingand let promotion decide, so this tier inherits the sameproof. Worth confirming that shape before implementing — a second, differently-shaped guard on the
oracle tier would be a rival implementation of the same distinction.
Evidence
Read on
origin/main@e094078. Not exploited; no coin was fabricated to demonstrate it.Parent: #390