Skip to content

Commit d782df0

Browse files
authored
Merge pull request #901 from jkczyz/2021-04-invoice-feature-semantics
Hide InvoiceFeatures behind InvoiceBuilder API
2 parents 58e4ce2 + 2226ae2 commit d782df0

File tree

4 files changed

+208
-50
lines changed

4 files changed

+208
-50
lines changed

lightning-invoice/src/lib.rs

+188-32
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ pub fn check_platform() {
168168
///
169169
/// (C-not exported) as we likely need to manually select one set of boolean type parameters.
170170
#[derive(Eq, PartialEq, Debug, Clone)]
171-
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> {
171+
pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> {
172172
currency: Currency,
173173
amount: Option<u64>,
174174
si_prefix: Option<SiPrefix>,
@@ -180,6 +180,7 @@ pub struct InvoiceBuilder<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> {
180180
phantom_h: std::marker::PhantomData<H>,
181181
phantom_t: std::marker::PhantomData<T>,
182182
phantom_c: std::marker::PhantomData<C>,
183+
phantom_s: std::marker::PhantomData<S>,
183184
}
184185

185186
/// Represents a syntactically and semantically correct lightning BOLT11 invoice.
@@ -432,7 +433,7 @@ pub mod constants {
432433
pub const TAG_FEATURES: u8 = 5;
433434
}
434435

435-
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False> {
436+
impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False, tb::False> {
436437
/// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before
437438
/// `InvoiceBuilder::build(self)` becomes available.
438439
pub fn new(currrency: Currency) -> Self {
@@ -448,14 +449,15 @@ impl InvoiceBuilder<tb::False, tb::False, tb::False, tb::False> {
448449
phantom_h: std::marker::PhantomData,
449450
phantom_t: std::marker::PhantomData,
450451
phantom_c: std::marker::PhantomData,
452+
phantom_s: std::marker::PhantomData,
451453
}
452454
}
453455
}
454456

455-
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C> {
457+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, C, S> {
456458
/// Helper function to set the completeness flags.
457-
fn set_flags<DN: tb::Bool, HN: tb::Bool, TN: tb::Bool, CN: tb::Bool>(self) -> InvoiceBuilder<DN, HN, TN, CN> {
458-
InvoiceBuilder::<DN, HN, TN, CN> {
459+
fn set_flags<DN: tb::Bool, HN: tb::Bool, TN: tb::Bool, CN: tb::Bool, SN: tb::Bool>(self) -> InvoiceBuilder<DN, HN, TN, CN, SN> {
460+
InvoiceBuilder::<DN, HN, TN, CN, SN> {
459461
currency: self.currency,
460462
amount: self.amount,
461463
si_prefix: self.si_prefix,
@@ -467,6 +469,7 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T,
467469
phantom_h: std::marker::PhantomData,
468470
phantom_t: std::marker::PhantomData,
469471
phantom_c: std::marker::PhantomData,
472+
phantom_s: std::marker::PhantomData,
470473
}
471474
}
472475

@@ -487,12 +490,6 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T,
487490
self
488491
}
489492

490-
/// Sets the payment secret
491-
pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> Self {
492-
self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret));
493-
self
494-
}
495-
496493
/// Sets the expiry time
497494
pub fn expiry_time(mut self, expiry_time: Duration) -> Self {
498495
match ExpiryTime::from_duration(expiry_time) {
@@ -516,16 +513,9 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T,
516513
}
517514
self
518515
}
519-
520-
/// Adds a features field which indicates the set of supported protocol extensions which the
521-
/// origin node supports.
522-
pub fn features(mut self, features: InvoiceFeatures) -> Self {
523-
self.tagged_fields.push(TaggedField::Features(features));
524-
self
525-
}
526516
}
527517

528-
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, tb::True, C> {
518+
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb::True, C, S> {
529519
/// Builds a `RawInvoice` if no `CreationError` occurred while construction any of the fields.
530520
pub fn build_raw(self) -> Result<RawInvoice, CreationError> {
531521

@@ -558,9 +548,9 @@ impl<D: tb::Bool, H: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, tb::True, C> {
558548
}
559549
}
560550

561-
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<tb::False, H, T, C> {
551+
impl<H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<tb::False, H, T, C, S> {
562552
/// Set the description. This function is only available if no description (hash) was set.
563-
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C> {
553+
pub fn description(mut self, description: String) -> InvoiceBuilder<tb::True, H, T, C, S> {
564554
match Description::new(description) {
565555
Ok(d) => self.tagged_fields.push(TaggedField::Description(d)),
566556
Err(e) => self.error = Some(e),
@@ -569,23 +559,23 @@ impl<H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<tb::False, H, T, C> {
569559
}
570560

571561
/// Set the description hash. This function is only available if no description (hash) was set.
572-
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C> {
562+
pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder<tb::True, H, T, C, S> {
573563
self.tagged_fields.push(TaggedField::DescriptionHash(Sha256(description_hash)));
574564
self.set_flags()
575565
}
576566
}
577567

578-
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, tb::False, T, C> {
568+
impl<D: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, tb::False, T, C, S> {
579569
/// Set the payment hash. This function is only available if no payment hash was set.
580-
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C> {
570+
pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder<D, tb::True, T, C, S> {
581571
self.tagged_fields.push(TaggedField::PaymentHash(Sha256(hash)));
582572
self.set_flags()
583573
}
584574
}
585575

586-
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, tb::False, C> {
576+
impl<D: tb::Bool, H: tb::Bool, C: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, tb::False, C, S> {
587577
/// Sets the timestamp.
588-
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C> {
578+
pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder<D, H, tb::True, C, S> {
589579
match PositiveTimestamp::from_system_time(time) {
590580
Ok(t) => self.timestamp = Some(t),
591581
Err(e) => self.error = Some(e),
@@ -595,22 +585,48 @@ impl<D: tb::Bool, H: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, tb::False, C> {
595585
}
596586

597587
/// Sets the timestamp to the current UNIX timestamp.
598-
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C> {
588+
pub fn current_timestamp(mut self) -> InvoiceBuilder<D, H, tb::True, C, S> {
599589
let now = PositiveTimestamp::from_system_time(SystemTime::now());
600590
self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen"));
601591
self.set_flags()
602592
}
603593
}
604594

605-
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T, tb::False> {
595+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, S: tb::Bool> InvoiceBuilder<D, H, T, tb::False, S> {
606596
/// Sets `min_final_cltv_expiry`.
607-
pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> InvoiceBuilder<D, H, T, tb::True> {
597+
pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> InvoiceBuilder<D, H, T, tb::True, S> {
608598
self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry)));
609599
self.set_flags()
610600
}
611601
}
612602

613-
impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True> {
603+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C, tb::False> {
604+
/// Sets the payment secret and relevant features.
605+
pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> InvoiceBuilder<D, H, T, C, tb::True> {
606+
let features = InvoiceFeatures::empty()
607+
.set_variable_length_onion_required()
608+
.set_payment_secret_required();
609+
self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret));
610+
self.tagged_fields.push(TaggedField::Features(features));
611+
self.set_flags()
612+
}
613+
}
614+
615+
impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool> InvoiceBuilder<D, H, T, C, tb::True> {
616+
/// Sets the `basic_mpp` feature as optional.
617+
pub fn basic_mpp(mut self) -> Self {
618+
self.tagged_fields = self.tagged_fields
619+
.drain(..)
620+
.map(|field| match field {
621+
TaggedField::Features(f) => TaggedField::Features(f.set_basic_mpp_optional()),
622+
_ => field,
623+
})
624+
.collect();
625+
self
626+
}
627+
}
628+
629+
impl<S: tb::Bool> InvoiceBuilder<tb::True, tb::True, tb::True, tb::True, S> {
614630
/// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail
615631
/// and MUST produce a recoverable signature valid for the given hash and if applicable also for
616632
/// the included payee public key.
@@ -649,6 +665,7 @@ impl InvoiceBuilder<tb::True, tb::True, tb::True, tb::True> {
649665
};
650666

651667
invoice.check_field_counts().expect("should be ensured by type signature of builder");
668+
invoice.check_feature_bits().expect("should be ensured by type signature of builder");
652669

653670
Ok(invoice)
654671
}
@@ -977,6 +994,41 @@ impl Invoice {
977994
Ok(())
978995
}
979996

997+
/// Check that feature bits are set as required
998+
fn check_feature_bits(&self) -> Result<(), SemanticError> {
999+
// "If the payment_secret feature is set, MUST include exactly one s field."
1000+
let payment_secret_count = self.tagged_fields().filter(|&tf| match *tf {
1001+
TaggedField::PaymentSecret(_) => true,
1002+
_ => false,
1003+
}).count();
1004+
if payment_secret_count > 1 {
1005+
return Err(SemanticError::MultiplePaymentSecrets);
1006+
}
1007+
1008+
// "A writer MUST set an s field if and only if the payment_secret feature is set."
1009+
let has_payment_secret = payment_secret_count == 1;
1010+
let features = self.tagged_fields().find(|&tf| match *tf {
1011+
TaggedField::Features(_) => true,
1012+
_ => false,
1013+
});
1014+
match features {
1015+
None if has_payment_secret => Err(SemanticError::InvalidFeatures),
1016+
None => Ok(()),
1017+
Some(TaggedField::Features(features)) => {
1018+
if features.supports_payment_secret() && has_payment_secret {
1019+
Ok(())
1020+
} else if has_payment_secret {
1021+
Err(SemanticError::InvalidFeatures)
1022+
} else if features.supports_payment_secret() {
1023+
Err(SemanticError::InvalidFeatures)
1024+
} else {
1025+
Ok(())
1026+
}
1027+
},
1028+
Some(_) => unreachable!(),
1029+
}
1030+
}
1031+
9801032
/// Check that the invoice is signed correctly and that key recovery works
9811033
pub fn check_signature(&self) -> Result<(), SemanticError> {
9821034
match self.signed_invoice.recover_payee_pub_key() {
@@ -1011,6 +1063,7 @@ impl Invoice {
10111063
signed_invoice: signed_invoice,
10121064
};
10131065
invoice.check_field_counts()?;
1066+
invoice.check_feature_bits()?;
10141067
invoice.check_signature()?;
10151068

10161069
Ok(invoice)
@@ -1303,6 +1356,12 @@ pub enum SemanticError {
13031356
/// The invoice contains multiple descriptions and/or description hashes which isn't allowed
13041357
MultipleDescriptions,
13051358

1359+
/// The invoice contains multiple payment secrets
1360+
MultiplePaymentSecrets,
1361+
1362+
/// The invoice's features are invalid
1363+
InvalidFeatures,
1364+
13061365
/// The recovery id doesn't fit the signature/pub key
13071366
InvalidRecoveryId,
13081367

@@ -1317,6 +1376,8 @@ impl Display for SemanticError {
13171376
SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"),
13181377
SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"),
13191378
SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"),
1379+
SemanticError::MultiplePaymentSecrets => f.write_str("The invoice contains multiple payment secrets"),
1380+
SemanticError::InvalidFeatures => f.write_str("The invoice's features are invalid"),
13201381
SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"),
13211382
SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"),
13221383
}
@@ -1467,6 +1528,97 @@ mod test {
14671528
assert!(new_signed.check_signature());
14681529
}
14691530

1531+
#[test]
1532+
fn test_check_feature_bits() {
1533+
use TaggedField::*;
1534+
use lightning::ln::features::InvoiceFeatures;
1535+
use secp256k1::Secp256k1;
1536+
use secp256k1::key::SecretKey;
1537+
use {RawInvoice, RawHrp, RawDataPart, Currency, Sha256, PositiveTimestamp, Invoice,
1538+
SemanticError};
1539+
1540+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
1541+
let payment_secret = lightning::ln::PaymentSecret([21; 32]);
1542+
let invoice_template = RawInvoice {
1543+
hrp: RawHrp {
1544+
currency: Currency::Bitcoin,
1545+
raw_amount: None,
1546+
si_prefix: None,
1547+
},
1548+
data: RawDataPart {
1549+
timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(),
1550+
tagged_fields: vec ! [
1551+
PaymentHash(Sha256(sha256::Hash::from_hex(
1552+
"0001020304050607080900010203040506070809000102030405060708090102"
1553+
).unwrap())).into(),
1554+
Description(
1555+
::Description::new(
1556+
"Please consider supporting this project".to_owned()
1557+
).unwrap()
1558+
).into(),
1559+
],
1560+
},
1561+
};
1562+
1563+
// Missing features
1564+
let invoice = {
1565+
let mut invoice = invoice_template.clone();
1566+
invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into());
1567+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1568+
}.unwrap();
1569+
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures));
1570+
1571+
// Missing feature bits
1572+
let invoice = {
1573+
let mut invoice = invoice_template.clone();
1574+
invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into());
1575+
invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into());
1576+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1577+
}.unwrap();
1578+
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures));
1579+
1580+
// Including payment secret and feature bits
1581+
let invoice = {
1582+
let mut invoice = invoice_template.clone();
1583+
invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into());
1584+
invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into());
1585+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1586+
}.unwrap();
1587+
assert!(Invoice::from_signed(invoice).is_ok());
1588+
1589+
// No payment secret or features
1590+
let invoice = {
1591+
let invoice = invoice_template.clone();
1592+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1593+
}.unwrap();
1594+
assert!(Invoice::from_signed(invoice).is_ok());
1595+
1596+
// No payment secret or feature bits
1597+
let invoice = {
1598+
let mut invoice = invoice_template.clone();
1599+
invoice.data.tagged_fields.push(Features(InvoiceFeatures::empty()).into());
1600+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1601+
}.unwrap();
1602+
assert!(Invoice::from_signed(invoice).is_ok());
1603+
1604+
// Missing payment secret
1605+
let invoice = {
1606+
let mut invoice = invoice_template.clone();
1607+
invoice.data.tagged_fields.push(Features(InvoiceFeatures::known()).into());
1608+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1609+
}.unwrap();
1610+
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::InvalidFeatures));
1611+
1612+
// Multiple payment secrets
1613+
let invoice = {
1614+
let mut invoice = invoice_template.clone();
1615+
invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into());
1616+
invoice.data.tagged_fields.push(PaymentSecret(payment_secret).into());
1617+
invoice.sign::<_, ()>(|hash| Ok(Secp256k1::new().sign_recoverable(hash, &private_key)))
1618+
}.unwrap();
1619+
assert_eq!(Invoice::from_signed(invoice), Err(SemanticError::MultiplePaymentSecrets));
1620+
}
1621+
14701622
#[test]
14711623
fn test_builder_amount() {
14721624
use ::*;
@@ -1624,14 +1776,16 @@ mod test {
16241776
.route(route_1.clone())
16251777
.route(route_2.clone())
16261778
.description_hash(sha256::Hash::from_slice(&[3;32][..]).unwrap())
1627-
.payment_hash(sha256::Hash::from_slice(&[21;32][..]).unwrap());
1779+
.payment_hash(sha256::Hash::from_slice(&[21;32][..]).unwrap())
1780+
.payment_secret(PaymentSecret([42; 32]))
1781+
.basic_mpp();
16281782

16291783
let invoice = builder.clone().build_signed(|hash| {
16301784
secp_ctx.sign_recoverable(hash, &private_key)
16311785
}).unwrap();
16321786

16331787
assert!(invoice.check_signature().is_ok());
1634-
assert_eq!(invoice.tagged_fields().count(), 8);
1788+
assert_eq!(invoice.tagged_fields().count(), 10);
16351789

16361790
assert_eq!(invoice.amount_pico_btc(), Some(123));
16371791
assert_eq!(invoice.currency(), Currency::BitcoinTestnet);
@@ -1649,6 +1803,8 @@ mod test {
16491803
InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap()))
16501804
);
16511805
assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap());
1806+
assert_eq!(invoice.payment_secret(), Some(&PaymentSecret([42; 32])));
1807+
assert_eq!(invoice.features(), Some(&InvoiceFeatures::known()));
16521808

16531809
let raw_invoice = builder.build_raw().unwrap();
16541810
assert_eq!(raw_invoice, *invoice.into_signed_raw().raw_invoice())

0 commit comments

Comments
 (0)