Immutable BlockSource interface
[rust-lightning] / lightning-block-sync / src / rpc.rs
1 //! Simple RPC client implementation which implements [`BlockSource`] against a Bitcoin Core RPC
2 //! endpoint.
3
4 use crate::{BlockHeaderData, BlockSource, AsyncBlockSourceResult};
5 use crate::http::{HttpClient, HttpEndpoint, HttpError, JsonResponse};
6
7 use bitcoin::blockdata::block::Block;
8 use bitcoin::hash_types::BlockHash;
9 use bitcoin::hashes::hex::ToHex;
10
11 use futures::lock::Mutex;
12
13 use serde_json;
14
15 use std::convert::TryFrom;
16 use std::convert::TryInto;
17 use std::sync::atomic::{AtomicUsize, Ordering};
18
19 /// A simple RPC client for calling methods using HTTP `POST`.
20 pub struct RpcClient {
21         basic_auth: String,
22         endpoint: HttpEndpoint,
23         client: Mutex<HttpClient>,
24         id: AtomicUsize,
25 }
26
27 impl RpcClient {
28         /// Creates a new RPC client connected to the given endpoint with the provided credentials. The
29         /// credentials should be a base64 encoding of a user name and password joined by a colon, as is
30         /// required for HTTP basic access authentication.
31         pub fn new(credentials: &str, endpoint: HttpEndpoint) -> std::io::Result<Self> {
32                 let client = Mutex::new(HttpClient::connect(&endpoint)?);
33                 Ok(Self {
34                         basic_auth: "Basic ".to_string() + credentials,
35                         endpoint,
36                         client,
37                         id: AtomicUsize::new(0),
38                 })
39         }
40
41         /// Calls a method with the response encoded in JSON format and interpreted as type `T`.
42         pub async fn call_method<T>(&self, method: &str, params: &[serde_json::Value]) -> std::io::Result<T>
43         where JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error> {
44                 let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port());
45                 let uri = self.endpoint.path();
46                 let content = serde_json::json!({
47                         "method": method,
48                         "params": params,
49                         "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string()
50                 });
51
52                 let mut response = match self.client.lock().await.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await {
53                         Ok(JsonResponse(response)) => response,
54                         Err(e) if e.kind() == std::io::ErrorKind::Other => {
55                                 match e.get_ref().unwrap().downcast_ref::<HttpError>() {
56                                         Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) {
57                                                 Ok(JsonResponse(response)) => response,
58                                                 Err(_) => Err(e)?,
59                                         },
60                                         None => Err(e)?,
61                                 }
62                         },
63                         Err(e) => Err(e)?,
64                 };
65
66                 if !response.is_object() {
67                         return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON object"));
68                 }
69
70                 let error = &response["error"];
71                 if !error.is_null() {
72                         // TODO: Examine error code for a more precise std::io::ErrorKind.
73                         let message = error["message"].as_str().unwrap_or("unknown error");
74                         return Err(std::io::Error::new(std::io::ErrorKind::Other, message));
75                 }
76
77                 let result = &mut response["result"];
78                 if result.is_null() {
79                         return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected JSON result"));
80                 }
81
82                 JsonResponse(result.take()).try_into()
83         }
84 }
85
86 impl BlockSource for RpcClient {
87         fn get_header<'a>(&'a self, header_hash: &'a BlockHash, _height: Option<u32>) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
88                 Box::pin(async move {
89                         let header_hash = serde_json::json!(header_hash.to_hex());
90                         Ok(self.call_method("getblockheader", &[header_hash]).await?)
91                 })
92         }
93
94         fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
95                 Box::pin(async move {
96                         let header_hash = serde_json::json!(header_hash.to_hex());
97                         let verbosity = serde_json::json!(0);
98                         Ok(self.call_method("getblock", &[header_hash, verbosity]).await?)
99                 })
100         }
101
102         fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<'a, (BlockHash, Option<u32>)> {
103                 Box::pin(async move {
104                         Ok(self.call_method("getblockchaininfo", &[]).await?)
105                 })
106         }
107 }
108
109 #[cfg(test)]
110 mod tests {
111         use super::*;
112         use crate::http::client_tests::{HttpServer, MessageBody};
113
114         /// Credentials encoded in base64.
115         const CREDENTIALS: &'static str = "dXNlcjpwYXNzd29yZA==";
116
117         /// Converts a JSON value into `u64`.
118         impl TryInto<u64> for JsonResponse {
119                 type Error = std::io::Error;
120
121                 fn try_into(self) -> std::io::Result<u64> {
122                         match self.0.as_u64() {
123                                 None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")),
124                                 Some(n) => Ok(n),
125                         }
126                 }
127         }
128
129         #[tokio::test]
130         async fn call_method_returning_unknown_response() {
131                 let server = HttpServer::responding_with_not_found();
132                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
133
134                 match client.call_method::<u64>("getblockcount", &[]).await {
135                         Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other),
136                         Ok(_) => panic!("Expected error"),
137                 }
138         }
139
140         #[tokio::test]
141         async fn call_method_returning_malfomred_response() {
142                 let response = serde_json::json!("foo");
143                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
144                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
145
146                 match client.call_method::<u64>("getblockcount", &[]).await {
147                         Err(e) => {
148                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
149                                 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
150                         },
151                         Ok(_) => panic!("Expected error"),
152                 }
153         }
154
155         #[tokio::test]
156         async fn call_method_returning_error() {
157                 let response = serde_json::json!({
158                         "error": { "code": -8, "message": "invalid parameter" },
159                 });
160                 let server = HttpServer::responding_with_server_error(response);
161                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
162
163                 let invalid_block_hash = serde_json::json!("foo");
164                 match client.call_method::<u64>("getblock", &[invalid_block_hash]).await {
165                         Err(e) => {
166                                 assert_eq!(e.kind(), std::io::ErrorKind::Other);
167                                 assert_eq!(e.get_ref().unwrap().to_string(), "invalid parameter");
168                         },
169                         Ok(_) => panic!("Expected error"),
170                 }
171         }
172
173         #[tokio::test]
174         async fn call_method_returning_missing_result() {
175                 let response = serde_json::json!({ "result": null });
176                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
177                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
178
179                 match client.call_method::<u64>("getblockcount", &[]).await {
180                         Err(e) => {
181                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
182                                 assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result");
183                         },
184                         Ok(_) => panic!("Expected error"),
185                 }
186         }
187
188         #[tokio::test]
189         async fn call_method_returning_malformed_result() {
190                 let response = serde_json::json!({ "result": "foo" });
191                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
192                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
193
194                 match client.call_method::<u64>("getblockcount", &[]).await {
195                         Err(e) => {
196                                 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
197                                 assert_eq!(e.get_ref().unwrap().to_string(), "not a number");
198                         },
199                         Ok(_) => panic!("Expected error"),
200                 }
201         }
202
203         #[tokio::test]
204         async fn call_method_returning_valid_result() {
205                 let response = serde_json::json!({ "result": 654470 });
206                 let server = HttpServer::responding_with_ok(MessageBody::Content(response));
207                 let client = RpcClient::new(CREDENTIALS, server.endpoint()).unwrap();
208
209                 match client.call_method::<u64>("getblockcount", &[]).await {
210                         Err(e) => panic!("Unexpected error: {:?}", e),
211                         Ok(count) => assert_eq!(count, 654470),
212                 }
213         }
214 }