@@ -61,6 +61,8 @@ use crate::offers::invoice_error::InvoiceError;
6161use crate :: offers:: invoice_request:: { InvoiceRequest , InvoiceRequestFields , InvoiceRequestVerifiedFromOffer } ;
6262use crate :: offers:: nonce:: Nonce ;
6363use crate :: offers:: parse:: Bolt12SemanticError ;
64+ use crate :: offers:: payer_proof:: { PayerProof , PayerProofError } ;
65+ use crate :: types:: payment:: PaymentPreimage ;
6466use crate :: onion_message:: messenger:: { DefaultMessageRouter , Destination , MessageSendInstructions , NodeIdMessageRouter , NullMessageRouter , PeeledOnion , DUMMY_HOPS_PATH_LENGTH , QR_CODED_DUMMY_HOPS_PATH_LENGTH } ;
6567use crate :: onion_message:: offers:: OffersMessage ;
6668use crate :: routing:: gossip:: { NodeAlias , NodeId } ;
@@ -264,6 +266,21 @@ fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessa
264266 }
265267}
266268
269+ /// Extract the payer's nonce from an invoice onion message received by the payer.
270+ ///
271+ /// When the payer receives an invoice through their reply path, the blinded path context
272+ /// contains the nonce originally used for deriving their payer signing key. This nonce is
273+ /// needed to build a [`PayerProof`] using [`PayerProofBuilder::build_with_derived_key`].
274+ fn extract_payer_context < ' a , ' b , ' c > ( node : & Node < ' a , ' b , ' c > , message : & OnionMessage ) -> ( PaymentId , Nonce ) {
275+ match node. onion_messenger . peel_onion_message ( message) {
276+ Ok ( PeeledOnion :: Offers ( _, Some ( OffersContext :: OutboundPaymentForOffer { payment_id, nonce, .. } ) , _) ) => ( payment_id, nonce) ,
277+ Ok ( PeeledOnion :: Offers ( _, context, _) ) => panic ! ( "Expected OutboundPaymentForOffer context, got: {:?}" , context) ,
278+ Ok ( PeeledOnion :: Forward ( _, _) ) => panic ! ( "Unexpected onion message forward" ) ,
279+ Ok ( _) => panic ! ( "Unexpected onion message" ) ,
280+ Err ( e) => panic ! ( "Failed to process onion message {:?}" , e) ,
281+ }
282+ }
283+
267284pub ( super ) fn extract_invoice_request < ' a , ' b , ' c > (
268285 node : & Node < ' a , ' b , ' c > , message : & OnionMessage
269286) -> ( InvoiceRequest , BlindedMessagePath ) {
@@ -2667,3 +2684,227 @@ fn creates_and_pays_for_phantom_offer() {
26672684 assert ! ( nodes[ 0 ] . onion_messenger. next_onion_message_for_peer( node_c_id) . is_none( ) ) ;
26682685 }
26692686}
2687+
2688+ /// Tests the full payer proof lifecycle: offer -> invoice_request -> invoice -> payment ->
2689+ /// proof creation with derived key signing -> verification -> bech32 round-trip.
2690+ ///
2691+ /// This exercises the primary API path where a wallet pays a BOLT 12 offer and then creates
2692+ /// a payer proof using the derived signing key (same key derivation as the invoice request).
2693+ #[ test]
2694+ fn creates_and_verifies_payer_proof_after_offer_payment ( ) {
2695+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
2696+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
2697+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
2698+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
2699+
2700+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , 10_000_000 , 1_000_000_000 ) ;
2701+
2702+ let alice = & nodes[ 0 ] ; // recipient (offer creator)
2703+ let alice_id = alice. node . get_our_node_id ( ) ;
2704+ let bob = & nodes[ 1 ] ; // payer
2705+ let bob_id = bob. node . get_our_node_id ( ) ;
2706+
2707+ // Alice creates an offer
2708+ let offer = alice. node
2709+ . create_offer_builder ( ) . unwrap ( )
2710+ . amount_msats ( 10_000_000 )
2711+ . build ( ) . unwrap ( ) ;
2712+
2713+ // Bob initiates payment
2714+ let payment_id = PaymentId ( [ 1 ; 32 ] ) ;
2715+ bob. node . pay_for_offer ( & offer, None , payment_id, Default :: default ( ) ) . unwrap ( ) ;
2716+ expect_recent_payment ! ( bob, RecentPaymentDetails :: AwaitingInvoice , payment_id) ;
2717+
2718+ // Bob sends invoice request to Alice
2719+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
2720+ alice. onion_messenger . handle_onion_message ( bob_id, & onion_message) ;
2721+
2722+ let ( invoice_request, _) = extract_invoice_request ( alice, & onion_message) ;
2723+
2724+ // Alice sends invoice back to Bob
2725+ let onion_message = alice. onion_messenger . next_onion_message_for_peer ( bob_id) . unwrap ( ) ;
2726+ bob. onion_messenger . handle_onion_message ( alice_id, & onion_message) ;
2727+
2728+ let ( invoice, _) = extract_invoice ( bob, & onion_message) ;
2729+ assert_eq ! ( invoice. amount_msats( ) , 10_000_000 ) ;
2730+
2731+ // Extract the payer nonce and payment_id from Bob's reply path context. In a real wallet,
2732+ // these would be persisted alongside the payment for later payer proof creation.
2733+ let ( context_payment_id, payer_nonce) = extract_payer_context ( bob, & onion_message) ;
2734+ assert_eq ! ( context_payment_id, payment_id) ;
2735+
2736+ // Route the payment
2737+ route_bolt12_payment ( bob, & [ alice] , & invoice) ;
2738+ expect_recent_payment ! ( bob, RecentPaymentDetails :: Pending , payment_id) ;
2739+
2740+ // Get the payment preimage from Alice's PaymentClaimable event and claim it.
2741+ // In a real wallet, the payer receives the preimage via Event::PaymentSent after the
2742+ // recipient claims. For the test, we extract it from the recipient's claimable event.
2743+ let payment_preimage = match get_event ! ( alice, Event :: PaymentClaimable ) {
2744+ Event :: PaymentClaimable { purpose, .. } => {
2745+ match & purpose {
2746+ PaymentPurpose :: Bolt12OfferPayment { payment_context, .. } => {
2747+ assert_eq ! ( payment_context. offer_id, offer. id( ) ) ;
2748+ assert_eq ! (
2749+ payment_context. invoice_request. payer_signing_pubkey,
2750+ invoice_request. payer_signing_pubkey( ) ,
2751+ ) ;
2752+ } ,
2753+ _ => panic ! ( "Expected Bolt12OfferPayment purpose" ) ,
2754+ }
2755+ purpose. preimage ( ) . unwrap ( )
2756+ } ,
2757+ _ => panic ! ( "Expected Event::PaymentClaimable" ) ,
2758+ } ;
2759+
2760+ claim_payment ( bob, & [ alice] , payment_preimage) ;
2761+ expect_recent_payment ! ( bob, RecentPaymentDetails :: Fulfilled , payment_id) ;
2762+
2763+ // --- Payer Proof Creation ---
2764+ // Bob (the payer) creates a proof-of-payment with selective disclosure.
2765+ // He includes the offer description and invoice amount, but omits other fields for privacy.
2766+ let expanded_key = bob. keys_manager . get_expanded_key ( ) ;
2767+ let proof = invoice. payer_proof_builder ( payment_preimage) . unwrap ( )
2768+ . include_offer_description ( )
2769+ . include_invoice_amount ( )
2770+ . include_invoice_created_at ( )
2771+ . build_with_derived_key ( & expanded_key, payer_nonce, payment_id, None )
2772+ . unwrap ( ) ;
2773+
2774+ // Check proof contents match the original payment
2775+ assert_eq ! ( proof. preimage( ) , payment_preimage) ;
2776+ assert_eq ! ( proof. payment_hash( ) , invoice. payment_hash( ) ) ;
2777+ assert_eq ! ( proof. payer_id( ) , invoice. payer_signing_pubkey( ) ) ;
2778+ assert_eq ! ( proof. issuer_signing_pubkey( ) , invoice. signing_pubkey( ) ) ;
2779+ assert ! ( proof. payer_note( ) . is_none( ) ) ;
2780+
2781+ // --- Serialization Round-Trip ---
2782+ // The proof can be serialized to a bech32 string (lnp...) for sharing.
2783+ let encoded = proof. to_string ( ) ;
2784+ assert ! ( encoded. starts_with( "lnp1" ) ) ;
2785+
2786+ // Round-trip through TLV bytes: re-parse the raw bytes (verification happens at parse time).
2787+ let decoded = PayerProof :: try_from ( proof. bytes ( ) . to_vec ( ) ) . unwrap ( ) ;
2788+ assert_eq ! ( decoded. preimage( ) , proof. preimage( ) ) ;
2789+ assert_eq ! ( decoded. payment_hash( ) , proof. payment_hash( ) ) ;
2790+ assert_eq ! ( decoded. payer_id( ) , proof. payer_id( ) ) ;
2791+ assert_eq ! ( decoded. issuer_signing_pubkey( ) , proof. issuer_signing_pubkey( ) ) ;
2792+ assert_eq ! ( decoded. merkle_root( ) , proof. merkle_root( ) ) ;
2793+ }
2794+
2795+ /// Tests payer proof creation with a payer note, selective disclosure of specific invoice
2796+ /// fields, and error cases. Verifies that:
2797+ /// - A wrong preimage is rejected
2798+ /// - A minimal proof (required fields only) works
2799+ /// - Selective disclosure with a payer note works
2800+ /// - The proof survives a bech32 round-trip with the note intact
2801+ #[ test]
2802+ fn creates_payer_proof_with_note_and_selective_disclosure ( ) {
2803+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
2804+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
2805+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
2806+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
2807+
2808+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , 10_000_000 , 1_000_000_000 ) ;
2809+
2810+ let alice = & nodes[ 0 ] ;
2811+ let alice_id = alice. node . get_our_node_id ( ) ;
2812+ let bob = & nodes[ 1 ] ;
2813+ let bob_id = bob. node . get_our_node_id ( ) ;
2814+
2815+ // Alice creates an offer with a description
2816+ let offer = alice. node
2817+ . create_offer_builder ( ) . unwrap ( )
2818+ . amount_msats ( 5_000_000 )
2819+ . description ( "Coffee beans - 1kg" . into ( ) )
2820+ . build ( ) . unwrap ( ) ;
2821+
2822+ // Bob pays for the offer
2823+ let payment_id = PaymentId ( [ 2 ; 32 ] ) ;
2824+ bob. node . pay_for_offer ( & offer, None , payment_id, Default :: default ( ) ) . unwrap ( ) ;
2825+ expect_recent_payment ! ( bob, RecentPaymentDetails :: AwaitingInvoice , payment_id) ;
2826+
2827+ // Exchange messages
2828+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
2829+ alice. onion_messenger . handle_onion_message ( bob_id, & onion_message) ;
2830+ let ( invoice_request, _) = extract_invoice_request ( alice, & onion_message) ;
2831+
2832+ let onion_message = alice. onion_messenger . next_onion_message_for_peer ( bob_id) . unwrap ( ) ;
2833+ bob. onion_messenger . handle_onion_message ( alice_id, & onion_message) ;
2834+
2835+ let ( invoice, _) = extract_invoice ( bob, & onion_message) ;
2836+ let ( context_payment_id, payer_nonce) = extract_payer_context ( bob, & onion_message) ;
2837+ assert_eq ! ( context_payment_id, payment_id) ;
2838+
2839+ // Route and claim the payment, extracting the preimage
2840+ route_bolt12_payment ( bob, & [ alice] , & invoice) ;
2841+ expect_recent_payment ! ( bob, RecentPaymentDetails :: Pending , payment_id) ;
2842+
2843+ let payment_preimage = match get_event ! ( alice, Event :: PaymentClaimable ) {
2844+ Event :: PaymentClaimable { purpose, .. } => {
2845+ match & purpose {
2846+ PaymentPurpose :: Bolt12OfferPayment { payment_context, .. } => {
2847+ assert_eq ! ( payment_context. offer_id, offer. id( ) ) ;
2848+ assert_eq ! (
2849+ payment_context. invoice_request. payer_signing_pubkey,
2850+ invoice_request. payer_signing_pubkey( ) ,
2851+ ) ;
2852+ } ,
2853+ _ => panic ! ( "Expected Bolt12OfferPayment purpose" ) ,
2854+ }
2855+ purpose. preimage ( ) . unwrap ( )
2856+ } ,
2857+ _ => panic ! ( "Expected Event::PaymentClaimable" ) ,
2858+ } ;
2859+
2860+ claim_payment ( bob, & [ alice] , payment_preimage) ;
2861+ expect_recent_payment ! ( bob, RecentPaymentDetails :: Fulfilled , payment_id) ;
2862+
2863+ // --- Test 1: Wrong preimage is rejected ---
2864+ let wrong_preimage = PaymentPreimage ( [ 0xDE ; 32 ] ) ;
2865+ assert ! ( invoice. payer_proof_builder( wrong_preimage) . is_err( ) ) ;
2866+
2867+ // --- Test 2: Wrong payment_id causes key derivation failure ---
2868+ let expanded_key = bob. keys_manager . get_expanded_key ( ) ;
2869+ let wrong_payment_id = PaymentId ( [ 0xFF ; 32 ] ) ;
2870+ let result = invoice. payer_proof_builder ( payment_preimage) . unwrap ( )
2871+ . build_with_derived_key ( & expanded_key, payer_nonce, wrong_payment_id, None ) ;
2872+ assert ! ( matches!( result, Err ( PayerProofError :: KeyDerivationFailed ) ) ) ;
2873+
2874+ // --- Test 3: Wrong nonce causes key derivation failure ---
2875+ let wrong_nonce = Nonce :: from_entropy_source ( & chanmon_cfgs[ 0 ] . keys_manager ) ;
2876+ let result = invoice. payer_proof_builder ( payment_preimage) . unwrap ( )
2877+ . build_with_derived_key ( & expanded_key, wrong_nonce, payment_id, None ) ;
2878+ assert ! ( matches!( result, Err ( PayerProofError :: KeyDerivationFailed ) ) ) ;
2879+
2880+ // --- Test 4: Minimal proof (only required fields) ---
2881+ let minimal_proof = invoice. payer_proof_builder ( payment_preimage) . unwrap ( )
2882+ . build_with_derived_key ( & expanded_key, payer_nonce, payment_id, None )
2883+ . unwrap ( ) ;
2884+ // --- Test 5: Proof with selective disclosure and payer note ---
2885+ let proof_with_note = invoice. payer_proof_builder ( payment_preimage) . unwrap ( )
2886+ . include_offer_description ( )
2887+ . include_offer_issuer ( )
2888+ . include_invoice_amount ( )
2889+ . include_invoice_created_at ( )
2890+ . build_with_derived_key ( & expanded_key, payer_nonce, payment_id, Some ( "Paid for coffee" ) )
2891+ . unwrap ( ) ;
2892+ assert_eq ! ( proof_with_note. payer_note( ) . map( |p| p. 0 ) , Some ( "Paid for coffee" ) ) ;
2893+
2894+ // Both proofs should verify and have the same core fields
2895+ assert_eq ! ( minimal_proof. preimage( ) , proof_with_note. preimage( ) ) ;
2896+ assert_eq ! ( minimal_proof. payment_hash( ) , proof_with_note. payment_hash( ) ) ;
2897+ assert_eq ! ( minimal_proof. payer_id( ) , proof_with_note. payer_id( ) ) ;
2898+ assert_eq ! ( minimal_proof. issuer_signing_pubkey( ) , proof_with_note. issuer_signing_pubkey( ) ) ;
2899+
2900+ // The merkle roots are the same since both reconstruct from the same invoice
2901+ assert_eq ! ( minimal_proof. merkle_root( ) , proof_with_note. merkle_root( ) ) ;
2902+
2903+ // --- Test 6: Round-trip the proof with note through TLV bytes ---
2904+ let encoded = proof_with_note. to_string ( ) ;
2905+ assert ! ( encoded. starts_with( "lnp1" ) ) ;
2906+
2907+ let decoded = PayerProof :: try_from ( proof_with_note. bytes ( ) . to_vec ( ) ) . unwrap ( ) ;
2908+ assert_eq ! ( decoded. payer_note( ) . map( |p| p. 0 ) , Some ( "Paid for coffee" ) ) ;
2909+ assert_eq ! ( decoded. preimage( ) , payment_preimage) ;
2910+ }
0 commit comments