]> git.bitcoin.ninja Git - rust-lightning/blob - lightning/src/routing/utxo.rs
Track in-flight `channel_announcement` lookups and avoid duplicates
[rust-lightning] / lightning / src / routing / utxo.rs
1 // This file is Copyright its original authors, visible in version control
2 // history.
3 //
4 // This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5 // or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7 // You may not use this file except in accordance with one or both of these
8 // licenses.
9
10 //! This module contains traits for LDK to access UTXOs to check gossip data is correct.
11 //!
12 //! When lightning nodes gossip channel information, they resist DoS attacks by checking that each
13 //! channel matches a UTXO on-chain, requiring at least some marginal on-chain transacting in
14 //! order to announce a channel. This module handles that checking.
15
16 use bitcoin::{BlockHash, TxOut};
17 use bitcoin::hashes::hex::ToHex;
18
19 use crate::ln::chan_utils::make_funding_redeemscript_from_slices;
20 use crate::ln::msgs::{self, LightningError, ErrorAction};
21 use crate::routing::gossip::{NetworkGraph, NodeId};
22 use crate::util::logger::{Level, Logger};
23 use crate::util::ser::Writeable;
24
25 use crate::prelude::*;
26
27 use alloc::sync::{Arc, Weak};
28 use crate::sync::Mutex;
29 use core::ops::Deref;
30
31 /// An error when accessing the chain via [`UtxoLookup`].
32 #[derive(Clone, Debug)]
33 pub enum UtxoLookupError {
34         /// The requested chain is unknown.
35         UnknownChain,
36
37         /// The requested transaction doesn't exist or hasn't confirmed.
38         UnknownTx,
39 }
40
41 /// The result of a [`UtxoLookup::get_utxo`] call. A call may resolve either synchronously,
42 /// returning the `Sync` variant, or asynchronously, returning an [`UtxoFuture`] in the `Async`
43 /// variant.
44 pub enum UtxoResult {
45         /// A result which was resolved synchronously. It either includes a [`TxOut`] for the output
46         /// requested or a [`UtxoLookupError`].
47         Sync(Result<TxOut, UtxoLookupError>),
48         /// A result which will be resolved asynchronously. It includes a [`UtxoFuture`], a `clone` of
49         /// which you must keep locally and call [`UtxoFuture::resolve`] on once the lookup completes.
50         ///
51         /// Note that in order to avoid runaway memory usage, the number of parallel checks is limited,
52         /// but only fairly loosely. Because a pending checks block all message processing, leaving
53         /// checks pending for an extended time may cause DoS of other functions. It is recommended you
54         /// keep a tight timeout on lookups, on the order of a few seconds.
55         Async(UtxoFuture),
56 }
57
58 /// The `UtxoLookup` trait defines behavior for accessing on-chain UTXOs.
59 pub trait UtxoLookup {
60         /// Returns the transaction output of a funding transaction encoded by [`short_channel_id`].
61         /// Returns an error if `genesis_hash` is for a different chain or if such a transaction output
62         /// is unknown.
63         ///
64         /// [`short_channel_id`]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#definition-of-short_channel_id
65         fn get_utxo(&self, genesis_hash: &BlockHash, short_channel_id: u64) -> UtxoResult;
66 }
67
68 enum ChannelAnnouncement {
69         Full(msgs::ChannelAnnouncement),
70         Unsigned(msgs::UnsignedChannelAnnouncement),
71 }
72
73 struct UtxoMessages {
74         complete: Option<Result<TxOut, UtxoLookupError>>,
75         channel_announce: Option<ChannelAnnouncement>,
76 }
77
78 /// Represents a future resolution of a [`UtxoLookup::get_utxo`] query resolving async.
79 ///
80 /// See [`UtxoResult::Async`] and [`UtxoFuture::resolve`] for more info.
81 #[derive(Clone)]
82 pub struct UtxoFuture {
83         state: Arc<Mutex<UtxoMessages>>,
84 }
85
86 /// A trivial implementation of [`UtxoLookup`] which is used to call back into the network graph
87 /// once we have a concrete resolution of a request.
88 struct UtxoResolver(Result<TxOut, UtxoLookupError>);
89 impl UtxoLookup for UtxoResolver {
90         fn get_utxo(&self, _genesis_hash: &BlockHash, _short_channel_id: u64) -> UtxoResult {
91                 UtxoResult::Sync(self.0.clone())
92         }
93 }
94
95 impl UtxoFuture {
96         /// Builds a new future for later resolution.
97         pub fn new() -> Self {
98                 Self { state: Arc::new(Mutex::new(UtxoMessages {
99                         complete: None,
100                         channel_announce: None,
101                 }))}
102         }
103
104         /// Resolves this future against the given `graph` and with the given `result`.
105         pub fn resolve<L: Deref>(&self, graph: &NetworkGraph<L>, result: Result<TxOut, UtxoLookupError>)
106         where L::Target: Logger {
107                 let announcement = {
108                         let mut pending_checks = graph.pending_checks.internal.lock().unwrap();
109                         let mut async_messages = self.state.lock().unwrap();
110
111                         if async_messages.channel_announce.is_none() {
112                                 // We raced returning to `check_channel_announcement` which hasn't updated
113                                 // `channel_announce` yet. That's okay, we can set the `complete` field which it will
114                                 // check once it gets control again.
115                                 async_messages.complete = Some(result);
116                                 return;
117                         }
118                         let announcement_msg = match async_messages.channel_announce.as_ref().unwrap() {
119                                 ChannelAnnouncement::Full(signed_msg) => &signed_msg.contents,
120                                 ChannelAnnouncement::Unsigned(msg) => &msg,
121                         };
122
123                         pending_checks.lookup_completed(announcement_msg, &Arc::downgrade(&self.state));
124
125                         async_messages.channel_announce.take().unwrap()
126                 };
127
128                 // Now that we've updated our internal state, pass the pending messages back through the
129                 // network graph with a different `UtxoLookup` which will resolve immediately.
130                 // Note that we ignore errors as we don't disconnect peers anyway, so there's nothing to do
131                 // with them.
132                 let resolver = UtxoResolver(result);
133                 match announcement {
134                         ChannelAnnouncement::Full(signed_msg) => {
135                                 let _ = graph.update_channel_from_announcement(&signed_msg, &Some(&resolver));
136                         },
137                         ChannelAnnouncement::Unsigned(msg) => {
138                                 let _ = graph.update_channel_from_unsigned_announcement(&msg, &Some(&resolver));
139                         },
140                 }
141         }
142 }
143
144 struct PendingChecksContext {
145         channels: HashMap<u64, Weak<Mutex<UtxoMessages>>>,
146 }
147
148 impl PendingChecksContext {
149         fn lookup_completed(&mut self,
150                 msg: &msgs::UnsignedChannelAnnouncement, completed_state: &Weak<Mutex<UtxoMessages>>
151         ) {
152                 if let hash_map::Entry::Occupied(e) = self.channels.entry(msg.short_channel_id) {
153                         if Weak::ptr_eq(e.get(), &completed_state) {
154                                 e.remove();
155                         }
156                 }
157         }
158 }
159
160 /// A set of messages which are pending UTXO lookups for processing.
161 pub(super) struct PendingChecks {
162         internal: Mutex<PendingChecksContext>,
163 }
164
165 impl PendingChecks {
166         pub(super) fn new() -> Self {
167                 PendingChecks { internal: Mutex::new(PendingChecksContext {
168                         channels: HashMap::new(),
169                 }) }
170         }
171
172         fn check_replace_previous_entry(msg: &msgs::UnsignedChannelAnnouncement,
173                 full_msg: Option<&msgs::ChannelAnnouncement>, replacement: Option<Weak<Mutex<UtxoMessages>>>,
174                 pending_channels: &mut HashMap<u64, Weak<Mutex<UtxoMessages>>>
175         ) -> Result<(), msgs::LightningError> {
176                 match pending_channels.entry(msg.short_channel_id) {
177                         hash_map::Entry::Occupied(mut e) => {
178                                 // There's already a pending lookup for the given SCID. Check if the messages
179                                 // are the same and, if so, return immediately (don't bother spawning another
180                                 // lookup if we haven't gotten that far yet).
181                                 match Weak::upgrade(&e.get()) {
182                                         Some(pending_msgs) => {
183                                                 let pending_matches = match &pending_msgs.lock().unwrap().channel_announce {
184                                                         Some(ChannelAnnouncement::Full(pending_msg)) => Some(pending_msg) == full_msg,
185                                                         Some(ChannelAnnouncement::Unsigned(pending_msg)) => pending_msg == msg,
186                                                         None => {
187                                                                 // This shouldn't actually be reachable. We set the
188                                                                 // `channel_announce` field under the same lock as setting the
189                                                                 // channel map entry. Still, we can just treat it as
190                                                                 // non-matching and let the new request fly.
191                                                                 debug_assert!(false);
192                                                                 false
193                                                         },
194                                                 };
195                                                 if pending_matches {
196                                                         return Err(LightningError {
197                                                                 err: "Channel announcement is already being checked".to_owned(),
198                                                                 action: ErrorAction::IgnoreDuplicateGossip,
199                                                         });
200                                                 } else {
201                                                         // The earlier lookup is a different message. If we have another
202                                                         // request in-flight now replace the original.
203                                                         // Note that in the replace case whether to replace is somewhat
204                                                         // arbitrary - both results will be handled, we're just updating the
205                                                         // value that will be compared to future lookups with the same SCID.
206                                                         if let Some(item) = replacement {
207                                                                 *e.get_mut() = item;
208                                                         }
209                                                 }
210                                         },
211                                         None => {
212                                                 // The earlier lookup already resolved. We can't be sure its the same
213                                                 // so just remove/replace it and move on.
214                                                 if let Some(item) = replacement {
215                                                         *e.get_mut() = item;
216                                                 } else { e.remove(); }
217                                         },
218                                 }
219                         },
220                         hash_map::Entry::Vacant(v) => {
221                                 if let Some(item) = replacement { v.insert(item); }
222                         },
223                 }
224                 Ok(())
225         }
226
227         pub(super) fn check_channel_announcement<U: Deref>(&self,
228                 utxo_lookup: &Option<U>, msg: &msgs::UnsignedChannelAnnouncement,
229                 full_msg: Option<&msgs::ChannelAnnouncement>
230         ) -> Result<Option<u64>, msgs::LightningError> where U::Target: UtxoLookup {
231                 let handle_result = |res| {
232                         match res {
233                                 Ok(TxOut { value, script_pubkey }) => {
234                                         let expected_script =
235                                                 make_funding_redeemscript_from_slices(msg.bitcoin_key_1.as_slice(), msg.bitcoin_key_2.as_slice()).to_v0_p2wsh();
236                                         if script_pubkey != expected_script {
237                                                 return Err(LightningError{
238                                                         err: format!("Channel announcement key ({}) didn't match on-chain script ({})",
239                                                                 expected_script.to_hex(), script_pubkey.to_hex()),
240                                                         action: ErrorAction::IgnoreError
241                                                 });
242                                         }
243                                         Ok(Some(value))
244                                 },
245                                 Err(UtxoLookupError::UnknownChain) => {
246                                         Err(LightningError {
247                                                 err: format!("Channel announced on an unknown chain ({})",
248                                                         msg.chain_hash.encode().to_hex()),
249                                                 action: ErrorAction::IgnoreError
250                                         })
251                                 },
252                                 Err(UtxoLookupError::UnknownTx) => {
253                                         Err(LightningError {
254                                                 err: "Channel announced without corresponding UTXO entry".to_owned(),
255                                                 action: ErrorAction::IgnoreError
256                                         })
257                                 },
258                         }
259                 };
260
261                 Self::check_replace_previous_entry(msg, full_msg, None,
262                         &mut self.internal.lock().unwrap().channels)?;
263
264                 match utxo_lookup {
265                         &None => {
266                                 // Tentatively accept, potentially exposing us to DoS attacks
267                                 Ok(None)
268                         },
269                         &Some(ref utxo_lookup) => {
270                                 match utxo_lookup.get_utxo(&msg.chain_hash, msg.short_channel_id) {
271                                         UtxoResult::Sync(res) => handle_result(res),
272                                         UtxoResult::Async(future) => {
273                                                 let mut pending_checks = self.internal.lock().unwrap();
274                                                 let mut async_messages = future.state.lock().unwrap();
275                                                 if let Some(res) = async_messages.complete.take() {
276                                                         // In the unlikely event the future resolved before we managed to get it,
277                                                         // handle the result in-line.
278                                                         handle_result(res)
279                                                 } else {
280                                                         Self::check_replace_previous_entry(msg, full_msg,
281                                                                 Some(Arc::downgrade(&future.state)), &mut pending_checks.channels)?;
282                                                         async_messages.channel_announce = Some(
283                                                                 if let Some(msg) = full_msg { ChannelAnnouncement::Full(msg.clone()) }
284                                                                 else { ChannelAnnouncement::Unsigned(msg.clone()) });
285                                                         Err(LightningError {
286                                                                 err: "Channel being checked async".to_owned(),
287                                                                 action: ErrorAction::IgnoreAndLog(Level::Gossip),
288                                                         })
289                                                 }
290                                         },
291                                 }
292                         }
293                 }
294         }
295 }