Skip to content

Commit 195e666

Browse files
authored
Merge pull request #2964 from jbesraa/prune-stale-chanmonitor
Add `archive_fully_resolved_monitors` to `ChainMonitor`
2 parents ae4c35c + 4b55043 commit 195e666

File tree

6 files changed

+226
-0
lines changed

6 files changed

+226
-0
lines changed

fuzz/src/utils/test_persister.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ impl chainmonitor::Persist<TestChannelSigner> for TestPersister {
1717
fn update_persisted_channel(&self, _funding_txo: OutPoint, _update: Option<&channelmonitor::ChannelMonitorUpdate>, _data: &channelmonitor::ChannelMonitor<TestChannelSigner>, _update_id: MonitorUpdateId) -> chain::ChannelMonitorUpdateStatus {
1818
self.update_ret.lock().unwrap().clone()
1919
}
20+
21+
fn archive_persisted_channel(&self, _: OutPoint) {
22+
}
2023
}

lightning/src/chain/chainmonitor.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ pub trait Persist<ChannelSigner: WriteableEcdsaChannelSigner> {
194194
///
195195
/// [`Writeable::write`]: crate::util::ser::Writeable::write
196196
fn update_persisted_channel(&self, channel_funding_outpoint: OutPoint, update: Option<&ChannelMonitorUpdate>, data: &ChannelMonitor<ChannelSigner>, update_id: MonitorUpdateId) -> ChannelMonitorUpdateStatus;
197+
/// Prevents the channel monitor from being loaded on startup.
198+
///
199+
/// Archiving the data in a backup location (rather than deleting it fully) is useful for
200+
/// hedging against data loss in case of unexpected failure.
201+
fn archive_persisted_channel(&self, channel_funding_outpoint: OutPoint);
197202
}
198203

199204
struct MonitorHolder<ChannelSigner: WriteableEcdsaChannelSigner> {
@@ -656,6 +661,41 @@ where C::Target: chain::Filter,
656661
}
657662
}
658663
}
664+
665+
/// Archives fully resolved channel monitors by calling [`Persist::archive_persisted_channel`].
666+
///
667+
/// This is useful for pruning fully resolved monitors from the monitor set and primary
668+
/// storage so they are not kept in memory and reloaded on restart.
669+
///
670+
/// Should be called occasionally (once every handful of blocks or on startup).
671+
///
672+
/// Depending on the implementation of [`Persist::archive_persisted_channel`] the monitor
673+
/// data could be moved to an archive location or removed entirely.
674+
pub fn archive_fully_resolved_channel_monitors(&self) {
675+
let mut have_monitors_to_prune = false;
676+
for (_, monitor_holder) in self.monitors.read().unwrap().iter() {
677+
let logger = WithChannelMonitor::from(&self.logger, &monitor_holder.monitor);
678+
if monitor_holder.monitor.is_fully_resolved(&logger) {
679+
have_monitors_to_prune = true;
680+
}
681+
}
682+
if have_monitors_to_prune {
683+
let mut monitors = self.monitors.write().unwrap();
684+
monitors.retain(|funding_txo, monitor_holder| {
685+
let logger = WithChannelMonitor::from(&self.logger, &monitor_holder.monitor);
686+
if monitor_holder.monitor.is_fully_resolved(&logger) {
687+
log_info!(logger,
688+
"Archiving fully resolved ChannelMonitor for funding txo {}",
689+
funding_txo
690+
);
691+
self.persister.archive_persisted_channel(*funding_txo);
692+
false
693+
} else {
694+
true
695+
}
696+
});
697+
}
698+
}
659699
}
660700

661701
impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>

lightning/src/chain/channelmonitor.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,9 @@ pub(crate) struct ChannelMonitorImpl<Signer: WriteableEcdsaChannelSigner> {
935935
/// Ordering of tuple data: (their_per_commitment_point, feerate_per_kw, to_broadcaster_sats,
936936
/// to_countersignatory_sats)
937937
initial_counterparty_commitment_info: Option<(PublicKey, u32, u64, u64)>,
938+
939+
/// The first block height at which we had no remaining claimable balances.
940+
balances_empty_height: Option<u32>,
938941
}
939942

