Skip to content

Commit 826c218

Browse files
committed
Implement the UtxoSource interface for REST/RPC clients
In LDK, we expect users operating nodes on the public network to implement the `UtxoSource` interface in order to validate the gossip they receive from the network. Sadly, because the DoS attack of flooding a node's gossip store isn't a common issue, and because we do not provide an implementation off-the-shelf to make doing so easily, many of our downstream users do not have a `UtxoSource` implementation. In order to change that, here we implement an async `UtxoSource` in the `lightning-block-sync` crate, providing one for users who sync the chain from Bitcoin Core's RPC or REST interfaces.
1 parent 2a5d1bc commit 826c218

File tree

5 files changed

+363
-0
lines changed

5 files changed

+363
-0
lines changed

lightning-block-sync/src/convert.rs

+66
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,19 @@ 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+
if self.0.len() != 32 {
53+
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "bad block hash length"))
54+
} else {
55+
Ok(BlockHash::from_slice(&self.0).unwrap())
56+
}
57+
}
58+
}
59+
4160
/// Converts a JSON value into block header data. The JSON value may be an object representing a
4261
/// block header or an array of such objects. In the latter case, the first object is converted.
4362
impl TryInto<BlockHeaderData> for JsonResponse {
@@ -226,6 +245,53 @@ impl TryInto<Transaction> for JsonResponse {
226245
}
227246
}
228247

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

