Fail UTXO lookups if the block doesn't have five confirmations 2023-04-gossip-check
authorMatt Corallo <git@bluematt.me>
Mon, 5 Jun 2023 17:22:36 +0000 (17:22 +0000)
committerMatt Corallo <git@bluematt.me>
Wed, 23 Aug 2023 21:48:03 +0000 (21:48 +0000)
The BOLT spec mandates that channels not be announced until they
have at least six confirmations. This is important to enforce not
because we particularly care about any specific DoS concerns, but
because if we do not we may have to handle reorgs of channel
funding transactions which change their SCID or have conflicting
SCIDs.

lightning-block-sync/src/gossip.rs

index 4e66c0ce9756f016523bb9a9cfd2d65f973f5eca..37f4268513f45ec2558a0d8f0635370c37e2d5f0 100644 (file)
@@ -2,7 +2,7 @@
 //! current UTXO set. This module defines an implementation of the LDK API required to do so
 //! against a [`BlockSource`] which implements a few additional methods for accessing the UTXO set.
 
-use crate::{AsyncBlockSourceResult, BlockData, BlockSource};
+use crate::{AsyncBlockSourceResult, BlockData, BlockSource, BlockSourceError};
 
 use bitcoin::blockdata::block::Block;
 use bitcoin::blockdata::transaction::{TxOut, OutPoint};
@@ -22,6 +22,8 @@ use std::sync::{Arc, Mutex};
 use std::collections::VecDeque;
 use std::future::Future;
 use std::ops::Deref;
+use std::pin::Pin;
+use std::task::Poll;
 
 /// A trait which extends [`BlockSource`] and can be queried to fetch the block at a given height
 /// as well as whether a given output is unspent (i.e. a member of the current UTXO set).
@@ -62,6 +64,65 @@ impl FutureSpawner for TokioSpawner {
        }
 }
 
+/// A trivial future which joins two other futures and polls them at the same time, returning only
+/// once both complete.
+pub(crate) struct Joiner<
+       A: Future<Output=Result<(BlockHash, Option<u32>), BlockSourceError>> + Unpin,
+       B: Future<Output=Result<BlockHash, BlockSourceError>> + Unpin,
+> {
+       pub a: A,
+       pub b: B,
+       a_res: Option<(BlockHash, Option<u32>)>,
+       b_res: Option<BlockHash>,
+}
+
+impl<
+       A: Future<Output=Result<(BlockHash, Option<u32>), BlockSourceError>> + Unpin,
+       B: Future<Output=Result<BlockHash, BlockSourceError>> + Unpin,
+> Joiner<A, B> {
+       fn new(a: A, b: B) -> Self { Self { a, b, a_res: None, b_res: None } }
+}
+
+impl<
+       A: Future<Output=Result<(BlockHash, Option<u32>), BlockSourceError>> + Unpin,
+       B: Future<Output=Result<BlockHash, BlockSourceError>> + Unpin,
+> Future for Joiner<A, B> {
+       type Output = Result<((BlockHash, Option<u32>), BlockHash), BlockSourceError>;
+       fn poll(mut self: Pin<&mut Self>, ctx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
+               if self.a_res.is_none() {
+                       match Pin::new(&mut self.a).poll(ctx) {
+                               Poll::Ready(res) => {
+                                       if let Ok(ok) = res {
+                                               self.a_res = Some(ok);
+                                       } else {
+                                               return Poll::Ready(Err(res.unwrap_err()));
+                                       }
+                               },
+                               Poll::Pending => {},
+                       }
+               }
+               if self.b_res.is_none() {
+                       match Pin::new(&mut self.b).poll(ctx) {
+                               Poll::Ready(res) => {
+                                       if let Ok(ok) = res {
+                                               self.b_res = Some(ok);
+                                       } else {
+                                               return Poll::Ready(Err(res.unwrap_err()));
+                                       }
+
+                               },
+                               Poll::Pending => {},
+                       }
+               }
+               if let Some(b_res) = self.b_res {
+                       if let Some(a_res) = self.a_res {
+                               return Poll::Ready(Ok((a_res, b_res)))
+                       }
+               }
+               Poll::Pending
+       }
+}
+
 /// A struct which wraps a [`UtxoSource`] and a few LDK objects and implements the LDK
 /// [`UtxoLookup`] trait.
 ///
@@ -156,8 +217,20 @@ impl<S: FutureSpawner,
                                }
                        }
 
-                       let block_hash = source.get_block_hash_by_height(block_height).await
+                       let ((_, tip_height_opt), block_hash) =
+                               Joiner::new(source.get_best_block(), source.get_block_hash_by_height(block_height))
+                               .await
                                .map_err(|_| UtxoLookupError::UnknownTx)?;
+                       if let Some(tip_height) = tip_height_opt {
+                               // If the block doesn't yet have five confirmations, error out.
+                               //
+                               // The BOLT spec requires nodes wait for six confirmations before announcing a
+                               // channel, and we give them one block of headroom in case we're delayed seeing a
+                               // block.
+                               if block_height + 5 > tip_height {
+                                       return Err(UtxoLookupError::UnknownTx);
+                               }
+                       }
                        let block_data = source.get_block(&block_hash).await
                                .map_err(|_| UtxoLookupError::UnknownTx)?;
                        let block = match block_data {