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