BlindedPath with unannounced introduction node
authorJeffrey Czyz <jkczyz@gmail.com>
Wed, 12 Jun 2024 15:58:18 +0000 (10:58 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Tue, 18 Jun 2024 14:26:26 +0000 (09:26 -0500)
When creating blinded paths for receiving onion messages, allow using
the recipient's only peer as the introduction node when the recipient is
unannounced. This allows for sending messages without going through an
intermediary, which is useful for testing or when only connected to an
LSP with an empty NetworkGraph.

lightning/src/blinded_path/message.rs
lightning/src/ln/offers_tests.rs
lightning/src/onion_message/functional_tests.rs
lightning/src/onion_message/messenger.rs

index 1f3f5a1fa38e70bdac2c7d01923fa06e04513f2e..369a12243f7faae6971673a398945492bf00c78f 100644 (file)
@@ -30,7 +30,7 @@ use core::mem;
 use core::ops::Deref;
 
 /// An intermediate node, and possibly a short channel id leading to the next node.
-#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
 pub struct ForwardNode {
        /// This node's pubkey.
        pub node_id: PublicKey,
index eca43afee8a020b59223a8edaeae26527de5cf9a..9f4f07df879ecf8e71865c7e1c2e9761a163b4b2 100644 (file)
@@ -950,9 +950,12 @@ fn pays_bolt12_invoice_asynchronously() {
        );
 }
 
-/// Fails creating an offer when a blinded path cannot be created without exposing the node's id.
+/// Checks that an offer can be created using an unannounced node as a blinded path's introduction
+/// node. This is only preferred if there are no other options which may indicated either the offer
+/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but
+/// the recipient doesn't have a network graph.
 #[test]
-fn fails_creating_offer_without_blinded_paths() {
+fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
        let chanmon_cfgs = create_chanmon_cfgs(2);
        let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
        let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
@@ -960,15 +963,38 @@ fn fails_creating_offer_without_blinded_paths() {
 
        create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
 
-       match nodes[0].node.create_offer_builder(None) {
-               Ok(_) => panic!("Expected error"),
-               Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths),
+       let alice = &nodes[0];
+       let alice_id = alice.node.get_our_node_id();
+       let bob = &nodes[1];
+       let bob_id = bob.node.get_our_node_id();
+
+       let offer = alice.node
+               .create_offer_builder(None).unwrap()
+               .amount_msats(10_000_000)
+               .build().unwrap();
+       assert_ne!(offer.signing_pubkey(), Some(alice_id));
+       assert!(!offer.paths().is_empty());
+       for path in offer.paths() {
+               assert_eq!(path.introduction_node, IntroductionNode::NodeId(bob_id));
        }
+
+       let payment_id = PaymentId([1; 32]);
+       bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap();
+       expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
+
+       let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap();
+       alice.onion_messenger.handle_onion_message(&bob_id, &onion_message);
+
+       let (_, reply_path) = extract_invoice_request(alice, &onion_message);
+       assert_eq!(reply_path.introduction_node, IntroductionNode::NodeId(alice_id));
 }
 
-/// Fails creating a refund when a blinded path cannot be created without exposing the node's id.
+/// Checks that a refund can be created using an unannounced node as a blinded path's introduction
+/// node. This is only preferred if there are no other options which may indicated either the refund
+/// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but
+/// the sender doesn't have a network graph.
 #[test]
-fn fails_creating_refund_without_blinded_paths() {
+fn creates_refund_with_blinded_path_using_unannounced_introduction_node() {
        let chanmon_cfgs = create_chanmon_cfgs(2);
        let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
        let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
@@ -976,17 +1002,23 @@ fn fails_creating_refund_without_blinded_paths() {
 
        create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
 
+       let alice = &nodes[0];
+       let alice_id = alice.node.get_our_node_id();
+       let bob = &nodes[1];
+       let bob_id = bob.node.get_our_node_id();
+
        let absolute_expiry = Duration::from_secs(u64::MAX);
        let payment_id = PaymentId([1; 32]);
-
-       match nodes[0].node.create_refund_builder(
-               10_000, absolute_expiry, payment_id, Retry::Attempts(0), None
-       ) {
-               Ok(_) => panic!("Expected error"),
-               Err(e) => assert_eq!(e, Bolt12SemanticError::MissingPaths),
+       let refund = bob.node
+               .create_refund_builder(10_000_000, absolute_expiry, payment_id, Retry::Attempts(0), None)
+               .unwrap()
+               .build().unwrap();
+       assert_ne!(refund.payer_id(), bob_id);
+       assert!(!refund.paths().is_empty());
+       for path in refund.paths() {
+               assert_eq!(path.introduction_node, IntroductionNode::NodeId(alice_id));
        }
-
-       assert!(nodes[0].node.list_recent_payments().is_empty());
+       expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id);
 }
 
 /// Fails creating or paying an offer when a blinded path cannot be created because no peers are
@@ -1165,8 +1197,7 @@ fn fails_sending_invoice_with_unsupported_chain_for_refund() {
        }
 }
 
-/// Fails creating an invoice request when a blinded reply path cannot be created without exposing
-/// the node's id.
+/// Fails creating an invoice request when a blinded reply path cannot be created.
 #[test]
 fn fails_creating_invoice_request_without_blinded_reply_path() {
        let chanmon_cfgs = create_chanmon_cfgs(6);
@@ -1183,7 +1214,7 @@ fn fails_creating_invoice_request_without_blinded_reply_path() {
        let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]);
 
        disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]);
-       disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]);
+       disconnect_peers(david, &[bob, charlie, &nodes[4], &nodes[5]]);
 
        let offer = alice.node
                .create_offer_builder(None).unwrap()
