Skip to content

Commit 3dffe54

Browse files
authored
Merge pull request #2248 from TheBlueMatt/2023-04-gossip-check
Implement the UtxoSource interface for REST/RPC clients
2 parents d9eb201 + 189c1fb commit 3dffe54

File tree

8 files changed

+491
-18
lines changed

8 files changed

+491
-18
lines changed

lightning-block-sync/src/convert.rs

+57
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ use serde_json;
1313
use std::convert::From;
1414
use std::convert::TryFrom;
1515
use std::convert::TryInto;
16+
use std::str::FromStr;
1617
use bitcoin::hashes::Hash;
1718

19+
impl TryInto<serde_json::Value> for JsonResponse {
20+
type Error = std::io::Error;
21+
fn try_into(self) -> Result<serde_json::Value, std::io::Error> { Ok(self.0) }
22+
}
23+
1824
/// Conversion from `std::io::Error` into `BlockSourceError`.
1925
impl From<std::io::Error> for BlockSourceError {
2026
fn from(e: std::io::Error) -> BlockSourceError {
@@ -38,6 +44,17 @@ impl TryInto<Block> for BinaryResponse {
3844
}
3945
}
4046

47+
/// Parses binary data as a block hash.
48+
impl TryInto<BlockHash> for BinaryResponse {
49+
type Error = std::io::Error;
50+
51+
fn try_into(self) -> std::io::Result<BlockHash> {
52+
BlockHash::from_slice(&self.0).map_err(|_|
53+
std::io::Error::new(std::io::ErrorKind::InvalidData, "bad block hash length")
54+
)
55+
}
56+
}
57+
4158
/// Converts a JSON value into block header data. The JSON value may be an object representing a
4259
/// block header or an array of such objects. In the latter case, the first object is converted.
4360
impl TryInto<BlockHeaderData> for JsonResponse {
@@ -226,6 +243,46 @@ impl TryInto<Transaction> for JsonResponse {
226243
}
227244
}
228245

246+
impl TryInto<BlockHash> for JsonResponse {
247+
type Error = std::io::Error;
248+
249+
fn try_into(self) -> std::io::Result<BlockHash> {
250+
match self.0.as_str() {
251+
None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON string")),
252+
Some(hex_data) if hex_data.len() != 64 =>
253+
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid hash length")),
254+
Some(hex_data) => BlockHash::from_str(hex_data)
255+
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid hex data")),
256+
}
257+
}
258+
}
259+
260+
/// The REST `getutxos` endpoint retuns a whole pile of data we don't care about and one bit we do
261+
/// - whether the `hit bitmap` field had any entries. Thus we condense the result down into only
262+
/// that.
263+
pub(crate) struct GetUtxosResponse {
264+
pub(crate) hit_bitmap_nonempty: bool
265+
}
266+
267+
impl TryInto<GetUtxosResponse> for JsonResponse {
268+
type Error = std::io::Error;
269+
270+
fn try_into(self) -> std::io::Result<GetUtxosResponse> {
271+
let bitmap_str =
272+
self.0.as_object().ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected an object"))?
273+
.get("bitmap").ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "missing bitmap field"))?
274+
.as_str().ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "bitmap should be an str"))?;
275+
let mut hit_bitmap_nonempty = false;
276+
for c in bitmap_str.chars() {
277+
if c < '0' || c > '9' {
278+
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid byte"));
279+
}
280+
if c > '0' { hit_bitmap_nonempty = true; }
281+
}
282+
Ok(GetUtxosResponse { hit_bitmap_nonempty })
283+
}
284+
}
285+
229286
#[cfg(test)]
230287
pub(crate) mod tests {
231288
use super::*;

lightning-block-sync/src/gossip.rs

+319
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
//! When fetching gossip from peers, lightning nodes need to validate that gossip against the
2+
//! current UTXO set. This module defines an implementation of the LDK API required to do so
3+
//! against a [`BlockSource`] which implements a few additional methods for accessing the UTXO set.
4+
5+
use crate::{AsyncBlockSourceResult, BlockData, BlockSource, BlockSourceError};
6+
7+
use bitcoin::blockdata::block::Block;
8+
use bitcoin::blockdata::transaction::{TxOut, OutPoint};
9+
use bitcoin::hash_types::BlockHash;
10+
11+
use lightning::sign::NodeSigner;
12+
13+
use lightning::ln::peer_handler::{CustomMessageHandler, PeerManager, SocketDescriptor};
14+
use lightning::ln::msgs::{ChannelMessageHandler, OnionMessageHandler};
15+
16+
use lightning::routing::gossip::{NetworkGraph, P2PGossipSync};
17+
use lightning::routing::utxo::{UtxoFuture, UtxoLookup, UtxoResult, UtxoLookupError};
18+
19+
use lightning::util::logger::Logger;
20+
21+
use std::sync::{Arc, Mutex};
22+
use std::collections::VecDeque;
23+
use std::future::Future;
24+
use std::ops::Deref;
25+
use std::pin::Pin;
26+
use std::task::Poll;
27+
28+
/// A trait which extends [`BlockSource`] and can be queried to fetch the block at a given height
29+
/// as well as whether a given output is unspent (i.e. a member of the current UTXO set).
30+
///
31+
/// Note that while this is implementable for a [`BlockSource`] which returns filtered block data
32+
/// (i.e. [`BlockData::HeaderOnly`] for [`BlockSource::get_block`] requests), such an
33+
/// implementation will reject all gossip as it is not fully able to verify the UTXOs referenced.
34+
pub trait UtxoSource : BlockSource + 'static {
35+
/// Fetches the block hash of the block at the given height.
36+
///
37+
/// This will, in turn, be passed to to [`BlockSource::get_block`] to fetch the block needed
38+
/// for gossip validation.
39+
fn get_block_hash_by_height<'a>(&'a self, block_height: u32) -> AsyncBlockSourceResult<'a, BlockHash>;
40+
41+
/// Returns true if the given output has *not* been spent, i.e. is a member of the current UTXO
42+
/// set.
43+
fn is_output_unspent<'a>(&'a self, outpoint: OutPoint) -> AsyncBlockSourceResult<'a, bool>;
44+
}
45+
46+
/// A generic trait which is able to spawn futures in the background.
47+
///
48+
/// If the `tokio` feature is enabled, this is implemented on `TokioSpawner` struct which
49+
/// delegates to `tokio::spawn()`.
50+
pub trait FutureSpawner : Send + Sync + 'static {
51+
/// Spawns the given future as a background task.
52+
///
53+
/// This method MUST NOT block on the given future immediately.
54+
fn spawn<T: Future<Output = ()> + Send + 'static>(&self, future: T);
55+
}
56+
57+
#[cfg(feature = "tokio")]
58+
/// A trivial [`FutureSpawner`] which delegates to `tokio::spawn`.
59+
pub struct TokioSpawner;
60+
#[cfg(feature = "tokio")]
61+
impl FutureSpawner for TokioSpawner {
62+
fn spawn<T: Future<Output = ()> + Send + 'static>(&self, future: T) {
63+
tokio::spawn(future);
64+
}
65+
}
66+
67+
/// A trivial future which joins two other futures and polls them at the same time, returning only
68+
/// once both complete.
69+
pub(crate) struct Joiner<
70+
A: Future<Output=Result<(BlockHash, Option<u32>), BlockSourceError>> + Unpin,
71+
B: Future<Output=Result<BlockHash, BlockSourceError>> + Unpin,
72+
> {
73+
pub a: A,
74+
pub b: B,
75+
a_res: Option<(BlockHash, Option<u32>)>,
76+
b_res: Option<BlockHash>,
77+
}
78+
79+
impl<
80+
A: Future<Output=Result<(BlockHash, Option<u32>), BlockSourceError>> + Unpin,
81+
B: Future<Output=Result<BlockHash, BlockSourceError>> + Unpin,
82+
> Joiner<A, B> {
83+
fn new(a: A, b: B) -> Self { Self { a, b, a_res: None, b_res: None } }
84+
}
85+
86+
impl<
87+
A: Future<Output=Result<(BlockHash, Option<u32>), BlockSourceError>> + Unpin,
88+
B: Future<Output=Result<BlockHash, BlockSourceError>> + Unpin,
89+
> Future for Joiner<A, B> {
90+
type Output = Result<((BlockHash, Option<u32>), BlockHash), BlockSourceError>;
91+
fn poll(mut self: Pin<&mut Self>, ctx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
92+
if self.a_res.is_none() {
93+
match Pin::new(&mut self.a).poll(ctx) {
94+
Poll::Ready(res) => {
95+
if let Ok(ok) = res {
96+
self.a_res = Some(ok);
97+
} else {
98+
return Poll::Ready(Err(res.unwrap_err()));
99+
}
100+
},
101+
Poll::Pending => {},
102+
}
103+
}
104+
if self.b_res.is_none() {
105+
match Pin::new(&mut self.b).poll(ctx) {
106+
Poll::Ready(res) => {
107+
if let Ok(ok) = res {
108+
self.b_res = Some(ok);
109+
} else {
110+
return Poll::Ready(Err(res.unwrap_err()));
111+
}
112+
113+
},
114+
Poll::Pending => {},
115+
}
116+
}
117+
if let Some(b_res) = self.b_res {
118+
if let Some(a_res) = self.a_res {
119+
return Poll::Ready(Ok((a_res, b_res)))
120+
}
121+
}
122+
Poll::Pending
123+
}
124+
}
125+
126+
/// A struct which wraps a [`UtxoSource`] and a few LDK objects and implements the LDK
127+
/// [`UtxoLookup`] trait.
128+
///
129+
/// Note that if you're using this against a Bitcoin Core REST or RPC server, you likely wish to
130+
/// increase the `rpcworkqueue` setting in Bitcoin Core as LDK attempts to parallelize requests (a
131+
/// value of 1024 should more than suffice), and ensure you have sufficient file descriptors
132+
/// available on both Bitcoin Core and your LDK application for each request to hold its own
133+
/// connection.
134+
pub struct GossipVerifier<S: FutureSpawner,
135+
Blocks: Deref + Send + Sync + 'static + Clone,
136+
L: Deref + Send + Sync + 'static,
137+
Descriptor: SocketDescriptor + Send + Sync + 'static,
138+
CM: Deref + Send + Sync + 'static,
139+
OM: Deref + Send + Sync + 'static,
140+
CMH: Deref + Send + Sync + 'static,
141+
NS: Deref + Send + Sync + 'static,
142+
> where
143+
Blocks::Target: UtxoSource,
144+
L::Target: Logger,
145+
CM::Target: ChannelMessageHandler,
146+
OM::Target: OnionMessageHandler,
147+
CMH::Target: CustomMessageHandler,
148+
NS::Target: NodeSigner,
149+
{
150+
source: Blocks,
151+
peer_manager: Arc<PeerManager<Descriptor, CM, Arc<P2PGossipSync<Arc<NetworkGraph<L>>, Self, L>>, OM, L, CMH, NS>>,
152+
gossiper: Arc<P2PGossipSync<Arc<NetworkGraph<L>>, Self, L>>,
153+
spawn: S,
154+
block_cache: Arc<Mutex<VecDeque<(u32, Block)>>>,
155+
}
156+
157+
const BLOCK_CACHE_SIZE: usize = 5;
158+
159+
impl<S: FutureSpawner,
160+
Blocks: Deref + Send + Sync + Clone,
161+
L: Deref + Send + Sync,
162+
Descriptor: SocketDescriptor + Send + Sync,
163+
CM: Deref + Send + Sync,
164+
OM: Deref + Send + Sync,
165+
CMH: Deref + Send + Sync,
166+
NS: Deref + Send + Sync,
167+
> GossipVerifier<S, Blocks, L, Descriptor, CM, OM, CMH, NS> where
168+
Blocks::Target: UtxoSource,
169+
L::Target: Logger,
170+
CM::Target: ChannelMessageHandler,
171+
OM::Target: OnionMessageHandler,
172+
CMH::Target: CustomMessageHandler,
173+
NS::Target: NodeSigner,
174+
{
175+
/// Constructs a new [`GossipVerifier`].
176+
///
177+
/// This is expected to be given to a [`P2PGossipSync`] (initially constructed with `None` for
178+
/// the UTXO lookup) via [`P2PGossipSync::add_utxo_lookup`].
179+
pub fn new(source: Blocks, spawn: S, gossiper: Arc<P2PGossipSync<Arc<NetworkGraph<L>>, Self, L>>, peer_manager: Arc<PeerManager<Descriptor, CM, Arc<P2PGossipSync<Arc<NetworkGraph<L>>, Self, L>>, OM, L, CMH, NS>>) -> Self {
180+
Self {
181+
source, spawn, gossiper, peer_manager,
182+
block_cache: Arc::new(Mutex::new(VecDeque::with_capacity(BLOCK_CACHE_SIZE))),
183+
}
184+
}
185+
186+
async fn retrieve_utxo(
187+
source: Blocks, block_cache: Arc<Mutex<VecDeque<(u32, Block)>>>, short_channel_id: u64
188+
) -> Result<TxOut, UtxoLookupError> {
189+
let block_height = (short_channel_id >> 5 * 8) as u32; // block height is most significant three bytes
190+
let transaction_index = ((short_channel_id >> 2 * 8) & 0xffffff) as u32;
191+
let output_index = (short_channel_id & 0xffff) as u16;
192+
193+
let (outpoint, output);
194+
195+
'tx_found: loop { // Used as a simple goto
196+
macro_rules! process_block {
197+
($block: expr) => { {
198+
if transaction_index as usize >= $block.txdata.len() {
199+
return Err(UtxoLookupError::UnknownTx);
200+
}
201+
let transaction = &$block.txdata[transaction_index as usize];
202+
if output_index as usize >= transaction.output.len() {
203+
return Err(UtxoLookupError::UnknownTx);
204+
}
205+
206+
outpoint = OutPoint::new(transaction.txid(), output_index.into());
207+
output = transaction.output[output_index as usize].clone();
208+
} }
209+
}
210+
{
211+
let recent_blocks = block_cache.lock().unwrap();
212+
for (height, block) in recent_blocks.iter() {
213+
if *height == block_height {
214+
process_block!(block);
215+
break 'tx_found;
216+
}
217+
}
218+
}
219+
220+
let ((_, tip_height_opt), block_hash) =
221+
Joiner::new(source.get_best_block(), source.get_block_hash_by_height(block_height))
222+
.await
223+
.map_err(|_| UtxoLookupError::UnknownTx)?;
224+
if let Some(tip_height) = tip_height_opt {
225+
// If the block doesn't yet have five confirmations, error out.
226+
//
227+
// The BOLT spec requires nodes wait for six confirmations before announcing a
228+
// channel, and we give them one block of headroom in case we're delayed seeing a
229+
// block.
230+
if block_height + 5 > tip_height {
231+
return Err(UtxoLookupError::UnknownTx);
232+
}
233+
}
234+
let block_data = source.get_block(&block_hash).await
235+
.map_err(|_| UtxoLookupError::UnknownTx)?;
236+
let block = match block_data {
237+
BlockData::HeaderOnly(_) => return Err(UtxoLookupError::UnknownTx),
238+
BlockData::FullBlock(block) => block,
239+
};
240+
process_block!(block);
241+
{
242+
let mut recent_blocks = block_cache.lock().unwrap();
243+
let mut insert = true;
244+
for (height, _) in recent_blocks.iter() {
245+
if *height == block_height {
246+
insert = false;
247+
}
248+
}
249+
if insert {
250+
if recent_blocks.len() >= BLOCK_CACHE_SIZE {
251+
recent_blocks.pop_front();
252+
}
253+
recent_blocks.push_back((block_height, block));
254+
}
255+
}
256+
break 'tx_found;
257+
};
258+
let outpoint_unspent =
259+
source.is_output_unspent(outpoint).await.map_err(|_| UtxoLookupError::UnknownTx)?;
260+
if outpoint_unspent {
261+
Ok(output)
262+
} else {
263+
Err(UtxoLookupError::UnknownTx)
264+
}
265+
}
266+
}
267+
268+
impl<S: FutureSpawner,
269+
Blocks: Deref + Send + Sync + Clone,
270+
L: Deref + Send + Sync,
271+
Descriptor: SocketDescriptor + Send + Sync,
272+
CM: Deref + Send + Sync,
273+
OM: Deref + Send + Sync,
274+
CMH: Deref + Send + Sync,
275+
NS: Deref + Send + Sync,
276+
> Deref for GossipVerifier<S, Blocks, L, Descriptor, CM, OM, CMH, NS> where
277+
Blocks::Target: UtxoSource,
278+
L::Target: Logger,
279+
CM::Target: ChannelMessageHandler,
280+
OM::Target: OnionMessageHandler,
281+
CMH::Target: CustomMessageHandler,
282+
NS::Target: NodeSigner,
283+
{
284+
type Target = Self;
285+
fn deref(&self) -> &Self { self }
286+
}
287+
288+
289+
impl<S: FutureSpawner,
290+
Blocks: Deref + Send + Sync + Clone,
291+
L: Deref + Send + Sync,
292+
Descriptor: SocketDescriptor + Send + Sync,
293+
CM: Deref + Send + Sync,
294+
OM: Deref + Send + Sync,
295+
CMH: Deref + Send + Sync,
296+
NS: Deref + Send + Sync,
297+
> UtxoLookup for GossipVerifier<S, Blocks, L, Descriptor, CM, OM, CMH, NS> where
298+
Blocks::Target: UtxoSource,
299+
L::Target: Logger,
300+
CM::Target: ChannelMessageHandler,
301+
OM::Target: OnionMessageHandler,
302+
CMH::Target: CustomMessageHandler,
303+
NS::Target: NodeSigner,
304+
{
305+
fn get_utxo(&self, _genesis_hash: &BlockHash, short_channel_id: u64) -> UtxoResult {
306+
let res = UtxoFuture::new();
307+
let fut = res.clone();
308+
let source = self.source.clone();
309+
let gossiper = Arc::clone(&self.gossiper);
310+
let block_cache = Arc::clone(&self.block_cache);
311+
let pm = Arc::clone(&self.peer_manager);
312+
self.spawn.spawn(async move {
313+
let res = Self::retrieve_utxo(source, block_cache, short_channel_id).await;
314+
fut.resolve(gossiper.network_graph(), &*gossiper, res);
315+
pm.process_events();
316+
});
317+
UtxoResult::Async(res)
318+
}
319+
}

0 commit comments

Comments
 (0)