Skip to content

Commit 01cae25

Browse files
committed
feat: add watchtower querier trait and mock implementation for testing
1 parent 7d1336b commit 01cae25

File tree

4 files changed

+262
-1
lines changed

4 files changed

+262
-1
lines changed

crates/fiber-lib/src/fiber/tests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ mod tlc_op;
2020
mod trampoline;
2121
mod types;
2222
mod utils;
23+
#[cfg(not(target_arch = "wasm32"))]
24+
mod watchtower_query;
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
use crate::fiber::payment::SendPaymentCommand;
2+
use crate::fiber::types::Hash256;
3+
use crate::fiber::watchtower_query::{TlcWatchtowerStatus, WatchtowerQuerier};
4+
use crate::gen_rand_sha256_hash;
5+
use crate::invoice::{Currency, InvoiceBuilder};
6+
use crate::test_utils::init_tracing;
7+
use crate::tests::test_utils::*;
8+
use std::collections::HashMap;
9+
use std::sync::{Arc, Mutex};
10+
11+
/// A mock watchtower querier that returns predefined TLC statuses.
12+
pub struct MockWatchtowerQuerier {
13+
statuses: Mutex<HashMap<(Hash256, Hash256), TlcWatchtowerStatus>>,
14+
}
15+
16+
impl MockWatchtowerQuerier {
17+
pub fn new() -> Self {
18+
Self {
19+
statuses: Mutex::new(HashMap::new()),
20+
}
21+
}
22+
23+
/// Set the status that will be returned for a given (channel_id, payment_hash) query.
24+
pub fn set_status(
25+
&self,
26+
channel_id: Hash256,
27+
payment_hash: Hash256,
28+
status: TlcWatchtowerStatus,
29+
) {
30+
self.statuses
31+
.lock()
32+
.unwrap()
33+
.insert((channel_id, payment_hash), status);
34+
}
35+
}
36+
37+
impl std::fmt::Debug for MockWatchtowerQuerier {
38+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39+
f.debug_struct("MockWatchtowerQuerier").finish()
40+
}
41+
}
42+
43+
#[async_trait::async_trait]
44+
impl WatchtowerQuerier for MockWatchtowerQuerier {
45+
async fn query_tlc_status(
46+
&self,
47+
channel_id: &Hash256,
48+
payment_hash: &Hash256,
49+
) -> Option<TlcWatchtowerStatus> {
50+
self.statuses
51+
.lock()
52+
.unwrap()
53+
.get(&(*channel_id, *payment_hash))
54+
.cloned()
55+
}
56+
}
57+
58+
/// Test: a->b payment where the preimage is only available via watchtower querier.
59+
///
60+
/// Scenario:
61+
/// 1. node_b has a mock watchtower querier.
62+
/// 2. node_b creates an invoice WITHOUT storing the preimage in its normal store.
63+
/// 3. node_a sends payment to node_b using the invoice.
64+
/// 4. node_b's CheckChannels picks up the preimage from the watchtower querier
65+
/// and settles the TLC with RemoveTlcFulfill.
66+
/// 5. node_a's payment should succeed.
67+
#[tokio::test]
68+
async fn test_payment_success_via_watchtower_preimage_direct() {
69+
init_tracing();
70+
71+
let watchtower_querier = Arc::new(MockWatchtowerQuerier::new());
72+
73+
// Create node_b with the mock watchtower querier
74+
let config_b = NetworkNodeConfigBuilder::new()
75+
.node_name(Some("node-b".to_string()))
76+
.base_dir_prefix("test-wt-direct-b-")
77+
.watchtower_querier(Some(watchtower_querier.clone()))
78+
.build();
79+
let mut node_b = NetworkNode::new_with_config(config_b).await;
80+
81+
// Create node_a normally
82+
let config_a = NetworkNodeConfigBuilder::new()
83+
.node_name(Some("node-a".to_string()))
84+
.base_dir_prefix("test-wt-direct-a-")
85+
.build();
86+
let mut node_a = NetworkNode::new_with_config(config_a).await;
87+
88+
// Connect and establish channel
89+
node_a.connect_to(&mut node_b).await;
90+
let (channel_id, _funding_tx_hash) = establish_channel_between_nodes(
91+
&mut node_a,
92+
&mut node_b,
93+
ChannelParameters::new(HUGE_CKB_AMOUNT, HUGE_CKB_AMOUNT),
94+
)
95+
.await;
96+
97+
// Generate a preimage and build an invoice on node_b
98+
let preimage = gen_rand_sha256_hash();
99+
let invoice = InvoiceBuilder::new(Currency::Fibd)
100+
.amount(Some(1000000000))
101+
.payment_preimage(preimage)
102+
.payee_pub_key(node_b.pubkey.into())
103+
.build()
104+
.expect("build invoice");
105+
let payment_hash = *invoice.payment_hash();
106+
107+
// Insert the invoice WITHOUT storing the preimage in the normal preimage store.
108+
// This simulates the scenario where the preimage is only known to the watchtower
109+
// (e.g., the node crashed and lost the preimage, but the watchtower observed it onchain).
110+
node_b.insert_invoice(invoice.clone(), None);
111+
112+
// Configure the watchtower to return the preimage when queried for this channel + payment_hash.
113+
watchtower_querier.set_status(
114+
channel_id,
115+
payment_hash,
116+
TlcWatchtowerStatus {
117+
preimage: Some(preimage),
118+
is_settled: true,
119+
},
120+
);
121+
122+
// Send payment from a to b using the invoice
123+
let res = node_a
124+
.send_payment(SendPaymentCommand {
125+
invoice: Some(invoice.to_string()),
126+
..Default::default()
127+
})
128+
.await
129+
.expect("send payment should succeed");
130+
131+
// The CheckChannels timer on node_b (every 3s in debug mode) should pick up the
132+
// preimage from the watchtower, settle the received TLC, and propagate back to node_a.
133+
node_a.wait_until_success(res.payment_hash).await;
134+
}
135+
136+
/// Test: a->b->c payment where c settles via watchtower (preimage available through
137+
/// the watchtower on node_b), the TLC offered by a to b should be settled offchain,
138+
/// and the payment should succeed.
139+
///
140+
/// Scenario:
141+
/// 1. node_b has a mock watchtower querier.
142+
/// 2. node_c creates an invoice WITHOUT storing the preimage.
143+
/// 3. node_a sends payment to node_c via node_b.
144+
/// 4. The TLC reaches node_c, but node_c cannot settle (no preimage in store).
145+
/// 5. node_b's watchtower querier returns the preimage (simulating watchtower
146+
/// observing c's onchain settlement on the b-c channel).
147+
/// 6. node_b's CheckChannels picks up the preimage for the received TLC on the
148+
/// a-b channel and sends RemoveTlcFulfill to node_a.
149+
/// 7. node_a's payment should succeed.
150+
#[tokio::test]
151+
async fn test_multi_hop_payment_success_via_watchtower_preimage() {
152+
init_tracing();
153+
154+
let watchtower_querier = Arc::new(MockWatchtowerQuerier::new());
155+
156+
// Create 3 nodes: a (normal), b (with watchtower querier), c (normal)
157+
let nodes = NetworkNode::new_n_interconnected_nodes_with_config(3, |i| {
158+
let mut builder = NetworkNodeConfigBuilder::new()
159+
.node_name(Some(format!("node-{}", i)))
160+
.base_dir_prefix(&format!("test-wt-multihop-{}-", i));
161+
if i == 1 {
162+
// node_b gets the watchtower querier
163+
builder = builder.watchtower_querier(Some(watchtower_querier.clone()));
164+
}
165+
builder.build()
166+
})
167+
.await;
168+
169+
let [mut node_a, mut node_b, mut node_c]: [NetworkNode; 3] = nodes.try_into().expect("3 nodes");
170+
171+
// Establish channels: a-b and b-c
172+
let (ab_channel_id, ab_funding_tx_hash) = establish_channel_between_nodes(
173+
&mut node_a,
174+
&mut node_b,
175+
ChannelParameters::new(HUGE_CKB_AMOUNT, HUGE_CKB_AMOUNT),
176+
)
177+
.await;
178+
179+
// Submit funding tx to all nodes for graph discovery
180+
let ab_funding_tx = node_a
181+
.get_transaction_view_from_hash(ab_funding_tx_hash)
182+
.await
183+
.expect("get funding tx");
184+
node_c.submit_tx(ab_funding_tx.clone()).await;
185+
node_c.add_channel_tx(ab_channel_id, ab_funding_tx_hash);
186+
187+
let (bc_channel_id, bc_funding_tx_hash) = establish_channel_between_nodes(
188+
&mut node_b,
189+
&mut node_c,
190+
ChannelParameters::new(HUGE_CKB_AMOUNT, HUGE_CKB_AMOUNT),
191+
)
192+
.await;
193+
194+
// Submit funding tx to all nodes for graph discovery
195+
let bc_funding_tx = node_b
196+
.get_transaction_view_from_hash(bc_funding_tx_hash)
197+
.await
198+
.expect("get funding tx");
199+
node_a.submit_tx(bc_funding_tx.clone()).await;
200+
node_a.add_channel_tx(bc_channel_id, bc_funding_tx_hash);
201+
202+
// Wait for graph to see both channels
203+
wait_for_network_graph_update(&node_a, 2).await;
204+
205+
// Generate a preimage and build an invoice on node_c
206+
let preimage = gen_rand_sha256_hash();
207+
let invoice = InvoiceBuilder::new(Currency::Fibd)
208+
.amount(Some(1000000000))
209+
.payment_preimage(preimage)
210+
.payee_pub_key(node_c.pubkey.into())
211+
.build()
212+
.expect("build invoice");
213+
let payment_hash = *invoice.payment_hash();
214+
215+
// Insert the invoice WITHOUT storing the preimage.
216+
// node_c has the invoice but cannot settle because the preimage is not in its store.
217+
node_c.insert_invoice(invoice.clone(), None);
218+
219+
// Configure node_b's watchtower querier to return the preimage for the a-b channel.
220+
// This simulates: c settled the TLC onchain on the b-c channel with the preimage,
221+
// and b's watchtower observed it and can now provide the preimage.
222+
watchtower_querier.set_status(
223+
ab_channel_id,
224+
payment_hash,
225+
TlcWatchtowerStatus {
226+
preimage: Some(preimage),
227+
is_settled: true,
228+
},
229+
);
230+
231+
// Send payment from a to c
232+
let res = node_a
233+
.send_payment(SendPaymentCommand {
234+
invoice: Some(invoice.to_string()),
235+
..Default::default()
236+
})
237+
.await
238+
.expect("send payment should succeed");
239+
240+
// node_b's CheckChannels should find the preimage via watchtower for the received TLC
241+
// on the a-b channel and settle it offchain with RemoveTlcFulfill.
242+
// This propagates back to node_a, marking the payment as successful.
243+
node_a.wait_until_success(res.payment_hash).await;
244+
}

