Skip to content

Commit bc76ab8

Browse files
jkczyzclaude
andcommitted
Apply funding status updates atomically with the funding-type check
apply_funding_status_update fetched the payment record, checked that it was a classified funding payment, merged in the new confirmation status, and wrote the result back as separate store operations. A classification racing in between -- merging tx_type and the contribution-derived figures into the record -- would be overwritten by the stale snapshot. Perform the check and the merge under the payment store's mutation lock so no write can interleave, and skip persisting when the merge changes nothing. Generated with assistance from Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d3bbdc8 commit bc76ab8

1 file changed

Lines changed: 46 additions & 26 deletions

File tree

src/wallet/mod.rs

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,34 +1650,54 @@ impl Wallet {
16501650
async fn apply_funding_status_update(
16511651
&self, payment_id: PaymentId, event_txid: Txid, confirmation_status: ConfirmationStatus,
16521652
) -> Result<bool, Error> {
1653-
let Some(mut payment) = self.payment_store.get(&payment_id) else {
1653+
// The funding-type gate, the candidate lookup, and the write share the store's mutation
1654+
// lock: against a separate `get`, a classification merging in between would have its
1655+
// `tx_type` and contribution figures clobbered by this stale snapshot.
1656+
let mut handled = None;
1657+
self.payment_store
1658+
.mutate(&payment_id, |existing| {
1659+
let payment = existing?;
1660+
let tx_type = match &payment.kind {
1661+
PaymentKind::Onchain {
1662+
tx_type:
1663+
tx_type @ Some(
1664+
TransactionType::Funding { .. }
1665+
| TransactionType::InteractiveFunding { .. },
1666+
),
1667+
..
1668+
} => tx_type.clone(),
1669+
_ => return None,
1670+
};
1671+
// Report the figures of the candidate that actually confirmed, which need not be
1672+
// the last one broadcast (an earlier, lower-fee candidate may win) and may carry
1673+
// no figures at all (`None`) for a round we didn't contribute to. (`direction` is
1674+
// invariant across a splice's candidates and cannot be changed through the store
1675+
// anyway.)
1676+
let mut target = payment.clone();
1677+
if let Some(pending) = self.pending_payment_store.get(&payment_id) {
1678+
if let Some(candidate) = pending.candidate(event_txid) {
1679+
target.amount_msat = candidate.amount_msat;
1680+
target.fee_paid_msat = candidate.fee_paid_msat;
1681+
}
1682+
}
1683+
target.kind =
1684+
PaymentKind::Onchain { txid: event_txid, status: confirmation_status, tx_type };
1685+
1686+
// Merge through the update machinery so its rules (e.g. which fields a merge may
1687+
// touch) keep applying, and skip the write when nothing changed.
1688+
let mut merged = payment.clone();
1689+
if merged.update(target.to_update()) {
1690+
handled = Some(merged.clone());
1691+
Some(merged)
1692+
} else {
1693+
handled = Some(payment.clone());
1694+
None
1695+
}
1696+
})
1697+
.await?;
1698+
let Some(payment) = handled else {
16541699
return Ok(false);
16551700
};
1656-
let tx_type = match &payment.kind {
1657-
PaymentKind::Onchain {
1658-
tx_type:
1659-
tx_type @ Some(
1660-
TransactionType::Funding { .. }
1661-
| TransactionType::InteractiveFunding { .. },
1662-
),
1663-
..
1664-
} => tx_type.clone(),
1665-
_ => return Ok(false),
1666-
};
1667-
// Report the figures of the candidate that actually confirmed, which need not be the last
1668-
// one broadcast (an earlier, lower-fee candidate may win) and may carry no figures at all
1669-
// (`None`) for a round we didn't contribute to. (`direction` is invariant across a splice's
1670-
// candidates and cannot be changed through the store anyway.)
1671-
if let Some(pending) = self.pending_payment_store.get(&payment_id) {
1672-
if let Some(candidate) = pending.candidate(event_txid) {
1673-
payment.amount_msat = candidate.amount_msat;
1674-
payment.fee_paid_msat = candidate.fee_paid_msat;
1675-
}
1676-
}
1677-
1678-
payment.kind =
1679-
PaymentKind::Onchain { txid: event_txid, status: confirmation_status, tx_type };
1680-
self.payment_store.insert_or_update(payment.clone()).await?;
16811701
// Mirror the refreshed confirmation status onto the pending entry: `ChainTipChanged`
16821702
// graduates by reading the pending entry's details, so it must see the new status. This is
16831703
// the same dual-write the default `TxConfirmed` path performs; an empty conflicting-txids

0 commit comments

Comments
 (0)