Skip to content

Commit 1e57b87

Browse files
authored
Merge pull request #233 from f3r10/feat/custom_preimage_in_spontaneous_send
feat: allow setting custom preimage in spontaneous-send
2 parents 7ffabc1 + 09435fc commit 1e57b87

7 files changed

Lines changed: 145 additions & 9 deletions

File tree

e2e-tests/tests/e2e.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use ldk_server_client::ldk_server_grpc::events::{
3333
use ldk_server_client::ldk_server_grpc::types::{
3434
bolt11_invoice_description, Bolt11InvoiceDescription,
3535
};
36+
use ldk_server_grpc::types::payment_kind;
3637

3738
const EVENT_TIMEOUT: Duration = Duration::from_secs(15);
3839

@@ -1531,3 +1532,40 @@ async fn test_metrics_endpoint_with_auth() {
15311532
assert!(metrics.contains("ldk_server_total_anchor_channels_reserve_sats 0"));
15321533
assert!(metrics.contains("ldk_server_total_lightning_balance_sats 0"));
15331534
}
1535+
1536+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1537+
async fn test_cli_spontaneous_send_with_preimage() {
1538+
let bitcoind = TestBitcoind::new();
1539+
let server_a = LdkServerHandle::start(&bitcoind).await;
1540+
let server_b = LdkServerHandle::start(&bitcoind).await;
1541+
1542+
let mut events_b = server_b.client().subscribe_events().await.unwrap();
1543+
1544+
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
1545+
1546+
// Generate a known preimage and compute its payment hash
1547+
let preimage_bytes = [43u8; 32];
1548+
let preimage_hex = preimage_bytes.to_lower_hex_string();
1549+
let payment_hash = sha256::Hash::hash(&preimage_bytes);
1550+
let payment_hash_hex = payment_hash.to_byte_array().to_lower_hex_string();
1551+
1552+
let output = run_cli(
1553+
&server_a,
1554+
&["spontaneous-send", server_b.node_id(), "10000sat", "--preimage", &preimage_hex],
1555+
);
1556+
1557+
assert!(!output["payment_id"].as_str().unwrap().is_empty());
1558+
1559+
// The receiver must observe in PaymentReceived for checking on Spontaneous payment.
1560+
let event_b = wait_for_event(&mut events_b, |e| matches!(e, Event::PaymentReceived(_))).await;
1561+
let Some(Event::PaymentReceived(pr)) = event_b.event else {
1562+
panic!("expected PaymentReceived");
1563+
};
1564+
1565+
let payment = pr.payment.unwrap();
1566+
1567+
let Some(payment_kind::Kind::Spontaneous(spont)) = payment.kind.unwrap().kind else {
1568+
panic!("expected spontaneous kind");
1569+
};
1570+
assert_eq!(spont.hash, payment_hash_hex);
1571+
}

ldk-server-cli/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ use serde::Serialize;
5656
use serde_json::{json, Value};
5757
use types::{
5858
Amount, CliListForwardedPaymentsResponse, CliListPaymentsResponse, CliPaginatedResponse,
59+
Preimage,
5960
};
6061

6162
mod types;
@@ -322,6 +323,11 @@ enum Commands {
322323
help = "Custom TLV record to attach, format: <type_num>:<hex_value>. Repeatable. type_num must be >= 65536."
323324
)]
324325
custom_tlvs: Vec<(u64, Vec<u8>)>,
326+
#[arg(
327+
long,
328+
help = "An optional hex-encoded 32-byte payment preimage. If provided, it will be used instead of generating a random one."
329+
)]
330+
preimage: Option<Preimage>,
325331
},
326332
#[command(
327333
about = "Pay a BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer"
@@ -819,6 +825,7 @@ async fn main() {
819825
max_path_count,
820826
max_channel_saturation_power_of_half,
821827
custom_tlvs,
828+
preimage,
822829
} => {
823830
let amount_msat = amount.to_msat();
824831
let max_total_routing_fee_msat = max_total_routing_fee.map(|a| a.to_msat());
@@ -843,6 +850,7 @@ async fn main() {
843850
node_id,
844851
route_parameters: Some(route_parameters),
845852
custom_tlvs: proto_custom_tlvs,
853+
preimage: preimage.map(|p| p.to_hex_string()),
846854
})
847855
.await,
848856
);

ldk-server-cli/src/types.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use std::fmt;
1717
use std::str::FromStr;
1818

19+
use hex_conservative::{DisplayHex, FromHex};
1920
use ldk_server_client::ldk_server_grpc::types::{ForwardedPayment, PageToken, Payment};
2021
use serde::Serialize;
2122

@@ -119,6 +120,26 @@ impl FromStr for Amount {
119120
}
120121
}
121122

123+
/// A validated 32-byte payment preimage, parsed from a 64-character hex string.
124+
#[derive(Debug, Clone)]
125+
pub struct Preimage(pub [u8; 32]);
126+
127+
impl Preimage {
128+
pub fn to_hex_string(&self) -> String {
129+
self.0.to_lower_hex_string()
130+
}
131+
}
132+
133+
impl FromStr for Preimage {
134+
type Err = String;
135+
136+
fn from_str(s: &str) -> Result<Self, Self::Err> {
137+
<[u8; 32]>::from_hex(s)
138+
.map(Preimage)
139+
.map_err(|_| "must be a 64-character hex string (32 bytes)".to_string())
140+
}
141+
}
142+
122143
#[cfg(test)]
123144
mod tests {
124145
use super::*;
@@ -185,4 +206,25 @@ mod tests {
185206
let big = format!("{}sat", u64::MAX);
186207
assert!(Amount::from_str(&big).is_err());
187208
}
209+
210+
#[test]
211+
fn preimage_parsing_and_roundtrip() {
212+
// valid 64-char hex string
213+
let hex = "2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b";
214+
let preimage = Preimage::from_str(hex).unwrap();
215+
assert_eq!(preimage.0, [0x2b; 32]);
216+
assert_eq!(preimage.to_hex_string(), hex);
217+
218+
// rejects empty string
219+
assert!(Preimage::from_str("").is_err());
220+
221+
// rejects too short (62 chars)
222+
assert!(Preimage::from_str(&"ab".repeat(31)).is_err());
223+
224+
// rejects too long (66 chars)
225+
assert!(Preimage::from_str(&"ab".repeat(33)).is_err());
226+
227+
// rejects non-hex characters
228+
assert!(Preimage::from_str(&"zz".repeat(32)).is_err());
229+
}
188230
}

