Skip to content

Commit a4d895e

Browse files
committed
Convert the invoice creation API to millisats and req it for parse
The BOLT 11 invalid invoice test vectors suggest failing to parse invoices which have an amount which is not a whole number of millisatoshis. lightning-invoice, however, happily parses such invoices. While we could continue to parse them, failing them makes for one less check on the user code side, so we might as well. In order to keep the invoice creation less likely to fail, we also switch the Builder amount-setting function to use millisatoshis.
1 parent 27bdd44 commit a4d895e

File tree

3 files changed

+30
-10
lines changed

3 files changed

+30
-10
lines changed

lightning-invoice/src/lib.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,9 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBui
480480
}
481481
}
482482

483-
/// Sets the amount in pico BTC. The optimal SI prefix is choosen automatically.
484-
pub fn amount_pico_btc(mut self, amount: u64) -> Self {
483+
/// Sets the amount in millisatoshis. The optimal SI prefix is choosen automatically.
484+
pub fn amount_milli_satoshis(mut self, amount_msat: u64) -> Self {
485+
let amount = amount_msat * 10; // Invoices are denominated in "pico BTC"
485486
let biggest_possible_si_prefix = SiPrefix::values_desc()
486487
.iter()
487488
.find(|prefix| amount % prefix.multiplier() == 0)
@@ -673,6 +674,7 @@ impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
673674

674675
invoice.check_field_counts().expect("should be ensured by type signature of builder");
675676
invoice.check_feature_bits().expect("should be ensured by type signature of builder");
677+
invoice.check_amount().expect("should be ensured by type signature of builder");
676678

677679
Ok(invoice)
678680
}
@@ -1019,6 +1021,16 @@ impl Invoice {
10191021
Ok(())
10201022
}
10211023

1024+
/// Check that amount is a whole number of millisatoshis
1025+
fn check_amount(&self) -> Result<(), SemanticError> {
1026+
if let Some(amount_pico_btc) = self.amount_pico_btc() {
1027+
if amount_pico_btc % 10 != 0 {
1028+
return Err(SemanticError::ImpreciseAmount);
1029+
}
1030+
}
1031+
Ok(())
1032+
}
1033+
10221034
/// Check that feature bits are set as required
10231035
fn check_feature_bits(&self) -> Result<(), SemanticError> {
10241036
// "If the payment_secret feature is set, MUST include exactly one s field."
@@ -1099,6 +1111,7 @@ impl Invoice {
10991111
invoice.check_field_counts()?;
11001112
invoice.check_feature_bits()?;
11011113
invoice.check_signature()?;
1114+
invoice.check_amount()?;
11021115

11031116
Ok(invoice)
11041117
}
@@ -1408,6 +1421,9 @@ pub enum SemanticError {
14081421

14091422
/// The invoice's signature is invalid
14101423
InvalidSignature,
1424+
1425+
/// The invoice's amount was not a whole number of millisatoshis
1426+
ImpreciseAmount,
14111427
}
14121428

14131429
impl Display for SemanticError {
@@ -1421,6 +1437,7 @@ impl Display for SemanticError {
14211437
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
14221438
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
14231439
SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"),
1440+
SemanticError::ImpreciseAmount => f.write_str("The invoice's amount was not a whole number of millisatoshis"),
14241441
}
14251442
}
14261443
}
@@ -1670,7 +1687,7 @@ mod test {
16701687
.current_timestamp();
16711688

16721689
let invoice = builder.clone()
1673-
.amount_pico_btc(15000)
1690+
.amount_milli_satoshis(1500)
16741691
.build_raw()
16751692
.unwrap();
16761693

@@ -1679,7 +1696,7 @@ mod test {
16791696

16801697

16811698
let invoice = builder.clone()
1682-
.amount_pico_btc(1500)
1699+
.amount_milli_satoshis(150)
16831700
.build_raw()
16841701
.unwrap();
16851702

@@ -1810,7 +1827,7 @@ mod test {
18101827
]);
18111828

18121829
let builder = InvoiceBuilder::new(Currency::BitcoinTestnet)
1813-
.amount_pico_btc(123)
1830+
.amount_milli_satoshis(123)
18141831
.timestamp(UNIX_EPOCH + Duration::from_secs(1234567))
18151832
.payee_pub_key(public_key.clone())
18161833
.expiry_time(Duration::from_secs(54321))
@@ -1830,7 +1847,7 @@ mod test {
18301847
assert!(invoice.check_signature().is_ok());
18311848
assert_eq!(invoice.tagged_fields().count(), 10);
18321849

1833-
assert_eq!(invoice.amount_pico_btc(), Some(123));
1850+
assert_eq!(invoice.amount_pico_btc(), Some(1230));
18341851
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
18351852
assert_eq!(
18361853
invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(),

lightning-invoice/src/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ where
6868
.basic_mpp()
6969
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
7070
if let Some(amt) = amt_msat {
71-
invoice = invoice.amount_pico_btc(amt * 10);
71+
invoice = invoice.amount_milli_satoshis(amt);
7272
}
7373
for hint in route_hints {
7474
invoice = invoice.private_route(hint);

lightning-invoice/tests/ser_de.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
4949
k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\
5050
9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(),
5151
InvoiceBuilder::new(Currency::Bitcoin)
52-
.amount_pico_btc(2500000000)
52+
.amount_milli_satoshis(250_000_000)
5353
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
5454
.payment_hash(sha256::Hash::from_hex(
5555
"0001020304050607080900010203040506070809000102030405060708090102"
@@ -78,7 +78,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
7878
dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\
7979
hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(),
8080
InvoiceBuilder::new(Currency::Bitcoin)
81-
.amount_pico_btc(20000000000)
81+
.amount_milli_satoshis(2_000_000_000)
8282
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
8383
.payment_hash(sha256::Hash::from_hex(
8484
"0001020304050607080900010203040506070809000102030405060708090102"
@@ -110,7 +110,7 @@ fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option<SemanticError>)> {
110110
"0001020304050607080900010203040506070809000102030405060708090102"
111111
).unwrap())
112112
.description("coffee beans".to_string())
113-
.amount_pico_btc(20000000000)
113+
.amount_milli_satoshis(2_000_000_000)
114114
.timestamp(UNIX_EPOCH + Duration::from_secs(1496314658))
115115
.payment_secret(PaymentSecret([42; 32]))
116116
.build_raw()
@@ -172,4 +172,7 @@ fn test_bolt_invalid_invoices() {
172172
assert_eq!(Invoice::from_str(
173173
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg"
174174
), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix)));
175+
assert_eq!(Invoice::from_str(
176+
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s"
177+
), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount)));
175178
}

0 commit comments

Comments
 (0)