Skip to content

Commit 9c13db6

Browse files
committed
Test justice tx formation from persistence
Here we implement `WatchtowerPersister`, which provides a test-only sample implementation of `Persist` similar to how we might imagine a user to build watchtower-like functionality in the persistence pipeline. We test that the `WatchtowerPersister` is able to successfully build and sign a valid justice transaction that sweeps a counterparty's funds if they broadcast an old commitment.
1 parent 39a2655 commit 9c13db6

File tree

3 files changed

+178
-4
lines changed

3 files changed

+178
-4
lines changed

lightning/src/ln/functional_test_utils.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! A bunch of useful utilities for building networks of nodes and exchanging messages between
1111
//! nodes for functional tests.
1212
13-
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
13+
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch, chainmonitor::Persist};
1414
use crate::sign::EntropySource;
1515
use crate::chain::channelmonitor::ChannelMonitor;
1616
use crate::chain::transaction::OutPoint;
@@ -2583,10 +2583,14 @@ pub fn create_chanmon_cfgs(node_count: usize) -> Vec<TestChanMonCfg> {
25832583
}
25842584

25852585
pub fn create_node_cfgs<'a>(node_count: usize, chanmon_cfgs: &'a Vec<TestChanMonCfg>) -> Vec<NodeCfg<'a>> {
2586+
create_node_cfgs_with_persisters(node_count, chanmon_cfgs, chanmon_cfgs.iter().map(|c| &c.persister).collect())
2587+
}
2588+
2589+
pub fn create_node_cfgs_with_persisters<'a>(node_count: usize, chanmon_cfgs: &'a Vec<TestChanMonCfg>, persisters: Vec<&'a impl Persist<EnforcingSigner>>) -> Vec<NodeCfg<'a>> {
25862590
let mut nodes = Vec::new();
25872591

25882592
for i in 0..node_count {
2589-
let chain_monitor = test_utils::TestChainMonitor::new(Some(&chanmon_cfgs[i].chain_source), &chanmon_cfgs[i].tx_broadcaster, &chanmon_cfgs[i].logger, &chanmon_cfgs[i].fee_estimator, &chanmon_cfgs[i].persister, &chanmon_cfgs[i].keys_manager);
2593+
let chain_monitor = test_utils::TestChainMonitor::new(Some(&chanmon_cfgs[i].chain_source), &chanmon_cfgs[i].tx_broadcaster, &chanmon_cfgs[i].logger, &chanmon_cfgs[i].fee_estimator, persisters[i], &chanmon_cfgs[i].keys_manager);
25902594
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &chanmon_cfgs[i].logger));
25912595
let seed = [i as u8; 32];
25922596
nodes.push(NodeCfg {

lightning/src/ln/functional_tests.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator;
1717
use crate::chain::channelmonitor;
1818
use crate::chain::channelmonitor::{CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY};
1919
use crate::chain::transaction::OutPoint;
20-
use crate::sign::{ChannelSigner, EcdsaChannelSigner, EntropySource};
20+
use crate::sign::{ChannelSigner, EcdsaChannelSigner, EntropySource, SignerProvider};
2121
use crate::events::{Event, MessageSendEvent, MessageSendEventsProvider, PathFailure, PaymentPurpose, ClosureReason, HTLCDestination, PaymentFailureReason};
2222
use crate::ln::{PaymentPreimage, PaymentSecret, PaymentHash};
2323
use crate::ln::channel::{commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, CONCURRENT_INBOUND_HTLC_FEE_BUFFER, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT, get_holder_selected_channel_reserve_satoshis, OutboundV1Channel, InboundV1Channel};
@@ -31,7 +31,7 @@ use crate::ln::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures};
3131
use crate::ln::msgs;
3232
use crate::ln::msgs::{ChannelMessageHandler, RoutingMessageHandler, ErrorAction};
3333
use crate::util::enforcing_trait_impls::EnforcingSigner;
34-
use crate::util::test_utils;
34+
use crate::util::test_utils::{self, WatchtowerPersister};
3535
use crate::util::errors::APIError;
3636
use crate::util::ser::{Writeable, ReadableArgs};
3737
use crate::util::string::UntrustedString;
@@ -2554,6 +2554,72 @@ fn revoked_output_claim() {
25542554
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed, [nodes[1].node.get_our_node_id()], 100000);
25552555
}
25562556

