Skip to content

Commit 8afb189

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 fb85710 commit 8afb189

File tree

3 files changed

+31
-10
lines changed

3 files changed

+31
-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."
@@ -1092,6 +1104,7 @@ impl Invoice {
10921104
invoice.check_field_counts()?;
10931105
invoice.check_feature_bits()?;
10941106
invoice.check_signature()?;
1107+
invoice.check_amount()?;
10951108

10961109
Ok(invoice)
10971110
}
@@ -1401,6 +1414,9 @@ pub enum SemanticError {
14011414

14021415
/// The invoice's signature is invalid
14031416
InvalidSignature,
1417+
1418+
/// The invoice's amount was not a whole number of millisatoshis
1419+
ImpreciseAmount,
14041420
}
14051421

14061422
impl Display for SemanticError {
@@ -1414,6 +1430,7 @@ impl Display for SemanticError {
14141430
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
14151431
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
14161432
SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"),
1433+
SemanticError::ImpreciseAmount => f.write_str("The invoice's amount was not a whole number of millisatoshis"),
14171434
}
14181435
}
14191436
}
@@ -1663,7 +1680,7 @@ mod test {
16631680
.current_timestamp();
16641681

16651682
let invoice = builder.clone()
1666-
.amount_pico_btc(15000)
1683+
.amount_milli_satoshis(1500)
16671684
.build_raw()
16681685
.unwrap();
16691686

@@ -1672,7 +1689,7 @@ mod test {
16721689

16731690

16741691
let invoice = builder.clone()
1675-
.amount_pico_btc(1500)
1692+
.amount_milli_satoshis(150)
16761693
.build_raw()
16771694
.unwrap();
16781695

@@ -1803,7 +1820,7 @@ mod test {
18031820
]);
18041821

18051822
let builder = InvoiceBuilder::new(Currency::BitcoinTestnet)
1806-
.amount_pico_btc(123)
1823+
.amount_milli_satoshis(123)
18071824
.timestamp(UNIX_EPOCH + Duration::from_secs(1234567))
18081825
.payee_pub_key(public_key.clone())
18091826
.expiry_time(Duration::from_secs(54321))
@@ -1823,7 +1840,7 @@ mod test {
18231840
assert!(invoice.check_signature().is_ok());
18241841
assert_eq!(invoice.tagged_fields().count(), 10);
18251842

1826-
assert_eq!(invoice.amount_pico_btc(), Some(123));
1843+
assert_eq!(invoice.amount_pico_btc(), Some(1230));
18271844
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
18281845
assert_eq!(
18291846
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: 7 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,8 @@ fn test_bolt_invaoid_invoices() {
172172
assert_eq!(Invoice::from_str(
173173
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg"
174174
), Err(ParseOrSemanticError::ParseError(ParseError::UnknownSiPrefix)));
175+
eprintln!("GO");
176+
assert_eq!(Invoice::from_str(
177+
"lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu7hqtk93pkf7sw55rdv4k9z2vj050rxdr6za9ekfs3nlt5lr89jqpdmxsmlj9urqumg0h9wzpqecw7th56tdms40p2ny9q4ddvjsedzcplva53s"
178+
), Err(ParseOrSemanticError::SemanticError(SemanticError::ImpreciseAmount)));
175179
}

0 commit comments

Comments
 (0)