Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ interface ClosureReason {

[Enum]
interface PaymentKind {
Onchain(Txid txid, ConfirmationStatus status);
Onchain(Txid txid, ConfirmationStatus status, sequence<Txid> conflicting_txids);
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits);
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
Expand Down
19 changes: 18 additions & 1 deletion src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use crate::io::utils::{
use crate::io::vss_store::VssStore;
use crate::io::{
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE, REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE,
};
use crate::liquidity::{
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
Expand All @@ -70,7 +71,7 @@ use crate::runtime::Runtime;
use crate::tx_broadcaster::TransactionBroadcaster;
use crate::types::{
ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter,
OnionMessenger, PaymentStore, PeerManager, Persister,
OnionMessenger, PaymentStore, PeerManager, Persister, ReplacedTransactionStore,
};
use crate::wallet::persist::KVStoreWalletPersister;
use crate::wallet::Wallet;
Expand Down Expand Up @@ -1239,6 +1240,21 @@ fn build_with_store_internal(
},
};

let replaced_tx_store =
match io::utils::read_replaced_txs(Arc::clone(&kv_store), Arc::clone(&logger)) {
Ok(replaced_txs) => Arc::new(ReplacedTransactionStore::new(
replaced_txs,
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
Arc::clone(&kv_store),
Arc::clone(&logger),
)),
Err(e) => {
log_error!(logger, "Failed to read replaced transaction data from store: {}", e);
return Err(BuildError::ReadFailed);
},
};

let wallet = Arc::new(Wallet::new(
bdk_wallet,
wallet_persister,
Expand All @@ -1247,6 +1263,7 @@ fn build_with_store_internal(
Arc::clone(&payment_store),
Arc::clone(&config),
Arc::clone(&logger),
Arc::clone(&replaced_tx_store),
));

let chain_source = match chain_data_source_config {
Expand Down
4 changes: 4 additions & 0 deletions src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ pub(crate) const BDK_WALLET_INDEXER_KEY: &str = "indexer";
///
/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices";

/// The replaced transaction information will be persisted under this prefix.
pub(crate) const REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE: &str = "replaced_txs";
pub(crate) const REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
33 changes: 33 additions & 0 deletions src/io/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ use crate::io::{
NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE,
};
use crate::logger::{log_error, LdkLogger, Logger};
use crate::payment::ReplacedOnchainTransactionDetails;
use crate::peer_store::PeerStore;
use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper};
use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper};
Expand Down Expand Up @@ -617,6 +618,38 @@ pub(crate) fn read_bdk_wallet_change_set(
Ok(Some(change_set))
}

/// Read previously persisted replaced transaction information from the store.
pub(crate) fn read_replaced_txs<L: Deref>(
kv_store: Arc<DynStore>, logger: L,
) -> Result<Vec<ReplacedOnchainTransactionDetails>, std::io::Error>
where
L::Target: LdkLogger,
{
let mut res = Vec::new();

for stored_key in KVStoreSync::list(
&*kv_store,
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE,
REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE,
)? {
let mut reader = Cursor::new(KVStoreSync::read(
&*kv_store,
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE,
REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE,
&stored_key,
)?);
let payment = ReplacedOnchainTransactionDetails::read(&mut reader).map_err(|e| {
log_error!(logger, "Failed to deserialize ReplacedOnchainTransactionDetails: {}", e);
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Failed to deserialize ReplacedOnchainTransactionDetails",
)
})?;
res.push(payment);
}
Ok(res)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 2 additions & 0 deletions src/payment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ pub(crate) mod asynchronous;
mod bolt11;
mod bolt12;
mod onchain;
mod replaced_transaction_store;
mod spontaneous;
pub(crate) mod store;
mod unified_qr;

pub use bolt11::Bolt11Payment;
pub use bolt12::Bolt12Payment;
pub use onchain::OnchainPayment;
pub use replaced_transaction_store::ReplacedOnchainTransactionDetails;
pub use spontaneous::SpontaneousPayment;
pub use store::{
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
Expand Down
104 changes: 104 additions & 0 deletions src/payment/replaced_transaction_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// This file is Copyright its original authors, visible in version control history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.

use bitcoin::Txid;
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::msgs::DecodeError;
use lightning::util::ser::{Readable, Writeable};
use lightning::{_init_and_read_len_prefixed_tlv_fields, write_tlv_fields};

use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};

/// Details of an on-chain transaction that has replaced a previous transaction (e.g., via RBF).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReplacedOnchainTransactionDetails {
/// The new transaction ID.
pub new_txid: Txid,
/// The original transaction ID that was replaced.
pub original_txid: Txid,
/// The payment ID associated with the transaction.
pub payment_id: PaymentId,
}

impl ReplacedOnchainTransactionDetails {
pub(crate) fn new(new_txid: Txid, original_txid: Txid, payment_id: PaymentId) -> Self {
Self { new_txid, original_txid, payment_id }
}
}

impl Writeable for ReplacedOnchainTransactionDetails {
fn write<W: lightning::util::ser::Writer>(
&self, writer: &mut W,
) -> Result<(), lightning::io::Error> {
write_tlv_fields!(writer, {
(0, self.new_txid, required),
(2, self.original_txid, required),
(4, self.payment_id, required),
});
Ok(())
}
}

impl Readable for ReplacedOnchainTransactionDetails {
fn read<R: lightning::io::Read>(
reader: &mut R,
) -> Result<ReplacedOnchainTransactionDetails, DecodeError> {
_init_and_read_len_prefixed_tlv_fields!(reader, {
(0, new_txid, required),
(2, original_txid, required),
(4, payment_id, required),
});

let new_txid: Txid = new_txid.0.ok_or(DecodeError::InvalidValue)?;
let original_txid: Txid = original_txid.0.ok_or(DecodeError::InvalidValue)?;
let payment_id: PaymentId = payment_id.0.ok_or(DecodeError::InvalidValue)?;

Ok(ReplacedOnchainTransactionDetails { new_txid, original_txid, payment_id })
}
}

impl StorableObjectId for Txid {
fn encode_to_hex_str(&self) -> String {
self.to_string()
}
}
impl StorableObject for ReplacedOnchainTransactionDetails {
type Id = Txid;
type Update = ReplacedOnchainTransactionDetailsUpdate;

fn id(&self) -> Self::Id {
self.new_txid
}

fn update(&mut self, _update: &Self::Update) -> bool {
// We don't update, we delete on confirmation
false
}

fn to_update(&self) -> Self::Update {
self.into()
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ReplacedOnchainTransactionDetailsUpdate {
pub id: Txid,
}

impl From<&ReplacedOnchainTransactionDetails> for ReplacedOnchainTransactionDetailsUpdate {
fn from(value: &ReplacedOnchainTransactionDetails) -> Self {
Self { id: value.new_txid }
}
}

impl StorableObjectUpdate<ReplacedOnchainTransactionDetails>
for ReplacedOnchainTransactionDetailsUpdate
{
fn id(&self) -> <ReplacedOnchainTransactionDetails as StorableObject>::Id {
self.id
}
}
23 changes: 20 additions & 3 deletions src/payment/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,15 @@ impl StorableObject for PaymentDetails {
}
}

if let Some(conflicting_txids_opt) = &update.conflicting_txids {
match self.kind {
PaymentKind::Onchain { ref mut conflicting_txids, .. } => {
update_if_necessary!(*conflicting_txids, conflicting_txids_opt.to_vec());
},
_ => {},
}
}

if updated {
self.latest_update_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down Expand Up @@ -351,6 +360,8 @@ pub enum PaymentKind {
txid: Txid,
/// The confirmation status of this payment.
status: ConfirmationStatus,
/// Transaction IDs that have replaced or conflict with this payment.
conflicting_txids: Vec<Txid>,
},
/// A [BOLT 11] payment.
///
Expand Down Expand Up @@ -448,6 +459,7 @@ pub enum PaymentKind {
impl_writeable_tlv_based_enum!(PaymentKind,
(0, Onchain) => {
(0, txid, required),
(1, conflicting_txids, optional_vec),
(2, status, required),
},
(2, Bolt11) => {
Expand Down Expand Up @@ -540,6 +552,7 @@ pub(crate) struct PaymentDetailsUpdate {
pub direction: Option<PaymentDirection>,
pub status: Option<PaymentStatus>,
pub confirmation_status: Option<ConfirmationStatus>,
pub conflicting_txids: Option<Vec<Txid>>,
}

impl PaymentDetailsUpdate {
Expand All @@ -555,6 +568,7 @@ impl PaymentDetailsUpdate {
direction: None,
status: None,
confirmation_status: None,
conflicting_txids: None,
}
}
}
Expand All @@ -570,9 +584,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
_ => (None, None, None),
};

let confirmation_status = match value.kind {
PaymentKind::Onchain { status, .. } => Some(status),
_ => None,
let (confirmation_status, conflicting_txids) = match &value.kind {
PaymentKind::Onchain { status, conflicting_txids, .. } => {
(Some(*status), conflicting_txids.clone())
},
_ => (None, Vec::new()),
};

let counterparty_skimmed_fee_msat = match value.kind {
Expand All @@ -593,6 +609,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
direction: Some(value.direction),
status: Some(value.status),
confirmation_status,
conflicting_txids: Some(conflicting_txids),
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::fee_estimator::OnchainFeeEstimator;
use crate::gossip::RuntimeSpawner;
use crate::logger::Logger;
use crate::message_handler::NodeCustomMessageHandler;
use crate::payment::PaymentDetails;
use crate::payment::{PaymentDetails, ReplacedOnchainTransactionDetails};

/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the
/// same time.
Expand Down Expand Up @@ -443,3 +443,6 @@ impl From<&(u64, Vec<u8>)> for CustomTlvRecord {
CustomTlvRecord { type_num: tlv.0, value: tlv.1.clone() }
}
}

pub(crate) type ReplacedTransactionStore =
DataStore<ReplacedOnchainTransactionDetails, Arc<Logger>>;
Loading