index 08be1b2c5027d64bdf599faca92f0b2ed510ef5c..ed601c047c28a1edfbd1bb930a74a3c8ccf33441 100644 (file)
@@ -492,8 +492,9 @@ fn async_response_with_reply_path_fails() {
        let path_id = Some([2; 32]);
        let reply_path = BlindedPath::new_for_message(&[], bob.node_id, &*bob.entropy_source, &secp_ctx).unwrap();
 
-       // Alice tries to asynchronously respond to Bob, but fails because the nodes are unannounced.
-       // Therefore, the reply_path cannot be used for the response.
+       // Alice tries to asynchronously respond to Bob, but fails because the nodes are unannounced and
+       // disconnected. Thus, a reply path could no be created for the response.
+       disconnect_peers(alice, bob);
        let responder = Responder::new(reply_path, path_id);
        alice.custom_message_handler.expect_message_and_response(message.clone());
        let response_instruction = alice.custom_message_handler.handle_custom_message(message, Some(responder));
index 0fb72c52d2784a03973bfb3ac2bbf846fc3daba6..d333eb2103c5ca516217518ab144dab2f84b9807 100644 (file)
@@ -489,7 +489,7 @@ where
        }
 
        fn create_blinded_paths_from_iter<
-               I: Iterator<Item = ForwardNode>,
+               I: ExactSizeIterator<Item = ForwardNode>,
                T: secp256k1::Signing + secp256k1::Verification
        >(
                &self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
@@ -505,13 +505,18 @@ where
                let is_recipient_announced =
                        network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));
 
+               let has_one_peer = peers.len() == 1;
                let mut peer_info = peers
-                       // Limit to peers with announced channels
+                       // Limit to peers with announced channels unless the recipient is unannounced.
                        .filter_map(|peer|
                                network_graph
                                        .node(&NodeId::from_pubkey(&peer.node_id))
                                        .filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
                                        .map(|info| (peer, info.is_tor_only(), info.channels.len()))
+                                       // Allow messages directly with the only peer when unannounced.
+                                       .or_else(|| (!is_recipient_announced && has_one_peer)
+                                               .then(|| (peer, false, 0))
+                                       )
                        )
                        // Exclude Tor-only nodes when the recipient is announced.
                        .filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))