#36313 wallet: fix persistent coin lock upgrade
| Reviewability | Reviews | Agreement | Size | ||||
|---|---|---|---|---|---|---|---|
| #36313 wallet: fix persistent coin lock upgrade | Ready | 1 | Strong | 4 + 39 tests | |||
|
|
|
| ||||
Reviewability: ReadyStalePaused Agreement: StrongPositivePositive w/ caveatsNeutralMildDisputedBlockedCrickets Reviews: current code-review ACKs, then (+stale ACKs) and -NACKs; greener = more ACKs. Size: lines added or modified outside tests, then in tests; greener = smaller. | |||||||
Goal
Updates `CWallet::LoadLockedCoin` to set `it->second |= persistent` when an entry already exists in `m_locked_coins`. This ensures that upgrading a non-persistent lock to a persistent lock updates the in-memory flag while preventing subsequent temporary locks from downgrading a persistent lock.
Problem: When a coin with an existing non-persistent lock (such as from `fundrawtransaction`) is upgraded to a persistent lock, `m_locked_coins.emplace()` leaves the in-memory persistence flag set to `false`. When the coin is subsequently spent, `UnlockCoin()` skips deleting the persistent `LOCKED_UTXO` database record, causing the spent coin to reappear as locked whenever the wallet is restarted or reloaded.
Evidence of importance:
- Fixes issue #36304: 'wallet: upgrading a non-persistent coin lock to persistent leaves a stale lock on the spent output after restart'
- PR author description confirms `m_locked_coins.emplace()` did not update the value, preventing `UnlockCoin()` from erasing the database record upon spending
Risk of a mistake: low (wallet, wallet_storage, tests). Flaws in wallet coin lock state tracking could lead to spent coins remaining locked or unspent coins failing to unlock properly across restarts.
Changed since the previous assessment: Switched from replacing entries with `insert_or_assign` to `it->second |= persistent` on existing entries, preventing temporary locks like `fundrawtransaction` from downgrading an existing persistent lock, and added a test case for downgrade prevention.
Reviewability
Ready
Nothing known that would invalidate a review now.
Waiting on (from the thread read):
- nothing: nothing pending; objection resolved and PR has tested ACK 2026-10-04
Agreement
Strong
substantive support, no open objection (vicjuma, vicjuma, jcastros, jcastros)
Thread notes: Author pushed a fix preventing non-persistent locks from downgrading persistent ones along with a regression test; jcastros verified the fix and gave a Tested ACK.
Objections, suggestions, and questions:
| Reviewer | Type | Harm, request, or question | Status | Must be | Author replied | Quote |
|---|---|---|---|---|---|---|
| achow101 (member) | suggestion, approach | Use insert_or_assign rather than setting the fields afterwards on failed emplacement | fixed · fix pushed · author updated the code to use insert_or_assign | answered | yes | 2026-09-29: 'This can use `insert_or_assign` rather than setting the fields afterwards on failed emplacement.' Status from: 2026-09-30: ArvinFarrelP: 'I've updated the code to use `insert_or_assign` as suggested.' |
| jcastros (none) | objection, correctness | A non-persistent lock overwrites an existing persistent lock in memory without removing the db record, causing spent outputs to be restored as locked on reload | accepted · fix pushed · author fixed downgrade and added test; reviewer confirmed and Tested ACKed | changed | yes | 2026-10-03: 'The other direction looks broken. Lock the coin with `lockunspent` persistent, then spend that same coin. On the base, `listlockunspent` is empty before and after a reload. On this commit it is empty ' Status from: 2026-10-04: jcastros: 'Tested ACK 23dbc1fe5c. The downgrade is gone.' |
Support:
- vicjuma (contributor) (Concept ACK, 2026-09-23) [read]: offers good UX and existing RPC validation already supports upgrading from non-persistent to persistent in a single call
- vicjuma (contributor) (2026-09-23) [tested]: tested solution and confirmed before/after behavior works as expected
- jcastros (none) (Concept ACK, 2026-10-03): Concept ACK on fixing #36304
- jcastros (none) (Tested ACK, 2026-10-04) [both: test/functional/wallet_basic.py]: verified persistent lock is not downgraded when spent with lockUnspents=true across unload/load wallet, and regression test catches insert_or_assign behavior
Participants:
- vicjuma (contributor, support, 2 statements 2026-09-23 to 2026-09-23): Concept ACK and manual testing confirmation
- achow101 (member, neutral, 1 statements 2026-09-29 to 2026-09-29): suggested using insert_or_assign
- jcastros (none, objection, 2 statements 2026-10-03 to 2026-10-04): objected to persistent lock downgrade regression, then Tested ACK after fix
Files
39 lines under test/bench/ci.
- test/functional/wallet_basic.py +39/-0
- src/wallet/wallet.cpp +4/-1
Card
The short text the ranking pass compares across PRs.
Updates LoadLockedCoin to set the persistence flag if an entry is already present in m_locked_coins instead of ignoring updates via emplace. Upgrading a temporary coin lock to persistent previously left the in-memory entry marked non-persistent; spending the coin then skipped deleting the persistent database record and left a phantom lock on reload. It also ensures non-persistent locking attempts cannot downgrade an existing persistent lock. Fixes issue #36304 with accompanying functional tests.
Data
thread read through 2026-10-04 (8 statements), head 23dbc1fe5c
labels: Wallet
Categories
Wallet
P3 · bug fix (ranking pass: Moved up: a reproducible persistent-lock bug with a linked issue outranks the P3 cleanups it was scored beside.)
Fixes a bug where upgrading an existing non-persistent coin lock to persistent leaves stale `LOCKED_UTXO` records in the database if the coin is spent, causing spent coins to reappear as locked upon wallet reload (#36304). While an annoying state inconsistency, it is an edge-case locking bug that does not risk funds or wallet corruption, making it reasonably deferrable.
Membership: Matches the Wallet label and modifies coin locking logic in `src/wallet/wallet.cpp`.
Factors: security/stability 1, bug 1, performance 0, user value 1, leverage 0