-
Notifications
You must be signed in to change notification settings - Fork 154
Fix funding-payment reclassification downgrade and deep-reorg duplication #962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,8 @@ use persist::KVStoreWalletPersister; | |
| use crate::config::Config; | ||
| use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; | ||
| use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; | ||
| use crate::payment::store::ConfirmationStatus; | ||
| use crate::payment::pending_payment_store::PendingPaymentDetailsUpdate; | ||
| use crate::payment::store::{ConfirmationStatus, PaymentDetailsUpdate}; | ||
| use crate::payment::{ | ||
| FundingTxCandidate, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, | ||
| PendingPaymentDetails, TransactionType, | ||
|
|
@@ -391,11 +392,6 @@ impl Wallet { | |
| continue; | ||
| }; | ||
|
|
||
| // Collect all conflict txids | ||
| let mut conflict_txids: Vec<Txid> = | ||
| conflicts.iter().map(|(_, conflict_txid)| *conflict_txid).collect(); | ||
|
|
||
| conflict_txids.push(txid); | ||
| // The payment already exists in the store at this point: `bump_fee_rbf` updates | ||
| // the payment store with the replacement txid before the next sync cycle, so we | ||
| // can safely fetch it here. | ||
|
|
@@ -406,8 +402,26 @@ impl Wallet { | |
| ); | ||
| let payment = | ||
| self.payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?; | ||
|
|
||
| // A graduated funding payment is resolvable here only through | ||
| // `find_payment_by_txid`'s payment-store fallback. Revert it like the | ||
| // `TxUnconfirmed`/`TxDropped` arms instead of mirroring a non-`Pending` record | ||
| // into the pending store, which graduation's pending-only scan would reject. | ||
| if payment.status != PaymentStatus::Pending | ||
| && self.apply_funding_status_update( | ||
| payment_id, | ||
| txid, | ||
| ConfirmationStatus::Unconfirmed, | ||
| )? { | ||
| continue; | ||
| } | ||
|
|
||
| // Collect all conflict txids | ||
| let mut conflict_txids: Vec<Txid> = | ||
| conflicts.iter().map(|(_, conflict_txid)| *conflict_txid).collect(); | ||
| conflict_txids.push(txid); | ||
| let pending_payment_details = | ||
| self.create_pending_payment_from_tx(payment, conflict_txids.clone()); | ||
| self.create_pending_payment_from_tx(payment, conflict_txids); | ||
|
|
||
| self.runtime.block_on( | ||
| self.pending_payment_store.insert_or_update(pending_payment_details), | ||
|
|
@@ -1343,9 +1357,28 @@ impl Wallet { | |
| async fn persist_funding_payment( | ||
| &self, details: PaymentDetails, candidates: Vec<FundingTxCandidate>, | ||
| ) -> Result<(), Error> { | ||
| self.payment_store.insert_or_update(details.clone()).await?; | ||
| let pending = PendingPaymentDetails::new(details, Vec::new(), candidates); | ||
| self.pending_payment_store.insert_or_update(pending).await?; | ||
| if !self.payment_store.contains_key(&details.id) { | ||
| // First time we record this funding payment: store it and index it for graduation. | ||
| self.payment_store.insert_or_update(details.clone()).await?; | ||
| let pending = PendingPaymentDetails::new(details, Vec::new(), candidates); | ||
| self.pending_payment_store.insert_or_update(pending).await?; | ||
| } else { | ||
| // An earlier candidate or a racing wallet sync already recorded this payment. Merge only | ||
| // the classification (`tx_type`) and our contribution figures, which the wallet can't | ||
| // recompute; the confirmation state is owned by wallet-sync events, so a late | ||
| // classification must not move it (which would downgrade an already-Confirmed/Succeeded | ||
| // record). `update` is a no-op when the entry is absent, so the pending index is not | ||
| // re-created for a payment the graduation path already removed. | ||
| let update = PaymentDetailsUpdate::funding_reclassification(details); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, given we still wait for 6-conf before deeming payment successful, I do wonder if we should just disallow unconfirming an onchain payment in |
||
| let pending_update = PendingPaymentDetailsUpdate { | ||
| id: update.id, | ||
| payment_update: Some(update.clone()), | ||
| conflicting_txids: None, | ||
| candidates, | ||
| }; | ||
| self.payment_store.update(update).await?; | ||
| self.pending_payment_store.update(pending_update).await?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -1434,6 +1467,34 @@ impl Wallet { | |
| return Some(replaced_details.details.id); | ||
| } | ||
|
|
||
| // A funding payment graduates out of the pending store, after which only the payment store | ||
| // retains it — under its first-candidate-anchored id, but stamped with the confirmed | ||
| // candidate's txid. Map a later event (e.g. a reorg returning the confirmed candidate to the | ||
| // mempool) back to that funding payment so it is reverted in place rather than duplicated as | ||
| // a generic on-chain payment under the candidate's txid. Only one funding record carries a | ||
| // given confirmed txid (its id is anchored to the first candidate and reclassification | ||
| // merges into it), so the first match is unambiguous. | ||
| if let Some(funding) = self | ||
| .payment_store | ||
| .list_filter(|p| { | ||
| matches!( | ||
| p.kind, | ||
| PaymentKind::Onchain { | ||
| txid, | ||
| tx_type: | ||
| Some( | ||
| TransactionType::Funding { .. } | ||
| | TransactionType::InteractiveFunding { .. }, | ||
| ), | ||
| .. | ||
| } if txid == target_txid | ||
| ) | ||
| }) | ||
| .first() | ||
| { | ||
| return Some(funding.id); | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
|
|
@@ -1471,6 +1532,14 @@ impl Wallet { | |
| } | ||
| } | ||
|
|
||
| // A reorg returning the transaction to the mempool reverts the payment to pending so wallet | ||
| // sync re-graduates it once it reconfirms. This also re-establishes the pending-store entry | ||
| // below (gated on `Pending`) that graduation removed; without it a graduated payment would | ||
| // be left `Succeeded` with an `Unconfirmed` kind and no way to re-graduate. | ||
| if matches!(confirmation_status, ConfirmationStatus::Unconfirmed) { | ||
| payment.status = PaymentStatus::Pending; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned over at #888 (comment) I'm not sure we do this, as our base assumption is that anything beyond |
||
| } | ||
|
|
||
| payment.kind = | ||
| PaymentKind::Onchain { txid: event_txid, status: confirmation_status, tx_type }; | ||
| self.runtime.block_on(self.payment_store.insert_or_update(payment.clone()))?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codex: