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