Problem
In cosigner-runtime/src/cosigner/handlers/vtxo_stream.rs, the delegate-invalidation path writes to two separate sled trees in sequence with no transaction wrapping them:
- Lines 85-89:
state.delegate_session = None;
super::helpers::delete_user_delegate(...); // sled write #1
...
save_user_vtxos(...); // sled write #2
- Same shape at lines 172-176.
If cosigner-runtime crashes between the two writes, sled is left with:
delegate_sessions row deleted ✓
vtxo_store row still pointing at outpoints the ASP just told us are spent ✗
Impact
Low in practice — the inconsistency self-heals on the next vtxo_stream message (the ASP re-emits the spent state on reconnect, and apply_stream_update reconciles). But there's a one-stream-update window where a freshly-spawned actor sees stale VTXOs without a covering delegate. A user opening the app in that window could see briefly-outdated state.
Suggested fix
Wrap both writes in a sled Batch (sled supports cross-tree batches) so the post-crash state is either both-applied or both-rolled-back.
Discovered during PR #32 review.
Problem
In
cosigner-runtime/src/cosigner/handlers/vtxo_stream.rs, the delegate-invalidation path writes to two separate sled trees in sequence with no transaction wrapping them:If cosigner-runtime crashes between the two writes, sled is left with:
delegate_sessionsrow deleted ✓vtxo_storerow still pointing at outpoints the ASP just told us are spent ✗Impact
Low in practice — the inconsistency self-heals on the next vtxo_stream message (the ASP re-emits the spent state on reconnect, and
apply_stream_updatereconciles). But there's a one-stream-update window where a freshly-spawned actor sees stale VTXOs without a covering delegate. A user opening the app in that window could see briefly-outdated state.Suggested fix
Wrap both writes in a sled
Batch(sled supports cross-tree batches) so the post-crash state is either both-applied or both-rolled-back.Discovered during PR #32 review.