Add a new `NodeCounters` utility to track counters when routing
authorMatt Corallo <git@bluematt.me>
Tue, 19 Mar 2024 19:29:06 +0000 (19:29 +0000)
committerMatt Corallo <git@bluematt.me>
Wed, 20 Mar 2024 00:51:20 +0000 (00:51 +0000)
In the next commit we'll stop using `NodeId`s to look up nodes when
routing, instead using the new per-node counters. Here we take the
first step, adding a local struct which tracks temporary counters
for route hints/source/destination nodes.

Because we must ensure we have a 1-to-1 mapping from node ids to
`node_counter`s, even across first-hop and last-hop hints, we have
to be careful to check the network graph first, then a new
`private_node_id_to_node_counter` map to ensure we only ever end up
with one counter per node id.

lightning/src/routing/gossip.rs
lightning/src/routing/router.rs

index 335876572a008ab0ab348f7e02d69851f1a6899e..f7fbff677b98b34d6040e08bd67a5bda21eac47c 100644 (file)
@@ -204,6 +204,7 @@ pub struct NetworkGraph<L: Deref> where L::Target: Logger {
 pub struct ReadOnlyNetworkGraph<'a> {
        channels: RwLockReadGuard<'a, IndexedMap<u64, ChannelInfo>>,
        nodes: RwLockReadGuard<'a, IndexedMap<NodeId, NodeInfo>>,
+       max_node_counter: u32,
 }
 
 /// Update to the [`NetworkGraph`] based on payment failure information conveyed via the Onion
@@ -1505,6 +1506,7 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
                ReadOnlyNetworkGraph {
                        channels,
                        nodes,
+                       max_node_counter: (self.next_node_counter.load(Ordering::Acquire) as u32).saturating_sub(1),
                }
        }
 
@@ -2194,6 +2196,11 @@ impl ReadOnlyNetworkGraph<'_> {
                self.nodes.get(&NodeId::from_pubkey(&pubkey))
                        .and_then(|node| node.announcement_info.as_ref().map(|ann| ann.addresses().to_vec()))
        }
+
+       /// Gets the maximum possible node_counter for a node in this graph
+       pub(crate) fn max_node_counter(&self) -> u32 {
+               self.max_node_counter
+       }
 }
 
 #[cfg(test)]
index 07b6ece4f1ba2408f2966886112e74c2fd9d8c18..5c793df5e7c0db384744dbf5460c29b2a8070e83 100644 (file)
@@ -1451,6 +1451,74 @@ enum CandidateHopId {
        Blinded(usize),
 }
 
+/// To avoid doing [`PublicKey`] -> [`PathBuildingHop`] hashtable lookups, we assign each
+/// [`PublicKey`]/node a `usize` index and simply keep a `Vec` of values.
+///
+/// While this is easy for gossip-originating nodes (the [`DirectedChannelInfo`] exposes "counters"
+/// for us for this purpose) we have to have our own indexes for nodes originating from invoice
+/// hints, local channels, or blinded path fake nodes.
+///
+/// This wrapper handles all this for us, allowing look-up of counters from the various contexts.
+///
+/// It is first built by passing all [`NodeId`]s that we'll ever care about either though
+/// [`NodeCountersBuilder::node_counter_from_pubkey`] or
+/// [`NodeCountersBuilder::node_counter_from_id`], then calling [`NodeCountersBuilder::build`] and
+/// using the resulting [`NodeCounters`] to look up any counters.
+///
+/// [`NodeCounters::node_counter_from_pubkey`], specifically, will return `Some` iff
+/// [`NodeCountersBuilder::node_counter_from_pubkey`] was called on the same key (not
+/// [`NodeCountersBuilder::node_counter_from_id`]). It will also returned a cached copy of the
+/// [`PublicKey`] -> [`NodeId`] conversion.
+struct NodeCounters<'a> {
+       network_graph: &'a ReadOnlyNetworkGraph<'a>,
+       private_node_id_to_node_counter: HashMap<NodeId, u32>,
+       private_hop_key_cache: HashMap<PublicKey, (NodeId, u32)>,
+}
+
+struct NodeCountersBuilder<'a>(NodeCounters<'a>);
+
+impl<'a> NodeCountersBuilder<'a> {
+       fn new(network_graph: &'a ReadOnlyNetworkGraph) -> Self {
+               Self(NodeCounters {
+                       network_graph,
+                       private_node_id_to_node_counter: new_hash_map(),
+                       private_hop_key_cache: new_hash_map(),
+               })
+       }
+
+       fn node_counter_from_pubkey(&mut self, pubkey: PublicKey) -> u32 {
+               let id = NodeId::from_pubkey(&pubkey);
+               let counter = self.node_counter_from_id(id);
+               self.0.private_hop_key_cache.insert(pubkey, (id, counter));
+               counter
+       }
+
+       fn node_counter_from_id(&mut self, node_id: NodeId) -> u32 {
+               // For any node_id, we first have to check if its in the existing network graph, and then
+               // ensure that we always look up in our internal map first.
+               self.0.network_graph.nodes().get(&node_id)
+                       .map(|node| node.node_counter)
+                       .unwrap_or_else(|| {
+                               let next_node_counter = self.0.network_graph.max_node_counter() + 1 +
+                                       self.0.private_node_id_to_node_counter.len() as u32;
+                               *self.0.private_node_id_to_node_counter.entry(node_id).or_insert(next_node_counter)
+                       })
+       }
+
+       fn build(self) -> NodeCounters<'a> { self.0 }
+}
+
+impl<'a> NodeCounters<'a> {
+       fn max_counter(&self) -> u32 {
+               self.network_graph.max_node_counter() +
+                       self.private_node_id_to_node_counter.len() as u32
+       }
+
+       fn node_counter_from_pubkey(&self, pubkey: &PublicKey) -> Option<&(NodeId, u32)> {
+               self.private_hop_key_cache.get(pubkey)
+       }
+}
+
 #[inline]
 fn max_htlc_from_capacity(capacity: EffectiveCapacity, max_channel_saturation_power_of_half: u8) -> u64 {
        let saturation_shift: u32 = max_channel_saturation_power_of_half as u32;
@@ -1968,6 +2036,17 @@ where L::Target: Logger {
                first_hops.map(|hops| hops.len()).unwrap_or(0), if first_hops.is_some() { "" } else { "not " },
                max_total_routing_fee_msat);
 
+       let mut node_counters = NodeCountersBuilder::new(&network_graph);
+
+       let payer_node_counter = node_counters.node_counter_from_pubkey(*our_node_pubkey);
+       let payee_node_counter = node_counters.node_counter_from_pubkey(maybe_dummy_payee_pk);
+
+       for route in payment_params.payee.unblinded_route_hints().iter() {
+               for hop in route.0.iter() {
+                       node_counters.node_counter_from_pubkey(hop.src_node_id);
+               }
+       }
+
        // Step (1).
        // Prepare the data we'll use for payee-to-payer search by
        // inserting first hops suggested by the caller as targets.