2557+
#[test]
2558+
fn test_forming_justice_tx_from_monitor_updates() {
2559+
do_test_forming_justice_tx_from_monitor_updates(true);
2560+
do_test_forming_justice_tx_from_monitor_updates(false);
2561+
}
2562+
2563+
fn do_test_forming_justice_tx_from_monitor_updates(broadcast_initial_commitment: bool) {
2564+
// Simple test to make sure that the justice tx formed in WatchtowerPersister
2565+
// is properly formed and can be broadcasted/confirmed successfully in the event
2566+
// that a revoked commitment transaction is broadcasted
2567+
// (Similar to `revoked_output_claim` test but we get the justice tx + broadcast manually)
2568+
let chanmon_cfgs = create_chanmon_cfgs(2);
2569+
let destination_script0 = chanmon_cfgs[0].keys_manager.get_destination_script().unwrap();
2570+
let destination_script1 = chanmon_cfgs[1].keys_manager.get_destination_script().unwrap();
2571+
let persisters = vec![WatchtowerPersister::new(destination_script0),
2572+
WatchtowerPersister::new(destination_script1)];
2573+
let node_cfgs = create_node_cfgs_with_persisters(2, &chanmon_cfgs, persisters.iter().collect());
2574+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
2575+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2576+
let (_, _, channel_id, funding_tx) = create_announced_chan_between_nodes(&nodes, 0, 1);
2577+
let funding_txo = OutPoint { txid: funding_tx.txid(), index: 0 };
2578+
2579+
if !broadcast_initial_commitment {
2580+
// Send a payment to move the channel forward
2581+
send_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000);
2582+
}
2583+
2584+
// node[0] is gonna to revoke an old state thus node[1] should be able to claim the revoked output.
2585+
// We'll keep this commitment transaction to broadcast once it's revoked.
2586+
let revoked_local_txn = get_local_commitment_txn!(nodes[0], channel_id);
2587+
assert_eq!(revoked_local_txn.len(), 1);
2588+
let revoked_commitment_tx = &revoked_local_txn[0];
2589+
2590+
// Send another payment, now revoking the previous commitment tx
2591+
send_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000);
2592+
2593+
let justice_tx = persisters[1].justice_tx(funding_txo, &revoked_commitment_tx.txid()).unwrap();
2594+
check_spends!(justice_tx, revoked_commitment_tx);
2595+
2596+
mine_transactions(&nodes[1], &[revoked_commitment_tx, &justice_tx]);
2597+
mine_transactions(&nodes[0], &[revoked_commitment_tx, &justice_tx]);
2598+
2599+
check_added_monitors!(nodes[1], 1);
2600+
check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, false,
2601+
&[nodes[0].node.get_our_node_id()], 100_000);
2602+
get_announce_close_broadcast_events(&nodes, 1, 0);
2603+
2604+
check_added_monitors!(nodes[0], 1);
2605+
check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, false,
2606+
&[nodes[1].node.get_our_node_id()], 100_000);
2607+
2608+
// Check that the justice tx has sent the revoked output value to nodes[1]
2609+
let monitor = get_monitor!(nodes[1], channel_id);
2610+
let total_claimable_balance = monitor.get_claimable_balances().iter().fold(0, |sum, balance| {
2611+
match balance {
2612+
channelmonitor::Balance::ClaimableAwaitingConfirmations { amount_satoshis, .. } => sum + amount_satoshis,
2613+
_ => panic!("Unexpected balance type"),
2614+
}
2615+
});
2616+
// On the first commitment, node[1]'s balance was below dust so it didn't have an output
2617+
let node1_channel_balance = if broadcast_initial_commitment { 0 } else { revoked_commitment_tx.output[0].value };
2618+
let expected_claimable_balance = node1_channel_balance + justice_tx.output[0].value;
2619+
assert_eq!(total_claimable_balance, expected_claimable_balance);
2620+
}
2621+
2622+
25572623
#[test]
25582624
fn claim_htlc_outputs_shared_tx() {
25592625
// Node revoked old state, htlcs haven't time out yet, claim them in shared justice tx

lightning/src/util/test_utils.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::chain;
1111
use crate::chain::WatchedOutput;
1212
use crate::chain::chaininterface;
1313
use crate::chain::chaininterface::ConfirmationTarget;
14+
use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW;
1415
use crate::chain::chainmonitor;
1516
use crate::chain::chainmonitor::MonitorUpdateId;
1617
use crate::chain::channelmonitor;
@@ -20,6 +21,7 @@ use crate::sign;
2021
use crate::events;
2122
use crate::events::bump_transaction::{WalletSource, Utxo};
2223
use crate::ln::channelmanager;
24+
use crate::ln::chan_utils::CommitmentTransaction;
2325
use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures};
2426
use crate::ln::{msgs, wire};
2527
use crate::ln::msgs::LightningError;
@@ -269,6 +271,108 @@ impl<'a> chain::Watch<EnforcingSigner> for TestChainMonitor<'a> {
269271
}
270272
}
271273

