Skip to content

Commit

Permalink
Test justice tx formation from persistence
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alecchendev committed Jul 31, 2023
1 parent 3e5ab3f commit 0e58741
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 4 deletions.
8 changes: 6 additions & 2 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! A bunch of useful utilities for building networks of nodes and exchanging messages between
//! nodes for functional tests.

use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch};
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch, chainmonitor::Persist};
use crate::sign::EntropySource;
use crate::chain::channelmonitor::ChannelMonitor;
use crate::chain::transaction::OutPoint;
Expand Down Expand Up @@ -2556,10 +2556,14 @@ pub fn create_chanmon_cfgs(node_count: usize) -> Vec<TestChanMonCfg> {
}

pub fn create_node_cfgs<'a>(node_count: usize, chanmon_cfgs: &'a Vec<TestChanMonCfg>) -> Vec<NodeCfg<'a>> {
create_node_cfgs_with_persisters(node_count, chanmon_cfgs, chanmon_cfgs.iter().map(|c| &c.persister).collect())
}

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>> {
let mut nodes = Vec::new();

for i in 0..node_count {
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);
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);
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &chanmon_cfgs[i].logger));
let seed = [i as u8; 32];
nodes.push(NodeCfg {
Expand Down
68 changes: 66 additions & 2 deletions lightning/src/ln/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator;
use crate::chain::channelmonitor;
use crate::chain::channelmonitor::{CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY};
use crate::chain::transaction::OutPoint;
use crate::sign::{ChannelSigner, EcdsaChannelSigner, EntropySource};
use crate::sign::{ChannelSigner, EcdsaChannelSigner, EntropySource, SignerProvider};
use crate::events::{Event, MessageSendEvent, MessageSendEventsProvider, PathFailure, PaymentPurpose, ClosureReason, HTLCDestination, PaymentFailureReason};
use crate::ln::{PaymentPreimage, PaymentSecret, PaymentHash};
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};
Expand All @@ -31,7 +31,7 @@ use crate::ln::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures};
use crate::ln::msgs;
use crate::ln::msgs::{ChannelMessageHandler, RoutingMessageHandler, ErrorAction};
use crate::util::enforcing_trait_impls::EnforcingSigner;
use crate::util::test_utils;
use crate::util::test_utils::{self, WatchtowerPersister};
use crate::util::errors::APIError;
use crate::util::ser::{Writeable, ReadableArgs};
use crate::util::string::UntrustedString;
Expand Down Expand Up @@ -2549,6 +2549,70 @@ fn revoked_output_claim() {
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed);
}

#[test]
fn test_forming_justice_tx_from_monitor_updates() {
do_test_forming_justice_tx_from_monitor_updates(true);
do_test_forming_justice_tx_from_monitor_updates(false);
}

fn do_test_forming_justice_tx_from_monitor_updates(broadcast_initial_commitment: bool) {
// Simple test to make sure that the justice tx formed in WatchtowerPersister
// is properly formed and can be broadcasted/confirmed successfully in the event
// that a revoked commitment transaction is broadcasted
// (Similar to `revoked_output_claim` test but we get the justice tx + broadcast manually)
let chanmon_cfgs = create_chanmon_cfgs(2);
let destination_script0 = chanmon_cfgs[0].keys_manager.get_destination_script().unwrap();
let destination_script1 = chanmon_cfgs[1].keys_manager.get_destination_script().unwrap();
let persisters = vec![WatchtowerPersister::new(destination_script0),
WatchtowerPersister::new(destination_script1)];
let node_cfgs = create_node_cfgs_with_persisters(2, &chanmon_cfgs, persisters.iter().collect());
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
let (_, _, channel_id, funding_tx) = create_announced_chan_between_nodes(&nodes, 0, 1);
let funding_txo = OutPoint { txid: funding_tx.txid(), index: 0 };

if !broadcast_initial_commitment {
// Send a payment to move the channel forward
send_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000);
}

// node[0] is gonna to revoke an old state thus node[1] should be able to claim the revoked output.
// We'll keep this commitment transaction to broadcast once it's revoked.
let revoked_local_txn = get_local_commitment_txn!(nodes[0], channel_id);
assert_eq!(revoked_local_txn.len(), 1);
let revoked_commitment_tx = &revoked_local_txn[0];

// Send another payment, now revoking the previous commitment tx
send_payment(&nodes[0], &vec!(&nodes[1])[..], 5_000_000);

let justice_tx = persisters[1].justice_tx(funding_txo, &revoked_commitment_tx.txid()).unwrap();
check_spends!(justice_tx, revoked_commitment_tx);

mine_transactions(&nodes[1], &[revoked_commitment_tx, &justice_tx]);
mine_transactions(&nodes[0], &[revoked_commitment_tx, &justice_tx]);

check_added_monitors!(nodes[1], 1);
check_closed_event!(nodes[1], 1, ClosureReason::CommitmentTxConfirmed);
get_announce_close_broadcast_events(&nodes, 1, 0);

check_added_monitors!(nodes[0], 1);
check_closed_event!(nodes[0], 1, ClosureReason::CommitmentTxConfirmed);

// Check that the justice tx has sent the revoked output value to nodes[1]
let monitor = get_monitor!(nodes[1], channel_id);
let total_claimable_balance = monitor.get_claimable_balances().iter().fold(0, |sum, balance| {
match balance {
channelmonitor::Balance::ClaimableAwaitingConfirmations { claimable_amount_satoshis, .. } => sum + claimable_amount_satoshis,
_ => panic!("Unexpected balance type"),
}
});
// On the first commitment, node[1]'s balance was below dust so it didn't have an output
let node1_channel_balance = if broadcast_initial_commitment { 0 } else { revoked_commitment_tx.output[0].value };
let expected_claimable_balance = node1_channel_balance + justice_tx.output[0].value;
assert_eq!(total_claimable_balance, expected_claimable_balance);
}


