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
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,19 @@ lto = true

[profile.dev]
panic = "abort"

# Redirect rust-lightning git deps transitively (through ldk-node) to our
# fork that exposes ChannelMonitor::channel_keys_id.
# See rsafier/rust-lightning branch rfc/channel-keys-id-accessor.
[patch.'https://github.com/lightningdevkit/rust-lightning']
lightning = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-types = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-invoice = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-net-tokio = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-persister = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-background-processor = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-rapid-gossip-sync = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-block-sync = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-transaction-sync = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-liquidity = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
lightning-macros = { git = "https://github.com/rsafier/rust-lightning.git", branch = "rfc/channel-keys-id-accessor" }
2 changes: 1 addition & 1 deletion e2e-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ ldk-server-client = { path = "../ldk-server-client" }
ldk-server-grpc = { path = "../ldk-server-grpc", features = ["serde"] }
serde_json = "1.0"
hex-conservative = { version = "0.2", features = ["std"] }
ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "c754e2fe85c70741b5e370334cd16856c615265e" }
ldk-node = { git = "https://github.com/rsafier/ldk-node.git", branch = "rfc/export-channel-attestation" }
22 changes: 21 additions & 1 deletion ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ use ldk_server_client::ldk_server_grpc::api::{
DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse,
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
ForceCloseChannelRequest, ForceCloseChannelResponse, GetBalancesRequest, GetBalancesResponse,
GetNodeInfoRequest, GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
GetChannelAttestationsRequest, GetChannelAttestationsResponse, GetNodeInfoRequest,
GetNodeInfoResponse, GetPaymentDetailsRequest, GetPaymentDetailsResponse,
GraphGetChannelRequest, GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse,
GraphListChannelsRequest, GraphListChannelsResponse, GraphListNodesRequest,
GraphListNodesResponse, ListChannelsRequest, ListChannelsResponse,
Expand Down Expand Up @@ -443,6 +444,16 @@ enum Commands {
},
#[command(about = "Return a list of known channels")]
ListChannels,
#[command(
about = "Return cryptographic attestation bundles for open channels (unsigned commit tx + counterparty sig + funding pubkeys)"
)]
GetChannelAttestations {
#[arg(
long,
help = "Hex channel_id (can be given multiple times). If omitted, all open channels."
)]
channel_id: Vec<String>,
},
#[command(about = "Retrieve list of all payments")]
ListPayments {
#[arg(short, long)]
Expand Down Expand Up @@ -985,6 +996,15 @@ async fn main() {
client.list_channels(ListChannelsRequest {}).await,
);
},
Commands::GetChannelAttestations { channel_id } => {
handle_response_result::<_, GetChannelAttestationsResponse>(
client
.get_channel_attestations(GetChannelAttestationsRequest {
channel_ids: channel_id,
})
.await,
);
},
Commands::ListPayments { number_of_payments, page_token } => {
let page_token = page_token
.map(|token_str| parse_page_token(&token_str).unwrap_or_else(|e| handle_error(e)));
Expand Down
17 changes: 14 additions & 3 deletions ldk-server-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use ldk_server_grpc::api::{
DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse,
DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest,
ExportPathfindingScoresResponse, ForceCloseChannelRequest, ForceCloseChannelResponse,
GetBalancesRequest, GetBalancesResponse, GetNodeInfoRequest, GetNodeInfoResponse,
GetBalancesRequest, GetBalancesResponse, GetChannelAttestationsRequest,
GetChannelAttestationsResponse, GetNodeInfoRequest, GetNodeInfoResponse,
GetPaymentDetailsRequest, GetPaymentDetailsResponse, GraphGetChannelRequest,
GraphGetChannelResponse, GraphGetNodeRequest, GraphGetNodeResponse, GraphListChannelsRequest,
GraphListChannelsResponse, GraphListNodesRequest, GraphListNodesResponse, ListChannelsRequest,
Expand All @@ -45,7 +46,8 @@ use ldk_server_grpc::endpoints::{
BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DECODE_INVOICE_PATH, DECODE_OFFER_PATH,
DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH,
GET_BALANCES_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH,
GET_BALANCES_PATH, GET_CHANNEL_ATTESTATIONS_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH,
GET_PAYMENT_DETAILS_PATH,
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
GRPC_SERVICE_PREFIX, LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH,
LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH,
Expand Down Expand Up @@ -288,6 +290,14 @@ impl LdkServerClient {
self.grpc_unary(&request, LIST_CHANNELS_PATH).await
}

/// Retrieve per-channel cryptographic attestation bundles (unsigned commit tx +
/// counterparty sig + both funding pubkeys + balances + pending HTLCs).
pub async fn get_channel_attestations(
&self, request: GetChannelAttestationsRequest,
) -> Result<GetChannelAttestationsResponse, LdkServerError> {
self.grpc_unary(&request, GET_CHANNEL_ATTESTATIONS_PATH).await
}

/// Retrieves list of all payments sent or received by us.
pub async fn list_payments(
&self, request: ListPaymentsRequest,
Expand Down Expand Up @@ -418,7 +428,8 @@ impl LdkServerClient {
///
/// Returns an [`EventStream`] that yields [`EventEnvelope`] messages as they arrive.
pub async fn subscribe_events(&self) -> Result<EventStream, LdkServerError> {
self.grpc_server_streaming(&SubscribeEventsRequest {}, SUBSCRIBE_EVENTS_PATH).await
self.grpc_server_streaming(&SubscribeEventsRequest::default(), SUBSCRIBE_EVENTS_PATH)
.await
}

/// Send a unary gRPC request and decode the response.
Expand Down
20 changes: 20 additions & 0 deletions ldk-server-grpc/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ fn generate_protos() {
"types.Bolt12Refund.secret",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_opt_bytes_hex\"))]",
)
.field_attribute(
"events.ChannelCommitmentBundle.holder_commitment_tx",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_bytes_hex\"))]",
)
.field_attribute(
"events.ChannelCommitmentBundle.counterparty_signature",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_bytes_hex\"))]",
)
.field_attribute(
"events.ChannelCommitmentBundle.holder_funding_pubkey",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_bytes_hex\"))]",
)
.field_attribute(
"events.ChannelCommitmentBundle.counterparty_funding_pubkey",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_bytes_hex\"))]",
)
.field_attribute(
"events.AttestationHtlc.payment_hash",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_bytes_hex\"))]",
)
.field_attribute(
"types.Payment.direction",
"#[cfg_attr(feature = \"serde\", serde(serialize_with = \"crate::serde_utils::serialize_payment_direction\"))]",
Expand Down
71 changes: 70 additions & 1 deletion ldk-server-grpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,9 +1152,78 @@ pub struct DecodeOfferResponse {
#[prost(bool, tag = "12")]
pub is_expired: bool,
}
/// Request a point-in-time attestation bundle for some or all channels.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetChannelAttestationsRequest {
/// Optional channel-id filter (hex-encoded). Empty = all channels.
#[prost(string, repeated, tag = "1")]
pub channel_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
/// Response: one ChannelCommitmentUpdated per channel, carrying the full
/// cryptographic bundle populated by `Node::export_channel_attestation`.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetChannelAttestationsResponse {
#[prost(message, repeated, tag = "1")]
pub attestations: ::prost::alloc::vec::Vec<super::events::ChannelCommitmentUpdated>,
}
/// Subscribe to a stream of server events.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SubscribeEventsRequest {}
pub struct SubscribeEventsRequest {
/// Optional event-kind filter. If empty, the server emits all event kinds
/// EXCEPT EVENT_KIND_CHANNEL_COMMITMENT (which is opt-in because its rate
/// can be much higher than the lifecycle / payment streams). Passing a
/// non-empty list restricts the stream to those kinds (including
/// EVENT_KIND_CHANNEL_COMMITMENT if explicitly requested).
#[prost(enumeration = "EventKind", repeated, tag = "1")]
pub only: ::prost::alloc::vec::Vec<i32>,
}
/// Classes of events emitted on SubscribeEvents.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum EventKind {
Unspecified = 0,
/// PaymentReceived / PaymentSuccessful / PaymentFailed / PaymentClaimable /
/// PaymentForwarded.
Payment = 1,
/// ChannelPending / ChannelReady / ChannelClosed.
ChannelLifecycle = 2,
/// ChannelCommitmentUpdated — per-commitment attestation bundles.
/// Excluded from the default subscription because a busy node can emit
/// these many times per second.
ChannelCommitment = 3,
}
impl EventKind {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
EventKind::Unspecified => "EVENT_KIND_UNSPECIFIED",
EventKind::Payment => "EVENT_KIND_PAYMENT",
EventKind::ChannelLifecycle => "EVENT_KIND_CHANNEL_LIFECYCLE",
EventKind::ChannelCommitment => "EVENT_KIND_CHANNEL_COMMITMENT",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"EVENT_KIND_UNSPECIFIED" => Some(Self::Unspecified),
"EVENT_KIND_PAYMENT" => Some(Self::Payment),
"EVENT_KIND_CHANNEL_LIFECYCLE" => Some(Self::ChannelLifecycle),
"EVENT_KIND_CHANNEL_COMMITMENT" => Some(Self::ChannelCommitment),
_ => None,
}
}
}
1 change: 1 addition & 0 deletions ldk-server-grpc/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub const SPLICE_OUT_PATH: &str = "SpliceOut";
pub const CLOSE_CHANNEL_PATH: &str = "CloseChannel";
pub const FORCE_CLOSE_CHANNEL_PATH: &str = "ForceCloseChannel";
pub const LIST_CHANNELS_PATH: &str = "ListChannels";
pub const GET_CHANNEL_ATTESTATIONS_PATH: &str = "GetChannelAttestations";
pub const LIST_PAYMENTS_PATH: &str = "ListPayments";
pub const LIST_FORWARDED_PAYMENTS_PATH: &str = "ListForwardedPayments";
pub const UPDATE_CHANNEL_CONFIG_PATH: &str = "UpdateChannelConfig";
Expand Down
151 changes: 150 additions & 1 deletion ldk-server-grpc/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct EventEnvelope {
#[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6, 7")]
#[prost(oneof = "event_envelope::Event", tags = "2, 3, 4, 6, 7, 8, 9, 10, 11")]
pub event: ::core::option::Option<event_envelope::Event>,
}
/// Nested message and enum types in `EventEnvelope`.
Expand All @@ -33,6 +33,16 @@ pub mod event_envelope {
PaymentForwarded(super::PaymentForwarded),
#[prost(message, tag = "7")]
PaymentClaimable(super::PaymentClaimable),
/// Channel-lifecycle events (RFC #134 parent scope).
#[prost(message, tag = "8")]
ChannelPending(super::ChannelPending),
#[prost(message, tag = "9")]
ChannelReady(super::ChannelReady),
#[prost(message, tag = "10")]
ChannelClosed(super::ChannelClosed),
/// Per-commitment-update event carrying the cryptographic attestation bundle.
#[prost(message, tag = "11")]
ChannelCommitmentUpdated(super::ChannelCommitmentUpdated),
}
}
/// PaymentReceived indicates a payment has been received.
Expand Down Expand Up @@ -85,3 +95,142 @@ pub struct PaymentForwarded {
#[prost(message, optional, tag = "1")]
pub forwarded_payment: ::core::option::Option<super::types::ForwardedPayment>,
}
/// ChannelPending indicates a new channel has been created and is awaiting
/// on-chain funding confirmation.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChannelPending {
/// Hex-encoded channel id.
#[prost(string, tag = "1")]
pub channel_id: ::prost::alloc::string::String,
/// Hex-encoded user-assigned channel id (16 bytes).
#[prost(string, tag = "2")]
pub user_channel_id: ::prost::alloc::string::String,
/// Pubkey of the counterparty node, hex-encoded.
#[prost(string, tag = "3")]
pub counterparty_node_id: ::prost::alloc::string::String,
/// Funding outpoint (txid:vout) as observed on chain.
#[prost(string, tag = "4")]
pub funding_txid: ::prost::alloc::string::String,
#[prost(uint32, tag = "5")]
pub funding_output_index: u32,
}
/// ChannelReady indicates a channel has reached the confirmed / usable state.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChannelReady {
#[prost(string, tag = "1")]
pub channel_id: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub user_channel_id: ::prost::alloc::string::String,
/// May be absent in edge cases (see ldk-node docs).
#[prost(string, optional, tag = "3")]
pub counterparty_node_id: ::core::option::Option<::prost::alloc::string::String>,
}
/// ChannelClosed indicates a channel has been closed.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChannelClosed {
#[prost(string, tag = "1")]
pub channel_id: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub user_channel_id: ::prost::alloc::string::String,
#[prost(string, optional, tag = "3")]
pub counterparty_node_id: ::core::option::Option<::prost::alloc::string::String>,
/// Free-form closure reason string as surfaced by ldk-node.
#[prost(string, optional, tag = "4")]
pub reason: ::core::option::Option<::prost::alloc::string::String>,
}
/// ChannelCommitmentUpdated fires once per commitment-state update and carries
/// the cryptographic bundle an external attestor needs to verify channel state
/// against the counterparty's funding pubkey.
///
/// See the RFC filed on ldk-server:
/// "include commitment-bundle data on channel-state events (extends #134)"
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChannelCommitmentUpdated {
/// Channel identity.
#[prost(string, tag = "1")]
pub channel_id: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub counterparty_node_id: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub funding_txid: ::prost::alloc::string::String,
#[prost(uint32, tag = "4")]
pub funding_outnum: u32,
/// Monotonic — increments with every new state.
#[prost(uint64, tag = "5")]
pub commitment_number: u64,
/// The cryptographic bundle the attestor cares about.
#[prost(message, optional, tag = "6")]
pub bundle: ::core::option::Option<ChannelCommitmentBundle>,
}
/// ChannelCommitmentBundle is the verifiable per-channel-state payload.
///
/// An external verifier reconstructs the BIP-143 sighash over
/// holder_commitment_tx using the 2-of-2 funding redeem script assembled from
/// holder_funding_pubkey / counterparty_funding_pubkey and verifies
/// counterparty_signature against counterparty_funding_pubkey. That closes
/// the loop independent of the operator.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChannelCommitmentBundle {
/// Raw wire-format unsigned commitment tx (no witnesses).
#[prost(bytes = "bytes", tag = "1")]
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_bytes_hex"))]
pub holder_commitment_tx: ::prost::bytes::Bytes,
/// Counterparty's ECDSA signature over holder_commitment_tx (BIP-143
/// sighash, SIGHASH_ALL). Receivers should try DER first and fall back to
/// a 64-byte raw (r||s) encoding.
#[prost(bytes = "bytes", tag = "2")]
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_bytes_hex"))]
pub counterparty_signature: ::prost::bytes::Bytes,
/// 33-byte compressed pubkeys of both funding-output participants.
#[prost(bytes = "bytes", tag = "3")]
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_bytes_hex"))]
pub holder_funding_pubkey: ::prost::bytes::Bytes,
#[prost(bytes = "bytes", tag = "4")]
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_bytes_hex"))]
pub counterparty_funding_pubkey: ::prost::bytes::Bytes,
#[prost(uint64, tag = "5")]
pub capacity_sats: u64,
#[prost(uint64, tag = "6")]
pub holder_balance_msat: u64,
#[prost(uint64, tag = "7")]
pub counterparty_balance_msat: u64,
#[prost(uint64, tag = "8")]
pub commit_fee_sats: u64,
#[prost(message, repeated, tag = "9")]
pub pending_htlcs: ::prost::alloc::vec::Vec<AttestationHtlc>,
/// Channel type marker, e.g. "anchors", "static_remotekey", "taproot".
#[prost(string, tag = "10")]
pub channel_type: ::prost::alloc::string::String,
}
/// AttestationHtlc describes a single pending HTLC as recorded on the commitment.
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AttestationHtlc {
/// True if we sent this HTLC (offered), false if received.
#[prost(bool, tag = "1")]
pub offered: bool,
#[prost(uint64, tag = "2")]
pub amount_msat: u64,
#[prost(bytes = "bytes", tag = "3")]
#[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde_utils::serialize_bytes_hex"))]
pub payment_hash: ::prost::bytes::Bytes,
#[prost(uint32, tag = "4")]
pub cltv_expiry: u32,
}
Loading