Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ enum NodeError {
"LnurlAuthTimeout",
"InvalidLnurl",
"ChainSourceNotSupported",
"ChannelMonitorNotFound",
};

typedef dictionary NodeStatus;
Expand Down
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ pub enum Error {
InvalidLnurl,
/// The configured chain source is not supported.
ChainSourceNotSupported,
/// No channel monitor could be found for the given channel ID.
ChannelMonitorNotFound,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -227,6 +229,9 @@ impl fmt::Display for Error {
Self::ChainSourceNotSupported => {
write!(f, "The configured chain source is not supported.")
},
Self::ChannelMonitorNotFound => {
write!(f, "No channel monitor could be found for the given channel ID.")
},
}
}
}
Expand Down
184 changes: 182 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub use bitcoin;
use bitcoin::secp256k1::PublicKey;
#[cfg(feature = "uniffi")]
pub use bitcoin::FeeRate;
use bitcoin::{Address, Amount, BlockHash, Network};
use bitcoin::{Address, Amount, BlockHash, Network, Transaction};
#[cfg(feature = "uniffi")]
pub use builder::ArcedNodeBuilder as Builder;
pub use builder::BuildError;
Expand All @@ -147,17 +147,20 @@ use gossip::GossipSource;
use graph::NetworkGraph;
use io::utils::update_and_persist_node_metrics;
pub use lightning;
use lightning::chain::channelmonitor::ChannelMonitorUpdate;
use lightning::chain::BlockLocator;
use lightning::impl_writeable_tlv_based;
use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
use lightning::ln::chan_utils::{CommitmentTransaction, FUNDING_TRANSACTION_WITNESS_WEIGHT};
use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails;
pub use lightning::ln::channel_state::ChannelShutdownState;
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::msgs::{BaseMessageHandler, SocketAddress};
use lightning::ln::peer_handler::CustomMessageHandler;
use lightning::ln::types::ChannelId;
use lightning::routing::gossip::NodeAlias;
use lightning::sign::EntropySource;
use lightning::util::persist::KVStore;
use lightning::util::ser::Readable;
use lightning::util::wallet_utils::{Input, Wallet as LdkWallet};
use lightning_background_processor::process_events_async;
pub use lightning_invoice;
Expand Down Expand Up @@ -2318,6 +2321,183 @@ impl Node {
})
}

/// Returns the counterparty's commitment transactions provided by the given
/// [`ChannelMonitorUpdate`] for the channel with the given `channel_id`.
///
/// This may be empty if the update doesn't include any new counterparty commitments.
/// Returned commitment transactions are unsigned.
///
/// This is provided so that watchtower clients (e.g., an [Eye of Satoshi](https://github.com/talaia-labs/rust-teos)
/// tower) are able to build justice transactions for each counterparty commitment. It is
/// expected that a watchtower client may use this method to retrieve the latest counterparty
/// commitment transaction(s), and then hold the necessary data until the commitment has been
/// revoked, at which point a justice transaction spending the `to_local` output can be signed
/// via [`Node::sign_to_local_justice_tx`].
///
/// This will only return a non-empty list for monitor updates that have been created after
/// upgrading to LDK 0.0.117+.
///
/// Returns [`Error::ChannelMonitorNotFound`] if no channel monitor exists for the given
/// `channel_id`.
pub fn counterparty_commitment_txs_from_update(
&self, channel_id: ChannelId, update: ChannelMonitorUpdate,
) -> Result<Vec<CommitmentTransaction>, Error> {
let monitor = self.chain_monitor.get_monitor(channel_id).map_err(|()| {
log_error!(self.logger, "No channel monitor found for channel ID {}", channel_id);
Error::ChannelMonitorNotFound
})?;
Ok(monitor.counterparty_commitment_txs_from_update(&update))
}

/// Returns the counterparty's initial commitment transaction for the channel with the given
/// `channel_id`. The returned commitment transaction is unsigned.
///
/// This is similar to [`Node::counterparty_commitment_txs_from_update`], except that for the
/// initial commitment transaction, we don't have a corresponding [`ChannelMonitorUpdate`].
///
/// This will only return `Some` for channel monitors that have been created after upgrading
/// to LDK 0.0.117+.
///
/// Returns [`Error::ChannelMonitorNotFound`] if no channel monitor exists for the given
/// `channel_id`.
pub fn initial_counterparty_commitment_tx(
&self, channel_id: ChannelId,
) -> Result<Option<CommitmentTransaction>, Error> {
let monitor = self.chain_monitor.get_monitor(channel_id).map_err(|()| {
log_error!(self.logger, "No channel monitor found for channel ID {}", channel_id);
Error::ChannelMonitorNotFound
})?;
Ok(monitor.initial_counterparty_commitment_tx())
}

