5690da12ea0495c1c8062655a95b27e83611cfa8
[rust-lightning] / lightning-block-sync / src / rest.rs
1 //! Simple REST client implementation which implements [`BlockSource`] against a Bitcoin Core REST
2 //! endpoint.
3
4 use crate::{BlockData, BlockHeaderData, BlockSource, AsyncBlockSourceResult};
5 use crate::http::{BinaryResponse, HttpEndpoint, HttpClient, JsonResponse};
6 use crate::gossip::UtxoSource;
7 use crate::convert::GetUtxosResponse;
8
9 use bitcoin::OutPoint;
10 use bitcoin::hash_types::BlockHash;
11 use bitcoin::hashes::hex::ToHex;
12
13 use std::convert::TryFrom;
14 use std::convert::TryInto;
15 use std::sync::Mutex;
16
17 /// A simple REST client for requesting resources using HTTP `GET`.
18 pub struct RestClient {
19         endpoint: HttpEndpoint,
20         client: Mutex<Option<HttpClient>>,
21 }
22
23 impl RestClient {
24         /// Creates a new REST client connected to the given endpoint.
25         ///
26         /// The endpoint should contain the REST path component (e.g., http://127.0.0.1:8332/rest).
27         pub fn new(endpoint: HttpEndpoint) -> std::io::Result<Self> {
28                 Ok(Self { endpoint, client: Mutex::new(None) })
29         }
30
31         /// Requests a resource encoded in `F` format and interpreted as type `T`.
32         pub async fn request_resource<F, T>(&self, resource_path: &str) -> std::io::Result<T>
33         where F: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
34                 let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
35                 let uri = format!("{}/{}", self.endpoint.path().trim_end_matches("/"), resource_path);
36                 let mut client = if let Some(client) = self.client.lock().unwrap().take() { client }
37                         else { HttpClient::connect(&self.endpoint)? };
38                 let res = client.get::<F>(&uri, &host).await?.try_into();
39                 *self.client.lock().unwrap() = Some(client);
40                 res
41         }
42 }
43
44 impl BlockSource for RestClient {
45         fn get_header<'a>(&'a self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
46                 Box::pin(async move {
47                         let resource_path = format!("headers/1/{}.json", header_hash.to_hex());
48                         Ok(self.request_resource::<JsonResponse, _>(&resource_path).await?)
49                 })
50         }
51
52         fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, BlockData> {
53                 Box::pin(async move {
54                         let resource_path = format!("block/{}.bin", header_hash.to_hex());
55                         Ok(BlockData::FullBlock(self.request_resource::<BinaryResponse, _>(&resource_path).await?))
56                 })
57         }
58
59         fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
60                 Box::pin(async move {
61                         Ok(self.request_resource::<JsonResponse, _>("chaininfo.json").await?)
62                 })
63         }
64 }
65
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
84 #[cfg(test)]
85 mod tests {
86         use super::*;
87         use crate::http::BinaryResponse;
88         use crate::http::client_tests::{HttpServer, MessageBody};
89         use bitcoin::hashes::Hash;
90
91         /// Parses binary data as a string-encoded `u32`.
92         impl TryInto<u32> for BinaryResponse {
93                 type Error = std::io::Error;
94
95                 fn try_into(self) -> std::io::Result<u32> {
96                         match std::str::from_utf8(&self.0) {
97                                 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
98                                 Ok(s) => match u32::from_str_radix(s, 10) {
99                                         Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
100                                         Ok(n) => Ok(n),
101                                 }
102                         }
103                 }
104         }
105
106         #[tokio::test]
107         async fn request_unknown_resource() {
108                 let server = HttpServer::responding_with_not_found();
109                 let client = RestClient::new(server.endpoint()).unwrap();
110
111                 match client.request_resource::<BinaryResponse, u32>("/").await {
112                         Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other),
113                         Ok(_) => panic!("Expected error"),
114                 }
115         }
116
117         #[tokio::test]
118         async fn request_malformed_resource() {
119                 let server = HttpServer::responding_with_ok(MessageBody::Content("foo"));
120                 let client = RestClient::new(server.endpoint()).unwrap();
121
122                 match client.request_resource::<BinaryResponse, u32>("/").await {
123                         Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::InvalidData),
124                         Ok(_) => panic!("Expected error"),
125                 }
126         }
127
128         #[tokio::test]
129         async fn request_valid_resource() {
130                 let server = HttpServer::responding_with_ok(MessageBody::Content(42));
131                 let client = RestClient::new(server.endpoint()).unwrap();
132
133                 match client.request_resource::<BinaryResponse, u32>("/").await {
134                         Err(e) => panic!("Unexpected error: {:?}", e),
135                         Ok(n) => assert_eq!(n, 42),
136                 }
137         }
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         }
166 }