940943
/// Transaction outputs to watch for on-chain spends.
@@ -1145,6 +1148,7 @@ impl<Signer: WriteableEcdsaChannelSigner> Writeable for ChannelMonitorImpl<Signe
11451148
(15, self.counterparty_fulfilled_htlcs, required),
11461149
(17, self.initial_counterparty_commitment_info, option),
11471150
(19, self.channel_id, required),
1151+
(21, self.balances_empty_height, option),
11481152
});
11491153

11501154
Ok(())
@@ -1328,6 +1332,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
13281332
best_block,
13291333
counterparty_node_id: Some(counterparty_node_id),
13301334
initial_counterparty_commitment_info: None,
1335+
balances_empty_height: None,
13311336
})
13321337
}
13331338

@@ -1856,6 +1861,52 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
18561861
spendable_outputs
18571862
}
18581863

1864+
/// Checks if the monitor is fully resolved. Resolved monitor is one that has claimed all of
1865+
/// its outputs and balances (i.e. [`Self::get_claimable_balances`] returns an empty set).
1866+
///
1867+
/// This function returns true only if [`Self::get_claimable_balances`] has been empty for at least
1868+
/// 2016 blocks as an additional protection against any bugs resulting in spuriously empty balance sets.
1869+
pub fn is_fully_resolved<L: Logger>(&self, logger: &L) -> bool {
1870+
let mut is_all_funds_claimed = self.get_claimable_balances().is_empty();
1871+
let current_height = self.current_best_block().height;
1872+
let mut inner = self.inner.lock().unwrap();
1873+
1874+
if is_all_funds_claimed {
1875+
if !inner.funding_spend_seen {
1876+
debug_assert!(false, "We should see funding spend by the time a monitor clears out");
1877+
is_all_funds_claimed = false;
1878+
}
1879+
}
1880+
1881+
match (inner.balances_empty_height, is_all_funds_claimed) {
1882+
(Some(balances_empty_height), true) => {
1883+
// Claimed all funds, check if reached the blocks threshold.
1884+
const BLOCKS_THRESHOLD: u32 = 4032; // ~four weeks
1885+
return current_height >= balances_empty_height + BLOCKS_THRESHOLD;
1886+
},
1887+
(Some(_), false) => {
1888+
// previously assumed we claimed all funds, but we have new funds to claim.
1889+
// Should not happen in practice.
1890+
debug_assert!(false, "Thought we were done claiming funds, but claimable_balances now has entries");
1891+
log_error!(logger,
1892+
"WARNING: LDK thought it was done claiming all the available funds in the ChannelMonitor for channel {}, but later decided it had more to claim. This is potentially an important bug in LDK, please report it at https://github.com/lightningdevkit/rust-lightning/issues/new",
1893+
inner.get_funding_txo().0);
1894+
inner.balances_empty_height = None;
1895+
false
1896+
},
1897+
(None, true) => {
1898+
// Claimed all funds but `balances_empty_height` is None. It is set to the
1899+
// current block height.
1900+
inner.balances_empty_height = Some(current_height);
1901+
false
1902+
},
1903+
(None, false) => {
1904+
// Have funds to claim.
1905+
false
1906+
},
1907+
}
1908+
}
1909+
18591910
#[cfg(test)]
18601911
pub fn get_counterparty_payment_script(&self) -> ScriptBuf {
18611912
self.inner.lock().unwrap().counterparty_payment_script.clone()
@@ -4632,6 +4683,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
46324683
let mut spendable_txids_confirmed = Some(Vec::new());
46334684
let mut counterparty_fulfilled_htlcs = Some(new_hash_map());
46344685
let mut initial_counterparty_commitment_info = None;
4686+
let mut balances_empty_height = None;
46354687
let mut channel_id = None;
46364688
read_tlv_fields!(reader, {
46374689
(1, funding_spend_confirmed, option),
@@ -4644,6 +4696,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
46444696
(15, counterparty_fulfilled_htlcs, option),
46454697
(17, initial_counterparty_commitment_info, option),
46464698
(19, channel_id, option),
4699+
(21, balances_empty_height, option),
46474700
});
46484701

46494702
// `HolderForceClosedWithInfo` replaced `HolderForceClosed` in v0.0.122. If we have both
@@ -4722,6 +4775,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
47224775
best_block,
47234776
counterparty_node_id,
47244777
initial_counterparty_commitment_info,
4778+
balances_empty_height,
47254779
})))
47264780
}
47274781
}

