@@ -41,11 +41,64 @@ impl TaggedHash {
4141 /// Creates a tagged hash with the given parameters.
4242 ///
4343 /// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record.
44- pub fn from_valid_tlv_stream_bytes ( tag : & ' static str , bytes : & [ u8 ] ) -> Self {
44+ pub ( super ) fn from_valid_tlv_stream_bytes ( tag : & ' static str , bytes : & [ u8 ] ) -> Self {
4545 let tlv_stream = TlvStream :: new ( bytes) ;
4646 Self :: from_tlv_stream ( tag, tlv_stream)
4747 }
4848
49+ /// Creates a tagged hash with the given parameters, validating the TLV stream.
50+ ///
51+ /// This is a low-level function exposed for specific use cases like command-line tools
52+ /// and testing. For production use, prefer higher-level methods like
53+ /// [`Bolt12Invoice::try_from`] which handle validation automatically.
54+ ///
55+ /// Returns an error if `bytes` is not a well-formed TLV stream containing at least one TLV record.
56+ ///
57+ /// [`Bolt12Invoice::try_from`]: crate::offers::invoice::Bolt12Invoice::try_from
58+ pub fn from_tlv_stream_bytes ( tag : & ' static str , bytes : & [ u8 ] ) -> Result < Self , TlvStreamError > {
59+ // Validate the TLV stream first
60+ if bytes. is_empty ( ) {
61+ return Err ( TlvStreamError :: EmptyStream ) ;
62+ }
63+
64+ // Try to parse the TLV stream to check validity
65+ let mut cursor = io:: Cursor :: new ( bytes) ;
66+ let mut has_records = false ;
67+
68+ while cursor. position ( ) < bytes. len ( ) as u64 {
69+ // Try to read type
70+ let type_result = <BigSize as Readable >:: read ( & mut cursor) ;
71+ if type_result. is_err ( ) {
72+ return Err ( TlvStreamError :: InvalidRecord ) ;
73+ }
74+
75+ // Try to read length
76+ let length_result = <BigSize as Readable >:: read ( & mut cursor) ;
77+ if length_result. is_err ( ) {
78+ return Err ( TlvStreamError :: InvalidRecord ) ;
79+ }
80+
81+ let length = length_result. unwrap ( ) . 0 ;
82+ let end_position = cursor. position ( ) + length;
83+
84+ // Check if the record extends beyond the buffer
85+ if end_position > bytes. len ( ) as u64 {
86+ return Err ( TlvStreamError :: InvalidRecord ) ;
87+ }
88+
89+ // Skip the value
90+ cursor. set_position ( end_position) ;
91+ has_records = true ;
92+ }
93+
94+ if !has_records {
95+ return Err ( TlvStreamError :: EmptyStream ) ;
96+ }
97+
98+ // If validation passes, create the tagged hash
99+ Ok ( Self :: from_valid_tlv_stream_bytes ( tag, bytes) )
100+ }
101+
49102 /// Creates a tagged hash with the given parameters.
50103 ///
51104 /// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record.
@@ -93,8 +146,17 @@ pub enum SignError {
93146 Verification ( secp256k1:: Error ) ,
94147}
95148
149+ /// Error when parsing TLV streams.
150+ #[ derive( Debug , PartialEq ) ]
151+ pub enum TlvStreamError {
152+ /// The TLV stream is empty (contains no records).
153+ EmptyStream ,
154+ /// The TLV stream contains an invalid record.
155+ InvalidRecord ,
156+ }
157+
96158/// A function for signing a [`TaggedHash`].
97- pub ( super ) trait SignFn < T : AsRef < TaggedHash > > {
159+ pub trait SignFn < T : AsRef < TaggedHash > > {
98160 /// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
99161 fn sign ( & self , message : & T ) -> Result < Signature , ( ) > ;
100162}
@@ -111,15 +173,17 @@ where
111173/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream, checking if it
112174/// can be verified with the supplied `pubkey`.
113175///
176+ /// This is a low-level function exposed for specific use cases like command-line tools
177+ /// and testing. For production use, prefer higher-level methods on invoice types that handle
178+ /// signing automatically.
179+ ///
114180/// Since `message` is any type that implements [`AsRef<TaggedHash>`], `sign` may be a closure that
115181/// takes a message such as [`Bolt12Invoice`] or [`InvoiceRequest`]. This allows further message
116182/// verification before signing its [`TaggedHash`].
117183///
118184/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
119185/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
120- pub fn sign_message < F , T > (
121- f : F , message : & T , pubkey : PublicKey ,
122- ) -> Result < Signature , SignError >
186+ pub fn sign_message < F , T > ( f : F , message : & T , pubkey : PublicKey ) -> Result < Signature , SignError >
123187where
124188 F : SignFn < T > ,
125189 T : AsRef < TaggedHash > ,
@@ -136,6 +200,12 @@ where
136200
137201/// Verifies the signature with a pubkey over the given message using a tagged hash as the message
138202/// digest.
203+ ///
204+ /// This is a low-level function exposed for specific use cases like command-line tools
205+ /// and testing. For production use, prefer higher-level methods like
206+ /// [`Bolt12Invoice::try_from`] which handle signature verification automatically.
207+ ///
208+ /// [`Bolt12Invoice::try_from`]: crate::offers::invoice::Bolt12Invoice::try_from
139209pub fn verify_signature (
140210 signature : & Signature , message : & TaggedHash , pubkey : PublicKey ,
141211) -> Result < ( ) , secp256k1:: Error > {
@@ -481,6 +551,119 @@ mod tests {
481551 assert_eq ! ( tlv_stream, invoice_request. bytes) ;
482552 }
483553
554+ #[ test]
555+ fn validates_tlv_stream_bytes ( ) {
556+ // Test with valid TLV stream
557+ const VALID_HEX : & ' static str = "010203e8" ;
558+ let valid_bytes = <Vec < u8 > >:: from_hex ( VALID_HEX ) . unwrap ( ) ;
559+ let result = super :: TaggedHash :: from_tlv_stream_bytes ( "test" , & valid_bytes) ;
560+ assert ! ( result. is_ok( ) ) ;
561+
562+ // Test with empty stream
563+ let empty_bytes = Vec :: new ( ) ;
564+ let result = super :: TaggedHash :: from_tlv_stream_bytes ( "test" , & empty_bytes) ;
565+ assert_eq ! ( result, Err ( super :: TlvStreamError :: EmptyStream ) ) ;
566+
567+ // Test with invalid TLV stream (truncated)
568+ let invalid_bytes = vec ! [ 0x01 , 0x02 ] ; // Type and length but no value
569+ let result = super :: TaggedHash :: from_tlv_stream_bytes ( "test" , & invalid_bytes) ;
570+ assert_eq ! ( result, Err ( super :: TlvStreamError :: InvalidRecord ) ) ;
571+ }
572+
573+ #[ test]
574+ fn consistent_results_between_validating_and_non_validating_functions ( ) {
575+ // Test vectors from BOLT 12
576+ let test_vectors = vec ! [
577+ "010203e8" ,
578+ "010203e802080000010000020003" ,
579+ "010203e802080000010000020003" , // Using same as above for simplicity
580+ ] ;
581+
582+ for hex_data in test_vectors {
583+ let bytes = <Vec < u8 > >:: from_hex ( hex_data) . unwrap ( ) ;
584+ let tag = "test_tag" ;
585+
586+ // Create tagged hash using the validating function
587+ let validating_result = super :: TaggedHash :: from_tlv_stream_bytes ( tag, & bytes) ;
588+ assert ! (
589+ validating_result. is_ok( ) ,
590+ "Validating function should succeed for valid TLV stream"
591+ ) ;
592+ let validating_hash = validating_result. unwrap ( ) ;
593+
594+ // Create tagged hash using the non-validating function
595+ let non_validating_hash = super :: TaggedHash :: from_valid_tlv_stream_bytes ( tag, & bytes) ;
596+
597+ // Both should produce identical results
598+ assert_eq ! (
599+ validating_hash. tag( ) ,
600+ non_validating_hash. tag( ) ,
601+ "Tags should be identical"
602+ ) ;
603+ assert_eq ! (
604+ validating_hash. merkle_root( ) ,
605+ non_validating_hash. merkle_root( ) ,
606+ "Merkle roots should be identical"
607+ ) ;
608+ assert_eq ! (
609+ validating_hash. as_digest( ) ,
610+ non_validating_hash. as_digest( ) ,
611+ "Digests should be identical"
612+ ) ;
613+ assert_eq ! ( validating_hash, non_validating_hash, "Tagged hashes should be identical" ) ;
614+ }
615+ }
616+
617+ #[ test]
618+ fn regression_test_with_invoice_request_data ( ) {
619+ // Use real invoice request data to ensure no regression
620+ let expanded_key = ExpandedKey :: new ( [ 42 ; 32 ] ) ;
621+ let nonce = Nonce ( [ 0u8 ; 16 ] ) ;
622+ let secp_ctx = Secp256k1 :: new ( ) ;
623+ let payment_id = PaymentId ( [ 1 ; 32 ] ) ;
624+
625+ let recipient_pubkey = {
626+ let secret_key = SecretKey :: from_slice ( & [ 41 ; 32 ] ) . unwrap ( ) ;
627+ Keypair :: from_secret_key ( & secp_ctx, & secret_key) . public_key ( )
628+ } ;
629+
630+ let invoice_request = OfferBuilder :: new ( recipient_pubkey)
631+ . amount_msats ( 100 )
632+ . build_unchecked ( )
633+ . request_invoice ( & expanded_key, nonce, & secp_ctx, payment_id)
634+ . unwrap ( )
635+ . build_and_sign ( )
636+ . unwrap ( ) ;
637+
638+ // Extract bytes without signature for testing
639+ let mut bytes_without_signature = Vec :: new ( ) ;
640+ let tlv_stream_without_signatures = TlvStream :: new ( & invoice_request. bytes )
641+ . filter ( |record| !SIGNATURE_TYPES . contains ( & record. r#type ) ) ;
642+ for record in tlv_stream_without_signatures {
643+ record. write ( & mut bytes_without_signature) . unwrap ( ) ;
644+ }
645+
646+ let tag = "invoice_request" ;
647+
648+ // Test both functions produce the same result
649+ let validating_result =
650+ super :: TaggedHash :: from_tlv_stream_bytes ( tag, & bytes_without_signature) ;
651+ assert ! (
652+ validating_result. is_ok( ) ,
653+ "Should successfully validate real invoice request data"
654+ ) ;
655+ let validating_hash = validating_result. unwrap ( ) ;
656+
657+ let non_validating_hash =
658+ super :: TaggedHash :: from_valid_tlv_stream_bytes ( tag, & bytes_without_signature) ;
659+
660+ // Verify they produce identical results
661+ assert_eq ! (
662+ validating_hash, non_validating_hash,
663+ "Both functions should produce identical results for real data"
664+ ) ;
665+ }
666+
484667 impl AsRef < [ u8 ] > for InvoiceRequest {
485668 fn as_ref ( & self ) -> & [ u8 ] {
486669 & self . bytes
0 commit comments