Skip to content

Commit 5419149

Browse files
committed
Monitor v0.7.0 serialization downgrade compatibility
Add a downgrade canary that writes current node state through the legacy v1 filesystem store and reopens it with ldk-node v0.7.0. This monitors whether serialized node, channel, and payment state remains usable by v0.7.0, including a restored channel and a post-restart payment. This does not assert that the current filesystem-store v2 IO layout can downgrade to v0.7.0's v1 layout. That IO-layer downgrade is unsupported: v2 stores empty namespaces under [empty], which v1 readers do not look up. Co-Authored-By: HAL 9000
1 parent cff8e5b commit 5419149

1 file changed

Lines changed: 381 additions & 0 deletions

File tree

tests/upgrade_downgrade_tests.rs

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
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+
// This file is a downgrade monitoring canary for serialized LDK Node state, not a
9+
// compatibility test for the filesystem-store IO layout itself. The current
10+
// `build_with_fs_store` path writes filesystem-store v2 data, while LDK Node v0.7.0
11+
// reads filesystem-store v1 data. There is no supported v2-to-v1 IO-layer downgrade:
12+
// v2 stores empty namespaces under `[empty]`, which v1 readers do not look up.
13+
//
14+
// To keep monitoring whether the serialized node/channel/payment state remains
15+
// understandable by v0.7.0, these tests intentionally write current state through
16+
// the legacy v1 filesystem-store implementation via `build_with_store`, then
17+
// reopen it with v0.7.0's `build_with_fs_store`.
18+
19+
mod common;
20+
21+
use std::path::PathBuf;
22+
use std::time::Duration;
23+
24+
use bitcoin::secp256k1::PublicKey;
25+
use bitcoin::Amount;
26+
use common::{
27+
generate_blocks_and_wait, generate_listening_addresses, premine_and_distribute_funds,
28+
random_storage_path, setup_bitcoind_and_electrsd, wait_for_tx,
29+
};
30+
use ldk_node::config::{Config, EsploraSyncConfig};
31+
use ldk_node::entropy::NodeEntropy;
32+
use ldk_node::lightning::ln::msgs::SocketAddress as CurrentSocketAddress;
33+
use ldk_node::lightning_invoice::{
34+
Bolt11InvoiceDescription as CurrentBolt11InvoiceDescription, Description as CurrentDescription,
35+
};
36+
use lightning_persister::fs_store::v1::FilesystemStore;
37+
38+
#[cfg(feature = "uniffi")]
39+
type CurrentNode = std::sync::Arc<ldk_node::Node>;
40+
#[cfg(not(feature = "uniffi"))]
41+
type CurrentNode = ldk_node::Node;
42+
43+
const NODE_A_SEED_BYTES: [u8; 64] = [42; 64];
44+
const NODE_B_SEED_BYTES: [u8; 64] = [43; 64];
45+
const FUNDING_AMOUNT_SAT: u64 = 2_000_000;
46+
const CHANNEL_AMOUNT_SAT: u64 = 1_000_000;
47+
const PUSH_AMOUNT_MSAT: u64 = 500_000_000;
48+
const PRE_DOWNGRADE_PAYMENT_MSAT: u64 = 100_000;
49+
const POST_DOWNGRADE_PAYMENT_MSAT: u64 = 200_000;
50+
51+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
52+
async fn monitor_v0_7_0_serialization_downgrade_channel_payment() {
53+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
54+
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
55+
56+
let storage_path_a = random_storage_path().to_str().unwrap().to_owned();
57+
let storage_path_b = random_storage_path().to_str().unwrap().to_owned();
58+
let current_addresses_a = generate_listening_addresses();
59+
let current_addresses_b = generate_listening_addresses();
60+
let v070_addresses_a = to_v070_socket_addresses(&current_addresses_a);
61+
let v070_addresses_b = to_v070_socket_addresses(&current_addresses_b);
62+
63+
let node_id_a;
64+
let node_id_b;
65+
66+
{
67+
let node_a = build_current_node(
68+
storage_path_a.clone(),
69+
NODE_A_SEED_BYTES,
70+
current_addresses_a.clone(),
71+
"downgrade-a",
72+
&esplora_url,
73+
);
74+
let node_b = build_current_node(
75+
storage_path_b.clone(),
76+
NODE_B_SEED_BYTES,
77+
current_addresses_b.clone(),
78+
"downgrade-b",
79+
&esplora_url,
80+
);
81+
node_id_a = node_a.node_id();
82+
node_id_b = node_b.node_id();
83+
84+
let addr_a = node_a.onchain_payment().new_address().unwrap();
85+
let addr_b = node_b.onchain_payment().new_address().unwrap();
86+
premine_and_distribute_funds(
87+
&bitcoind.client,
88+
&electrsd.client,
89+
vec![addr_a, addr_b],
90+
Amount::from_sat(FUNDING_AMOUNT_SAT),
91+
)
92+
.await;
93+
node_a.sync_wallets().unwrap();
94+
node_b.sync_wallets().unwrap();
95+
assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, FUNDING_AMOUNT_SAT);
96+
assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, FUNDING_AMOUNT_SAT);
97+
98+
let funding_txo = open_current_channel(&node_a, &node_b).await;
99+
wait_for_tx(&electrsd.client, funding_txo.txid).await;
100+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
101+
node_a.sync_wallets().unwrap();
102+
node_b.sync_wallets().unwrap();
103+
expect_current_channel_ready(&node_a, node_id_b).await;
104+
expect_current_channel_ready(&node_b, node_id_a).await;
105+
assert_current_channel_ready(&node_a, node_id_b);
106+
assert_current_channel_ready(&node_b, node_id_a);
107+
108+
send_current_bolt11_payment(&node_a, &node_b, PRE_DOWNGRADE_PAYMENT_MSAT, "pre-downgrade")
109+
.await;
110+
111+
node_a.stop().unwrap();
112+
node_b.stop().unwrap();
113+
}
114+
115+
let node_a_v070 = build_v070_node(
116+
storage_path_a,
117+
NODE_A_SEED_BYTES,
118+
v070_addresses_a.clone(),
119+
"downgrade-a",
120+
&esplora_url,
121+
);
122+
let node_b_v070 = build_v070_node(
123+
storage_path_b,
124+
NODE_B_SEED_BYTES,
125+
v070_addresses_b.clone(),
126+
"downgrade-b",
127+
&esplora_url,
128+
);
129+
130+
assert_eq!(node_a_v070.node_id(), node_id_a);
131+
assert_eq!(node_b_v070.node_id(), node_id_b);
132+
133+
node_a_v070.sync_wallets().unwrap();
134+
node_b_v070.sync_wallets().unwrap();
135+
node_a_v070.connect(node_id_b, v070_addresses_b.first().unwrap().clone(), true).unwrap();
136+
wait_for_v070_usable_channel(&node_a_v070, node_id_b).await;
137+
wait_for_v070_usable_channel(&node_b_v070, node_id_a).await;
138+
drain_v070_events(&node_a_v070).await;
139+
drain_v070_events(&node_b_v070).await;
140+
141+
send_v070_bolt11_payment(
142+
&node_a_v070,
143+
&node_b_v070,
144+
POST_DOWNGRADE_PAYMENT_MSAT,
145+
"post-downgrade",
146+
)
147+
.await;
148+
149+
node_a_v070.stop().unwrap();
150+
node_b_v070.stop().unwrap();
151+
}
152+
153+
fn build_current_node(
154+
storage_path: String, seed_bytes: [u8; 64], listening_addresses: Vec<CurrentSocketAddress>,
155+
alias: &str, esplora_url: &str,
156+
) -> CurrentNode {
157+
let mut config = Config::default();
158+
config.network = bitcoin::Network::Regtest;
159+
config.storage_dir_path = storage_path;
160+
config.listening_addresses = Some(listening_addresses);
161+
config.anchor_channels_config = None;
162+
163+
// Use the v1 filesystem layout that v0.7.0's filesystem builder can reopen.
164+
let mut fs_store_path = PathBuf::from(&config.storage_dir_path);
165+
fs_store_path.push("fs_store");
166+
let mut builder = ldk_node::Builder::from_config(config);
167+
builder.set_node_alias(alias.to_string()).unwrap();
168+
169+
let mut sync_config = EsploraSyncConfig::default();
170+
sync_config.background_sync_config = None;
171+
builder.set_chain_source_esplora(esplora_url.to_owned(), Some(sync_config));
172+
173+
#[cfg(feature = "uniffi")]
174+
let node_entropy = std::sync::Arc::new(NodeEntropy::from_seed_bytes(seed_bytes.to_vec()).unwrap());
175+
#[cfg(not(feature = "uniffi"))]
176+
let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes);
177+
178+
let kv_store = FilesystemStore::new(fs_store_path);
179+
let node = builder.build_with_store(node_entropy.into(), kv_store).unwrap();
180+
node.start().unwrap();
181+
node
182+
}
183+
184+
fn build_v070_node(
185+
storage_path: String, seed_bytes: [u8; 64],
186+
listening_addresses: Vec<ldk_node_070::lightning::ln::msgs::SocketAddress>, alias: &str,
187+
esplora_url: &str,
188+
) -> ldk_node_070::Node {
189+
let mut builder = ldk_node_070::Builder::new();
190+
builder.set_network(bitcoin::Network::Regtest);
191+
builder.set_storage_dir_path(storage_path);
192+
builder.set_entropy_seed_bytes(seed_bytes);
193+
builder.set_listening_addresses(listening_addresses).unwrap();
194+
builder.set_node_alias(alias.to_string()).unwrap();
195+
builder.set_chain_source_esplora(esplora_url.to_owned(), None);
196+
let node = builder.build_with_fs_store().unwrap();
197+
node.start().unwrap();
198+
node
199+
}
200+
201+
async fn open_current_channel(node_a: &CurrentNode, node_b: &CurrentNode) -> bitcoin::OutPoint {
202+
node_a
203+
.open_channel(
204+
node_b.node_id(),
205+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
206+
CHANNEL_AMOUNT_SAT,
207+
Some(PUSH_AMOUNT_MSAT),
208+
None,
209+
)
210+
.unwrap();
211+
212+
let funding_txo_a = expect_current_channel_pending(node_a, node_b.node_id()).await;
213+
let funding_txo_b = expect_current_channel_pending(node_b, node_a.node_id()).await;
214+
assert_eq!(funding_txo_a, funding_txo_b);
215+
funding_txo_a
216+
}
217+
218+
async fn send_current_bolt11_payment(
219+
payer: &CurrentNode, payee: &CurrentNode, amount_msat: u64, description: &str,
220+
) {
221+
let invoice_description = CurrentBolt11InvoiceDescription::Direct(
222+
CurrentDescription::new(description.to_owned()).unwrap(),
223+
);
224+
let invoice = payee.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap();
225+
let payment_id = payer.bolt11_payment().send(&invoice, None).unwrap();
226+
expect_current_payment_successful(payer, &payment_id).await;
227+
expect_current_payment_received(payee, amount_msat).await;
228+
assert_eq!(
229+
payer.payment(&payment_id).unwrap().status,
230+
ldk_node::payment::PaymentStatus::Succeeded
231+
);
232+
}
233+
234+
async fn send_v070_bolt11_payment(
235+
payer: &ldk_node_070::Node, payee: &ldk_node_070::Node, amount_msat: u64, description: &str,
236+
) {
237+
let invoice_description = ldk_node_070::lightning_invoice::Bolt11InvoiceDescription::Direct(
238+
ldk_node_070::lightning_invoice::Description::new(description.to_owned()).unwrap(),
239+
);
240+
let invoice = payee.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap();
241+
let payment_id = payer.bolt11_payment().send(&invoice, None).unwrap();
242+
expect_v070_payment_successful(payer, &payment_id).await;
243+
expect_v070_payment_received(payee, amount_msat).await;
244+
assert_eq!(
245+
payer.payment(&payment_id).unwrap().status,
246+
ldk_node_070::payment::PaymentStatus::Succeeded
247+
);
248+
}
249+
250+
async fn expect_current_channel_pending(
251+
node: &CurrentNode, expected_counterparty: PublicKey,
252+
) -> bitcoin::OutPoint {
253+
match next_current_event(node).await {
254+
ldk_node::Event::ChannelPending { counterparty_node_id, funding_txo, .. } => {
255+
assert_eq!(counterparty_node_id, expected_counterparty);
256+
node.event_handled().unwrap();
257+
funding_txo
258+
},
259+
event => panic!("{} got unexpected event: {:?}", node.node_id(), event),
260+
}
261+
}
262+
263+
async fn expect_current_channel_ready(node: &CurrentNode, expected_counterparty: PublicKey) {
264+
match next_current_event(node).await {
265+
ldk_node::Event::ChannelReady { counterparty_node_id, .. } => {
266+
assert_eq!(counterparty_node_id, Some(expected_counterparty));
267+
node.event_handled().unwrap();
268+
},
269+
event => panic!("{} got unexpected event: {:?}", node.node_id(), event),
270+
}
271+
}
272+
273+
async fn expect_current_payment_successful(
274+
node: &CurrentNode, expected_payment_id: &ldk_node::lightning::ln::channelmanager::PaymentId,
275+
) {
276+
match next_current_event(node).await {
277+
ldk_node::Event::PaymentSuccessful { payment_id, .. } => {
278+
assert_eq!(payment_id.as_ref(), Some(expected_payment_id));
279+
node.event_handled().unwrap();
280+
},
281+
event => panic!("{} got unexpected event: {:?}", node.node_id(), event),
282+
}
283+
}
284+
285+
async fn expect_current_payment_received(node: &CurrentNode, expected_amount_msat: u64) {
286+
match next_current_event(node).await {
287+
ldk_node::Event::PaymentReceived { amount_msat, payment_id, .. } => {
288+
assert_eq!(amount_msat, expected_amount_msat);
289+
assert!(payment_id.is_some());
290+
node.event_handled().unwrap();
291+
},
292+
event => panic!("{} got unexpected event: {:?}", node.node_id(), event),
293+
}
294+
}
295+
296+
async fn expect_v070_payment_successful(
297+
node: &ldk_node_070::Node,
298+
expected_payment_id: &ldk_node_070::lightning::ln::channelmanager::PaymentId,
299+
) {
300+
match next_v070_event(node).await {
301+
ldk_node_070::Event::PaymentSuccessful { payment_id, .. } => {
302+
assert_eq!(payment_id.as_ref(), Some(expected_payment_id));
303+
node.event_handled().unwrap();
304+
},
305+
event => panic!("{} got unexpected event: {:?}", node.node_id(), event),
306+
}
307+
}
308+
309+
async fn expect_v070_payment_received(node: &ldk_node_070::Node, expected_amount_msat: u64) {
310+
match next_v070_event(node).await {
311+
ldk_node_070::Event::PaymentReceived { amount_msat, payment_id, .. } => {
312+
assert_eq!(amount_msat, expected_amount_msat);
313+
assert!(payment_id.is_some());
314+
node.event_handled().unwrap();
315+
},
316+
event => panic!("{} got unexpected event: {:?}", node.node_id(), event),
317+
}
318+
}
319+
320+
async fn next_current_event(node: &CurrentNode) -> ldk_node::Event {
321+
tokio::time::timeout(Duration::from_secs(common::INTEROP_TIMEOUT_SECS), node.next_event_async())
322+
.await
323+
.unwrap_or_else(|_| panic!("{} timed out waiting for event", node.node_id()))
324+
}
325+
326+
async fn next_v070_event(node: &ldk_node_070::Node) -> ldk_node_070::Event {
327+
tokio::time::timeout(Duration::from_secs(common::INTEROP_TIMEOUT_SECS), node.next_event_async())
328+
.await
329+
.unwrap_or_else(|_| panic!("{} timed out waiting for event", node.node_id()))
330+
}
331+
332+
async fn drain_v070_events(node: &ldk_node_070::Node) {
333+
while tokio::time::timeout(Duration::from_millis(250), node.next_event_async()).await.is_ok() {
334+
node.event_handled().unwrap();
335+
}
336+
}
337+
338+
async fn wait_for_v070_usable_channel(node: &ldk_node_070::Node, counterparty_node_id: PublicKey) {
339+
for _ in 0..40 {
340+
let channels = node.list_channels();
341+
if let Some(channel) =
342+
channels.iter().find(|c| c.counterparty_node_id == counterparty_node_id)
343+
{
344+
assert_eq!(channel.channel_value_sats, CHANNEL_AMOUNT_SAT);
345+
if channel.is_channel_ready && channel.is_usable {
346+
return;
347+
}
348+
}
349+
tokio::time::sleep(Duration::from_millis(250)).await;
350+
}
351+
352+
panic!(
353+
"{} failed to restore a usable v0.7.0 channel with {}",
354+
node.node_id(),
355+
counterparty_node_id
356+
);
357+
}
358+
359+
fn assert_current_channel_ready(node: &CurrentNode, counterparty_node_id: PublicKey) {
360+
let channels = node.list_channels();
361+
let channel = channels.iter().find(|c| c.counterparty_node_id == counterparty_node_id).unwrap();
362+
assert_eq!(channel.channel_value_sats, CHANNEL_AMOUNT_SAT);
363+
assert!(channel.is_channel_ready);
364+
}
365+
366+
fn to_v070_socket_addresses(
367+
addresses: &[CurrentSocketAddress],
368+
) -> Vec<ldk_node_070::lightning::ln::msgs::SocketAddress> {
369+
addresses
370+
.iter()
371+
.map(|address| match address {
372+
CurrentSocketAddress::TcpIpV4 { addr, port } => {
373+
ldk_node_070::lightning::ln::msgs::SocketAddress::TcpIpV4 {
374+
addr: *addr,
375+
port: *port,
376+
}
377+
},
378+
_ => panic!("unexpected non-IPv4 test address: {:?}", address),
379+
})
380+
.collect()
381+
}

0 commit comments

Comments
 (0)