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