#[test]
fn claim_htlc_outputs_shared_tx() {
// Node revoked old state, htlcs haven't time out yet, claim them in shared justice tx
Expand Down
92 changes: 92 additions & 0 deletions lightning/src/util/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ use crate::chain;
use crate::chain::WatchedOutput;
use crate::chain::chaininterface;
use crate::chain::chaininterface::ConfirmationTarget;
use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW;
use crate::chain::chainmonitor;
use crate::chain::chainmonitor::MonitorUpdateId;
use crate::chain::channelmonitor;
use crate::chain::channelmonitor::MonitorEvent;
use crate::chain::package::WEIGHT_REVOKED_OUTPUT;
use crate::chain::transaction::OutPoint;
use crate::sign;
use crate::events;
use crate::ln::channelmanager;
use crate::ln::chan_utils::CommitmentTransaction;
use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures};
use crate::ln::{msgs, wire};
use crate::ln::msgs::LightningError;
Expand Down Expand Up @@ -266,6 +269,95 @@ impl<'a> chain::Watch<EnforcingSigner> for TestChainMonitor<'a> {
}
}

pub(crate) struct WatchtowerPersister {
persister: TestPersister,
/// Upon a new commitment_signed, we'll get a
/// ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTxInfo. We'll store the commitment
/// tx so we can build the justice tx after our counterparty revokes it.
counterparty_commitment_txs: Mutex<HashMap<OutPoint, VecDeque<CommitmentTransaction>>>,
/// After receiving a revoke_and_ack for a commitment number, we'll form and store the justice
/// tx which would be used to provide a watchtower with the data it needs.
watchtower_state: Mutex<HashMap<OutPoint, HashMap<Txid, Transaction>>>,
destination_script: Script,
}

impl WatchtowerPersister {
pub(crate) fn new(destination_script: Script) -> Self {
WatchtowerPersister {
persister: TestPersister::new(),
counterparty_commitment_txs: Mutex::new(HashMap::new()),
watchtower_state: Mutex::new(HashMap::new()),
destination_script,
}
}

pub(crate) fn justice_tx(&self, funding_txo: OutPoint, commitment_txid: &Txid) -> Option<Transaction> {
self.watchtower_state.lock().unwrap().get(&funding_txo).unwrap().get(commitment_txid).cloned()
}
}

impl<Signer: sign::WriteableEcdsaChannelSigner> chainmonitor::Persist<Signer> for WatchtowerPersister {
fn persist_new_channel(&self, funding_txo: OutPoint, data: &channelmonitor::ChannelMonitor<Signer>, id: MonitorUpdateId) -> chain::ChannelMonitorUpdateStatus {
assert!(self.counterparty_commitment_txs.lock().unwrap()
.insert(funding_txo, VecDeque::new()).is_none());
assert!(self.watchtower_state.lock().unwrap()
.insert(funding_txo, HashMap::new()).is_none());

let initial_counterparty_commitment_tx = data.initial_counterparty_commitment_tx()
.expect("First and only call expects Some");
self.counterparty_commitment_txs.lock().unwrap().get_mut(&funding_txo).unwrap().push_back(initial_counterparty_commitment_tx);

self.persister.persist_new_channel(funding_txo, data, id)
}

fn update_persisted_channel(&self, funding_txo: OutPoint, update: Option<&channelmonitor::ChannelMonitorUpdate>, data: &channelmonitor::ChannelMonitor<Signer>, update_id: MonitorUpdateId) -> chain::ChannelMonitorUpdateStatus {
if let Some(update) = update {
// Track new counterparty commitment txs
let commitment_txs = data.counterparty_commitment_txs_from_update(update);
let mut channels_counterparty_commitments = self.counterparty_commitment_txs.lock().unwrap();
let channel_state = channels_counterparty_commitments.get_mut(&funding_txo).unwrap();
channel_state.extend(commitment_txs.into_iter());

// Form justice txs from revoked counterparty commitment txs
while let Some(counterparty_commitment_tx) = channel_state.front() {
let trusted_tx = counterparty_commitment_tx.trust();
let built_tx = trusted_tx.built_transaction();
let output_idx = match counterparty_commitment_tx.revokeable_output_index() {
Some(idx) => idx,
None => {
channel_state.pop_front();
continue;
}
};

let value = built_tx.transaction.output[output_idx as usize].value;
let mut justice_tx = counterparty_commitment_tx.build_justice_tx(
output_idx as u32, value, self.destination_script.clone());

let weight = justice_tx.weight() as u64 + WEIGHT_REVOKED_OUTPUT;
let min_feerate_per_kw = FEERATE_FLOOR_SATS_PER_KW;
let fee = min_feerate_per_kw as u64 * weight / 1000;
justice_tx.output[0].value -= fee;

let input_idx = 0;
let commitment_number = counterparty_commitment_tx.commitment_number();
let commitment_txid = built_tx.txid;
match data.sign_justice_tx(justice_tx, input_idx, value, commitment_number) {
Ok(signed_justice_tx) => {
let dup = self.watchtower_state.lock().unwrap()
.get_mut(&funding_txo).unwrap()
.insert(commitment_txid, signed_justice_tx);
assert!(dup.is_none());
channel_state.pop_front();
},
Err(_) => break,
}
}
}
self.persister.update_persisted_channel(funding_txo, update, data, update_id)
}
}

pub struct TestPersister {
/// The queue of update statuses we'll return. If none are queued, ::Completed will always be
/// returned.
Expand Down

0 comments on commit 0e58741

Please sign in to comment.