lightning_block_sync/
rest.rs

1//! Simple REST client implementation which implements [`BlockSource`] against a Bitcoin Core REST
2//! endpoint.
3
4use crate::convert::GetUtxosResponse;
5use crate::gossip::UtxoSource;
6use crate::http::{BinaryResponse, HttpClient, HttpEndpoint, JsonResponse};
7use crate::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource};
8
9use bitcoin::hash_types::BlockHash;
10use bitcoin::OutPoint;
11
12use std::convert::TryFrom;
13use std::convert::TryInto;
14use std::sync::Mutex;
15
16/// A simple REST client for requesting resources using HTTP `GET`.
17pub struct RestClient {
18	endpoint: HttpEndpoint,
19	client: Mutex<Option<HttpClient>>,
20}
21
22impl RestClient {
23	/// Creates a new REST client connected to the given endpoint.
24	///
25	/// The endpoint should contain the REST path component (e.g., http://127.0.0.1:8332/rest).
26	pub fn new(endpoint: HttpEndpoint) -> Self {
27		Self { endpoint, client: Mutex::new(None) }
28	}
29
30	/// Requests a resource encoded in `F` format and interpreted as type `T`.
31	pub async fn request_resource<F, T>(&self, resource_path: &str) -> std::io::Result<T>
32	where
33		F: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error>,
34	{
35		let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
36		let uri = format!("{}/{}", self.endpoint.path().trim_end_matches("/"), resource_path);
37		let reserved_client = self.client.lock().unwrap().take();
38		let mut client = if let Some(client) = reserved_client {
39			client
40		} else {
41			HttpClient::connect(&self.endpoint)?
42		};
43		let res = client.get::<F>(&uri, &host).await?.try_into();
44		*self.client.lock().unwrap() = Some(client);
45		res
46	}
47}
48
49impl BlockSource for RestClient {
50	fn get_header<'a>(
51		&'a self, header_hash: &'a BlockHash, _height: Option<u32>,
52	) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
53		Box::pin(async move {
54			let resource_path = format!("headers/1/{}.json", header_hash.to_string());
55			Ok(self.request_resource::<JsonResponse, _>(&resource_path).await?)
56		})
57	}
58
59	fn get_block<'a>(
60		&'a self, header_hash: &'a BlockHash,
61	) -> AsyncBlockSourceResult<'a, BlockData> {
62		Box::pin(async move {
63			let resource_path = format!("block/{}.bin", header_hash.to_string());
64			Ok(BlockData::FullBlock(
65				self.request_resource::<BinaryResponse, _>(&resource_path).await?,
66			))
67		})
68	}
69
70	fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
71		Box::pin(
72			async move { Ok(self.request_resource::<JsonResponse, _>("chaininfo.json").await?) },
73		)
74	}
75}
76
77impl UtxoSource for RestClient {
78	fn get_block_hash_by_height<'a>(
79		&'a self, block_height: u32,
80	) -> AsyncBlockSourceResult<'a, BlockHash> {
81		Box::pin(async move {
82			let resource_path = format!("blockhashbyheight/{}.bin", block_height);
83			Ok(self.request_resource::<BinaryResponse, _>(&resource_path).await?)
84		})
85	}
86
87	fn is_output_unspent<'a>(&'a self, outpoint: OutPoint) -> AsyncBlockSourceResult<'a, bool> {
88		Box::pin(async move {
89			let resource_path =
90				format!("getutxos/{}-{}.json", outpoint.txid.to_string(), outpoint.vout);
91			let utxo_result =
92				self.request_resource::<JsonResponse, GetUtxosResponse>(&resource_path).await?;
93			Ok(utxo_result.hit_bitmap_nonempty)
94		})
95	}
96}
97
98#[cfg(test)]
99mod tests {
100	use super::*;
101	use crate::http::client_tests::{HttpServer, MessageBody};
102	use crate::http::BinaryResponse;
103	use bitcoin::hashes::Hash;
104
105	/// Parses binary data as a string-encoded `u32`.
106	impl TryInto<u32> for BinaryResponse {
107		type Error = std::io::Error;
108
109		fn try_into(self) -> std::io::Result<u32> {
110			match std::str::from_utf8(&self.0) {
111				Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
112				Ok(s) => match u32::from_str_radix(s, 10) {
113					Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
114					Ok(n) => Ok(n),
115				},
116			}
117		}
118	}
119
120	#[tokio::test]
121	async fn request_unknown_resource() {
122		let server = HttpServer::responding_with_not_found();
123		let client = RestClient::new(server.endpoint());
124
125		match client.request_resource::<BinaryResponse, u32>("/").await {
126			Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other),
127			Ok(_) => panic!("Expected error"),
128		}
129	}
130
131	#[tokio::test]
132	async fn request_malformed_resource() {
133		let server = HttpServer::responding_with_ok(MessageBody::Content("foo"));
134		let client = RestClient::new(server.endpoint());
135
136		match client.request_resource::<BinaryResponse, u32>("/").await {
137			Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::InvalidData),
138			Ok(_) => panic!("Expected error"),
139		}
140	}
141
142	#[tokio::test]
143	async fn request_valid_resource() {
144		let server = HttpServer::responding_with_ok(MessageBody::Content(42));
145		let client = RestClient::new(server.endpoint());
146
147		match client.request_resource::<BinaryResponse, u32>("/").await {
148			Err(e) => panic!("Unexpected error: {:?}", e),
149			Ok(n) => assert_eq!(n, 42),
150		}
151	}
152
153	#[tokio::test]
154	async fn parses_negative_getutxos() {
155		let server = HttpServer::responding_with_ok(MessageBody::Content(
156			// A real response contains a few more fields, but we actually only look at the
157			// "bitmap" field, so this should suffice for testing
158			"{\"chainHeight\": 1, \"bitmap\":\"0\",\"utxos\":[]}",
159		));
160		let client = RestClient::new(server.endpoint());
161
162		let outpoint = OutPoint::new(bitcoin::Txid::from_byte_array([0; 32]), 0);
163		let unspent_output = client.is_output_unspent(outpoint).await.unwrap();
164		assert_eq!(unspent_output, false);
165	}
166
167	#[tokio::test]
168	async fn parses_positive_getutxos() {
169		let server = HttpServer::responding_with_ok(MessageBody::Content(
170			// A real response contains lots more data, but we actually only look at the "bitmap"
171			// field, so this should suffice for testing
172			"{\"chainHeight\": 1, \"bitmap\":\"1\",\"utxos\":[]}",
173		));
174		let client = RestClient::new(server.endpoint());
175
176		let outpoint = OutPoint::new(bitcoin::Txid::from_byte_array([0; 32]), 0);
177		let unspent_output = client.is_output_unspent(outpoint).await.unwrap();
178		assert_eq!(unspent_output, true);
179	}
180}