lightning/src/ln/monitor_tests.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,60 @@ fn revoked_output_htlc_resolution_timing() {
158158
expect_payment_failed!(nodes[1], payment_hash_1, false);
159159
}
160160

161+
#[test]
162+
fn archive_fully_resolved_monitors() {
163+
// Test we can archive fully resolved channel monitor.
164+
let chanmon_cfgs = create_chanmon_cfgs(2);
165+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
166+
let mut user_config = test_default_channel_config();
167+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]);
168+
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
169+
170+
let (_, _, chan_id, funding_tx) =
171+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 1_000_000);
172+
173+
nodes[0].node.close_channel(&chan_id, &nodes[1].node.get_our_node_id()).unwrap();
174+
let node_0_shutdown = get_event_msg!(nodes[0], MessageSendEvent::SendShutdown, nodes[1].node.get_our_node_id());
175+
nodes[1].node.handle_shutdown(&nodes[0].node.get_our_node_id(), &node_0_shutdown);
176+
let node_1_shutdown = get_event_msg!(nodes[1], MessageSendEvent::SendShutdown, nodes[0].node.get_our_node_id());
177+
nodes[0].node.handle_shutdown(&nodes[1].node.get_our_node_id(), &node_1_shutdown);
178+
179+
let node_0_closing_signed = get_event_msg!(nodes[0], MessageSendEvent::SendClosingSigned, nodes[1].node.get_our_node_id());
180+
nodes[1].node.handle_closing_signed(&nodes[0].node.get_our_node_id(), &node_0_closing_signed);
181+
let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, nodes[0].node.get_our_node_id());
182+
nodes[0].node.handle_closing_signed(&nodes[1].node.get_our_node_id(), &node_1_closing_signed);
183+
let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, nodes[1].node.get_our_node_id());
184+
nodes[1].node.handle_closing_signed(&nodes[0].node.get_our_node_id(), &node_0_2nd_closing_signed.unwrap());
185+
let (_, _) = get_closing_signed_broadcast!(nodes[1].node, nodes[0].node.get_our_node_id());
186+
187+
let shutdown_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0);
188+
189+
mine_transaction(&nodes[0], &shutdown_tx[0]);
190+
mine_transaction(&nodes[1], &shutdown_tx[0]);
191+
192+
connect_blocks(&nodes[0], 6);
193+
connect_blocks(&nodes[1], 6);
194+
195+
check_closed_event!(nodes[0], 1, ClosureReason::LocallyInitiatedCooperativeClosure, [nodes[1].node.get_our_node_id()], 1000000);
196+
check_closed_event!(nodes[1], 1, ClosureReason::CounterpartyInitiatedCooperativeClosure, [nodes[0].node.get_our_node_id()], 1000000);
197+
198+
assert_eq!(nodes[0].chain_monitor.chain_monitor.list_monitors().len(), 1);
199+
// First archive should set balances_empty_height to current block height
200+
nodes[0].chain_monitor.chain_monitor.archive_fully_resolved_channel_monitors();
201+
assert_eq!(nodes[0].chain_monitor.chain_monitor.list_monitors().len(), 1);
202+
connect_blocks(&nodes[0], 4032);
203+
// Second call after 4032 blocks, should archive the monitor
204+
nodes[0].chain_monitor.chain_monitor.archive_fully_resolved_channel_monitors();
205+
// Should have no monitors left
206+
assert_eq!(nodes[0].chain_monitor.chain_monitor.list_monitors().len(), 0);
207+
// Remove the corresponding outputs and transactions the chain source is
208+
// watching. This is to make sure the `Drop` function assertions pass.
209+
nodes.get_mut(0).unwrap().chain_source.remove_watched_txn_and_outputs(
210+
OutPoint { txid: funding_tx.txid(), index: 0 },
211+
funding_tx.output[0].script_pubkey.clone()
212+
);
213+
}
214+
161215
fn do_chanmon_claim_value_coop_close(anchors: bool) {
162216
// Tests `get_claimable_balances` returns the correct values across a simple cooperative claim.
163217
// Specifically, this tests that the channel non-HTLC balances show up in

lightning/src/util/persist.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ pub const CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
5656
/// The primary namespace under which [`ChannelMonitorUpdate`]s will be persisted.
5757
pub const CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE: &str = "monitor_updates";
5858

59+
/// The primary namespace under which archived [`ChannelMonitor`]s will be persisted.
60+
pub const ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE: &str = "archived_monitors";
61+
/// The secondary namespace under which archived [`ChannelMonitor`]s will be persisted.
62+
pub const ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE: &str = "";
63+
5964
/// The primary namespace under which the [`NetworkGraph`] will be persisted.
6065
pub const NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE: &str = "";
6166
/// The secondary namespace under which the [`NetworkGraph`] will be persisted.
@@ -226,6 +231,33 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner, K: KVStore + ?Sized> Persist<Ch
226231
Err(_) => chain::ChannelMonitorUpdateStatus::UnrecoverableError
227232
}
228233
}
234+
235+
fn archive_persisted_channel(&self, funding_txo: OutPoint) {
236+
let monitor_name = MonitorName::from(funding_txo);
237+
let monitor = match self.read(
238+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
239+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
240+
monitor_name.as_str(),
241+
) {
242+
Ok(monitor) => monitor,
243+
Err(_) => return
244+
};
245+
match self.write(
246+
ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
247+
ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
248+
monitor_name.as_str(),
249+
&monitor,
250+
) {
251+
Ok(()) => {}
252+
Err(_e) => return
253+
};
254+
let _ = self.remove(
255+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
256+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
257+
monitor_name.as_str(),
258+
true,
259+
);
260+
}
229261
}
230262