274+
struct JusticeTxData {
275+
justice_tx: Transaction,
276+
value: u64,
277+
commitment_number: u64,
278+
}
279+
280+
pub(crate) struct WatchtowerPersister {
281+
persister: TestPersister,
282+
/// Upon a new commitment_signed, we'll get a
283+
/// ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTxInfo. We'll store the justice tx
284+
/// amount, and commitment number so we can build the justice tx after our counterparty
285+
/// revokes it.
286+
unsigned_justice_tx_data: Mutex<HashMap<OutPoint, VecDeque<JusticeTxData>>>,
287+
/// After receiving a revoke_and_ack for a commitment number, we'll form and store the justice
288+
/// tx which would be used to provide a watchtower with the data it needs.
289+
watchtower_state: Mutex<HashMap<OutPoint, HashMap<Txid, Transaction>>>,
290+
destination_script: Script,
291+
}
292+
293+
impl WatchtowerPersister {
294+
pub(crate) fn new(destination_script: Script) -> Self {
295+
WatchtowerPersister {
296+
persister: TestPersister::new(),
297+
unsigned_justice_tx_data: Mutex::new(HashMap::new()),
298+
watchtower_state: Mutex::new(HashMap::new()),
299+
destination_script,
300+
}
301+
}
302+
303+
pub(crate) fn justice_tx(&self, funding_txo: OutPoint, commitment_txid: &Txid)
304+
-> Option<Transaction> {
305+
self.watchtower_state.lock().unwrap().get(&funding_txo).unwrap().get(commitment_txid).cloned()
306+
}
307+
308+
fn form_justice_data_from_commitment(&self, counterparty_commitment_tx: &CommitmentTransaction)
309+
-> Option<JusticeTxData> {
310+
let output_idx = counterparty_commitment_tx.revokeable_output_index()?;
311+
let trusted_tx = counterparty_commitment_tx.trust();
312+
let built_tx = trusted_tx.built_transaction();
313+
let value = built_tx.transaction.output[output_idx as usize].value;
314+
let justice_tx = counterparty_commitment_tx.build_to_local_justice_tx(
315+
FEERATE_FLOOR_SATS_PER_KW as u64, self.destination_script.clone()).ok()?;
316+
let commitment_number = counterparty_commitment_tx.commitment_number();
317+
Some(JusticeTxData { justice_tx, value, commitment_number })
318+
}
319+
}
320+
321+
impl<Signer: sign::WriteableEcdsaChannelSigner> chainmonitor::Persist<Signer> for WatchtowerPersister {
322+
fn persist_new_channel(&self, funding_txo: OutPoint,
323+
data: &channelmonitor::ChannelMonitor<Signer>, id: MonitorUpdateId
324+
) -> chain::ChannelMonitorUpdateStatus {
325+
let res = self.persister.persist_new_channel(funding_txo, data, id);
326+
327+
assert!(self.unsigned_justice_tx_data.lock().unwrap()
328+
.insert(funding_txo, VecDeque::new()).is_none());
329+
assert!(self.watchtower_state.lock().unwrap()
330+
.insert(funding_txo, HashMap::new()).is_none());
331+
332+
let initial_counterparty_commitment_tx = data.initial_counterparty_commitment_tx()
333+
.expect("First and only call expects Some");
334+
if let Some(justice_data)
335+
= self.form_justice_data_from_commitment(&initial_counterparty_commitment_tx) {
336+
self.unsigned_justice_tx_data.lock().unwrap()
337+
.get_mut(&funding_txo).unwrap()
338+
.push_back(justice_data);
339+
}
340+
res
341+
}
342+
343+
fn update_persisted_channel(
344+
&self, funding_txo: OutPoint, update: Option<&channelmonitor::ChannelMonitorUpdate>,
345+
data: &channelmonitor::ChannelMonitor<Signer>, update_id: MonitorUpdateId
346+
) -> chain::ChannelMonitorUpdateStatus {
347+
let res = self.persister.update_persisted_channel(funding_txo, update, data, update_id);
348+
349+
if let Some(update) = update {
350+
let commitment_txs = data.counterparty_commitment_txs_from_update(update);
351+
let justice_datas = commitment_txs.into_iter()
352+
.filter_map(|commitment_tx| self.form_justice_data_from_commitment(&commitment_tx));
353+
let mut channels_justice_txs = self.unsigned_justice_tx_data.lock().unwrap();
354+
let channel_state = channels_justice_txs.get_mut(&funding_txo).unwrap();
355+
channel_state.extend(justice_datas);
356+
357+
while let Some(JusticeTxData { justice_tx, value, commitment_number }) = channel_state.front() {
358+
let input_idx = 0;
359+
let commitment_txid = justice_tx.input[input_idx].previous_output.txid;
360+
match data.sign_justice_tx(justice_tx.clone(), input_idx, *value, *commitment_number) {
361+
Ok(signed_justice_tx) => {
362+
let dup = self.watchtower_state.lock().unwrap()
363+
.get_mut(&funding_txo).unwrap()
364+
.insert(commitment_txid, signed_justice_tx);
365+
assert!(dup.is_none());
366+
channel_state.pop_front();
367+
},
368+
Err(_) => break,
369+
}
370+
}
371+
}
372+
res
373+
}
374+
}
375+
272376
pub struct TestPersister {
273377
/// The queue of update statuses we'll return. If none are queued, ::Completed will always be
274378
/// returned.

0 commit comments

Comments
 (0)