Skip to content

Commit 56cb6a1

Browse files
committed
Avoid redundant {channel,node}_announcement signature checks
If we receive `{channel,node}_announcement` messages which we already have, we first validate their signatures and then look in our graph and discover that we should discard the messages. This avoids a second lock in `node_announcement` handling but does not impact our locking in `channel_announcement` handling. It also avoids lock contention in cases where the signatures are invalid, but that should be exceedingly rare. For nodes with relatively few peers, this is a fine state to be in, however for nodes with many peers, we may see the same messages hundreds of times. This causes a rather substantial waste of CPU resources validating gossip messages. Instead, here, we change to checking our network graph first and then validate the signatures only if we don't already have the message.
1 parent d35239c commit 56cb6a1

File tree

1 file changed

+70
-45
lines changed

1 file changed

+70
-45
lines changed

lightning/src/routing/gossip.rs

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,15 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
17201720
/// RoutingMessageHandler implementation to call it indirectly. This may be useful to accept
17211721
/// routing messages from a source using a protocol other than the lightning P2P protocol.
17221722
pub fn update_node_from_announcement(&self, msg: &msgs::NodeAnnouncement) -> Result<(), LightningError> {
1723+
// First check if we have the announcement already to avoid the CPU cost of validating a
1724+
// redundant announcement.
1725+
if let Some(node) = self.nodes.read().unwrap().get(&msg.contents.node_id) {
1726+
if let Some(node_info) = node.announcement_info.as_ref() {
1727+
if node_info.last_update() == msg.contents.timestamp {
1728+
return Err(LightningError{err: "Update had the same timestamp as last processed update".to_owned(), action: ErrorAction::IgnoreDuplicateGossip});
1729+
}
1730+
}
1731+
}
17231732
verify_node_announcement(msg, &self.secp_ctx)?;
17241733
self.update_node_from_announcement_intern(&msg.contents, Some(&msg))
17251734
}
@@ -1788,6 +1797,7 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
17881797
where
17891798
U::Target: UtxoLookup,
17901799
{
1800+
self.pre_channel_announcement_validation_check(&msg.contents, utxo_lookup)?;
17911801
verify_channel_announcement(msg, &self.secp_ctx)?;
17921802
self.update_channel_from_unsigned_announcement_intern(&msg.contents, Some(msg), utxo_lookup)
17931803
}
@@ -1817,6 +1827,7 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
18171827
where
18181828
U::Target: UtxoLookup,
18191829
{
1830+
self.pre_channel_announcement_validation_check(&msg, utxo_lookup)?;
18201831
self.update_channel_from_unsigned_announcement_intern(msg, None, utxo_lookup)
18211832
}
18221833

@@ -1911,6 +1922,52 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
19111922
Ok(())
19121923
}
19131924

1925+
/// If we already have all the information for a channel that we're gonna get, there's no
1926+
/// reason to redundantly process it.
1927+
///
1928+
/// In those cases, this will return an `Err` that we can return immediately. Otherwise it will
1929+
/// return an `Ok(())`.
1930+
fn pre_channel_announcement_validation_check<U: Deref>(
1931+
&self, msg: &msgs::UnsignedChannelAnnouncement, utxo_lookup: &Option<U>,
1932+
) -> Result<(), LightningError> where U::Target: UtxoLookup {
1933+
let channels = self.channels.read().unwrap();
1934+
1935+
if let Some(chan) = channels.get(&msg.short_channel_id) {
1936+
if chan.capacity_sats.is_some() {
1937+
// If we'd previously looked up the channel on-chain and checked the script
1938+
// against what appears on-chain, ignore the duplicate announcement.
1939+
//
1940+
// Because a reorg could replace one channel with another at the same SCID, if
1941+
// the channel appears to be different, we re-validate. This doesn't expose us
1942+
// to any more DoS risk than not, as a peer can always flood us with
1943+
// randomly-generated SCID values anyway.
1944+
//
1945+
// We use the Node IDs rather than the bitcoin_keys to check for "equivalence"
1946+
// as we didn't (necessarily) store the bitcoin keys, and we only really care
1947+
// if the peers on the channel changed anyway.
1948+
if msg.node_id_1 == chan.node_one && msg.node_id_2 == chan.node_two {
1949+
return Err(LightningError {
1950+
err: "Already have chain-validated channel".to_owned(),
1951+
action: ErrorAction::IgnoreDuplicateGossip
1952+
});
1953+
}
1954+
} else if utxo_lookup.is_none() {
1955+
// Similarly, if we can't check the chain right now anyway, ignore the
1956+
// duplicate announcement without bothering to take the channels write lock.
1957+
return Err(LightningError {
1958+
err: "Already have non-chain-validated channel".to_owned(),
1959+
action: ErrorAction::IgnoreDuplicateGossip
1960+
});
1961+
}
1962+
}
1963+
1964+
Ok(())
1965+
}
1966+
1967+
/// Update channel information from a received announcement.
1968+
///
1969+
/// Generally [`Self::pre_channel_announcement_validation_check`] should have been called
1970+
/// first.
19141971
fn update_channel_from_unsigned_announcement_intern<U: Deref>(
19151972
&self, msg: &msgs::UnsignedChannelAnnouncement, full_msg: Option<&msgs::ChannelAnnouncement>, utxo_lookup: &Option<U>
19161973
) -> Result<(), LightningError>
@@ -1928,39 +1985,6 @@ impl<L: Deref> NetworkGraph<L> where L::Target: Logger {
19281985
});
19291986
}
19301987

