Skip to content

Commit d4ff9a1

Browse files
committed
WIP: Offer semantic checks
1 parent 9152b15 commit d4ff9a1

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

lightning/src/offers/mod.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use bitcoin::hash_types::BlockHash;
1717
use bitcoin::network::constants::Network;
1818
use bitcoin::secp256k1::{PublicKey, XOnlyPublicKey};
1919
use bitcoin::secp256k1::schnorr::Signature;
20+
use core::convert::TryFrom;
2021
use core::str::FromStr;
2122
use core::time::Duration;
2223
use ln::PaymentHash;
@@ -132,6 +133,8 @@ pub enum Amount {
132133
},
133134
}
134135

136+
const ISO4217_CODE_LEN: usize = 3;
137+
135138
///
136139
#[derive(Debug)]
137140
pub enum Destination {
@@ -225,6 +228,31 @@ pub enum ParseError {
225228
Bech32(bech32::Error),
226229
/// The bech32 decoded string could not be decoded as the expected message type.
227230
Decode(DecodeError),
231+
/// The parsed message has invalid semantics.
232+
InvalidSemantics(SemanticError),
233+
}
234+
235+
#[derive(Debug, PartialEq)]
236+
///
237+
pub enum SemanticError {
238+
///
239+
UnsupportedChain,
240+
///
241+
UnexpectedCurrency,
242+
///
243+
InvalidCurrencyEncoding,
244+
///
245+
MissingDescription,
246+
///
247+
MissingDestination,
248+
///
249+
DuplicateDestination,
250+
///
251+
MissingPaths,
252+
///
253+
InvalidQuantity,
254+
///
255+
UnexpectedRefund,
228256
}
229257

230258
impl From<bech32::Error> for ParseError {
@@ -239,6 +267,111 @@ impl From<DecodeError> for ParseError {
239267
}
240268
}
241269

270+
impl From<SemanticError> for ParseError {
271+
fn from(error: SemanticError) -> Self {
272+
Self::InvalidSemantics(error)
273+
}
274+
}
275+
276+
impl FromStr for Offer {
277+
type Err = ParseError;
278+
279+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
280+
let tlv_stream = OfferTlvStream::from_str(s)?;
281+
Ok(Offer::try_from(tlv_stream)?)
282+
}
283+
}
284+
285+
impl TryFrom<OfferTlvStream> for Offer {
286+
type Error = SemanticError;
287+
288+
fn try_from(tlv_stream: OfferTlvStream) -> Result<Self, Self::Error> {
289+
let OfferTlvStream {
290+
chains, currency, amount, description, features, absolute_expiry, paths, issuer,
291+
quantity_min, quantity_max, recurrence, node_id, send_invoice, refund_for, signature,
292+
} = tlv_stream;
293+
294+
let supported_chains = [
295+
genesis_block(Network::Bitcoin).block_hash(),
296+
genesis_block(Network::Testnet).block_hash(),
297+
genesis_block(Network::Signet).block_hash(),
298+
genesis_block(Network::Regtest).block_hash(),
299+
];
300+
let chains = match chains {
301+
None => None,
302+
Some(WithoutLength(chains)) => match chains.first() {
303+
None => Some(chains),
304+
Some(chain) if supported_chains.contains(chain) => Some(chains),
305+
_ => return Err(SemanticError::UnsupportedChain),
306+
},
307+
};
308+
309+
let amount = match (currency, amount) {
310+
(None, None) => None,
311+
(None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats: amount_msats.0 }),
312+
(Some(_), None) => return Err(SemanticError::UnexpectedCurrency),
313+
(Some(WithoutLength(iso4217_code)), Some(HighZeroBytesDroppedVarInt(amount))) => {
314+
if iso4217_code.len() != ISO4217_CODE_LEN {
315+
return Err(SemanticError::InvalidCurrencyEncoding);
316+
}
317+
Some(Amount::Currency { iso4217_code, amount })
318+
},
319+
};
320+
321+
if description.is_none() {
322+
return Err(SemanticError::MissingDescription);
323+
}
324+
325+
let destination = match (node_id, paths) {
326+
(None, None) => return Err(SemanticError::MissingDestination),
327+
(Some(_), Some(_)) => return Err(SemanticError::DuplicateDestination),
328+
(Some(node_id), None) => Destination::NodeId(node_id),
329+
(None, Some(paths)) if paths.0.is_empty() => return Err(SemanticError::MissingPaths),
330+
(None, Some(WithoutLength(paths))) => Destination::Paths(paths),
331+
};
332+
333+
if let Some(HighZeroBytesDroppedVarInt(quantity_min)) = quantity_min {
334+
if quantity_min < 1 {
335+
return Err(SemanticError::InvalidQuantity);
336+
}
337+
338+
if let Some(HighZeroBytesDroppedVarInt(quantity_max)) = quantity_max {
339+
if quantity_min > quantity_max {
340+
return Err(SemanticError::InvalidQuantity);
341+
}
342+
}
343+
}
344+
345+
if let Some(HighZeroBytesDroppedVarInt(quantity_max)) = quantity_max {
346+
if quantity_max < 1 {
347+
return Err(SemanticError::InvalidQuantity);
348+
}
349+
}
350+
351+
let send_invoice = match (send_invoice, refund_for) {
352+
(None, None) => None,
353+
(None, Some(_)) => return Err(SemanticError::UnexpectedRefund),
354+
(Some(_), _) => Some(SendInvoice { refund_for }),
355+
};
356+
357+
Ok(Offer {
358+
chains,
359+
amount,
360+
description: description.unwrap().0,
361+
features,
362+
absolute_expiry:
363+
absolute_expiry.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch.0)),
364+
issuer: issuer.map(|issuer| issuer.0),
365+
destination,
366+
quantity_min: quantity_min.map(|quantity_min| quantity_min.0),
367+
quantity_max: quantity_max.map(|quantity_max| quantity_max.0),
368+
recurrence,
369+
send_invoice,
370+
signature,
371+
})
372+
}
373+
}
374+
242375
const OFFER_BECH32_HRP: &str = "lno";
243376

244377
impl FromStr for OfferTlvStream {

0 commit comments

Comments
 (0)