/// Signs the input at `input_idx` of the given `justice_tx`, claiming the `to_local` output
/// of a revoked counterparty commitment transaction of the channel with the given
/// `channel_id`.
///
/// This is a wrapper around
/// [`ChannelMonitor::sign_to_local_justice_tx`] intended to
/// allow watchtower clients to finalize justice transactions they built from counterparty
/// commitment transactions retrieved via [`Node::counterparty_commitment_txs_from_update`]
/// or [`Node::initial_counterparty_commitment_tx`].
///
/// Note that this method will only produce a valid signature for a transaction spending the
/// `to_local` output of a commitment transaction, i.e., this cannot be used for revoked HTLC
/// outputs.
///
/// `value_sat` is the value, in satoshis, of the output being spent by the input at
/// `input_idx`, committed in the BIP 143 signature.
///
/// This method will only succeed if the channel monitor has received the revocation secret
/// for the given `commitment_number`.
///
/// Returns [`Error::ChannelMonitorNotFound`] if no channel monitor exists for the given
/// `channel_id`, and [`Error::OnchainTxSigningFailed`] if signing the justice transaction
/// failed.
///
/// [`ChannelMonitor::sign_to_local_justice_tx`]: lightning::chain::channelmonitor::ChannelMonitor::sign_to_local_justice_tx
pub fn sign_to_local_justice_tx(
&self, channel_id: ChannelId, justice_tx: Transaction, input_idx: usize, value_sat: u64,
commitment_number: u64,
) -> Result<Transaction, Error> {
let monitor = self.chain_monitor.get_monitor(channel_id).map_err(|()| {
log_error!(self.logger, "No channel monitor found for channel ID {}", channel_id);
Error::ChannelMonitorNotFound
})?;
monitor
.sign_to_local_justice_tx(justice_tx, input_idx, value_sat, commitment_number)
.map_err(|()| {
log_error!(
self.logger,
"Failed to sign justice transaction for channel ID {}",
channel_id
);
Error::OnchainTxSigningFailed
})
}

/// Returns all [`ChannelMonitorUpdate`]s persisted for the channel with the given
/// `channel_id`, ordered by their update IDs.
///
/// The returned updates can be passed to [`Node::counterparty_commitment_txs_from_update`]
/// to retrieve the corresponding counterparty commitment transactions, e.g., by watchtower
/// clients that need to (re-)build justice transaction data for past channel states.
///
/// Note that the returned list may be empty if no updates have been persisted for the given
/// channel, or if the given `channel_id` is unknown.
pub fn channel_monitor_updates(
&self, channel_id: ChannelId,
) -> Result<Vec<ChannelMonitorUpdate>, Error> {
use lightning::util::persist::CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE;

// Channel monitor updates are persisted under a secondary namespace derived from the
// funding outpoint for v1 channels, and from the channel ID for v2 channels. As the
// channel might have been closed since (and hence might not be returned by
// `list_channels` anymore), we fall back to trying the channel ID namespace.
let mut namespaces = Vec::with_capacity(2);
if let Some(channel) =
self.channel_manager.list_channels().into_iter().find(|c| c.channel_id == channel_id)
{
if let Some(funding_txo) = channel.funding_txo {
// Note that this matches `MonitorName`'s encoding, not `OutPoint`'s `Display`.
namespaces.push(format!("{}_{}", funding_txo.txid, funding_txo.index));
}
}
namespaces.push(channel_id.to_string());

let mut updates = Vec::new();
for namespace in namespaces {
let mut keys = self
.runtime
.block_on(KVStore::list(
&*self.kv_store,
CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE,
&namespace,
))
.map_err(|e| {
log_error!(
self.logger,
"Failed to access store while reading channel monitor updates: {}",
e
);
Error::PersistenceFailed
})?;

// Update keys are the string-encoded update IDs.
keys.sort_by_key(|k| k.parse::<u64>().unwrap_or(u64::MAX));

for key in keys {
let buf = self
.runtime
.block_on(KVStore::read(
&*self.kv_store,
CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE,
&namespace,
&key,
))
.map_err(|e| {
log_error!(
self.logger,
"Failed to access store while reading channel monitor update: {}",
e
);
Error::PersistenceFailed
})?;

let update = ChannelMonitorUpdate::read(&mut &buf[..]).map_err(|e| {
log_error!(self.logger, "Failed to deserialize channel monitor update: {}", e);
Error::PersistenceFailed
})?;
updates.push(update);
}

if !updates.is_empty() {
break;
}
}

Ok(updates)
}

