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
171 changes: 86 additions & 85 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ lto = true

[profile.dev]
panic = "abort"

[patch."https://github.com/lightningdevkit/ldk-node"]
# Temporary: watchtower APIs pending upstream merge of https://github.com/lightningdevkit/ldk-node/pull/1031
ldk-node = { git = "https://github.com/vincenzopalazzo/ldk-node", branch = "watchtower-apis-16eaa6f" }
50 changes: 50 additions & 0 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use ldk_server_client::ldk_server_grpc::api::{
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
SpontaneousSendResponse, UnifiedSendRequest, UnifiedSendResponse, UpdateChannelConfigRequest,
UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse,
WatchtowerStateExportRequest, WatchtowerStateExportResponse,
};
use ldk_server_client::ldk_server_grpc::types::{
bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, PageToken,
Expand Down Expand Up @@ -528,6 +529,16 @@ enum Commands {
},
#[command(about = "Export the pathfinding scores used by the router")]
ExportPathfindingScores,
#[command(
about = "Export per-channel watchtower state (counterparty commitments and justice transactions) for an external watchtower client"
)]
WatchtowerStateExport {
#[arg(
long,
help = "Only export the state of the channel with this hex-encoded user_channel_id"
)]
user_channel_id: Option<String>,
},
#[command(about = "List all known short channel IDs in the network graph")]
GraphListChannels,
#[command(about = "Get channel information from the network graph by short channel ID")]
Expand Down Expand Up @@ -1078,6 +1089,14 @@ async fn main() {
),
);
},
Commands::WatchtowerStateExport { user_channel_id } => {
handle_response_result::<_, Value>(
client
.watchtower_state_export(WatchtowerStateExportRequest { user_channel_id })
.await
.map(watchtower_state_export_to_json),
);
},
Commands::GraphListChannels => {
handle_response_result::<_, GraphListChannelsResponse>(
client.graph_list_channels(GraphListChannelsRequest {}).await,
Expand Down Expand Up @@ -1195,6 +1214,37 @@ fn sanitize_for_terminal(s: String) -> String {
out
}

fn watchtower_state_export_to_json(response: WatchtowerStateExportResponse) -> Value {
let channel_states: Vec<Value> = response
.channel_states
.into_iter()
.map(|state| {
let justice_transactions: Vec<Value> = state
.justice_transactions
.into_iter()
.map(|justice_tx| {
json!({
"commitment_txid": justice_tx.commitment_txid,
"commitment_number": justice_tx.commitment_number,
"to_local_value_sats": justice_tx.to_local_value_sats,
"justice_tx_hex": justice_tx.justice_tx.as_hex().to_string(),
"signed": justice_tx.signed,
})
})
.collect();
json!({
"funding_txo": state.funding_txo,
"user_channel_id": state.user_channel_id,
"channel_id": state.channel_id,
"counterparty_node_id": state.counterparty_node_id,
"to_self_delay": state.to_self_delay,
"justice_transactions": justice_transactions,
})
})
.collect();
json!({ "channel_states": channel_states })
}

fn handle_response_result<Rs, Js>(response: Result<Rs, LdkServerError>)
where
Rs: Into<Js>,
Expand Down
11 changes: 10 additions & 1 deletion ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use ldk_server_grpc::api::{
SpliceInRequest, SpliceInResponse, SpliceOutRequest, SpliceOutResponse, SpontaneousSendRequest,
SpontaneousSendResponse, SubscribeEventsRequest, UnifiedSendRequest, UnifiedSendResponse,
UpdateChannelConfigRequest, UpdateChannelConfigResponse, VerifySignatureRequest,
VerifySignatureResponse,
VerifySignatureResponse, WatchtowerStateExportRequest, WatchtowerStateExportResponse,
};
use ldk_server_grpc::endpoints::{
BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH,
Expand All @@ -51,6 +51,7 @@ use ldk_server_grpc::endpoints::{
LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH,
SPLICE_IN_PATH, SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, SUBSCRIBE_EVENTS_PATH,
UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
WATCHTOWER_STATE_EXPORT_PATH,
};
use ldk_server_grpc::events::EventEnvelope;
use ldk_server_grpc::grpc::{
Expand Down Expand Up @@ -386,6 +387,14 @@ impl LdkServerClient {
self.grpc_unary(&request, EXPORT_PATHFINDING_SCORES_PATH).await
}

/// Export per-channel watchtower state (latest counterparty commitment transactions
/// and corresponding justice transactions) for an external watchtower client.
pub async fn watchtower_state_export(
&self, request: WatchtowerStateExportRequest,
) -> Result<WatchtowerStateExportResponse, LdkServerError> {
self.grpc_unary(&request, WATCHTOWER_STATE_EXPORT_PATH).await
}

/// Returns a list of all known short channel IDs in the network graph.
pub async fn graph_list_channels(
&self, request: GraphListChannelsRequest,
Expand Down
29 changes: 29 additions & 0 deletions ldk-server-grpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,3 +1237,32 @@ pub struct DecodeOfferResponse {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SubscribeEventsRequest {}
/// Export per-channel watchtower state (latest counterparty commitment transaction(s)
/// and corresponding justice transactions) for consumption by an external watchtower
/// client such as a rust-teos (Eye of Satoshi) client.
///
/// The endpoint is only served if `watchtower.export_enabled` is set in the server
/// configuration; otherwise a gRPC error status is returned.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct WatchtowerStateExportRequest {
/// If set, only the state of the channel with this hex-encoded `user_channel_id`
/// is exported. Otherwise the state of all channels is exported.
#[prost(string, optional, tag = "1")]
pub user_channel_id: ::core::option::Option<::prost::alloc::string::String>,
}
/// The response for the `WatchtowerStateExport` RPC. On failure, a gRPC error status
/// is returned.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct WatchtowerStateExportResponse {
/// The exported watchtower state, one entry per channel.
#[prost(message, repeated, tag = "1")]
pub channel_states: ::prost::alloc::vec::Vec<super::types::WatchtowerChannelState>,
}
1 change: 1 addition & 0 deletions ldk-server-grpc/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ pub const DECODE_INVOICE_PATH: &str = "DecodeInvoice";
pub const DECODE_OFFER_PATH: &str = "DecodeOffer";
pub const GET_METRICS_PATH: &str = "metrics";
pub const SUBSCRIBE_EVENTS_PATH: &str = "SubscribeEvents";
pub const WATCHTOWER_STATE_EXPORT_PATH: &str = "WatchtowerStateExport";
21 changes: 21 additions & 0 deletions ldk-server-grpc/src/proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,25 @@ message DecodeOfferResponse {
// Subscribe to a stream of server events.
message SubscribeEventsRequest {}

// Export per-channel watchtower state (latest counterparty commitment transaction(s)
// and corresponding justice transactions) for consumption by an external watchtower
// client such as a rust-teos (Eye of Satoshi) client.
//
// The endpoint is only served if `watchtower.export_enabled` is set in the server
// configuration; otherwise a gRPC error status is returned.
message WatchtowerStateExportRequest {
// If set, only the state of the channel with this hex-encoded `user_channel_id`
// is exported. Otherwise the state of all channels is exported.
optional string user_channel_id = 1;
}

// The response for the `WatchtowerStateExport` RPC. On failure, a gRPC error status
// is returned.
message WatchtowerStateExportResponse {
// The exported watchtower state, one entry per channel.
repeated types.WatchtowerChannelState channel_states = 1;
}

service LightningNode {
// Retrieve the latest node info.
rpc GetNodeInfo(GetNodeInfoRequest) returns (GetNodeInfoResponse);
Expand Down Expand Up @@ -976,4 +995,6 @@ service LightningNode {
rpc GraphGetNode(GraphGetNodeRequest) returns (GraphGetNodeResponse);
// Subscribe to a stream of server events.
rpc SubscribeEvents(SubscribeEventsRequest) returns (stream events.EventEnvelope);
// Export per-channel watchtower state for an external watchtower client.
rpc WatchtowerStateExport(WatchtowerStateExportRequest) returns (WatchtowerStateExportResponse);
}
53 changes: 53 additions & 0 deletions ldk-server-grpc/src/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -945,3 +945,56 @@ message Bolt11Feature {
// Whether this feature is known.
bool is_known = 3;
}

// A justice (penalty) transaction claiming the `to_local` output of a counterparty
// commitment transaction via the revocation path.
message JusticeTransaction {
// The txid (hex-encoded) of the counterparty commitment transaction this justice
// transaction spends from. This is also the source of the watchtower locator
// (the first 16 bytes of the txid).
string commitment_txid = 1;

// The commitment number of the counterparty commitment transaction.
uint64 commitment_number = 2;

// The value, in satoshis, of the `to_local` output being claimed.
uint64 to_local_value_sats = 3;

// The fully-built justice transaction, consensus-serialized. It pays the claimed
// funds (minus fees) to a fresh address controlled by this node's on-chain wallet.
//
// The transaction is signed iff `signed` is true. An unsigned transaction is
// returned when the revocation secret for this commitment state has not been
// received from the counterparty yet (i.e., it is their latest state); clients
// should poll again after the next state update to retrieve the signed version.
bytes justice_tx = 4;

// Whether `justice_tx` carries a valid signature spending the commitment
// transaction's `to_local` output.
bool signed = 5;
}

// The watchtower-relevant state of a single channel, as exported by the
// `WatchtowerStateExport` RPC.
message WatchtowerChannelState {
// The channel's funding outpoint.
OutPoint funding_txo = 1;

// The hex-encoded local `user_channel_id` of this channel.
string user_channel_id = 2;

// The channel ID (hex-encoded).
string channel_id = 3;

// The node ID of the channel's remote counterparty.
string counterparty_node_id = 4;

// The `to_self_delay` (in blocks) encumbering the counterparty's `to_local`
// output, i.e., the CSV delay the justice transactions bypass via the
// revocation path.
uint32 to_self_delay = 5;

// The justice transactions for the latest known counterparty commitment
// state(s). More than one entry may be present while a splice is in flight.
repeated JusticeTransaction justice_transactions = 6;
}
63 changes: 63 additions & 0 deletions ldk-server-grpc/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,69 @@ pub struct Bolt11Feature {
#[prost(bool, tag = "3")]
pub is_known: bool,
}
/// A justice (penalty) transaction claiming the `to_local` output of a counterparty
/// commitment transaction via the revocation path.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct JusticeTransaction {
/// The txid (hex-encoded) of the counterparty commitment transaction this justice
/// transaction spends from. This is also the source of the watchtower locator
/// (the first 16 bytes of the txid).
#[prost(string, tag = "1")]
pub commitment_txid: ::prost::alloc::string::String,
/// The commitment number of the counterparty commitment transaction.
#[prost(uint64, tag = "2")]
pub commitment_number: u64,
/// The value, in satoshis, of the `to_local` output being claimed.
#[prost(uint64, tag = "3")]
pub to_local_value_sats: u64,
/// The fully-built justice transaction, consensus-serialized. It pays the claimed
/// funds (minus fees) to a fresh address controlled by this node's on-chain wallet.
///
/// The transaction is signed iff `signed` is true. An unsigned transaction is
/// returned when the revocation secret for this commitment state has not been
/// received from the counterparty yet (i.e., it is their latest state); clients
/// should poll again after the next state update to retrieve the signed version.
#[prost(bytes = "bytes", tag = "4")]
pub justice_tx: ::prost::bytes::Bytes,
/// Whether `justice_tx` carries a valid signature spending the commitment
/// transaction's `to_local` output.
#[prost(bool, tag = "5")]
pub signed: bool,
}
/// The watchtower-relevant state of a single channel, as exported by the
/// `WatchtowerStateExport` RPC.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[cfg_attr(feature = "serde", serde(default))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct WatchtowerChannelState {
/// The channel's funding outpoint.
#[prost(message, optional, tag = "1")]
pub funding_txo: ::core::option::Option<OutPoint>,
/// The hex-encoded local `user_channel_id` of this channel.
#[prost(string, tag = "2")]
pub user_channel_id: ::prost::alloc::string::String,
/// The channel ID (hex-encoded).
#[prost(string, tag = "3")]
pub channel_id: ::prost::alloc::string::String,
/// The node ID of the channel's remote counterparty.
#[prost(string, tag = "4")]
pub counterparty_node_id: ::prost::alloc::string::String,
/// The `to_self_delay` (in blocks) encumbering the counterparty's `to_local`
/// output, i.e., the CSV delay the justice transactions bypass via the
/// revocation path.
#[prost(uint32, tag = "5")]
pub to_self_delay: u32,
/// The justice transactions for the latest known counterparty commitment
/// state(s). More than one entry may be present while a splice is in flight.
#[prost(message, repeated, tag = "6")]
pub justice_transactions: ::prost::alloc::vec::Vec<JusticeTransaction>,
}
/// Represents the direction of a payment.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
Expand Down
1 change: 1 addition & 0 deletions ldk-server/src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl From<NodeError> for LdkServerError {
| NodeError::InvalidOffer
| NodeError::InvalidRefund
| NodeError::InvalidChannelId
| NodeError::ChannelMonitorNotFound
| NodeError::InvalidNetwork
| NodeError::InvalidUri
| NodeError::InvalidQuantity
Expand Down
1 change: 1 addition & 0 deletions ldk-server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pub(crate) mod spontaneous_send;
pub(crate) mod unified_send;
pub(crate) mod update_channel_config;
pub(crate) mod verify_signature;
pub(crate) mod watchtower_state_export;

pub(crate) fn build_channel_config_from_proto(
default_config: ChannelConfig, proto_channel_config: ldk_server_grpc::types::ChannelConfig,
Expand Down
Loading