lightning/offers/
parse.rs

1// This file is Copyright its original authors, visible in version control
2// history.
3//
4// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7// You may not use this file except in accordance with one or both of these
8// licenses.
9
10//! Parsing and formatting for bech32 message encoding.
11
12use bitcoin::secp256k1;
13use crate::io;
14use crate::ln::msgs::DecodeError;
15use crate::util::ser::CursorReadable;
16use bech32::primitives::decode::CheckedHrpstringError;
17
18#[allow(unused_imports)]
19use crate::prelude::*;
20
21#[cfg(not(fuzzing))]
22pub(super) use sealed::Bech32Encode;
23
24#[cfg(fuzzing)]
25pub use sealed::Bech32Encode;
26
27mod sealed {
28	use bech32::{EncodeError, Hrp, NoChecksum, encode_to_fmt};
29	use bech32::primitives::decode::CheckedHrpstring;
30	use core::fmt;
31	use super::Bolt12ParseError;
32
33	#[allow(unused_imports)]
34	use crate::prelude::*;
35
36	/// Indicates a message can be encoded using bech32.
37	pub trait Bech32Encode: AsRef<[u8]> + TryFrom<Vec<u8>, Error=Bolt12ParseError> {
38		/// Human readable part of the message's bech32 encoding.
39		const BECH32_HRP: &'static str;
40
41		/// Parses a bech32-encoded message into a TLV stream.
42		fn from_bech32_str(s: &str) -> Result<Self, Bolt12ParseError> {
43			// Offer encoding may be split by '+' followed by optional whitespace.
44			let encoded = match s.split('+').skip(1).next() {
45				Some(_) => {
46					for chunk in s.split('+') {
47						let chunk = chunk.trim_start();
48						if chunk.is_empty() || chunk.contains(char::is_whitespace) {
49							return Err(Bolt12ParseError::InvalidContinuation);
50						}
51					}
52
53					let s: String = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect();
54					Bech32String::Owned(s)
55				},
56				None => Bech32String::Borrowed(s),
57			};
58
59			let parsed = CheckedHrpstring::new::<NoChecksum>(encoded.as_ref())?;
60			let hrp = parsed.hrp();
61			// Compare the lowercase'd iter to allow for all-uppercase HRPs
62			if hrp.lowercase_char_iter().ne(Self::BECH32_HRP.chars()) {
63				return Err(Bolt12ParseError::InvalidBech32Hrp);
64			}
65
66			let data = parsed.byte_iter().collect::<Vec<u8>>();
67			Self::try_from(data)
68		}
69
70		/// Formats the message using bech32-encoding.
71		fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
72			encode_to_fmt::<NoChecksum, _>(f, Hrp::parse(Self::BECH32_HRP).unwrap(), self.as_ref())
73				.map_err(|e| match e {
74					EncodeError::Fmt(e) => e,
75					_ => fmt::Error {},
76				})
77		}
78	}
79
80	// Used to avoid copying a bech32 string not containing the continuation character (+).
81	enum Bech32String<'a> {
82		Borrowed(&'a str),
83		Owned(String),
84	}
85
86	impl<'a> AsRef<str> for Bech32String<'a> {
87		fn as_ref(&self) -> &str {
88			match self {
89				Bech32String::Borrowed(s) => s,
90				Bech32String::Owned(s) => s,
91			}
92		}
93	}
94}
95
96/// A wrapper for reading a message as a TLV stream `T` from a byte sequence, while still
97/// maintaining ownership of the bytes for later use.
98pub(super) struct ParsedMessage<T: CursorReadable> {
99	pub bytes: Vec<u8>,
100	pub tlv_stream: T,
101}
102
103impl<T: CursorReadable> TryFrom<Vec<u8>> for ParsedMessage<T> {
104	type Error = DecodeError;
105
106	fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
107		let mut cursor = io::Cursor::new(bytes);
108		let tlv_stream: T = CursorReadable::read(&mut cursor)?;
109
110		// Ensure that there are no more TLV records left to parse.
111		if cursor.position() < cursor.get_ref().len() as u64 {
112			return Err(DecodeError::InvalidValue);
113		}
114
115		let bytes = cursor.into_inner();
116		Ok(Self { bytes, tlv_stream })
117	}
118}
119
120/// Error when parsing a bech32 encoded message using [`str::parse`].
121#[derive(Clone, Debug, PartialEq)]
122pub enum Bolt12ParseError {
123	/// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
124	/// across multiple parts (i.e., '+' followed by whitespace).
125	InvalidContinuation,
126	/// The bech32 encoding's human-readable part does not match what was expected for the message
127	/// being parsed.
128	InvalidBech32Hrp,
129	/// The string could not be bech32 decoded.
130	Bech32(CheckedHrpstringError),
131	/// The bech32 decoded string could not be decoded as the expected message type.
132	Decode(DecodeError),
133	/// The parsed message has invalid semantics.
134	InvalidSemantics(Bolt12SemanticError),
135	/// The parsed message has an invalid signature.
136	InvalidSignature(secp256k1::Error),
137}
138
139/// Error when interpreting a TLV stream as a specific type.
140#[derive(Clone, Debug, PartialEq)]
141pub enum Bolt12SemanticError {
142	/// The current system time is past the offer or invoice's expiration.
143	AlreadyExpired,
144	/// The provided chain hash does not correspond to a supported chain.
145	UnsupportedChain,
146	/// A chain was provided but was not expected.
147	UnexpectedChain,
148	/// An amount was expected but was missing.
149	MissingAmount,
150	/// The amount exceeded the total bitcoin supply or didn't match an expected amount.
151	InvalidAmount,
152	/// An amount was provided but was not sufficient in value.
153	InsufficientAmount,
154	/// An amount was provided but was not expected.
155	UnexpectedAmount,
156	/// A currency was provided that is not supported.
157	UnsupportedCurrency,
158	/// A feature was required but is unknown.
159	UnknownRequiredFeatures,
160	/// Features were provided but were not expected.
161	UnexpectedFeatures,
162	/// A required description was not provided.
163	MissingDescription,
164	/// An issuer's signing pubkey was not provided.
165	MissingIssuerSigningPubkey,
166	/// An issuer's signing pubkey was provided but was not expected.
167	UnexpectedIssuerSigningPubkey,
168	/// A quantity was expected but was missing.
169	MissingQuantity,
170	/// An unsupported quantity was provided.
171	InvalidQuantity,
172	/// A quantity or quantity bounds was provided but was not expected.
173	UnexpectedQuantity,
174	/// Metadata could not be used to verify the offers message.
175	InvalidMetadata,
176	/// Metadata was provided but was not expected.
177	UnexpectedMetadata,
178	/// Payer metadata was expected but was missing.
179	MissingPayerMetadata,
180	/// A payer signing pubkey was expected but was missing.
181	MissingPayerSigningPubkey,
182	/// The payment id for a refund or request is already in use.
183	DuplicatePaymentId,
184	/// Blinded paths were expected but were missing.
185	MissingPaths,
186	/// Blinded paths were provided but were not expected.
187	UnexpectedPaths,
188	/// The blinded payinfo given does not match the number of blinded path hops.
189	InvalidPayInfo,
190	/// An invoice creation time was expected but was missing.
191	MissingCreationTime,
192	/// An invoice payment hash was expected but was missing.
193	MissingPaymentHash,
194	/// An invoice payment hash was provided but was not expected.
195	UnexpectedPaymentHash,
196	/// A signing pubkey was not provided.
197	MissingSigningPubkey,
198	/// A signing pubkey was provided but a different one was expected.
199	InvalidSigningPubkey,
200	/// A signature was expected but was missing.
201	MissingSignature,
202	/// A Human Readable Name was provided but was not expected (i.e. was included in a
203	/// [`Refund`]).
204	///
205	/// [`Refund`]: super::refund::Refund
206	UnexpectedHumanReadableName,
207}
208
209impl From<CheckedHrpstringError> for Bolt12ParseError {
210	fn from(error: CheckedHrpstringError) -> Self {
211		Self::Bech32(error)
212	}
213}
214
215impl From<DecodeError> for Bolt12ParseError {
216	fn from(error: DecodeError) -> Self {
217		Self::Decode(error)
218	}
219}
220
221impl From<Bolt12SemanticError> for Bolt12ParseError {
222	fn from(error: Bolt12SemanticError) -> Self {
223		Self::InvalidSemantics(error)
224	}
225}
226
227impl From<secp256k1::Error> for Bolt12ParseError {
228	fn from(error: secp256k1::Error) -> Self {
229		Self::InvalidSignature(error)
230	}
231}
232
233#[cfg(test)]
234mod bolt12_tests {
235	use super::Bolt12ParseError;
236	use crate::offers::offer::Offer;
237	use bech32::primitives::decode::{CheckedHrpstringError, UncheckedHrpstringError, CharError};
238
239	#[test]
240	fn encodes_offer_as_bech32_without_checksum() {
241		let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg";
242		let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
243		let reencoded_offer = offer.to_string();
244		dbg!(reencoded_offer.parse::<Offer>().unwrap());
245		assert_eq!(reencoded_offer, encoded_offer);
246	}
247
248	#[test]
249	fn parses_bech32_encoded_offers() {
250		let offers = [
251			// A complete string is valid
252			"lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
253
254			// Uppercase is valid
255			"LNO1PQPS7SJQPGTYZM3QV4UXZMTSD3JJQER9WD3HY6TSW35K7MSJZFPY7NZ5YQCNYGRFDEJ82UM5WF5K2UCKYYPWA3EYT44H6TXTXQUQH7LZ5DJGE4AFGFJN7K4RGRKUAG0JSD5XVXG",
256
257			// + can join anywhere
258			"l+no1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
259
260			// Multiple + can join
261			"lno1pqps7sjqpgt+yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+5k7msjzfpy7nz5yqcn+ygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+5xvxg",
262
263			// + can be followed by whitespace
264			"lno1pqps7sjqpgt+ yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+  5k7msjzfpy7nz5yqcn+\nygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+\r\n 5xvxg",
265		];
266		for encoded_offer in &offers {
267			if let Err(e) = encoded_offer.parse::<Offer>() {
268				panic!("Invalid offer ({:?}): {}", e, encoded_offer);
269			}
270		}
271	}
272
273	#[test]
274	fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
275		let offers = [
276			// + must be surrounded by bech32 characters
277			"lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+",
278			"lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+ ",
279			"+lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
280			"+ lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
281			"ln++o1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
282		];
283		for encoded_offer in &offers {
284			match encoded_offer.parse::<Offer>() {
285				Ok(_) => panic!("Valid offer: {}", encoded_offer),
286				Err(e) => assert_eq!(e, Bolt12ParseError::InvalidContinuation),
287			}
288		}
289	}
290
291	#[test]
292	fn fails_parsing_bech32_encoded_offers_with_mixed_casing() {
293		// We assert that mixed-case encoding fails to parse.
294		let mixed_case_offer = "LnO1PqPs7sJqPgTyZm3qV4UxZmTsD3JjQeR9Wd3hY6TsW35k7mSjZfPy7nZ5YqCnYgRfDeJ82uM5Wf5k2uCkYyPwA3EyT44h6tXtXqUqH7Lz5dJgE4AfGfJn7k4rGrKuAg0jSd5xVxG";
295		match mixed_case_offer.parse::<Offer>() {
296			Ok(_) => panic!("Valid offer: {}", mixed_case_offer),
297			Err(e) => assert_eq!(e, Bolt12ParseError::Bech32(CheckedHrpstringError::Parse(UncheckedHrpstringError::Char(CharError::MixedCase)))),
298		}
299	}
300}
301
302#[cfg(test)]
303mod tests {
304	use super::Bolt12ParseError;
305	use crate::ln::msgs::DecodeError;
306	use crate::offers::offer::Offer;
307	use bech32::primitives::decode::{CharError, CheckedHrpstringError, UncheckedHrpstringError};
308
309	#[test]
310	fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
311		let encoded_offer = "lni1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg";
312		match encoded_offer.parse::<Offer>() {
313			Ok(_) => panic!("Valid offer: {}", encoded_offer),
314			Err(e) => assert_eq!(e, Bolt12ParseError::InvalidBech32Hrp),
315		}
316	}
317
318	#[test]
319	fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
320		let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxo";
321		match encoded_offer.parse::<Offer>() {
322			Ok(_) => panic!("Valid offer: {}", encoded_offer),
323			Err(e) => assert_eq!(e, Bolt12ParseError::Bech32(CheckedHrpstringError::Parse(UncheckedHrpstringError::Char(CharError::InvalidChar('o'))))),
324		}
325	}
326
327	#[test]
328	fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
329		let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxgqqqqq";
330		match encoded_offer.parse::<Offer>() {
331			Ok(_) => panic!("Valid offer: {}", encoded_offer),
332			Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
333		}
334	}
335}