lightning-block-sync/src/gossip.rs

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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};
6+
7+
use bitcoin::blockdata::transaction::{TxOut, OutPoint};
8+
use bitcoin::hash_types::BlockHash;
9+
10+
use lightning::chain::keysinterface::NodeSigner;
11+
12+
use lightning::ln::peer_handler::{CustomMessageHandler, PeerManager, SocketDescriptor};
13+
use lightning::ln::msgs::{ChannelMessageHandler, OnionMessageHandler};
14+
15+
use lightning::routing::gossip::{NetworkGraph, P2PGossipSync};
16+
use lightning::routing::utxo::{UtxoFuture, UtxoLookup, UtxoResult, UtxoLookupError};
17+
18+
use lightning::util::logger::Logger;
19+
20+
use std::sync::Arc;
21+
use std::future::Future;
22+
use std::ops::Deref;
23+
24+
/// A trait which extends [`BlockSource`] and can be queried to fetch the block at a given height
25+
/// as well as whether a given output is unspent (i.e. a member of the current UTXO set).
26+
///
27+
/// Note that while this is implementable for a [`BlockSource`] which returns filtered block data
28+
/// (i.e. [`BlockData::HeaderOnly`] for [`BlockSource::get_block`] requests), such an
29+
/// implementation will reject all gossip as it is not fully able to verify the UTXOs referenced.
30+
///
31+
/// For effeciency, an implementation may consider caching some set of blocks, as many redundant
32+
/// calls may be made.
33+
pub trait UtxoSource : BlockSource + 'static {
34+
/// Fetches the block hash of the block at the given height.
35+
///
36+
/// This will, in turn, be passed to to [`BlockSource::get_block`] to fetch the block needed to
37+
/// validate gossip.
38+
fn get_block_hash_by_height<'a>(&'a self, block_height: u32) -> AsyncBlockSourceResult<'a, BlockHash>;
39+
40+
/// Returns true if the given output has *not* been spent, i.e. is a member of the current UTXO
41+
/// set.
42+
fn is_output_unspent<'a>(&'a self, outpoint: OutPoint) -> AsyncBlockSourceResult<'a, bool>;
43+
}
44+
45+
/// A generic trait which is able to spawn futures in the background.
46+
///
47+
/// If the `tokio` feature is enabled, this is implemented on `TokioSpawner` struct which
48+
/// delegates to `tokio::spawn()`.
49+
pub trait FutureSpawner : Send + Sync + 'static {
50+
/// Spawns the given future as a background task.
51+
///
52+
/// This method MUST NOT block on the given future immediately.
53+
fn spawn<T: Future<Output = ()> + Send + 'static>(&self, future: T);
54+
}
55+
56+
#[cfg(feature = "tokio")]
57+
/// A trivial [`FutureSpawner`] which delegates to `tokio::spawn`.
58+
pub struct TokioSpawner;
59+
#[cfg(feature = "tokio")]
60+
impl FutureSpawner for TokioSpawner {
61+
fn spawn<T: Future<Output = ()> + Send + 'static>(&self, future: T) {
62+
tokio::spawn(future);
63+
}
64+
}
65+
66+
/// A struct which wraps a [`UtxoSource`] and a few LDK objects and implements the LDK
67+
/// [`UtxoLookup`] trait.
68+
///
69+
/// Note that if you're using this against a Bitcoin Core REST or RPC server, you likely wish to
70+
/// increase the `rpcworkqueue` setting in Bitcoin Core as LDK attempts to parallelize requests (a
71+
/// value of 1024 should more than suffice), and ensure you have sufficient file descriptors
72+
/// available on both Bitcoin Core and your LDK application for each request to hold its own
73+
/// connection.
74+
pub struct GossipVerifier<S: FutureSpawner,
75+
Blocks: Deref + Send + Sync + 'static + Clone,
76+
L: Deref + Send + Sync + 'static,
77+
Descriptor: SocketDescriptor + Send + Sync + 'static,
78+
CM: Deref + Send + Sync + 'static,
79+
OM: Deref + Send + Sync + 'static,
80+
CMH: Deref + Send + Sync + 'static,
81+
NS: Deref + Send + Sync + 'static,
82+
> where
83+
Blocks::Target: UtxoSource,
84+
L::Target: Logger,
85+
CM::Target: ChannelMessageHandler,
86+
OM::Target: OnionMessageHandler,
87+
CMH::Target: CustomMessageHandler,
88+
NS::Target: NodeSigner,
89+
{
90+
source: Blocks,
91+
peer_manager: Arc<PeerManager<Descriptor, CM, Arc<P2PGossipSync<Arc<NetworkGraph<L>>, Self, L>>, OM, L, CMH, NS>>,
92+
gossiper: Arc<P2PGossipSync<Arc<NetworkGraph<L>>, Self, L>>,
93+
spawn: S,
94+
}
95+
96+
impl<S: FutureSpawner,
97+
Blocks: Deref + Send + Sync + Clone,
98+
L: Deref + Send + Sync,
99+
Descriptor: SocketDescriptor + Send + Sync,
100+
CM: Deref + Send + Sync,
101+
OM: Deref + Send + Sync,
102+
CMH: Deref + Send + Sync,
103+
NS: Deref + Send + Sync,
104+
> GossipVerifier<S, Blocks, L, Descriptor, CM, OM, CMH, NS> where
105+
Blocks::Target: UtxoSource,
106+
L::Target: Logger,
107+
CM::Target: ChannelMessageHandler,
108+
OM::Target: OnionMessageHandler,
109+
CMH::Target: CustomMessageHandler,
110+
NS::Target: NodeSigner,
111+
{
112+
/// Constructs a new [`GossipVerifier`].
113+
///
114+
/// This is expected to be given to a [`P2PGossipSync`] (initially constructed with `None` for
115+
/// the UTXO lookup) via [`P2PGossipSync::add_utxo_lookup`].
116+
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 {
117+
Self { source, spawn, gossiper, peer_manager }
118+
}
119+
120+
async fn retrieve_utxo(source: Blocks, short_channel_id: u64) -> Result<TxOut, UtxoLookupError> {
121+
let block_height = (short_channel_id >> 5 * 8) as u32; // block height is most significant three bytes
122+
let transaction_index = ((short_channel_id >> 2 * 8) & 0xffffff) as u32;
123+
let output_index = (short_channel_id & 0xffff) as u16;
124+
125+
let block_hash = source.get_block_hash_by_height(block_height).await
126+
.map_err(|_| UtxoLookupError::UnknownTx)?;
127+
let block_data = source.get_block(&block_hash).await
128+
.map_err(|_| UtxoLookupError::UnknownTx)?;
129+
let mut block = match block_data {
130+
BlockData::HeaderOnly(_) => return Err(UtxoLookupError::UnknownTx),
131+
BlockData::FullBlock(block) => block,
132+
};
133+
if transaction_index as usize >= block.txdata.len() {
134+
return Err(UtxoLookupError::UnknownTx);
135+
}
136+
let mut transaction = block.txdata.swap_remove(transaction_index as usize);
137+
if output_index as usize >= transaction.output.len() {
138+
return Err(UtxoLookupError::UnknownTx);
139+
}
140+
let outpoint_unspent =
141+
source.is_output_unspent(OutPoint::new(transaction.txid(), output_index.into())).await
142+
.map_err(|_| UtxoLookupError::UnknownTx)?;
143+
if outpoint_unspent {
144+
Ok(transaction.output.swap_remove(output_index as usize))
145+
} else {
146+
Err(UtxoLookupError::UnknownTx)
147+
}
148+
}
149+
}
150+
151+
impl<S: FutureSpawner,
152+
Blocks: Deref + Send + Sync + Clone,
153+
L: Deref + Send + Sync,
154+
Descriptor: SocketDescriptor + Send + Sync,
155+
CM: Deref + Send + Sync,
156+
OM: Deref + Send + Sync,
157+
CMH: Deref + Send + Sync,
158+
NS: Deref + Send + Sync,
159+
> Deref for GossipVerifier<S, Blocks, L, Descriptor, CM, OM, CMH, NS> where
160+
Blocks::Target: UtxoSource,
161+
L::Target: Logger,
162+
CM::Target: ChannelMessageHandler,
163+
OM::Target: OnionMessageHandler,
164+
CMH::Target: CustomMessageHandler,
165+
NS::Target: NodeSigner,
166+
{
167+
type Target = Self;
168+
fn deref(&self) -> &Self { self }
169+
}
170+
171+
172+
impl<S: FutureSpawner,
173+
Blocks: Deref + Send + Sync + Clone,
174+
L: Deref + Send + Sync,
175+
Descriptor: SocketDescriptor + Send + Sync,
176+
CM: Deref + Send + Sync,
177+
OM: Deref + Send + Sync,
178+
CMH: Deref + Send + Sync,
179+
NS: Deref + Send + Sync,
180+
> UtxoLookup for GossipVerifier<S, Blocks, L, Descriptor, CM, OM, CMH, NS> where
181+
Blocks::Target: UtxoSource,
182+
L::Target: Logger,
183+
CM::Target: ChannelMessageHandler,
184+
OM::Target: OnionMessageHandler,
185+
CMH::Target: CustomMessageHandler,
186+
NS::Target: NodeSigner,
187+
{
188+
fn get_utxo(&self, _genesis_hash: &BlockHash, short_channel_id: u64) -> UtxoResult {
189+
let res = UtxoFuture::new();
190+
let fut = res.clone();
191+
let source = self.source.clone();
192+
let gossiper = Arc::clone(&self.gossiper);
193+
let pm = Arc::clone(&self.peer_manager);
194+
self.spawn.spawn(async move {
195+
let res = Self::retrieve_utxo(source, short_channel_id).await;
196+
fut.resolve(gossiper.network_graph(), &*gossiper, res);
197+
pm.process_events();
198+
});
199+
UtxoResult::Async(res)
200+
}
201+
}