crates/fiber-lib/src/fiber/watchtower_query.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::fiber::types::Hash256;
22

33
/// Status of a TLC as reported by the watchtower
4+
#[derive(Clone)]
45
pub struct TlcWatchtowerStatus {
56
/// The preimage for the payment hash, if known by the watchtower
67
pub preimage: Option<Hash256>,

crates/fiber-lib/src/tests/test_utils.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ pub struct NetworkNodeConfig {
276276
rpc_config: Option<RpcConfig>,
277277
ckb_config: Option<CkbConfig>,
278278
mock_chain_actor_middleware: Option<Box<dyn MockChainActorMiddleware>>,
279+
watchtower_querier: Option<std::sync::Arc<dyn crate::fiber::WatchtowerQuerier>>,
279280
}
280281

281282
impl NetworkNodeConfig {
@@ -293,6 +294,7 @@ pub struct NetworkNodeConfigBuilder {
293294
#[allow(clippy::type_complexity)]
294295
fiber_config_updater: Option<Box<dyn FnOnce(&mut FiberConfig) + 'static>>,
295296
mock_chain_actor_middleware: Option<Box<dyn MockChainActorMiddleware>>,
297+
watchtower_querier: Option<std::sync::Arc<dyn crate::fiber::WatchtowerQuerier>>,
296298
}
297299

298300
impl Default for NetworkNodeConfigBuilder {
@@ -309,6 +311,7 @@ impl NetworkNodeConfigBuilder {
309311
rpc_config: None,
310312
fiber_config_updater: None,
311313
mock_chain_actor_middleware: None,
314+
watchtower_querier: None,
312315
}
313316
}
314317

@@ -347,6 +350,14 @@ impl NetworkNodeConfigBuilder {
347350
self
348351
}
349352

353+
pub fn watchtower_querier(
354+
mut self,
355+
querier: Option<std::sync::Arc<dyn crate::fiber::WatchtowerQuerier>>,
356+
) -> Self {
357+
self.watchtower_querier = querier;
358+
self
359+
}
360+
350361
pub fn build(self) -> NetworkNodeConfig {
351362
let base_dir = self
352363
.base_dir
@@ -388,6 +399,7 @@ impl NetworkNodeConfigBuilder {
388399
fiber_config,
389400
rpc_config,
390401
mock_chain_actor_middleware: self.mock_chain_actor_middleware,
402+
watchtower_querier: self.watchtower_querier,
391403
};
392404

393405
if let Some(updater) = self.fiber_config_updater {
@@ -1478,6 +1490,7 @@ impl NetworkNode {
14781490
rpc_config,
14791491
ckb_config,
14801492
mock_chain_actor_middleware,
1493+
watchtower_querier,
14811494
} = config;
14821495

14831496
let _span = tracing::info_span!("NetworkNode", node_name = &node_name).entered();
@@ -1518,7 +1531,7 @@ impl NetworkNode {
15181531
store.clone(),
15191532
network_graph.clone(),
15201533
chain_client.clone(),
1521-
None,
1534+
watchtower_querier,
15221535
),
15231536
NetworkActorStartArguments {
15241537
config: fiber_config.clone(),
@@ -1648,6 +1661,7 @@ impl NetworkNode {
16481661
fiber_config: self.fiber_config.clone(),
16491662
rpc_config: self.rpc_config.clone(),
16501663
mock_chain_actor_middleware: self.mock_chain_actor_middleware.clone(),
1664+
watchtower_querier: None,
16511665
}
16521666
}
16531667

0 commit comments

Comments
 (0)