231263
/// Read previously persisted [`ChannelMonitor`]s from the store.
@@ -732,6 +764,29 @@ where
732764
self.persist_new_channel(funding_txo, monitor, monitor_update_call_id)
733765
}
734766
}
767+
768+
fn archive_persisted_channel(&self, funding_txo: OutPoint) {
769+
let monitor_name = MonitorName::from(funding_txo);
770+
let monitor = match self.read_monitor(&monitor_name) {
771+
Ok((_block_hash, monitor)) => monitor,
772+
Err(_) => return
773+
};
774+
match self.kv_store.write(
775+
ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
776+
ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
777+
monitor_name.as_str(),
778+
&monitor.encode()
779+
) {
780+
Ok(()) => {},
781+
Err(_e) => return,
782+
};
783+
let _ = self.kv_store.remove(
784+
CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE,
785+
CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE,
786+
monitor_name.as_str(),
787+
true,
788+
);
789+
}
735790
}
736791

737792
impl<K: Deref, L: Deref, ES: Deref, SP: Deref> MonitorUpdatingPersister<K, L, ES, SP>

lightning/src/util/test_utils.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,10 @@ impl<Signer: sign::ecdsa::WriteableEcdsaChannelSigner> chainmonitor::Persist<Sig
504504
}
505505
res
506506
}
507+
508+
fn archive_persisted_channel(&self, funding_txo: OutPoint) {
509+
<TestPersister as chainmonitor::Persist<TestChannelSigner>>::archive_persisted_channel(&self.persister, funding_txo);
510+
}
507511
}
508512

509513
pub struct TestPersister {
@@ -552,6 +556,18 @@ impl<Signer: sign::ecdsa::WriteableEcdsaChannelSigner> chainmonitor::Persist<Sig
552556
}
553557
ret
554558
}
559+
560+
fn archive_persisted_channel(&self, funding_txo: OutPoint) {
561+
// remove the channel from the offchain_monitor_updates map
562+
match self.offchain_monitor_updates.lock().unwrap().remove(&funding_txo) {
563+
Some(_) => {},
564+
None => {
565+
// If the channel was not in the offchain_monitor_updates map, it should be in the
566+
// chain_sync_monitor_persistences map.
567+
assert!(self.chain_sync_monitor_persistences.lock().unwrap().remove(&funding_txo).is_some());
568+
}
569+
};
570+
}
555571
}
556572

557573
pub struct TestStore {
@@ -1366,6 +1382,10 @@ impl TestChainSource {
13661382
watched_outputs: Mutex::new(new_hash_set()),
13671383
}
13681384
}
1385+
pub fn remove_watched_txn_and_outputs(&self, outpoint: OutPoint, script_pubkey: ScriptBuf) {
1386+
self.watched_outputs.lock().unwrap().remove(&(outpoint, script_pubkey.clone()));
1387+
self.watched_txn.lock().unwrap().remove(&(outpoint.txid, script_pubkey));
1388+
}
13691389
}
13701390

13711391
impl UtxoLookup for TestChainSource {

0 commit comments

Comments
 (0)