lightning-block-sync/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub mod http;
2828
pub mod init;
2929
pub mod poll;
3030

31+
pub mod gossip;
32+
3133
#[cfg(feature = "rest-client")]
3234
pub mod rest;
3335

lightning-block-sync/src/rest.rs

+50
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
44
use crate::{BlockData, BlockHeaderData, BlockSource, AsyncBlockSourceResult};
55
use crate::http::{BinaryResponse, HttpEndpoint, HttpClient, JsonResponse};
6+
use crate::gossip::UtxoSource;
7+
use crate::convert::GetUtxosResponse;
68

9+
use bitcoin::OutPoint;
710
use bitcoin::hash_types::BlockHash;
811
use bitcoin::hashes::hex::ToHex;
912

@@ -60,11 +63,30 @@ impl BlockSource for RestClient {
6063
}
6164
}
6265

66+
impl UtxoSource for RestClient {
67+
fn get_block_hash_by_height<'a>(&'a self, block_height: u32) -> AsyncBlockSourceResult<'a, BlockHash> {
68+
Box::pin(async move {
69+
let resource_path = format!("blockhashbyheight/{}.bin", block_height);
70+
Ok(self.request_resource::<BinaryResponse, _>(&resource_path).await?)
71+
})
72+
}
73+
74+
fn is_output_unspent<'a>(&'a self, outpoint: OutPoint) -> AsyncBlockSourceResult<'a, bool> {
75+
Box::pin(async move {
76+
let resource_path = format!("getutxos/{}-{}.json", outpoint.txid.to_hex(), outpoint.vout);
77+
let utxo_result =
78+
self.request_resource::<JsonResponse, GetUtxosResponse>(&resource_path).await?;
79+
Ok(utxo_result.hit_bitmap_nonempty)
80+
})
81+
}
82+
}
83+
6384
#[cfg(test)]
6485
mod tests {
6586
use super::*;
6687
use crate::http::BinaryResponse;
6788
use crate::http::client_tests::{HttpServer, MessageBody};
89+
use bitcoin::hashes::Hash;
6890

6991
/// Parses binary data as a string-encoded `u32`.
7092
impl TryInto<u32> for BinaryResponse {
@@ -113,4 +135,32 @@ mod tests {
113135
Ok(n) => assert_eq!(n, 42),
114136
}
115137
}
138+
139+
#[tokio::test]
140+
async fn parses_negative_getutxos() {
141+
let server = HttpServer::responding_with_ok(MessageBody::Content(
142+
// A real response contains a few more fields, but we actually only look at the
143+
// "bitmap" field, so this should suffice for testing
144+
"{\"chainHeight\": 1, \"bitmap\":\"0\",\"utxos\":[]}"
145+
));
146+
let client = RestClient::new(server.endpoint()).unwrap();
147+
148+
let outpoint = OutPoint::new(bitcoin::Txid::from_inner([0; 32]), 0);
149+
let unspent_output = client.is_output_unspent(outpoint).await.unwrap();
150+
assert_eq!(unspent_output, false);
151+
}
152+
153+
#[tokio::test]
154+
async fn parses_positive_getutxos() {
155+
let server = HttpServer::responding_with_ok(MessageBody::Content(
156+
// A real response contains lots more data, but we actually only look at the "bitmap"
157+
// field, so this should suffice for testing
158+
"{\"chainHeight\": 1, \"bitmap\":\"1\",\"utxos\":[]}"
159+
));
160+
let client = RestClient::new(server.endpoint()).unwrap();
161+
162+
let outpoint = OutPoint::new(bitcoin::Txid::from_inner([0; 32]), 0);
163+
let unspent_output = client.is_output_unspent(outpoint).await.unwrap();
164+
assert_eq!(unspent_output, true);
165+
}
116166
}

0 commit comments

Comments
 (0)