1use std::boxed::Box;
5use std::fmt::Write;
6use std::str::FromStr;
7
8use serde::Deserialize;
9
10use bitcoin::hashes::sha256::Hash as Sha256;
11use bitcoin::hashes::Hash as _;
12
13use dnssec_prover::query::{ProofBuilder, QueryBuf};
14use dnssec_prover::rr::{Name, TXT_TYPE};
15
16use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef};
17
18use crate::amount::Amount;
19use crate::dnssec_utils::resolve_proof;
20use crate::hrn_resolution::{
21 HrnResolution, HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture,
22};
23
24const DOH_ENDPOINT: &'static str = "https://dns.google/dns-query?dns=";
25
26#[derive(Debug, Clone)]
32pub struct HTTPHrnResolver {
33 client: reqwest::Client,
34}
35
36impl HTTPHrnResolver {
37 pub fn new() -> Self {
39 HTTPHrnResolver::default()
40 }
41
42 pub fn with_client(client: reqwest::Client) -> Self {
44 HTTPHrnResolver { client }
45 }
46}
47
48impl Default for HTTPHrnResolver {
49 fn default() -> Self {
50 HTTPHrnResolver { client: reqwest::Client::new() }
51 }
52}
53
54const B64_CHAR: [u8; 64] = [
56 b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P',
57 b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'a', b'b', b'c', b'd', b'e', b'f',
58 b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v',
59 b'w', b'x', b'y', b'z', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'-', b'_',
60];
61
62#[rustfmt::skip]
63fn write_base64(mut bytes: &[u8], out: &mut String) {
64 while bytes.len() >= 3 {
65 let (byte_a, byte_b, byte_c) = (bytes[0] as usize, bytes[1] as usize, bytes[2] as usize);
66 out.push(B64_CHAR[ (byte_a & 0b1111_1100) >> 2] as char);
67 out.push(B64_CHAR[((byte_a & 0b0000_0011) << 4) | ((byte_b & 0b1111_0000) >> 4)] as char);
68 out.push(B64_CHAR[((byte_b & 0b0000_1111) << 2) | ((byte_c & 0b1100_0000) >> 6)] as char);
69 out.push(B64_CHAR[ byte_c & 0b0011_1111] as char);
70 bytes = &bytes[3..];
71 }
72 match bytes.len() {
73 2 => {
74 let (byte_a, byte_b, byte_c) = (bytes[0] as usize, bytes[1] as usize, 0usize);
75 out.push(B64_CHAR[ (byte_a & 0b1111_1100) >> 2] as char);
76 out.push(B64_CHAR[((byte_a & 0b0000_0011) << 4) | ((byte_b & 0b1111_0000) >> 4)] as char);
77 out.push(B64_CHAR[((byte_b & 0b0000_1111) << 2) | ((byte_c & 0b1100_0000) >> 6)] as char);
78 },
79 1 => {
80 let (byte_a, byte_b) = (bytes[0] as usize, 0usize);
81 out.push(B64_CHAR[ (byte_a & 0b1111_1100) >> 2] as char);
82 out.push(B64_CHAR[((byte_a & 0b0000_0011) << 4) | ((byte_b & 0b1111_0000) >> 4)] as char);
83 },
84 _ => debug_assert_eq!(bytes.len(), 0),
85 }
86}
87
88fn query_to_url(query: QueryBuf) -> String {
89 let base64_len = (query.len() * 8 + 5) / 6;
90 let mut query_string = String::with_capacity(base64_len + DOH_ENDPOINT.len());
91
92 query_string += DOH_ENDPOINT;
93 write_base64(&query[..], &mut query_string);
94
95 debug_assert_eq!(query_string.len(), base64_len + DOH_ENDPOINT.len());
96
97 query_string
98}
99
100#[derive(Deserialize)]
101struct LNURLInitResponse {
102 callback: String,
103 #[serde(rename = "maxSendable")]
104 max_sendable: u64,
105 #[serde(rename = "minSendable")]
106 min_sendable: u64,
107 metadata: String,
108 tag: String,
109}
110
111#[derive(Deserialize)]
112struct LNURLMetadata(Vec<(String, String)>);
113
114#[derive(Deserialize)]
115struct LNURLCallbackResponse {
116 pr: String,
117 routes: Vec<String>,
118}
119
120const DNS_ERR: &'static str = "DNS Request to dns.google failed";
121
122impl HTTPHrnResolver {
123 async fn resolve_dns(&self, hrn: &HumanReadableName) -> Result<HrnResolution, &'static str> {
124 let dns_name =
125 Name::try_from(format!("{}.user._bitcoin-payment.{}.", hrn.user(), hrn.domain()))
126 .map_err(|_| "The provided HRN was too long to fit in a DNS name")?;
127 let (mut proof_builder, initial_query) = ProofBuilder::new(&dns_name, TXT_TYPE);
128 let mut pending_queries = vec![initial_query];
129
130 while let Some(query) = pending_queries.pop() {
131 let request_url = query_to_url(query);
132 let req =
133 self.client.get(request_url).header("accept", "application/dns-message").build();
134 let resp = self.client.execute(req.map_err(|_| DNS_ERR)?).await.map_err(|_| DNS_ERR)?;
135 let body = resp.bytes().await.map_err(|_| DNS_ERR)?;
136
137 let mut answer = QueryBuf::new_zeroed(0);
138 answer.extend_from_slice(&body[..]);
139 match proof_builder.process_response(&answer) {
140 Ok(queries) => {
141 for query in queries {
142 pending_queries.push(query);
143 }
144 },
145 Err(_) => {
146 return Err(DNS_ERR);
147 },
148 }
149 }
150
151 let err = "Too many queries required to build proof";
152 let proof = proof_builder.finish_proof().map(|(proof, _ttl)| proof).map_err(|()| err)?;
153
154 resolve_proof(&dns_name, proof)
155 }
156
157 async fn resolve_lnurl_impl(&self, lnurl_url: &str) -> Result<HrnResolution, &'static str> {
158 let err = "Failed to fetch LN-Address initial well-known endpoint";
159 let init_result = self.client.get(lnurl_url).send().await.map_err(|_| err)?;
160 let init: LNURLInitResponse = init_result.json().await.map_err(|_| err)?;
161
162 if init.tag != "payRequest" {
163 return Err("LNURL initial init_response had an incorrect tag value");
164 }
165 if init.min_sendable > init.max_sendable {
166 return Err("LNURL initial init_response had no sendable amounts");
167 }
168
169 let err = "LNURL metadata was not in the correct format";
170 let metadata: LNURLMetadata = serde_json::from_str(&init.metadata).map_err(|_| err)?;
171 let mut recipient_description = None;
172 for (ty, value) in metadata.0 {
173 if ty == "text/plain" {
174 recipient_description = Some(value);
175 }
176 }
177 let expected_description_hash = Sha256::hash(init.metadata.as_bytes()).to_byte_array();
178 Ok(HrnResolution::LNURLPay {
179 min_value: Amount::from_milli_sats(init.min_sendable)
180 .map_err(|_| "LNURL initial response had a minimum amount greater than 21M BTC")?,
181 max_value: Amount::from_milli_sats(init.max_sendable).unwrap_or(Amount::MAX),
182 callback: init.callback,
183 expected_description_hash,
184 recipient_description,
185 })
186 }
187}
188
189impl HrnResolver for HTTPHrnResolver {
190 fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> {
191 Box::pin(async move {
192 match self.resolve_dns(hrn).await {
194 Ok(r) => Ok(r),
195 Err(e) if e == DNS_ERR => {
196 let init_url =
199 format!("https://{}/.well-known/lnurlp/{}", hrn.domain(), hrn.user());
200 self.resolve_lnurl(&init_url).await
201 },
202 Err(e) => Err(e),
203 }
204 })
205 }
206
207 fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> {
208 Box::pin(async move { self.resolve_lnurl_impl(url).await })
209 }
210
211 fn resolve_lnurl_to_invoice<'a>(
212 &'a self, mut callback: String, amt: Amount, expected_description_hash: [u8; 32],
213 ) -> LNURLResolutionFuture<'a> {
214 Box::pin(async move {
215 let err = "LN-Address callback failed";
216 if callback.contains('?') {
217 write!(&mut callback, "&amount={}", amt.milli_sats()).expect("Write to String");
218 } else {
219 write!(&mut callback, "?amount={}", amt.milli_sats()).expect("Write to String");
220 }
221 let http_response = self.client.get(callback).send().await.map_err(|_| err)?;
222 let response: LNURLCallbackResponse = http_response.json().await.map_err(|_| err)?;
223
224 if !response.routes.is_empty() {
225 return Err("LNURL callback response contained a non-empty routes array");
226 }
227
228 let invoice = Bolt11Invoice::from_str(&response.pr).map_err(|_| err)?;
229 if invoice.amount_milli_satoshis() != Some(amt.milli_sats()) {
230 return Err("LNURL callback response contained an invoice with the wrong amount");
231 }
232 match invoice.description() {
233 Bolt11InvoiceDescriptionRef::Hash(hash) => {
234 if hash.0.as_byte_array() != &expected_description_hash {
235 Err("Incorrect invoice description hash")
236 } else {
237 Ok(invoice)
238 }
239 },
240 Bolt11InvoiceDescriptionRef::Direct(_) => {
241 Err("BOLT 11 invoice resolved via LNURL must have a matching description hash")
242 },
243 }
244 })
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::*;
252
253 fn to_base64(bytes: &[u8]) -> String {
254 let expected_len = (bytes.len() * 8 + 5) / 6;
255 let mut res = String::with_capacity(expected_len);
256 write_base64(bytes, &mut res);
257 assert_eq!(res.len(), expected_len);
258 res
259 }
260
261 #[test]
262 fn test_base64() {
263 assert_eq!(&to_base64(b"f"), "Zg");
265 assert_eq!(&to_base64(b"fo"), "Zm8");
266 assert_eq!(&to_base64(b"foo"), "Zm9v");
267 assert_eq!(&to_base64(b"foob"), "Zm9vYg");
268 assert_eq!(&to_base64(b"fooba"), "Zm9vYmE");
269 assert_eq!(&to_base64(b"foobar"), "Zm9vYmFy");
270 assert_eq!(
272 &to_base64(b"Many hands make light work."),
273 "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu"
274 );
275 assert_eq!(&to_base64(b"Man"), "TWFu");
276 }
277
278 #[tokio::test]
279 async fn test_dns_via_http_hrn_resolver() {
280 let resolver = HTTPHrnResolver::default();
281 let instructions = PaymentInstructions::parse(
282 "send.some@satsto.me",
283 bitcoin::Network::Bitcoin,
284 &resolver,
285 true,
286 )
287 .await
288 .unwrap();
289
290 let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions {
291 assert_eq!(instr.min_amt(), None);
292 assert_eq!(instr.max_amt(), None);
293
294 assert_eq!(instr.pop_callback(), None);
295 assert!(instr.bip_353_dnssec_proof().is_some());
296
297 let hrn = instr.human_readable_name().as_ref().unwrap();
298 assert_eq!(hrn.user(), "send.some");
299 assert_eq!(hrn.domain(), "satsto.me");
300
301 instr.set_amount(Amount::from_sats(100_000).unwrap(), &resolver).await.unwrap()
302 } else {
303 panic!();
304 };
305
306 assert_eq!(resolved.pop_callback(), None);
307 assert!(resolved.bip_353_dnssec_proof().is_some());
308
309 let hrn = resolved.human_readable_name().as_ref().unwrap();
310 assert_eq!(hrn.user(), "send.some");
311 assert_eq!(hrn.domain(), "satsto.me");
312
313 for method in resolved.methods() {
314 match method {
315 PaymentMethod::LightningBolt11(_) => {
316 panic!("Should only have static payment instructions");
317 },
318 PaymentMethod::LightningBolt12(_) => {},
319 PaymentMethod::OnChain { .. } => {},
320 }
321 }
322 }
323
324 #[tokio::test]
325 async fn test_http_hrn_resolver() {
326 let resolver = HTTPHrnResolver::default();
327 let instructions = PaymentInstructions::parse(
328 "lnurltest@bitcoin.ninja",
329 bitcoin::Network::Bitcoin,
330 &resolver,
331 true,
332 )
333 .await
334 .unwrap();
335
336 let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions {
337 assert!(instr.min_amt().is_some());
338 assert!(instr.max_amt().is_some());
339
340 assert_eq!(instr.pop_callback(), None);
341 assert!(instr.bip_353_dnssec_proof().is_none());
342
343 let hrn = instr.human_readable_name().as_ref().unwrap();
344 assert_eq!(hrn.user(), "lnurltest");
345 assert_eq!(hrn.domain(), "bitcoin.ninja");
346
347 instr.set_amount(Amount::from_sats(100_000).unwrap(), &resolver).await.unwrap()
348 } else {
349 panic!();
350 };
351
352 assert_eq!(resolved.pop_callback(), None);
353 assert!(resolved.bip_353_dnssec_proof().is_none());
354
355 let hrn = resolved.human_readable_name().as_ref().unwrap();
356 assert_eq!(hrn.user(), "lnurltest");
357 assert_eq!(hrn.domain(), "bitcoin.ninja");
358
359 for method in resolved.methods() {
360 match method {
361 PaymentMethod::LightningBolt11(invoice) => {
362 assert_eq!(invoice.amount_milli_satoshis(), Some(100_000_000));
363 },
364 PaymentMethod::LightningBolt12(_) => panic!("Should only resolve to BOLT 11"),
365 PaymentMethod::OnChain(_) => panic!("Should only resolve to BOLT 11"),
366 }
367 }
368 }
369
370 #[tokio::test]
371 async fn test_http_lnurl_resolver() {
372 let resolver = HTTPHrnResolver::default();
373 let instructions = PaymentInstructions::parse(
374 "lnurl1dp68gurn8ghj7cnfw33k76tw9ehxjmn2vyhjuam9d3kz66mwdamkutmvde6hymrs9akxuatjd36x2um5ahcq39",
376 Network::Bitcoin,
377 &resolver,
378 true,
379 )
380 .await
381 .unwrap();
382
383 let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions {
384 assert!(instr.min_amt().is_some());
385 assert!(instr.max_amt().is_some());
386
387 assert_eq!(instr.pop_callback(), None);
388 assert!(instr.bip_353_dnssec_proof().is_none());
389
390 instr.set_amount(Amount::from_sats(100_000).unwrap(), &resolver).await.unwrap()
391 } else {
392 panic!();
393 };
394
395 assert_eq!(resolved.pop_callback(), None);
396 assert!(resolved.bip_353_dnssec_proof().is_none());
397
398 for method in resolved.methods() {
399 match method {
400 PaymentMethod::LightningBolt11(invoice) => {
401 assert_eq!(invoice.amount_milli_satoshis(), Some(100_000_000));
402 },
403 PaymentMethod::LightningBolt12(_) => panic!("Should only resolve to BOLT 11"),
404 PaymentMethod::OnChain(_) => panic!("Should only resolve to BOLT 11"),
405 }
406 }
407 }
408}