Move RGS `GraphSyncError` into the top-level module
[rust-lightning] / lightning-rapid-gossip-sync / src / lib.rs
1 #![deny(rustdoc::broken_intra_doc_links)]
2 #![deny(rustdoc::private_intra_doc_links)]
3
4 #![deny(missing_docs)]
5 #![deny(unsafe_code)]
6 #![deny(non_upper_case_globals)]
7 #![deny(non_camel_case_types)]
8 #![deny(non_snake_case)]
9 #![deny(unused_mut)]
10 #![deny(unused_variables)]
11 #![deny(unused_imports)]
12 //! This crate exposes client functionality to rapidly sync gossip data, aimed primarily at mobile
13 //! devices.
14 //!
15 //! The rapid gossip sync server will provide a compressed response containing differential gossip
16 //! data. The gossip data is formatted compactly, omitting signatures and opportunistically
17 //! incremental where previous channel updates are known. This mechanism is enabled when the
18 //! timestamp of the last known channel update is communicated. A reference server implementation
19 //! can be found [on Github](https://github.com/lightningdevkit/rapid-gossip-sync-server).
20 //!
21 //! The primary benefit of this syncing mechanism is that it allows a low-powered client to offload
22 //! the validation of gossip signatures to a semi-trusted server. This enables the client to
23 //! privately calculate routes for payments, and to do so much faster than requiring a full
24 //! peer-to-peer gossip sync to complete.
25 //!
26 //! The server calculates its response on the basis of a client-provided `latest_seen` timestamp,
27 //! i.e., the server will return all rapid gossip sync data it has seen after the given timestamp.
28 //!
29 //! # Getting Started
30 //! Firstly, the data needs to be retrieved from the server. For example, you could use the server
31 //! at <https://rapidsync.lightningdevkit.org> with the following request format:
32 //!
33 //! ```shell
34 //! curl -o rapid_sync.lngossip https://rapidsync.lightningdevkit.org/snapshot/<last_sync_timestamp>
35 //! ```
36 //! Note that the first ever rapid sync should use `0` for `last_sync_timestamp`.
37 //!
38 //! After the gossip data snapshot has been downloaded, one of the client's graph processing
39 //! functions needs to be called. In this example, we process the update by reading its contents
40 //! from disk, which we do by calling [`RapidGossipSync::update_network_graph`]:
41 //!
42 //! ```
43 //! use bitcoin::blockdata::constants::genesis_block;
44 //! use bitcoin::Network;
45 //! use lightning::routing::gossip::NetworkGraph;
46 //! use lightning_rapid_gossip_sync::RapidGossipSync;
47 //!
48 //! # use lightning::util::logger::{Logger, Record};
49 //! # struct FakeLogger {}
50 //! # impl Logger for FakeLogger {
51 //! #     fn log(&self, record: Record) { }
52 //! # }
53 //! # let logger = FakeLogger {};
54 //!
55 //! let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
56 //! let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
57 //! let snapshot_contents: &[u8] = &[0; 0];
58 //! // In no-std you need to provide the current time in unix epoch seconds
59 //! // otherwise you can use update_network_graph
60 //! let current_time_unix = 0;
61 //! let new_last_sync_timestamp_result = rapid_sync.update_network_graph_no_std(snapshot_contents, Some(current_time_unix));
62 //! ```
63
64 #![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
65
66 #[cfg(ldk_bench)] extern crate criterion;
67
68 #[cfg(not(feature = "std"))]
69 extern crate alloc;
70
71 #[cfg(feature = "std")]
72 use std::fs::File;
73 use core::ops::Deref;
74 use core::sync::atomic::{AtomicBool, Ordering};
75 use core::fmt::Debug;
76 use core::fmt::Formatter;
77
78 use lightning::io;
79 use lightning::ln::msgs::{DecodeError, LightningError};
80 use lightning::routing::gossip::NetworkGraph;
81 use lightning::util::logger::Logger;
82
83 /// Core functionality of this crate
84 mod processing;
85
86 /// All-encompassing standard error type that processing can return
87 pub enum GraphSyncError {
88         /// Error trying to read the update data, typically due to an erroneous data length indication
89         /// that is greater than the actual amount of data provided
90         DecodeError(DecodeError),
91         /// Error applying the patch to the network graph, usually the result of updates that are too
92         /// old or missing prerequisite data to the application of updates out of order
93         LightningError(LightningError),
94 }
95
96 impl From<lightning::io::Error> for GraphSyncError {
97         fn from(error: lightning::io::Error) -> Self {
98                 Self::DecodeError(DecodeError::Io(error.kind()))
99         }
100 }
101
102 impl From<DecodeError> for GraphSyncError {
103         fn from(error: DecodeError) -> Self {
104                 Self::DecodeError(error)
105         }
106 }
107
108 impl From<LightningError> for GraphSyncError {
109         fn from(error: LightningError) -> Self {
110                 Self::LightningError(error)
111         }
112 }
113
114 impl Debug for GraphSyncError {
115         fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
116                 match self {
117                         GraphSyncError::DecodeError(e) => f.write_fmt(format_args!("DecodeError: {:?}", e)),
118                         GraphSyncError::LightningError(e) => f.write_fmt(format_args!("LightningError: {:?}", e))
119                 }
120         }
121 }
122
123 /// The main Rapid Gossip Sync object.
124 ///
125 /// See [crate-level documentation] for usage.
126 ///
127 /// [crate-level documentation]: crate
128 pub struct RapidGossipSync<NG: Deref<Target=NetworkGraph<L>>, L: Deref>
129 where L::Target: Logger {
130         network_graph: NG,
131         logger: L,
132         is_initial_sync_complete: AtomicBool
133 }
134
135 impl<NG: Deref<Target=NetworkGraph<L>>, L: Deref> RapidGossipSync<NG, L> where L::Target: Logger {
136         /// Instantiate a new [`RapidGossipSync`] instance.
137         pub fn new(network_graph: NG, logger: L) -> Self {
138                 Self {
139                         network_graph,
140                         logger,
141                         is_initial_sync_complete: AtomicBool::new(false)
142                 }
143         }
144
145         /// Sync gossip data from a file.
146         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
147         ///
148         /// `network_graph`: The network graph to apply the updates to
149         ///
150         /// `sync_path`: Path to the file where the gossip update data is located
151         ///
152         #[cfg(feature = "std")]
153         pub fn sync_network_graph_with_file_path(
154                 &self,
155                 sync_path: &str,
156         ) -> Result<u32, GraphSyncError> {
157                 let mut file = File::open(sync_path)?;
158                 self.update_network_graph_from_byte_stream(&mut file)
159         }
160
161         /// Update network graph from binary data.
162         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
163         ///
164         /// `update_data`: `&[u8]` binary stream that comprises the update data
165         #[cfg(feature = "std")]
166         pub fn update_network_graph(&self, update_data: &[u8]) -> Result<u32, GraphSyncError> {
167                 let mut read_cursor = io::Cursor::new(update_data);
168                 self.update_network_graph_from_byte_stream(&mut read_cursor)
169         }
170
171         /// Update network graph from binary data.
172         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
173         ///
174         /// `update_data`: `&[u8]` binary stream that comprises the update data
175         /// `current_time_unix`: `Option<u64>` optional current timestamp to verify data age
176         pub fn update_network_graph_no_std(&self, update_data: &[u8], current_time_unix: Option<u64>) -> Result<u32, GraphSyncError> {
177                 let mut read_cursor = io::Cursor::new(update_data);
178                 self.update_network_graph_from_byte_stream_no_std(&mut read_cursor, current_time_unix)
179         }
180
181         /// Gets a reference to the underlying [`NetworkGraph`] which was provided in
182         /// [`RapidGossipSync::new`].
183         ///
184         /// This is not exported to bindings users as bindings don't support a reference-to-a-reference yet
185         pub fn network_graph(&self) -> &NG {
186                 &self.network_graph
187         }
188
189         /// Returns whether a rapid gossip sync has completed at least once.
190         pub fn is_initial_sync_complete(&self) -> bool {
191                 self.is_initial_sync_complete.load(Ordering::Acquire)
192         }
193 }
194
195 #[cfg(feature = "std")]
196 #[cfg(test)]
197 mod tests {
198         use std::fs;
199
200         use bitcoin::Network;
201
202         use lightning::ln::msgs::DecodeError;
203         use lightning::routing::gossip::NetworkGraph;
204         use lightning::util::test_utils::TestLogger;
205         use crate::{GraphSyncError, RapidGossipSync};
206
207         #[test]
208         fn test_sync_from_file() {
209                 struct FileSyncTest {
210                         directory: String,
211                 }
212
213                 impl FileSyncTest {
214                         fn new(tmp_directory: &str, valid_response: &[u8]) -> FileSyncTest {
215                                 let test = FileSyncTest { directory: tmp_directory.to_owned() };
216
217                                 let graph_sync_test_directory = test.get_test_directory();
218                                 fs::create_dir_all(graph_sync_test_directory).unwrap();
219
220                                 let graph_sync_test_file = test.get_test_file_path();
221                                 fs::write(graph_sync_test_file, valid_response).unwrap();
222
223                                 test
224                         }
225
226                         fn get_test_directory(&self) -> String {
227                                 self.directory.clone() + "/graph-sync-tests"
228                         }
229
230                         fn get_test_file_path(&self) -> String {
231                                 self.get_test_directory() + "/test_data.lngossip"
232                         }
233                 }
234
235                 impl Drop for FileSyncTest {
236                         fn drop(&mut self) {
237                                 fs::remove_dir_all(self.directory.clone()).unwrap();
238                         }
239                 }
240
241                 // same as incremental_only_update_fails_without_prior_same_direction_updates
242                 let valid_response = vec![
243                         76, 68, 75, 1, 111, 226, 140, 10, 182, 241, 179, 114, 193, 166, 162, 70, 174, 99, 247,
244                         79, 147, 30, 131, 101, 225, 90, 8, 156, 104, 214, 25, 0, 0, 0, 0, 0, 97, 227, 98, 218,
245                         0, 0, 0, 4, 2, 22, 7, 207, 206, 25, 164, 197, 231, 230, 231, 56, 102, 61, 250, 251,
246                         187, 172, 38, 46, 79, 247, 108, 44, 155, 48, 219, 238, 252, 53, 192, 6, 67, 2, 36, 125,
247                         157, 176, 223, 175, 234, 116, 94, 248, 201, 225, 97, 235, 50, 47, 115, 172, 63, 136,
248                         88, 216, 115, 11, 111, 217, 114, 84, 116, 124, 231, 107, 2, 158, 1, 242, 121, 152, 106,
249                         204, 131, 186, 35, 93, 70, 216, 10, 237, 224, 183, 89, 95, 65, 3, 83, 185, 58, 138,
250                         181, 64, 187, 103, 127, 68, 50, 2, 201, 19, 17, 138, 136, 149, 185, 226, 156, 137, 175,
251                         110, 32, 237, 0, 217, 90, 31, 100, 228, 149, 46, 219, 175, 168, 77, 4, 143, 38, 128,
252                         76, 97, 0, 0, 0, 2, 0, 0, 255, 8, 153, 192, 0, 2, 27, 0, 0, 0, 1, 0, 0, 255, 2, 68,
253                         226, 0, 6, 11, 0, 1, 2, 3, 0, 0, 0, 2, 0, 40, 0, 0, 0, 0, 0, 0, 3, 232, 0, 0, 3, 232,
254                         0, 0, 0, 1, 0, 0, 0, 0, 58, 85, 116, 216, 255, 8, 153, 192, 0, 2, 27, 0, 0, 25, 0, 0,
255                         0, 1, 0, 0, 0, 125, 255, 2, 68, 226, 0, 6, 11, 0, 1, 5, 0, 0, 0, 0, 29, 129, 25, 192,
256                 ];
257
258                 let tmp_directory = "./rapid-gossip-sync-tests-tmp";
259                 let sync_test = FileSyncTest::new(tmp_directory, &valid_response);
260                 let graph_sync_test_file = sync_test.get_test_file_path();
261
262                 let logger = TestLogger::new();
263                 let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
264
265                 assert_eq!(network_graph.read_only().channels().len(), 0);
266
267                 let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
268                 let sync_result = rapid_sync.sync_network_graph_with_file_path(&graph_sync_test_file);
269
270                 if sync_result.is_err() {
271                         panic!("Unexpected sync result: {:?}", sync_result)
272                 }
273
274                 assert_eq!(network_graph.read_only().channels().len(), 2);
275                 let after = network_graph.to_string();
276                 assert!(
277                         after.contains("021607cfce19a4c5e7e6e738663dfafbbbac262e4ff76c2c9b30dbeefc35c00643")
278                 );
279                 assert!(
280                         after.contains("02247d9db0dfafea745ef8c9e161eb322f73ac3f8858d8730b6fd97254747ce76b")
281                 );
282                 assert!(
283                         after.contains("029e01f279986acc83ba235d46d80aede0b7595f410353b93a8ab540bb677f4432")
284                 );
285                 assert!(
286                         after.contains("02c913118a8895b9e29c89af6e20ed00d95a1f64e4952edbafa84d048f26804c61")
287                 );
288                 assert!(after.contains("619737530008010752"));
289                 assert!(after.contains("783241506229452801"));
290         }
291
292         #[test]
293         fn measure_native_read_from_file() {
294                 let logger = TestLogger::new();
295                 let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
296
297                 assert_eq!(network_graph.read_only().channels().len(), 0);
298
299                 let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
300                 let start = std::time::Instant::now();
301                 let sync_result = rapid_sync
302                         .sync_network_graph_with_file_path("./res/full_graph.lngossip");
303                 if let Err(GraphSyncError::DecodeError(DecodeError::Io(io_error))) = &sync_result {
304                         let error_string = format!("Input file lightning-rapid-gossip-sync/res/full_graph.lngossip is missing! Download it from https://bitcoin.ninja/ldk-compressed_graph-285cb27df79-2022-07-21.bin\n\n{:?}", io_error);
305                         #[cfg(not(require_route_graph_test))]
306                         {
307                                 println!("{}", error_string);
308                                 return;
309                         }
310                         #[cfg(require_route_graph_test)]
311                         panic!("{}", error_string);
312                 }
313                 let elapsed = start.elapsed();
314                 println!("initialization duration: {:?}", elapsed);
315                 if sync_result.is_err() {
316                         panic!("Unexpected sync result: {:?}", sync_result)
317                 }
318         }
319 }
320
321 #[cfg(ldk_bench)]
322 /// Benches
323 pub mod bench {
324         use bitcoin::Network;
325
326         use criterion::Criterion;
327
328         use std::fs;
329
330         use lightning::routing::gossip::NetworkGraph;
331         use lightning::util::test_utils::TestLogger;
332
333         use crate::RapidGossipSync;
334
335         /// Bench!
336         pub fn bench_reading_full_graph_from_file(b: &mut Criterion) {
337                 let logger = TestLogger::new();
338                 b.bench_function("read_full_graph_from_rgs", |b| b.iter(|| {
339                         let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
340                         let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
341                         let mut file = match fs::read("../lightning-rapid-gossip-sync/res/full_graph.lngossip") {
342                                 Ok(f) => f,
343                                 Err(io_error) => {
344                                         let error_string = format!(
345                                                 "Input file lightning-rapid-gossip-sync/res/full_graph.lngossip is missing! Download it from https://bitcoin.ninja/ldk-compressed_graph-bc08df7542-2022-05-05.bin\n\n{:?}",
346                                                 io_error);
347                                         #[cfg(not(require_route_graph_test))]
348                                         {
349                                                 println!("{}", error_string);
350                                                 return;
351                                         }
352                                         #[cfg(require_route_graph_test)]
353                                         panic!("{}", error_string);
354                                 },
355                         };
356                         rapid_sync.update_network_graph_no_std(&mut file, None).unwrap();
357                 }));
358         }
359 }