/// Return the features used in node announcement.
fn node_features(&self) -> LdkNodeFeatures {
let gossip_features = match self.gossip_source.as_gossip_sync() {
Expand Down
100 changes: 100 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use ldk_node::payment::{
};
use ldk_node::{BuildError, Builder, Event, Node, NodeError, ReserveType};
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::types::ChannelId;
use lightning::routing::gossip::{NodeAlias, NodeId};
use lightning::routing::router::RouteParametersConfig;
use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse};
Expand Down Expand Up @@ -410,6 +411,105 @@ async fn channel_open_fails_when_funds_insufficient() {
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn watchtower_commitment_and_justice_apis() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind);
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false);

let addr_a = node_a.onchain_payment().new_address().unwrap();
let premine_amount_sat = 500_000;
premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![addr_a],
Amount::from_sat(premine_amount_sat),
)
.await;
node_a.sync_wallets().unwrap();

open_channel(&node_a, &node_b, 200_000, false, &electrsd).await;
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

let channel_id = node_a.list_channels().first().unwrap().channel_id;

// The initial counterparty commitment transaction is available for monitors created with
// LDK 0.0.117+.
assert!(node_a.initial_counterparty_commitment_tx(channel_id).unwrap().is_some());
assert!(node_b.initial_counterparty_commitment_tx(channel_id).unwrap().is_some());

// An unknown channel ID yields `ChannelMonitorNotFound`.
let unknown_channel_id = ChannelId::from_bytes([42u8; 32]);
assert_eq!(
Err(NodeError::ChannelMonitorNotFound),
node_a.initial_counterparty_commitment_tx(unknown_channel_id)
);
assert!(node_a.channel_monitor_updates(unknown_channel_id).unwrap().is_empty());

// Send a payment to generate channel monitor updates.
let invoice_description =
Bolt11InvoiceDescription::Direct(Description::new(String::from("watchtower")).unwrap());
let invoice =
node_b.bolt11_payment().receive(10_000_000, &invoice_description.into(), 3600).unwrap();
let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap();
expect_payment_received_event!(&node_b, 10_000_000);
expect_payment_successful_event!(node_a, Some(payment_id), None);

// The persisted monitor updates are readable and ordered by update ID. Monitor updates are
// persisted asynchronously, so we retry a few times to avoid racing the persister.
let updates = {
let mut updates = Vec::new();
for _ in 0..10 {
updates = node_a.channel_monitor_updates(channel_id).unwrap();
if !updates.is_empty() {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
updates
};
assert!(!updates.is_empty());
let update_ids: Vec<u64> = updates.iter().map(|u| u.update_id).collect();
let mut sorted_update_ids = update_ids.clone();
sorted_update_ids.sort_unstable();
assert_eq!(update_ids, sorted_update_ids);

// We can build counterparty commitment transactions from the persisted updates.
let commitment_tx_count: usize = updates
.iter()
.map(|u| {
node_a.counterparty_commitment_txs_from_update(channel_id, u.clone()).unwrap().len()
})
.sum();
assert!(commitment_tx_count > 0);

assert_eq!(
Err(NodeError::ChannelMonitorNotFound),
node_a.counterparty_commitment_txs_from_update(
unknown_channel_id,
updates.first().unwrap().clone()
)
);

// Signing a justice transaction fails if the monitor has not received the revocation
// secret for the given commitment number.
let justice_tx = bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![bitcoin::TxIn::default()],
output: vec![],
};
assert_eq!(
Err(NodeError::OnchainTxSigningFailed),
node_a.sign_to_local_justice_tx(channel_id, justice_tx, 0, 1000, u64::MAX)
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn multi_hop_sending() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
Expand Down
Loading