ecc52553038ed763fd228851f5816e91327b6087
[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
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 L::Target: Logger {
126         network_graph: NG,
127         logger: L,
128         is_initial_sync_complete: AtomicBool
129 }
130
131 impl<NG: Deref<Target=NetworkGraph<L>>, L: Deref> RapidGossipSync<NG, L> where L::Target: Logger {
132         /// Instantiate a new [`RapidGossipSync`] instance.
133         pub fn new(network_graph: NG, logger: L) -> Self {
134                 Self {
135                         network_graph,
136                         logger,
137                         is_initial_sync_complete: AtomicBool::new(false)
138                 }
139         }
140
141         /// Sync gossip data from a file.
142         /// Returns the last sync timestamp to be used the next time rapid sync data is queried.
143         ///
144         /// `network_graph`: The network graph to apply the updates to
145         ///
146         /// `sync_path`: Path to the file where the gossip update data is located
147         ///
148         #[cfg(feature = "std")]
149         pub fn sync_network_graph_with_file_path(
150                 &self,
151                 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(&self, update_data: &[u8], current_time_unix: Option<u64>) -> Result<u32, GraphSyncError> {
173                 let mut read_cursor = io::Cursor::new(update_data);
174                 self.update_network_graph_from_byte_stream_no_std(&mut read_cursor, current_time_unix)
175         }
176
177         /// Gets a reference to the underlying [`NetworkGraph`] which was provided in
178         /// [`RapidGossipSync::new`].
179         ///
180         /// This is not exported to bindings users as bindings don't support a reference-to-a-reference yet
181         pub fn network_graph(&self) -> &NG {
182                 &self.network_graph
183         }
184
185         /// Returns whether a rapid gossip sync has completed at least once.
186         pub fn is_initial_sync_complete(&self) -> bool {
187                 self.is_initial_sync_complete.load(Ordering::Acquire)
188         }
189 }
190
191 #[cfg(feature = "std")]
192 #[cfg(test)]
193 mod tests {
194         use std::fs;
195
196         use bitcoin::Network;
197
198         use lightning::ln::msgs::DecodeError;
199         use lightning::routing::gossip::NetworkGraph;
200         use lightning::util::test_utils::TestLogger;
201         use crate::{GraphSyncError, RapidGossipSync};
202
203         #[test]
204         fn test_sync_from_file() {
205                 struct FileSyncTest {
206                         directory: String,
207                 }
208
209                 impl FileSyncTest {
210                         fn new(tmp_directory: &str, valid_response: &[u8]) -> FileSyncTest {
211                                 let test = FileSyncTest { directory: tmp_directory.to_owned() };
212
213                                 let graph_sync_test_directory = test.get_test_directory();
214                                 fs::create_dir_all(graph_sync_test_directory).unwrap();
215
216                                 let graph_sync_test_file = test.get_test_file_path();
217                                 fs::write(graph_sync_test_file, valid_response).unwrap();
218
219                                 test
220                         }
221
222                         fn get_test_directory(&self) -> String {
223                                 self.directory.clone() + "/graph-sync-tests"
224                         }
225
226                         fn get_test_file_path(&self) -> String {
227                                 self.get_test_directory() + "/test_data.lngossip"
228                         }
229                 }
230
231                 impl Drop for FileSyncTest {
232                         fn drop(&mut self) {
233                                 fs::remove_dir_all(self.directory.clone()).unwrap();
234                         }
235                 }
236
237                 // same as incremental_only_update_fails_without_prior_same_direction_updates
238                 let valid_response = vec![
239                         76, 68, 75, 1, 111, 226, 140, 10, 182, 241, 179, 114, 193, 166, 162, 70, 174, 99, 247,
240                         79, 147, 30, 131, 101, 225, 90, 8, 156, 104, 214, 25, 0, 0, 0, 0, 0, 97, 227, 98, 218,
241                         0, 0, 0, 4, 2, 22, 7, 207, 206, 25, 164, 197, 231, 230, 231, 56, 102, 61, 250, 251,
242                         187, 172, 38, 46, 79, 247, 108, 44, 155, 48, 219, 238, 252, 53, 192, 6, 67, 2, 36, 125,
243                         157, 176, 223, 175, 234, 116, 94, 248, 201, 225, 97, 235, 50, 47, 115, 172, 63, 136,
244                         88, 216, 115, 11, 111, 217, 114, 84, 116, 124, 231, 107, 2, 158, 1, 242, 121, 152, 106,
245                         204, 131, 186, 35, 93, 70, 216, 10, 237, 224, 183, 89, 95, 65, 3, 83, 185, 58, 138,
246                         181, 64, 187, 103, 127, 68, 50, 2, 201, 19, 17, 138, 136, 149, 185, 226, 156, 137, 175,
247                         110, 32, 237, 0, 217, 90, 31, 100, 228, 149, 46, 219, 175, 168, 77, 4, 143, 38, 128,
248                         76, 97, 0, 0, 0, 2, 0, 0, 255, 8, 153, 192, 0, 2, 27, 0, 0, 0, 1, 0, 0, 255, 2, 68,
249                         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,
250                         0, 0, 0, 1, 0, 0, 0, 0, 58, 85, 116, 216, 255, 8, 153, 192, 0, 2, 27, 0, 0, 25, 0, 0,
251                         0, 1, 0, 0, 0, 125, 255, 2, 68, 226, 0, 6, 11, 0, 1, 5, 0, 0, 0, 0, 29, 129, 25, 192,
252                 ];
253
254                 let tmp_directory = "./rapid-gossip-sync-tests-tmp";
255                 let sync_test = FileSyncTest::new(tmp_directory, &valid_response);
256                 let graph_sync_test_file = sync_test.get_test_file_path();
257
258                 let logger = TestLogger::new();
259                 let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
260
261                 assert_eq!(network_graph.read_only().channels().len(), 0);
262
263                 let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
264                 let sync_result = rapid_sync.sync_network_graph_with_file_path(&graph_sync_test_file);
265
266                 if sync_result.is_err() {
267                         panic!("Unexpected sync result: {:?}", sync_result)
268                 }
269
270                 assert_eq!(network_graph.read_only().channels().len(), 2);
271                 let after = network_graph.to_string();
272                 assert!(
273                         after.contains("021607cfce19a4c5e7e6e738663dfafbbbac262e4ff76c2c9b30dbeefc35c00643")
274                 );
275                 assert!(
276                         after.contains("02247d9db0dfafea745ef8c9e161eb322f73ac3f8858d8730b6fd97254747ce76b")
277                 );
278                 assert!(
279                         after.contains("029e01f279986acc83ba235d46d80aede0b7595f410353b93a8ab540bb677f4432")
280                 );
281                 assert!(
282                         after.contains("02c913118a8895b9e29c89af6e20ed00d95a1f64e4952edbafa84d048f26804c61")
283                 );
284                 assert!(after.contains("619737530008010752"));
285                 assert!(after.contains("783241506229452801"));
286         }
287
288         #[test]
289         fn measure_native_read_from_file() {
290                 let logger = TestLogger::new();
291                 let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
292
293                 assert_eq!(network_graph.read_only().channels().len(), 0);
294
295                 let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
296                 let start = std::time::Instant::now();
297                 let sync_result = rapid_sync
298                         .sync_network_graph_with_file_path("./res/full_graph.lngossip");
299                 if let Err(GraphSyncError::DecodeError(DecodeError::Io(io_error))) = &sync_result {
300                         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);
301                         #[cfg(not(require_route_graph_test))]
302                         {
303                                 println!("{}", error_string);
304                                 return;
305                         }
306                         #[cfg(require_route_graph_test)]
307                         panic!("{}", error_string);
308                 }
309                 let elapsed = start.elapsed();
310                 println!("initialization duration: {:?}", elapsed);
311                 if sync_result.is_err() {
312                         panic!("Unexpected sync result: {:?}", sync_result)
313                 }
314         }
315 }
316
317 #[cfg(ldk_bench)]
318 /// Benches
319 pub mod bench {
320         use bitcoin::Network;
321
322         use criterion::Criterion;
323
324         use std::fs;
325
326         use lightning::routing::gossip::NetworkGraph;
327         use lightning::util::test_utils::TestLogger;
328
329         use crate::RapidGossipSync;
330
331         /// Bench!
332         pub fn bench_reading_full_graph_from_file(b: &mut Criterion) {
333                 let logger = TestLogger::new();
334                 b.bench_function("read_full_graph_from_rgs", |b| b.iter(|| {
335                         let network_graph = NetworkGraph::new(Network::Bitcoin, &logger);
336                         let rapid_sync = RapidGossipSync::new(&network_graph, &logger);
337                         let mut file = match fs::read("../lightning-rapid-gossip-sync/res/full_graph.lngossip") {
338                                 Ok(f) => f,
339                                 Err(io_error) => {
340                                         let error_string = format!(
341                                                 "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{:?}",
342                                                 io_error);
343                                         #[cfg(not(require_route_graph_test))]
344                                         {
345                                                 println!("{}", error_string);
346                                                 return;
347                                         }
348                                         #[cfg(require_route_graph_test)]
349                                         panic!("{}", error_string);
350                                 },
351                         };
352                         rapid_sync.update_network_graph_no_std(&mut file, None).unwrap();
353                 }));
354         }
355 }