1931-
{
1932-
let channels = self.channels.read().unwrap();
1933-
1934-
if let Some(chan) = channels.get(&msg.short_channel_id) {
1935-
if chan.capacity_sats.is_some() {
1936-
// If we'd previously looked up the channel on-chain and checked the script
1937-
// against what appears on-chain, ignore the duplicate announcement.
1938-
//
1939-
// Because a reorg could replace one channel with another at the same SCID, if
1940-
// the channel appears to be different, we re-validate. This doesn't expose us
1941-
// to any more DoS risk than not, as a peer can always flood us with
1942-
// randomly-generated SCID values anyway.
1943-
//
1944-
// We use the Node IDs rather than the bitcoin_keys to check for "equivalence"
1945-
// as we didn't (necessarily) store the bitcoin keys, and we only really care
1946-
// if the peers on the channel changed anyway.
1947-
if msg.node_id_1 == chan.node_one && msg.node_id_2 == chan.node_two {
1948-
return Err(LightningError {
1949-
err: "Already have chain-validated channel".to_owned(),
1950-
action: ErrorAction::IgnoreDuplicateGossip
1951-
});
1952-
}
1953-
} else if utxo_lookup.is_none() {
1954-
// Similarly, if we can't check the chain right now anyway, ignore the
1955-
// duplicate announcement without bothering to take the channels write lock.
1956-
return Err(LightningError {
1957-
err: "Already have non-chain-validated channel".to_owned(),
1958-
action: ErrorAction::IgnoreDuplicateGossip
1959-
});
1960-
}
1961-
}
1962-
}
1963-
19641988
{
19651989
let removed_channels = self.removed_channels.lock().unwrap();
19661990
let removed_nodes = self.removed_nodes.lock().unwrap();
@@ -2562,11 +2586,6 @@ pub(crate) mod tests {
25622586
};
25632587
}
25642588

2565-
match gossip_sync.handle_node_announcement(&valid_announcement) {
2566-
Ok(res) => assert!(res),
2567-
Err(_) => panic!()
2568-
};
2569-
25702589
let fake_msghash = hash_to_message!(zero_hash.as_byte_array());
25712590
match gossip_sync.handle_node_announcement(
25722591
&NodeAnnouncement {
@@ -2577,6 +2596,11 @@ pub(crate) mod tests {
25772596
Err(e) => assert_eq!(e.err, "Invalid signature on node_announcement message")
25782597
};
25792598

2599+
match gossip_sync.handle_node_announcement(&valid_announcement) {
2600+
Ok(res) => assert!(res),
2601+
Err(_) => panic!()
2602+
};
2603+
25802604
let announcement_with_data = get_signed_node_announcement(|unsigned_announcement| {
25812605
unsigned_announcement.timestamp += 1000;
25822606
unsigned_announcement.excess_data.resize(MAX_EXCESS_BYTES_FOR_RELAY + 1, 0);
@@ -2698,23 +2722,24 @@ pub(crate) mod tests {
26982722
}
26992723
}
27002724

2701-
// Don't relay valid channels with excess data
2702-
let valid_announcement = get_signed_channel_announcement(|unsigned_announcement| {
2725+
let valid_excess_data_announcement = get_signed_channel_announcement(|unsigned_announcement| {
27032726
unsigned_announcement.short_channel_id += 4;
27042727
unsigned_announcement.excess_data.resize(MAX_EXCESS_BYTES_FOR_RELAY + 1, 0);
27052728
}, node_1_privkey, node_2_privkey, &secp_ctx);
2706-
match gossip_sync.handle_channel_announcement(&valid_announcement) {
2707-
Ok(res) => assert!(!res),
2708-
_ => panic!()
2709-
};
27102729

2711-
let mut invalid_sig_announcement = valid_announcement.clone();
2730+
let mut invalid_sig_announcement = valid_excess_data_announcement.clone();
27122731
invalid_sig_announcement.contents.excess_data = Vec::new();
27132732
match gossip_sync.handle_channel_announcement(&invalid_sig_announcement) {
27142733
Ok(_) => panic!(),
27152734
Err(e) => assert_eq!(e.err, "Invalid signature on channel_announcement message")
27162735
};
27172736

2737+
// Don't relay valid channels with excess data
2738+
match gossip_sync.handle_channel_announcement(&valid_excess_data_announcement) {
2739+
Ok(res) => assert!(!res),
2740+
_ => panic!()
2741+
};
2742+
27182743
let channel_to_itself_announcement = get_signed_channel_announcement(|_| {}, node_1_privkey, node_1_privkey, &secp_ctx);
27192744
match gossip_sync.handle_channel_announcement(&channel_to_itself_announcement) {
27202745
Ok(_) => panic!(),

0 commit comments

Comments
 (0)