use bitcoin::bech32;
use bitcoin::secp256k1;
-use core::convert::TryFrom;
use crate::io;
use crate::ln::msgs::DecodeError;
use crate::util::ser::SeekReadable;
+#[allow(unused_imports)]
use crate::prelude::*;
#[cfg(not(fuzzing))]
mod sealed {
use bitcoin::bech32;
use bitcoin::bech32::{FromBase32, ToBase32};
- use core::convert::TryFrom;
use core::fmt;
- use super::ParseError;
+ use super::Bolt12ParseError;
+ #[allow(unused_imports)]
use crate::prelude::*;
/// Indicates a message can be encoded using bech32.
- pub trait Bech32Encode: AsRef<[u8]> + TryFrom<Vec<u8>, Error=ParseError> {
+ pub trait Bech32Encode: AsRef<[u8]> + TryFrom<Vec<u8>, Error=Bolt12ParseError> {
/// Human readable part of the message's bech32 encoding.
const BECH32_HRP: &'static str;
/// Parses a bech32-encoded message into a TLV stream.
- fn from_bech32_str(s: &str) -> Result<Self, ParseError> {
+ fn from_bech32_str(s: &str) -> Result<Self, Bolt12ParseError> {
// Offer encoding may be split by '+' followed by optional whitespace.
let encoded = match s.split('+').skip(1).next() {
Some(_) => {
for chunk in s.split('+') {
let chunk = chunk.trim_start();
if chunk.is_empty() || chunk.contains(char::is_whitespace) {
- return Err(ParseError::InvalidContinuation);
+ return Err(Bolt12ParseError::InvalidContinuation);
}
}
let (hrp, data) = bech32::decode_without_checksum(encoded.as_ref())?;
if hrp != Self::BECH32_HRP {
- return Err(ParseError::InvalidBech32Hrp);
+ return Err(Bolt12ParseError::InvalidBech32Hrp);
}
let data = Vec::<u8>::from_base32(&data)?;
}
/// Error when parsing a bech32 encoded message using [`str::parse`].
-#[derive(Debug, PartialEq)]
-pub enum ParseError {
+#[derive(Clone, Debug, PartialEq)]
+pub enum Bolt12ParseError {
/// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
/// across multiple parts (i.e., '+' followed by whitespace).
InvalidContinuation,
/// The bech32 decoded string could not be decoded as the expected message type.
Decode(DecodeError),
/// The parsed message has invalid semantics.
- InvalidSemantics(SemanticError),
+ InvalidSemantics(Bolt12SemanticError),
/// The parsed message has an invalid signature.
InvalidSignature(secp256k1::Error),
}
/// Error when interpreting a TLV stream as a specific type.
-#[derive(Debug, PartialEq)]
-pub enum SemanticError {
+#[derive(Clone, Debug, PartialEq)]
+pub enum Bolt12SemanticError {
/// The current [`std::time::SystemTime`] is past the offer or invoice's expiration.
AlreadyExpired,
/// The provided chain hash does not correspond to a supported chain.
InvalidQuantity,
/// A quantity or quantity bounds was provided but was not expected.
UnexpectedQuantity,
+ /// Metadata could not be used to verify the offers message.
+ InvalidMetadata,
/// Metadata was provided but was not expected.
UnexpectedMetadata,
/// Payer metadata was expected but was missing.
MissingPayerMetadata,
/// A payer id was expected but was missing.
MissingPayerId,
+ /// The payment id for a refund or request is already in use.
+ DuplicatePaymentId,
/// Blinded paths were expected but were missing.
MissingPaths,
/// The blinded payinfo given does not match the number of blinded path hops.
MissingSignature,
}
-impl From<bech32::Error> for ParseError {
+impl From<bech32::Error> for Bolt12ParseError {
fn from(error: bech32::Error) -> Self {
Self::Bech32(error)
}
}
-impl From<DecodeError> for ParseError {
+impl From<DecodeError> for Bolt12ParseError {
fn from(error: DecodeError) -> Self {
Self::Decode(error)
}
}
-impl From<SemanticError> for ParseError {
- fn from(error: SemanticError) -> Self {
+impl From<Bolt12SemanticError> for Bolt12ParseError {
+ fn from(error: Bolt12SemanticError) -> Self {
Self::InvalidSemantics(error)
}
}
-impl From<secp256k1::Error> for ParseError {
+impl From<secp256k1::Error> for Bolt12ParseError {
fn from(error: secp256k1::Error) -> Self {
Self::InvalidSignature(error)
}
}
+
+#[cfg(test)]
+mod bolt12_tests {
+ use super::Bolt12ParseError;
+ use crate::offers::offer::Offer;
+
+ #[test]
+ fn encodes_offer_as_bech32_without_checksum() {
+ let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg";
+ let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
+ let reencoded_offer = offer.to_string();
+ dbg!(reencoded_offer.parse::<Offer>().unwrap());
+ assert_eq!(reencoded_offer, encoded_offer);
+ }
+
+ #[test]
+ fn parses_bech32_encoded_offers() {
+ let offers = [
+ // A complete string is valid
+ "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+
+ // + can join anywhere
+ "l+no1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+
+ // Multiple + can join
+ "lno1pqps7sjqpgt+yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+5k7msjzfpy7nz5yqcn+ygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+5xvxg",
+
+ // + can be followed by whitespace
+ "lno1pqps7sjqpgt+ yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+ 5k7msjzfpy7nz5yqcn+\nygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+\r\n 5xvxg",
+ ];
+ for encoded_offer in &offers {
+ if let Err(e) = encoded_offer.parse::<Offer>() {
+ panic!("Invalid offer ({:?}): {}", e, encoded_offer);
+ }
+ }
+ }
+
+ #[test]
+ fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
+ let offers = [
+ // + must be surrounded by bech32 characters
+ "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+",
+ "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+ ",
+ "+lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+ "+ lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+ "ln++o1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+ ];
+ for encoded_offer in &offers {
+ match encoded_offer.parse::<Offer>() {
+ Ok(_) => panic!("Valid offer: {}", encoded_offer),
+ Err(e) => assert_eq!(e, Bolt12ParseError::InvalidContinuation),
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Bolt12ParseError;
+ use bitcoin::bech32;
+ use crate::ln::msgs::DecodeError;
+ use crate::offers::offer::Offer;
+
+ #[test]
+ fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
+ let encoded_offer = "lni1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg";
+ match encoded_offer.parse::<Offer>() {
+ Ok(_) => panic!("Valid offer: {}", encoded_offer),
+ Err(e) => assert_eq!(e, Bolt12ParseError::InvalidBech32Hrp),
+ }
+ }
+
+ #[test]
+ fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
+ let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxo";
+ match encoded_offer.parse::<Offer>() {
+ Ok(_) => panic!("Valid offer: {}", encoded_offer),
+ Err(e) => assert_eq!(e, Bolt12ParseError::Bech32(bech32::Error::InvalidChar('o'))),
+ }
+ }
+
+ #[test]
+ fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
+ let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxgqqqqq";
+ match encoded_offer.parse::<Offer>() {
+ Ok(_) => panic!("Valid offer: {}", encoded_offer),
+ Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+ }
+ }
+}