Skip to content

Commit 51c77e1

Browse files
fix: expose disclosed payer proof fields
Preserve selected invoice fields inside PayerProof and add accessors for verifiers. The parser and builder now both populate a shared disclosed-field structure so locally produced proofs and parsed proofs expose the same API surface. This keeps selective disclosure useful to callers instead of limiting the proof to merkle reconstruction and signature verification only.
1 parent d931762 commit 51c77e1

File tree

1 file changed

+108
-12
lines changed

1 file changed

+108
-12
lines changed

lightning/src/offers/payer_proof.rs

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@ use crate::offers::offer::{OFFER_DESCRIPTION_TYPE, OFFER_ISSUER_TYPE};
3535
use crate::offers::parse::Bech32Encode;
3636
use crate::offers::payer::PAYER_METADATA_TYPE;
3737
use crate::types::payment::{PaymentHash, PaymentPreimage};
38-
use crate::util::ser::{BigSize, Readable, Writeable};
38+
use crate::util::ser::{BigSize, HighZeroBytesDroppedBigSize, Readable, Writeable};
3939
use lightning_types::string::PrintableString;
4040

4141
use bitcoin::hashes::{sha256, Hash, HashEngine};
4242
use bitcoin::secp256k1::schnorr::Signature;
4343
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1};
4444

4545
use core::convert::TryFrom;
46+
use core::time::Duration;
4647

4748
#[allow(unused_imports)]
4849
use crate::prelude::*;
@@ -118,6 +119,15 @@ struct PayerProofContents {
118119
invoice_signature: Signature,
119120
payer_signature: Signature,
120121
payer_note: Option<String>,
122+
disclosed_fields: DisclosedFields,
123+
}
124+
125+
#[derive(Clone, Debug, Default)]
126+
struct DisclosedFields {
127+
offer_description: Option<String>,
128+
offer_issuer: Option<String>,
129+
invoice_amount_msats: Option<u64>,
130+
invoice_created_at: Option<Duration>,
121131
}
122132

123133
/// Builds a [`PayerProof`] from a paid invoice and its preimage.
@@ -243,6 +253,10 @@ impl<'a> PayerProofBuilder<'a> {
243253
for r in TlvStream::new(&invoice_bytes).filter(|r| !SIGNATURE_TYPES.contains(&r.r#type)) {
244254
bytes_without_sig.extend_from_slice(r.record_bytes);
245255
}
256+
let disclosed_fields = extract_disclosed_fields(
257+
TlvStream::new(&invoice_bytes)
258+
.filter(|r| self.included_types.contains(&r.r#type) && !SIGNATURE_TYPES.contains(&r.r#type)),
259+
)?;
246260

247261
let disclosure =
248262
merkle::compute_selective_disclosure(&bytes_without_sig, &self.included_types)?;
@@ -257,6 +271,7 @@ impl<'a> PayerProofBuilder<'a> {
257271
issuer_signing_pubkey: self.invoice.signing_pubkey(),
258272
invoice_bytes,
259273
included_types: self.included_types,
274+
disclosed_fields,
260275
disclosure,
261276
})
262277
}
@@ -271,6 +286,7 @@ struct UnsignedPayerProof {
271286
issuer_signing_pubkey: PublicKey,
272287
invoice_bytes: Vec<u8>,
273288
included_types: BTreeSet<u64>,
289+
disclosed_fields: DisclosedFields,
274290
disclosure: SelectiveDisclosure,
275291
}
276292

@@ -299,6 +315,7 @@ impl UnsignedPayerProof {
299315
invoice_signature: self.invoice_signature,
300316
payer_signature,
301317
payer_note: note.map(String::from),
318+
disclosed_fields: self.disclosed_fields,
302319
},
303320
merkle_root: self.disclosure.merkle_root,
304321
})
@@ -426,6 +443,26 @@ impl PayerProof {
426443
self.contents.payer_signature
427444
}
428445

446+
/// The disclosed offer description, if included in the proof.
447+
pub fn offer_description(&self) -> Option<PrintableString<'_>> {
448+
self.contents.disclosed_fields.offer_description.as_deref().map(PrintableString)
449+
}
450+
451+
/// The disclosed offer issuer, if included in the proof.
452+
pub fn offer_issuer(&self) -> Option<PrintableString<'_>> {
453+
self.contents.disclosed_fields.offer_issuer.as_deref().map(PrintableString)
454+
}
455+
456+
/// The disclosed invoice amount, if included in the proof.
457+
pub fn invoice_amount_msats(&self) -> Option<u64> {
458+
self.contents.disclosed_fields.invoice_amount_msats
459+
}
460+
461+
/// The disclosed invoice creation time, if included in the proof.
462+
pub fn invoice_created_at(&self) -> Option<Duration> {
463+
self.contents.disclosed_fields.invoice_created_at
464+
}
465+
429466
/// The payer's note, if any.
430467
pub fn payer_note(&self) -> Option<PrintableString<'_>> {
431468
self.contents.payer_note.as_deref().map(PrintableString)
@@ -473,6 +510,47 @@ fn validate_tlv_framing(bytes: &[u8]) -> Result<(), crate::ln::msgs::DecodeError
473510
Ok(())
474511
}
475512

