bitcoin_payment_instructions/
http_resolver.rs

1//! A [`HrnResolver`] which uses `reqwest` and `dns.google` (8.8.8.8) to resolve Human Readable
2//! Names into bitcoin payment instructions.
3
4use 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/// An [`HrnResolver`] which uses `reqwest` and `dns.google` (8.8.8.8) to resolve Human Readable
27/// Names into bitcoin payment instructions.
28///
29/// Note that using this may reveal our IP address to the recipient and information about who we're
30/// paying to Google (via `dns.google`).
31#[derive(Debug, Clone)]
32pub struct HTTPHrnResolver {
33	client: reqwest::Client,
34}
35
36impl HTTPHrnResolver {
37	/// Create a new `HTTPHrnResolver` with a default `reqwest::Client`.
38	pub fn new() -> Self {
39		HTTPHrnResolver::default()
40	}
41
42	/// Create a new `HTTPHrnResolver` with a custom `reqwest::Client`.
43	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
54/// The "URL and Filename safe" Base64 Alphabet from RFC 4648
55const 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			// First try to resolve the HRN using BIP 353 DNSSEC proof building
193			match self.resolve_dns(hrn).await {
194				Ok(r) => Ok(r),
195				Err(e) if e == DNS_ERR => {
196					// If we got an error that might indicate the recipient doesn't support BIP
197					// 353, try LN-Address via LNURL
198					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		// RFC 4648
264		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		// Wikipedia
271		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			// lnurl encoding for lnurltest@bitcoin.ninja
375			"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}