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