513+
fn update_disclosed_fields(
514+
record: &crate::offers::merkle::TlvRecord<'_>, disclosed_fields: &mut DisclosedFields,
515+
) -> Result<(), crate::ln::msgs::DecodeError> {
516+
use crate::ln::msgs::DecodeError;
517+
518+
match record.r#type {
519+
OFFER_DESCRIPTION_TYPE => {
520+
disclosed_fields.offer_description = Some(
521+
String::from_utf8(record.value_bytes.to_vec()).map_err(|_| DecodeError::InvalidValue)?,
522+
);
523+
},
524+
OFFER_ISSUER_TYPE => {
525+
disclosed_fields.offer_issuer = Some(
526+
String::from_utf8(record.value_bytes.to_vec()).map_err(|_| DecodeError::InvalidValue)?,
527+
);
528+
},
529+
INVOICE_CREATED_AT_TYPE => {
530+
disclosed_fields.invoice_created_at = Some(Duration::from_secs(
531+
record.read_value::<HighZeroBytesDroppedBigSize<u64>>()?.0,
532+
));
533+
},
534+
INVOICE_AMOUNT_TYPE => {
535+
disclosed_fields.invoice_amount_msats =
536+
Some(record.read_value::<HighZeroBytesDroppedBigSize<u64>>()?.0);
537+
},
538+
_ => {},
539+
}
540+
541+
Ok(())
542+
}
543+
544+
fn extract_disclosed_fields<'a>(
545+
records: impl core::iter::Iterator<Item = crate::offers::merkle::TlvRecord<'a>>,
546+
) -> Result<DisclosedFields, crate::ln::msgs::DecodeError> {
547+
let mut disclosed_fields = DisclosedFields::default();
548+
for record in records {
549+
update_disclosed_fields(&record, &mut disclosed_fields)?;
550+
}
551+
Ok(disclosed_fields)
552+
}
553+
476554
// Payer proofs use manual TLV parsing rather than `ParsedMessage` / `tlv_stream!`
477555
// because of their hybrid structure: a dynamic, variable set of included invoice
478556
// TLV records (types 0-239, preserved as raw bytes for merkle reconstruction) plus
@@ -502,6 +580,7 @@ impl TryFrom<Vec<u8>> for PayerProof {
502580
let mut preimage: Option<PaymentPreimage> = None;
503581
let mut payer_signature: Option<Signature> = None;
504582
let mut payer_note: Option<String> = None;
583+
let mut disclosed_fields = DisclosedFields::default();
505584

506585
let mut leaf_hashes: Vec<sha256::Hash> = Vec::new();
507586
let mut omitted_markers: Vec<u64> = Vec::new();
@@ -522,6 +601,7 @@ impl TryFrom<Vec<u8>> for PayerProof {
522601
}
523602
}
524603
prev_tlv_type = Some(tlv_type);
604+
update_disclosed_fields(&record, &mut disclosed_fields)?;
525605

526606
match tlv_type {
527607
INVOICE_REQUEST_PAYER_ID_TYPE => {
@@ -677,17 +757,18 @@ impl TryFrom<Vec<u8>> for PayerProof {
677757

678758
Ok(PayerProof {
679759
bytes,
680-
contents: PayerProofContents {
681-
payer_id,
682-
payment_hash,
683-
issuer_signing_pubkey,
684-
preimage,
685-
invoice_signature,
686-
payer_signature,
687-
payer_note,
688-
},
689-
merkle_root,
690-
})
760+
contents: PayerProofContents {
761+
payer_id,
762+
payment_hash,
763+
issuer_signing_pubkey,
764+
preimage,
765+
invoice_signature,
766+
payer_signature,
767+
payer_note,
768+
disclosed_fields,
769+
},
770+
merkle_root,
771+
})
691772
}
692773
}
693774

@@ -834,6 +915,10 @@ mod tests {
834915
]
835916
.into_iter()
836917
.collect();
918+
let disclosed_fields = extract_disclosed_fields(
919+
TlvStream::new(&invoice_bytes).filter(|r| included_types.contains(&r.r#type)),
920+
)
921+
.unwrap();
837922
let disclosure = compute_selective_disclosure(&invoice_bytes, &included_types).unwrap();
838923

839924
let unsigned = UnsignedPayerProof {
@@ -844,6 +929,7 @@ mod tests {
844929
issuer_signing_pubkey,
845930
invoice_bytes,
846931
included_types,
932+
disclosed_fields,
847933
disclosure,
848934
};
849935

@@ -882,6 +968,10 @@ mod tests {
882968
[INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_PAYMENT_HASH_TYPE, INVOICE_NODE_ID_TYPE]
883969
.into_iter()
884970
.collect();
971+
let disclosed_fields = extract_disclosed_fields(
972+
TlvStream::new(&invoice_bytes).filter(|r| included_types.contains(&r.r#type)),
973+
)
974+
.unwrap();
885975
let disclosure = compute_selective_disclosure(&invoice_bytes, &included_types).unwrap();
886976
assert_eq!(disclosure.omitted_markers, vec![177, 178]);
887977

@@ -893,6 +983,7 @@ mod tests {
893983
issuer_signing_pubkey,
894984
invoice_bytes,
895985
included_types,
986+
disclosed_fields,
896987
disclosure,
897988
};
898989

@@ -948,6 +1039,10 @@ mod tests {
9481039
]
9491040
.into_iter()
9501041
.collect();
1042+
let disclosed_fields = extract_disclosed_fields(
1043+
TlvStream::new(&invoice_bytes).filter(|r| included_types.contains(&r.r#type)),
1044+
)
1045+
.unwrap();
9511046
let disclosure = compute_selective_disclosure(&invoice_bytes, &included_types).unwrap();
9521047

9531048
let unsigned = UnsignedPayerProof {
@@ -958,6 +1053,7 @@ mod tests {
9581053
issuer_signing_pubkey,
9591054
invoice_bytes,
9601055
included_types,
1056+
disclosed_fields,
9611057
disclosure,
9621058
};
9631059

0 commit comments

Comments
 (0)