Throw error for RGS data that's more than two weeks old, fixing #1785
[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) { unimplemented!() }
53 //! # }
54 //! # let logger = FakeLogger {};
55 //!
56 //! let block_hash = genesis_block(Network::Bitcoin).header.block_hash();
57 //! let network_graph = NetworkGraph::new(block_hash, &logger);
58 //! let rapid_sync = RapidGossipSync::new(&network_graph);
59 //! let snapshot_contents: &[u8] = &[0; 0];
60 //! let new_last_sync_timestamp_result = rapid_sync.update_network_graph(snapshot_contents);
61 //! ```
62
63 #![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
64
65 // Allow and import test features for benching
66 #![cfg_attr(all(test, feature = "_bench_unstable"), feature(test))]
67 #[cfg(all(test, feature = "_bench_unstable"))]
68 extern crate test;
69
70 #[cfg(not(feature = "std"))]
71 extern crate alloc;
72
73 #[cfg(feature = "std")]
74 use std::fs::File;
75 use core::ops::Deref;
76 use core::sync::atomic::{AtomicBool, Ordering};
77
78 use lightning::io;
79 use lightning::routing::gossip::NetworkGraph;
80 use lightning::util::logger::Logger;
81
82 pub use crate::error::GraphSyncError;
83
84 /// Error types that these functions can return
85 mod error;
86
87 /// Core functionality of this crate
88 mod processing;
89
90 /// The main Rapid Gossip Sync object.
91 ///
92 /// See [crate-level documentation] for usage.
93 ///
94 /// [crate-level documentation]: crate
95 pub struct RapidGossipSync<NG: Deref<Target=NetworkGraph<L>>, L: Deref>
96 where L::Target: Logger {
97         network_graph: NG,
98         is_initial_sync_complete: AtomicBool
99 }
100
101 impl<NG: Deref<Target=NetworkGraph<L>>, L: Deref> RapidGossipSync<NG, L> where L::Target: Logger {
102         /// Instantiate a new [`RapidGossipSync`] instance.
103         pub fn new(network_graph: NG) -> Self {
104                 Self {
105                         network_graph,
106                         is_initial_sync_complete: AtomicBool::new(false)
107                 }
108         }
109
110         /// Sync gossip data from a file.
111         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
112         ///
113         /// `network_graph`: The network graph to apply the updates to
114         ///
115         /// `sync_path`: Path to the file where the gossip update data is located
116         ///
117         #[cfg(feature = "std")]
118         pub fn sync_network_graph_with_file_path(
119                 &self,
120                 sync_path: &str,
121         ) -> Result<u32, GraphSyncError> {
122                 let mut file = File::open(sync_path)?;
123                 self.update_network_graph_from_byte_stream(&mut file)
124         }
125
126         /// Update network graph from binary data.
127         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
128         ///
129         /// `update_data`: `&[u8]` binary stream that comprises the update data
130         pub fn update_network_graph(&self, update_data: &[u8]) -> Result<u32, GraphSyncError> {
131                 let mut read_cursor = io::Cursor::new(update_data);
132                 self.update_network_graph_from_byte_stream(&mut read_cursor)
133         }
134
135         /// Update network graph from binary data.
136         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
137         ///
138         /// `update_data`: `&[u8]` binary stream that comprises the update data
139         /// `current_time_unix`: `Option<u64>` optional current timestamp to verify data age
140         pub fn update_network_graph_no_std(&self, update_data: &[u8], current_time_unix: Option<u64>) -> Result<u32, GraphSyncError> {
141                 let mut read_cursor = io::Cursor::new(update_data);
142                 self.update_network_graph_from_byte_stream_no_std(&mut read_cursor, current_time_unix)
143         }
144
145         /// Gets a reference to the underlying [`NetworkGraph`] which was provided in
146         /// [`RapidGossipSync::new`].
147         ///
148         /// (C-not exported) as bindings don't support a reference-to-a-reference yet
149         pub fn network_graph(&self) -> &NG {
150                 &self.network_graph
151         }
152
153         /// Returns whether a rapid gossip sync has completed at least once.
154         pub fn is_initial_sync_complete(&self) -> bool {
155                 self.is_initial_sync_complete.load(Ordering::Acquire)
156         }
157 }
158
159 #[cfg(feature = "std")]
160 #[cfg(test)]
161 mod tests {
162         use std::fs;
163
164         use bitcoin::blockdata::constants::genesis_block;
165         use bitcoin::Network;
166
167         use lightning::ln::msgs::DecodeError;
168         use lightning::routing::gossip::NetworkGraph;
169         use lightning::util::test_utils::TestLogger;
170         use crate::RapidGossipSync;
171
172         #[test]
173         fn test_sync_from_file() {
174                 struct FileSyncTest {
175                         directory: String,
176                 }
177
178                 impl FileSyncTest {
179                         fn new(tmp_directory: &str, valid_response: &[u8]) -> FileSyncTest {
180                                 let test = FileSyncTest { directory: tmp_directory.to_owned() };
181
182                                 let graph_sync_test_directory = test.get_test_directory();
183                                 fs::create_dir_all(graph_sync_test_directory).unwrap();
184
185                                 let graph_sync_test_file = test.get_test_file_path();
186                                 fs::write(&graph_sync_test_file, valid_response).unwrap();
187
188                                 test
189                         }
190                         fn get_test_directory(&self) -> String {
191                                 let graph_sync_test_directory = self.directory.clone() + "/graph-sync-tests";
192                                 graph_sync_test_directory
193                         }
194                         fn get_test_file_path(&self) -> String {
195                                 let graph_sync_test_directory = self.get_test_directory();
196                                 let graph_sync_test_file = graph_sync_test_directory.to_owned() + "/test_data.lngossip";
197                                 graph_sync_test_file
198                         }
199                 }
200
201                 impl Drop for FileSyncTest {
202                         fn drop(&mut self) {
203                                 fs::remove_dir_all(self.directory.clone()).unwrap();
204                         }
205                 }
206
207                 // same as incremental_only_update_fails_without_prior_same_direction_updates
208                 let valid_response = vec![
209                         76, 68, 75, 1, 111, 226, 140, 10, 182, 241, 179, 114, 193, 166, 162, 70, 174, 99, 247,
210                         79, 147, 30, 131, 101, 225, 90, 8, 156, 104, 214, 25, 0, 0, 0, 0, 0, 97, 227, 98, 218,
211                         0, 0, 0, 4, 2, 22, 7, 207, 206, 25, 164, 197, 231, 230, 231, 56, 102, 61, 250, 251,
212                         187, 172, 38, 46, 79, 247, 108, 44, 155, 48, 219, 238, 252, 53, 192, 6, 67, 2, 36, 125,
213                         157, 176, 223, 175, 234, 116, 94, 248, 201, 225, 97, 235, 50, 47, 115, 172, 63, 136,
214                         88, 216, 115, 11, 111, 217, 114, 84, 116, 124, 231, 107, 2, 158, 1, 242, 121, 152, 106,
215                         204, 131, 186, 35, 93, 70, 216, 10, 237, 224, 183, 89, 95, 65, 3, 83, 185, 58, 138,
216                         181, 64, 187, 103, 127, 68, 50, 2, 201, 19, 17, 138, 136, 149, 185, 226, 156, 137, 175,
217                         110, 32, 237, 0, 217, 90, 31, 100, 228, 149, 46, 219, 175, 168, 77, 4, 143, 38, 128,
218                         76, 97, 0, 0, 0, 2, 0, 0, 255, 8, 153, 192, 0, 2, 27, 0, 0, 0, 1, 0, 0, 255, 2, 68,
219                         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,
220                         0, 0, 0, 1, 0, 0, 0, 0, 58, 85, 116, 216, 255, 8, 153, 192, 0, 2, 27, 0, 0, 25, 0, 0,
221                         0, 1, 0, 0, 0, 125, 255, 2, 68, 226, 0, 6, 11, 0, 1, 5, 0, 0, 0, 0, 29, 129, 25, 192,
222                 ];
223
224                 let tmp_directory = "./rapid-gossip-sync-tests-tmp";
225                 let sync_test = FileSyncTest::new(tmp_directory, &valid_response);
226                 let graph_sync_test_file = sync_test.get_test_file_path();
227
228                 let block_hash = genesis_block(Network::Bitcoin).block_hash();
229                 let logger = TestLogger::new();
230                 let network_graph = NetworkGraph::new(block_hash, &logger);
231
232                 assert_eq!(network_graph.read_only().channels().len(), 0);
233
234                 let rapid_sync = RapidGossipSync::new(&network_graph);
235                 let sync_result = rapid_sync.sync_network_graph_with_file_path(&graph_sync_test_file);
236
237                 if sync_result.is_err() {
238                         panic!("Unexpected sync result: {:?}", sync_result)
239                 }
240
241                 assert_eq!(network_graph.read_only().channels().len(), 2);
242                 let after = network_graph.to_string();
243                 assert!(
244                         after.contains("021607cfce19a4c5e7e6e738663dfafbbbac262e4ff76c2c9b30dbeefc35c00643")
245                 );
246                 assert!(
247                         after.contains("02247d9db0dfafea745ef8c9e161eb322f73ac3f8858d8730b6fd97254747ce76b")
248                 );
249                 assert!(
250                         after.contains("029e01f279986acc83ba235d46d80aede0b7595f410353b93a8ab540bb677f4432")
251                 );
252                 assert!(
253                         after.contains("02c913118a8895b9e29c89af6e20ed00d95a1f64e4952edbafa84d048f26804c61")
254                 );
255                 assert!(after.contains("619737530008010752"));
256                 assert!(after.contains("783241506229452801"));
257         }
258
259         #[test]
260         fn measure_native_read_from_file() {
261                 let block_hash = genesis_block(Network::Bitcoin).block_hash();
262                 let logger = TestLogger::new();
263                 let network_graph = NetworkGraph::new(block_hash, &logger);
264
265                 assert_eq!(network_graph.read_only().channels().len(), 0);
266
267                 let rapid_sync = RapidGossipSync::new(&network_graph);
268                 let start = std::time::Instant::now();
269                 let sync_result = rapid_sync
270                         .sync_network_graph_with_file_path("./res/full_graph.lngossip");
271                 if let Err(crate::error::GraphSyncError::DecodeError(DecodeError::Io(io_error))) = &sync_result {
272                         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);
273                         #[cfg(not(require_route_graph_test))]
274                         {
275                                 println!("{}", error_string);
276                                 return;
277                         }
278                         #[cfg(require_route_graph_test)]
279                         panic!("{}", error_string);
280                 }
281                 let elapsed = start.elapsed();
282                 println!("initialization duration: {:?}", elapsed);
283                 if sync_result.is_err() {
284                         panic!("Unexpected sync result: {:?}", sync_result)
285                 }
286         }
287 }
288
289 #[cfg(all(test, feature = "_bench_unstable"))]
290 pub mod bench {
291         use test::Bencher;
292
293         use bitcoin::blockdata::constants::genesis_block;
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 block_hash = genesis_block(Network::Bitcoin).block_hash();
305                 let logger = TestLogger::new();
306                 b.iter(|| {
307                         let network_graph = NetworkGraph::new(block_hash, &logger);
308                         let rapid_sync = RapidGossipSync::new(&network_graph);
309                         let sync_result = rapid_sync.sync_network_graph_with_file_path("./res/full_graph.lngossip");
310                         if let Err(crate::error::GraphSyncError::DecodeError(DecodeError::Io(io_error))) = &sync_result {
311                                 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);
312                                 #[cfg(not(require_route_graph_test))]
313                                 {
314                                         println!("{}", error_string);
315                                         return;
316                                 }
317                                 #[cfg(require_route_graph_test)]
318                                 panic!("{}", error_string);
319                         }
320                         assert!(sync_result.is_ok())
321                 });
322         }
323 }