ldk-server-grpc/src/api.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,10 @@ pub struct SpontaneousSendRequest {
474474
/// Custom TLV records to attach to the outgoing payment.
475475
#[prost(message, repeated, tag = "4")]
476476
pub custom_tlvs: ::prost::alloc::vec::Vec<super::types::CustomTlvRecord>,
477+
/// An optional hex-encoded 32-byte payment preimage. If provided, it will be used instead of
478+
/// generating a random one. The payment hash will be the SHA256 of this value.
479+
#[prost(string, optional, tag = "5")]
480+
pub preimage: ::core::option::Option<::prost::alloc::string::String>,
477481
}
478482
/// The response for the `SpontaneousSend` RPC. On failure, a gRPC error status is returned.
479483
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

ldk-server-grpc/src/proto/api.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ message SpontaneousSendRequest {
372372

373373
// Custom TLV records to attach to the outgoing payment.
374374
repeated types.CustomTlvRecord custom_tlvs = 4;
375+
376+
// An optional hex-encoded 32-byte payment preimage. If provided, it will be used instead of
377+
// generating a random one. The payment hash will be the SHA256 of this value.
378+
optional string preimage = 5;
375379
}
376380

377381
// The response for the `SpontaneousSend` RPC. On failure, a gRPC error status is returned.

ldk-server-mcp/src/tools/schema.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,11 @@ pub fn spontaneous_send_schema() -> Value {
374374
"type": "string",
375375
"description": "The hex-encoded public key of the destination node"
376376
},
377-
"route_parameters": route_parameters_config_schema()
377+
"route_parameters": route_parameters_config_schema(),
378+
"preimage": {
379+
"type": "string",
380+
"description": "The hex-encoded 32-byte payment preimage"
381+
}
378382
},
379383
"required": ["amount_msat", "node_id"]
380384
})

ldk-server/src/api/spontaneous_send.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
use std::str::FromStr;
1111
use std::sync::Arc;
1212

13+
use hex::FromHex;
1314
use ldk_node::bitcoin::secp256k1::PublicKey;
15+
use ldk_node::lightning_types::payment::PaymentPreimage;
16+
use ldk_node::CustomTlvRecord;
1417
use ldk_server_grpc::api::{SpontaneousSendRequest, SpontaneousSendResponse};
1518

1619
use crate::api::error::LdkServerError;
@@ -27,19 +30,52 @@ pub(crate) async fn handle_spontaneous_send_request(
2730

2831
let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?;
2932

30-
let payment_id = if request.custom_tlvs.is_empty() {
31-
context.node.spontaneous_payment().send(request.amount_msat, node_id, route_parameters)?
33+
let preimage = request
34+
.preimage
35+
.map(|p| {
36+
<[u8; 32]>::from_hex(&p).map(PaymentPreimage).map_err(|_| {
37+
LdkServerError::new(
38+
InvalidRequestError,
39+
"Invalid preimage, must be a 32-byte hex string.".to_string(),
40+
)
41+
})
42+
})
43+
.transpose()?;
44+
45+
let custom_tlvs: Option<Vec<CustomTlvRecord>> = if request.custom_tlvs.is_empty() {
46+
None
3247
} else {
33-
let custom_tlvs: Vec<_> =
34-
request.custom_tlvs.iter().map(proto_to_node_custom_tlv).collect();
35-
context.node.spontaneous_payment().send_with_custom_tlvs(
48+
Some(request.custom_tlvs.iter().map(proto_to_node_custom_tlv).collect())
49+
};
50+
51+
let payment_id = match (preimage, custom_tlvs) {
52+
(None, None) => context.node.spontaneous_payment().send(
53+
request.amount_msat,
54+
node_id,
55+
route_parameters,
56+
)?,
57+
(None, Some(custom_tlvs)) => context.node.spontaneous_payment().send_with_custom_tlvs(
3658
request.amount_msat,
3759
node_id,
3860
route_parameters,
3961
custom_tlvs,
40-
)?
62+
)?,
63+
(Some(preimage), None) => context.node.spontaneous_payment().send_with_preimage(
64+
request.amount_msat,
65+
node_id,
66+
preimage,
67+
route_parameters,
68+
)?,
69+
(Some(preimage), Some(custom_tlvs)) => {
70+
context.node.spontaneous_payment().send_with_preimage_and_custom_tlvs(
71+
request.amount_msat,
72+
node_id,
73+
custom_tlvs,
74+
preimage,
75+
route_parameters,
76+
)?
77+
},
4178
};
4279

43-
let response = SpontaneousSendResponse { payment_id: payment_id.to_string() };
44-
Ok(response)
80+
Ok(SpontaneousSendResponse { payment_id: payment_id.to_string() })
4581
}

0 commit comments

Comments
 (0)