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