@@ -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:: { self , Bolt12InvoiceType , 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 [`payer_proof::PaidBolt12Invoice::prove_payer_derived`].
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,246 @@ 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 secp_ctx = Secp256k1 :: new ( ) ;
2768+ let paid_invoice = payer_proof:: PaidBolt12Invoice :: new (
2769+ Bolt12InvoiceType :: Bolt12Invoice ( invoice. clone ( ) ) ,
2770+ payment_preimage,
2771+ Some ( payer_nonce) ,
2772+ ) ;
2773+ let proof = paid_invoice
2774+ . prove_payer_derived ( & expanded_key, payment_id, & secp_ctx) . unwrap ( )
2775+ . include_offer_description ( )
2776+ . include_invoice_amount ( )
2777+ . include_invoice_created_at ( )
2778+ . build_and_sign ( None )
2779+ . unwrap ( ) ;
2780+
2781+ // Check proof contents match the original payment
2782+ assert_eq ! ( proof. payment_preimage( ) , payment_preimage) ;
2783+ assert_eq ! ( proof. payment_hash( ) , invoice. payment_hash( ) ) ;
2784+ assert_eq ! ( proof. payer_signing_pubkey( ) , invoice. payer_signing_pubkey( ) ) ;
2785+ assert_eq ! ( proof. issuer_signing_pubkey( ) , invoice. signing_pubkey( ) ) ;
2786+ assert ! ( proof. payer_note( ) . is_none( ) ) ;
2787+
2788+ // --- Serialization Round-Trip ---
2789+ // The proof can be serialized to a bech32 string (lnp...) for sharing.
2790+ let encoded = proof. to_string ( ) ;
2791+ assert ! ( encoded. starts_with( "lnp1" ) ) ;
2792+
2793+ // Round-trip through TLV bytes: re-parse the raw bytes (verification happens at parse time).
2794+ let decoded = PayerProof :: try_from ( proof. bytes ( ) . to_vec ( ) ) . unwrap ( ) ;
2795+ assert_eq ! ( decoded. payment_preimage( ) , proof. payment_preimage( ) ) ;
2796+ assert_eq ! ( decoded. payment_hash( ) , proof. payment_hash( ) ) ;
2797+ assert_eq ! ( decoded. payer_signing_pubkey( ) , proof. payer_signing_pubkey( ) ) ;
2798+ assert_eq ! ( decoded. issuer_signing_pubkey( ) , proof. issuer_signing_pubkey( ) ) ;
2799+ assert_eq ! ( decoded. merkle_root( ) , proof. merkle_root( ) ) ;
2800+ }
2801+
2802+ /// Tests payer proof creation with a payer note, selective disclosure of specific invoice
2803+ /// fields, and error cases. Verifies that:
2804+ /// - A wrong preimage is rejected
2805+ /// - A minimal proof (required fields only) works
2806+ /// - Selective disclosure with a payer note works
2807+ /// - The proof survives a bech32 round-trip with the note intact
2808+ #[ test]
2809+ fn creates_payer_proof_with_note_and_selective_disclosure ( ) {
2810+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
2811+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
2812+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
2813+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
2814+
2815+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , 10_000_000 , 1_000_000_000 ) ;
2816+
2817+ let alice = & nodes[ 0 ] ;
2818+ let alice_id = alice. node . get_our_node_id ( ) ;
2819+ let bob = & nodes[ 1 ] ;
2820+ let bob_id = bob. node . get_our_node_id ( ) ;
2821+
2822+ // Alice creates an offer with a description
2823+ let offer = alice. node
2824+ . create_offer_builder ( ) . unwrap ( )
2825+ . amount_msats ( 5_000_000 )
2826+ . description ( "Coffee beans - 1kg" . into ( ) )
2827+ . build ( ) . unwrap ( ) ;
2828+
2829+ // Bob pays for the offer
2830+ let payment_id = PaymentId ( [ 2 ; 32 ] ) ;
2831+ bob. node . pay_for_offer ( & offer, None , payment_id, Default :: default ( ) ) . unwrap ( ) ;
2832+ expect_recent_payment ! ( bob, RecentPaymentDetails :: AwaitingInvoice , payment_id) ;
2833+
2834+ // Exchange messages
2835+ let onion_message = bob. onion_messenger . next_onion_message_for_peer ( alice_id) . unwrap ( ) ;
2836+ alice. onion_messenger . handle_onion_message ( bob_id, & onion_message) ;
2837+ let ( invoice_request, _) = extract_invoice_request ( alice, & onion_message) ;
2838+
2839+ let onion_message = alice. onion_messenger . next_onion_message_for_peer ( bob_id) . unwrap ( ) ;
2840+ bob. onion_messenger . handle_onion_message ( alice_id, & onion_message) ;
2841+
2842+ let ( invoice, _) = extract_invoice ( bob, & onion_message) ;
2843+ let ( context_payment_id, payer_nonce) = extract_payer_context ( bob, & onion_message) ;
2844+ assert_eq ! ( context_payment_id, payment_id) ;
2845+
2846+ // Route and claim the payment, extracting the preimage
2847+ route_bolt12_payment ( bob, & [ alice] , & invoice) ;
2848+ expect_recent_payment ! ( bob, RecentPaymentDetails :: Pending , payment_id) ;
2849+
2850+ let payment_preimage = match get_event ! ( alice, Event :: PaymentClaimable ) {
2851+ Event :: PaymentClaimable { purpose, .. } => {
2852+ match & purpose {
2853+ PaymentPurpose :: Bolt12OfferPayment { payment_context, .. } => {
2854+ assert_eq ! ( payment_context. offer_id, offer. id( ) ) ;
2855+ assert_eq ! (
2856+ payment_context. invoice_request. payer_signing_pubkey,
2857+ invoice_request. payer_signing_pubkey( ) ,
2858+ ) ;
2859+ } ,
2860+ _ => panic ! ( "Expected Bolt12OfferPayment purpose" ) ,
2861+ }
2862+ purpose. preimage ( ) . unwrap ( )
2863+ } ,
2864+ _ => panic ! ( "Expected Event::PaymentClaimable" ) ,
2865+ } ;
2866+
2867+ claim_payment ( bob, & [ alice] , payment_preimage) ;
2868+ expect_recent_payment ! ( bob, RecentPaymentDetails :: Fulfilled , payment_id) ;
2869+
2870+ // --- Test 1: Wrong preimage is rejected ---
2871+ let wrong_preimage = PaymentPreimage ( [ 0xDE ; 32 ] ) ;
2872+ let wrong_paid = payer_proof:: PaidBolt12Invoice :: new (
2873+ Bolt12InvoiceType :: Bolt12Invoice ( invoice. clone ( ) ) , wrong_preimage, Some ( payer_nonce) ,
2874+ ) ;
2875+ assert ! ( matches!( wrong_paid. prove_payer( ) , Err ( PayerProofError :: PreimageMismatch ) ) ) ;
2876+
2877+ // --- Test 2: Wrong payment_id causes key derivation failure ---
2878+ let expanded_key = bob. keys_manager . get_expanded_key ( ) ;
2879+ let secp_ctx = Secp256k1 :: new ( ) ;
2880+ let paid_invoice = payer_proof:: PaidBolt12Invoice :: new (
2881+ Bolt12InvoiceType :: Bolt12Invoice ( invoice. clone ( ) ) ,
2882+ payment_preimage,
2883+ Some ( payer_nonce) ,
2884+ ) ;
2885+ let wrong_payment_id = PaymentId ( [ 0xFF ; 32 ] ) ;
2886+ let result = paid_invoice. prove_payer_derived ( & expanded_key, wrong_payment_id, & secp_ctx) ;
2887+ assert ! ( matches!( result, Err ( PayerProofError :: KeyDerivationFailed ) ) ) ;
2888+
2889+ // --- Test 3: Wrong nonce causes key derivation failure ---
2890+ let wrong_nonce = Nonce :: from_entropy_source ( & chanmon_cfgs[ 0 ] . keys_manager ) ;
2891+ let wrong_nonce_paid = payer_proof:: PaidBolt12Invoice :: new (
2892+ Bolt12InvoiceType :: Bolt12Invoice ( invoice. clone ( ) ) , payment_preimage, Some ( wrong_nonce) ,
2893+ ) ;
2894+ let result = wrong_nonce_paid. prove_payer_derived ( & expanded_key, payment_id, & secp_ctx) ;
2895+ assert ! ( matches!( result, Err ( PayerProofError :: KeyDerivationFailed ) ) ) ;
2896+
2897+ // --- Test 4: Minimal proof (only required fields) ---
2898+ let minimal_proof = paid_invoice
2899+ . prove_payer_derived ( & expanded_key, payment_id, & secp_ctx) . unwrap ( )
2900+ . build_and_sign ( None )
2901+ . unwrap ( ) ;
2902+ // --- Test 5: Proof with selective disclosure and payer note ---
2903+ let proof_with_note = paid_invoice
2904+ . prove_payer_derived ( & expanded_key, payment_id, & secp_ctx) . unwrap ( )
2905+ . include_offer_description ( )
2906+ . include_offer_issuer ( )
2907+ . include_invoice_amount ( )
2908+ . include_invoice_created_at ( )
2909+ . build_and_sign ( Some ( "Paid for coffee" . into ( ) ) )
2910+ . unwrap ( ) ;
2911+ assert_eq ! ( proof_with_note. payer_note( ) . map( |p| p. 0 ) , Some ( "Paid for coffee" ) ) ;
2912+
2913+ // Both proofs should verify and have the same core fields
2914+ assert_eq ! ( minimal_proof. payment_preimage( ) , proof_with_note. payment_preimage( ) ) ;
2915+ assert_eq ! ( minimal_proof. payment_hash( ) , proof_with_note. payment_hash( ) ) ;
2916+ assert_eq ! ( minimal_proof. payer_signing_pubkey( ) , proof_with_note. payer_signing_pubkey( ) ) ;
2917+ assert_eq ! ( minimal_proof. issuer_signing_pubkey( ) , proof_with_note. issuer_signing_pubkey( ) ) ;
2918+
2919+ // The merkle roots are the same since both reconstruct from the same invoice
2920+ assert_eq ! ( minimal_proof. merkle_root( ) , proof_with_note. merkle_root( ) ) ;
2921+
2922+ // --- Test 6: Round-trip the proof with note through TLV bytes ---
2923+ let encoded = proof_with_note. to_string ( ) ;
2924+ assert ! ( encoded. starts_with( "lnp1" ) ) ;
2925+
2926+ let decoded = PayerProof :: try_from ( proof_with_note. bytes ( ) . to_vec ( ) ) . unwrap ( ) ;
2927+ assert_eq ! ( decoded. payer_note( ) . map( |p| p. 0 ) , Some ( "Paid for coffee" ) ) ;
2928+ assert_eq ! ( decoded. payment_preimage( ) , payment_preimage) ;
2929+ }
0 commit comments