Skip to content

Make splice select+reserve atomic under the wallet lock#43

Open
amackillop wants to merge 1 commit into
lsp-0.7.0from
austin_mdk-1094_select-confirmed-txs-for-splice-2
Open

Make splice select+reserve atomic under the wallet lock#43
amackillop wants to merge 1 commit into
lsp-0.7.0from
austin_mdk-1094_select-confirmed-txs-for-splice-2

Conversation

@amackillop

@amackillop amackillop commented Jun 29, 2026

Copy link
Copy Markdown

A splice keeps its coins live in the in-memory reserved_utxos set rather than marking them spent, because signing happens later in a detached event. An earlier deadlock fix made the wallet and reserved locks strictly non-overlapping: select_utxos_inner read reserved, took the wallet lock, selected a coin, dropped the wallet lock, and only then published the reservation. The other selection paths likewise snapshotted reserved before taking the wallet lock.

That left a window where a splice-selected coin was both live in the BDK graph and absent from the reserved set. A concurrent open or on-chain withdrawal could read the stale reserved set under its own wallet lock and select the same coin, double-spending it. The service runs LSPS4 splices in the background while exposing gRPC payments, so these two can race.

Establish a wallet-outer / reserved-inner lock hierarchy instead: every selection path reads reserved while holding the wallet lock, and select_utxos_inner publishes the reservation before dropping it. Because all selection paths serialize on the wallet lock, select+reserve is now one critical section and no two builds can pick the same coin. Opens were never affected: they mark inputs spent in the graph under the same lock.

Deadlock-free because no path holds reserved while acquiring the wallet lock; the release paths take reserved alone.

Considered folding reserved_utxos into the wallet Mutex to make the atomicity type-enforced, but that touches every inner.lock() site and makes release paths contend on the full wallet lock. The hierarchy is small and documented once on the Wallet struct.

A splice keeps its coins live in the in-memory reserved_utxos set rather
than marking them spent, because signing happens later in a detached
event. An earlier deadlock fix made the wallet and reserved locks strictly
non-overlapping: select_utxos_inner read reserved, took the wallet lock,
selected a coin, dropped the wallet lock, and only then published the
reservation. The other selection paths likewise snapshotted reserved
before taking the wallet lock.

That left a window where a splice-selected coin was both live in the BDK
graph and absent from the reserved set. A concurrent open or on-chain
withdrawal could read the stale reserved set under its own wallet lock and
select the same coin, double-spending it. The service runs LSPS4 splices
in the background while exposing gRPC payments, so these two can race.

Establish a wallet-outer / reserved-inner lock hierarchy instead: every
selection path reads reserved while holding the wallet lock, and
select_utxos_inner publishes the reservation before dropping it. Because
all selection paths serialize on the wallet lock, select+reserve is now
one critical section and no two builds can pick the same coin. Opens were
never affected: they mark inputs spent in the graph under the same lock.

Deadlock-free because no path holds reserved while acquiring the wallet
lock; the release paths take reserved alone.

Considered folding reserved_utxos into the wallet Mutex to make the
atomicity type-enforced, but that touches every inner.lock() site and
makes release paths contend on the full wallet lock. The hierarchy is
small and documented once on the Wallet struct.
@amackillop amackillop force-pushed the austin_mdk-1094_select-confirmed-txs-for-splice-2 branch from 22e40c2 to 1c32fee Compare June 29, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant