Skip to content

Commit 3332e4e

Browse files
committed
Add RBF replacement tracking with new persisted lookup table
Introduce a new lookup `ReplacedTransactionStore` that maps old/replaced transaction IDs to their current replacement transaction IDs, enabling reliable tracking of replaced transactions throughout the replacement chain. Key changes: - Add persisted storage for RBF replacement relationships - Link transactions in replacement trees using payment IDs - Remove entire replacement chains from persistence when any transaction in the tree is confirmed
1 parent a2c8a55 commit 3332e4e

File tree

7 files changed

+303
-48
lines changed

7 files changed

+303
-48
lines changed

src/builder.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ use crate::io::utils::{
5858
use crate::io::vss_store::VssStore;
5959
use crate::io::{
6060
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
61+
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE, REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE,
6162
};
6263
use crate::liquidity::{
6364
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
@@ -70,7 +71,7 @@ use crate::runtime::Runtime;
7071
use crate::tx_broadcaster::TransactionBroadcaster;
7172
use crate::types::{
7273
ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter,
73-
OnionMessenger, PaymentStore, PeerManager, Persister,
74+
OnionMessenger, PaymentStore, PeerManager, Persister, ReplacedTransactionStore,
7475
};
7576
use crate::wallet::persist::KVStoreWalletPersister;
7677
use crate::wallet::Wallet;
@@ -1239,6 +1240,21 @@ fn build_with_store_internal(
12391240
},
12401241
};
12411242

1243+
let replaced_tx_store =
1244+
match io::utils::read_replaced_txs(Arc::clone(&kv_store), Arc::clone(&logger)) {
1245+
Ok(replaced_txs) => Arc::new(ReplacedTransactionStore::new(
1246+
replaced_txs,
1247+
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE.to_string(),
1248+
REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE.to_string(),
1249+
Arc::clone(&kv_store),
1250+
Arc::clone(&logger),
1251+
)),
1252+
Err(e) => {
1253+
log_error!(logger, "Failed to read replaced transaction data from store: {}", e);
1254+
return Err(BuildError::ReadFailed);
1255+
},
1256+
};
1257+
12421258
let wallet = Arc::new(Wallet::new(
12431259
bdk_wallet,
12441260
wallet_persister,
@@ -1247,6 +1263,7 @@ fn build_with_store_internal(
12471263
Arc::clone(&payment_store),
12481264
Arc::clone(&config),
12491265
Arc::clone(&logger),
1266+
Arc::clone(&replaced_tx_store),
12501267
));
12511268

12521269
let chain_source = match chain_data_source_config {

src/io/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,8 @@ pub(crate) const BDK_WALLET_INDEXER_KEY: &str = "indexer";
7878
///
7979
/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
8080
pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices";
81+
82+
83+
/// The replaced transaction information will be persisted under this prefix.
84+
pub(crate) const REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE: &str = "replaced_txs";
85+
pub(crate) const REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";

src/io/utils.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use crate::io::{
4545
NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE,
4646
};
4747
use crate::logger::{log_error, LdkLogger, Logger};
48+
use crate::payment::ReplacedOnchainTransactionDetails;
4849
use crate::peer_store::PeerStore;
4950
use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper};
5051
use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper};
@@ -617,6 +618,38 @@ pub(crate) fn read_bdk_wallet_change_set(
617618
Ok(Some(change_set))
618619
}
619620

621+
/// Read previously persisted replaced transaction information from the store.
622+
pub(crate) fn read_replaced_txs<L: Deref>(
623+
kv_store: Arc<DynStore>, logger: L,
624+
) -> Result<Vec<ReplacedOnchainTransactionDetails>, std::io::Error>
625+
where
626+
L::Target: LdkLogger,
627+
{
628+
let mut res = Vec::new();
629+
630+
for stored_key in KVStoreSync::list(
631+
&*kv_store,
632+
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE,
633+
REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE,
634+
)? {
635+
let mut reader = Cursor::new(KVStoreSync::read(
636+
&*kv_store,
637+
REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE,
638+
REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE,
639+
&stored_key,
640+
)?);
641+
let payment = ReplacedOnchainTransactionDetails::read(&mut reader).map_err(|e| {
642+
log_error!(logger, "Failed to deserialize ReplacedOnchainTransactionDetails: {}", e);
643+
std::io::Error::new(
644+
std::io::ErrorKind::InvalidData,
645+
"Failed to deserialize ReplacedOnchainTransactionDetails",
646+
)
647+
})?;
648+
res.push(payment);
649+
}
650+
Ok(res)
651+
}
652+
620653
#[cfg(test)]
621654
mod tests {
622655
use super::*;

src/payment/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ pub(crate) mod asynchronous;
1111
mod bolt11;
1212
mod bolt12;
1313
mod onchain;
14+
mod replaced_transaction_store;
1415
mod spontaneous;
1516
pub(crate) mod store;
1617
mod unified_qr;
1718

1819
pub use bolt11::Bolt11Payment;
1920
pub use bolt12::Bolt12Payment;
2021
pub use onchain::OnchainPayment;
22+
pub use replaced_transaction_store::ReplacedOnchainTransactionDetails;
2123
pub use spontaneous::SpontaneousPayment;
2224
pub use store::{
2325
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
9+
10+
use bitcoin::Txid;
11+
use lightning::ln::channelmanager::PaymentId;
12+
use lightning::ln::msgs::DecodeError;
13+
use lightning::util::ser::{Readable, Writeable};
14+
use lightning::{_init_and_read_len_prefixed_tlv_fields, write_tlv_fields};
15+
16+
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
17+
use crate::payment::ConfirmationStatus;
18+
19+
/// Details of an on-chain transaction that has replaced a previous transaction (e.g., via RBF).
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
pub struct ReplacedOnchainTransactionDetails {
22+
/// The new transaction ID.
23+
pub new_txid: Txid,
24+
/// The original transaction ID that was replaced.
25+
pub original_txid: Txid,
26+
/// The payment ID associated with the transaction.
27+
pub payment_id: PaymentId,
28+
/// The timestamp, in seconds since start of the UNIX epoch, when this entry was last updated.
29+
pub latest_update_timestamp: u64,
30+
/// The confirmation status of this payment.
31+
pub status: ConfirmationStatus,
32+
}
33+
34+
impl ReplacedOnchainTransactionDetails {
35+
pub(crate) fn new(
36+
new_txid: Txid, original_txid: Txid, payment_id: PaymentId, status: ConfirmationStatus,
37+
) -> Self {
38+
let latest_update_timestamp = SystemTime::now()
39+
.duration_since(UNIX_EPOCH)
40+
.unwrap_or(Duration::from_secs(0))
41+
.as_secs();
42+
Self { new_txid, original_txid, payment_id, latest_update_timestamp, status }
43+
}
44+
}
45+
46+
impl Writeable for ReplacedOnchainTransactionDetails {
47+
fn write<W: lightning::util::ser::Writer>(
48+
&self, writer: &mut W,
49+
) -> Result<(), lightning::io::Error> {
50+
write_tlv_fields!(writer, {
51+
(0, self.new_txid, required),
52+
(2, self.original_txid, required),
53+
(4, self.payment_id, required),
54+
(6, self.latest_update_timestamp, required),
55+
(8, self.status, required),
56+
});
57+
Ok(())
58+
}
59+
}
60+
61+
impl Readable for ReplacedOnchainTransactionDetails {
62+
fn read<R: lightning::io::Read>(
63+
reader: &mut R,
64+
) -> Result<ReplacedOnchainTransactionDetails, DecodeError> {
65+
let unix_time_secs = SystemTime::now()
66+
.duration_since(UNIX_EPOCH)
67+
.unwrap_or(Duration::from_secs(0))
68+
.as_secs();
69+
_init_and_read_len_prefixed_tlv_fields!(reader, {
70+
(0, new_txid, required),
71+
(2, original_txid, required),
72+
(4, payment_id, required),
73+
(6, latest_update_timestamp, (default_value, unix_time_secs)),
74+
(8, status, required)
75+
});
76+
77+
let new_txid: Txid = new_txid.0.ok_or(DecodeError::InvalidValue)?;
78+
let original_txid: Txid = original_txid.0.ok_or(DecodeError::InvalidValue)?;
79+
let payment_id: PaymentId = payment_id.0.ok_or(DecodeError::InvalidValue)?;
80+
let latest_update_timestamp: u64 =
81+
latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?;
82+
let status: ConfirmationStatus = status.0.ok_or(DecodeError::InvalidValue)?;
83+
84+
Ok(ReplacedOnchainTransactionDetails {
85+
new_txid,
86+
original_txid,
87+
payment_id,
88+
latest_update_timestamp,
89+
status,
90+
})
91+
}
92+
}
93+
94+
impl StorableObjectId for Txid {
95+
fn encode_to_hex_str(&self) -> String {
96+
self.to_string()
97+
}
98+
}
99+
impl StorableObject for ReplacedOnchainTransactionDetails {
100+
type Id = Txid;
101+
type Update = ReplacedOnchainTransactionDetailsUpdate;
102+
103+
fn id(&self) -> Self::Id {
104+
self.new_txid
105+
}
106+
107+
fn update(&mut self, update: &Self::Update) -> bool {
108+
debug_assert_eq!(
109+
self.new_txid, update.id,
110+
"We should only ever override data for the same replacement transaction id"
111+
);
112+
113+
let mut updated = false;
114+
115+
macro_rules! update_if_necessary {
116+
($val:expr, $update:expr) => {
117+
if $val != $update {
118+
$val = $update;
119+
updated = true;
120+
}
121+
};
122+
}
123+
124+
if let Some(status) = update.confirmation_status {
125+
update_if_necessary!(self.status, status);
126+
}
127+
128+
if updated {
129+
self.latest_update_timestamp = SystemTime::now()
130+
.duration_since(UNIX_EPOCH)
131+
.unwrap_or(Duration::from_secs(0))
132+
.as_secs();
133+
}
134+
135+
updated
136+
}
137+
138+
fn to_update(&self) -> Self::Update {
139+
self.into()
140+
}
141+
}
142+
143+
#[derive(Clone, Debug, PartialEq, Eq)]
144+
pub(crate) struct ReplacedOnchainTransactionDetailsUpdate {
145+
pub id: Txid,
146+
pub confirmation_status: Option<ConfirmationStatus>,
147+
}
148+
149+
impl From<&ReplacedOnchainTransactionDetails> for ReplacedOnchainTransactionDetailsUpdate {
150+
fn from(value: &ReplacedOnchainTransactionDetails) -> Self {
151+
Self { id: value.new_txid, confirmation_status: Some(value.status) }
152+
}
153+
}
154+
155+
impl StorableObjectUpdate<ReplacedOnchainTransactionDetails>
156+
for ReplacedOnchainTransactionDetailsUpdate
157+
{
158+
fn id(&self) -> <ReplacedOnchainTransactionDetails as StorableObject>::Id {
159+
self.id
160+
}
161+
}

src/types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::fee_estimator::OnchainFeeEstimator;
3434
use crate::gossip::RuntimeSpawner;
3535
use crate::logger::Logger;
3636
use crate::message_handler::NodeCustomMessageHandler;
37-
use crate::payment::PaymentDetails;
37+
use crate::payment::{PaymentDetails, ReplacedOnchainTransactionDetails};
3838

3939
/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the
4040
/// same time.
@@ -437,3 +437,5 @@ impl From<&(u64, Vec<u8>)> for CustomTlvRecord {
437437
CustomTlvRecord { type_num: tlv.0, value: tlv.1.clone() }
438438
}
439439
}
440+
441+
pub(crate) type ReplacedTransactionStore = DataStore<ReplacedOnchainTransactionDetails, Arc<Logger>>;

0 commit comments

Comments
 (0)