1use std::{
2 cmp::min,
3 collections::{HashMap, VecDeque},
4};
5
6use blake3::Hasher;
7use monero_oxide::{
8 block::Block,
9 transaction::{Input, Transaction},
10};
11use tower::{Service, ServiceExt};
12
13use cuprate_blockchain::service::BlockchainReadHandle;
14use cuprate_consensus::transactions::new_tx_verification_data;
15use cuprate_consensus_context::BlockchainContext;
16use cuprate_p2p::block_downloader::ChainEntry;
17use cuprate_p2p_core::NetworkZone;
18use cuprate_types::{
19 blockchain::{BlockchainReadRequest, BlockchainResponse},
20 Chain, VerifiedBlockInformation, VerifiedTransactionInformation,
21};
22
23pub const FAST_SYNC_BATCH_LEN: usize = 512;
25
26pub const fn fast_sync_stop_height(hashes: &[[u8; 32]]) -> usize {
28 hashes.len() * FAST_SYNC_BATCH_LEN
29}
30
31pub async fn validate_entries<N: NetworkZone>(
45 mut entries: VecDeque<ChainEntry<N>>,
46 start_height: usize,
47 blockchain_read_handle: &mut BlockchainReadHandle,
48 fast_sync_hashes: &[[u8; 32]],
49) -> Result<(VecDeque<ChainEntry<N>>, VecDeque<ChainEntry<N>>), tower::BoxError> {
50 let stop_height = fast_sync_stop_height(fast_sync_hashes);
51
52 if start_height >= stop_height {
54 return Ok((entries, VecDeque::new()));
55 }
56
57 let hashes_start_height = (start_height / FAST_SYNC_BATCH_LEN) * FAST_SYNC_BATCH_LEN;
74 let amount_of_hashes = entries.iter().map(|e| e.ids.len()).sum::<usize>();
75 let last_height = amount_of_hashes + start_height;
76
77 let hashes_stop_height = min(
78 (last_height / FAST_SYNC_BATCH_LEN) * FAST_SYNC_BATCH_LEN,
79 stop_height,
80 );
81
82 let mut hashes_stop_diff_last_height = last_height - hashes_stop_height;
83
84 let starting_hashes = if hashes_start_height == start_height {
85 vec![]
86 } else {
87 let BlockchainResponse::BlockHashInRange(starting_hashes) = blockchain_read_handle
89 .ready()
90 .await?
91 .call(BlockchainReadRequest::BlockHashInRange(
92 hashes_start_height..start_height,
93 Chain::Main,
94 ))
95 .await?
96 else {
97 unreachable!()
98 };
99
100 starting_hashes
101 };
102
103 if amount_of_hashes + starting_hashes.len() < FAST_SYNC_BATCH_LEN {
105 return Ok((VecDeque::new(), entries));
106 }
107
108 let mut unknown = VecDeque::new();
109
110 while !entries.is_empty() && hashes_stop_diff_last_height != 0 {
113 let back = entries.back_mut().unwrap();
114
115 if back.ids.len() >= hashes_stop_diff_last_height {
116 unknown.push_front(ChainEntry {
118 ids: back
119 .ids
120 .drain((back.ids.len() - hashes_stop_diff_last_height)..)
121 .collect(),
122 peer: back.peer,
123 handle: back.handle.clone(),
124 });
125
126 break;
127 }
128
129 let back = entries.pop_back().unwrap();
131 hashes_stop_diff_last_height -= back.ids.len();
132 unknown.push_front(back);
133 }
134
135 let mut hasher = Hasher::default();
137 let mut last_i = 1;
138 for (i, hash) in starting_hashes
139 .iter()
140 .chain(entries.iter().flat_map(|e| e.ids.iter()))
141 .enumerate()
142 {
143 hasher.update(hash);
144
145 if (i + 1) % FAST_SYNC_BATCH_LEN == 0 {
146 let got_hash = hasher.finalize();
147
148 if got_hash != fast_sync_hashes[get_hash_index_for_height(hashes_start_height + i)] {
149 return Err("Hashes do not match".into());
150 }
151 hasher.reset();
152 }
153
154 last_i = i + 1;
155 }
156 assert_eq!(last_i % FAST_SYNC_BATCH_LEN, 0);
158
159 Ok((entries, unknown))
160}
161
162const fn get_hash_index_for_height(height: usize) -> usize {
164 height / FAST_SYNC_BATCH_LEN
165}
166
167pub fn block_to_verified_block_information(
173 block: Block,
174 txs: Vec<Transaction>,
175 blockchin_ctx: &BlockchainContext,
176) -> VerifiedBlockInformation {
177 let block_hash = block.hash();
178
179 let block_blob = block.serialize();
180
181 let Some(Input::Gen(height)) = block.miner_transaction().prefix().inputs.first() else {
182 panic!("fast sync block invalid");
183 };
184
185 assert_eq!(
186 *height, blockchin_ctx.chain_height,
187 "fast sync block invalid"
188 );
189
190 let mut txs = txs
191 .into_iter()
192 .map(|tx| {
193 let data = new_tx_verification_data(tx).expect("fast sync block invalid");
194
195 (data.tx_hash, data)
196 })
197 .collect::<HashMap<_, _>>();
198
199 let mut verified_txs = Vec::with_capacity(txs.len());
200 for tx in &block.transactions {
201 let data = txs.remove(tx).expect("fast sync block invalid");
202
203 let (tx, prunable) = data.tx.pruned_with_prunable();
204 verified_txs.push(VerifiedTransactionInformation {
205 tx_prunable_blob: prunable,
206 tx_pruned: tx.serialize(),
207 tx_weight: data.tx_weight,
208 fee: data.fee,
209 tx_hash: data.tx_hash,
210 tx,
211 });
212 }
213
214 let total_fees = verified_txs.iter().map(|tx| tx.fee).sum::<u64>();
215 let total_outputs = block
216 .miner_transaction()
217 .prefix()
218 .outputs
219 .iter()
220 .map(|output| output.amount.unwrap_or(0))
221 .sum::<u64>();
222
223 let generated_coins = total_outputs - total_fees;
224
225 let weight = block.miner_transaction().weight()
226 + verified_txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
227
228 VerifiedBlockInformation {
229 block_blob,
230 txs: verified_txs,
231 block_hash,
232 pow_hash: [u8::MAX; 32],
233 height: *height,
234 generated_coins,
235 weight,
236 long_term_weight: blockchin_ctx.next_block_long_term_weight(weight),
237 cumulative_difficulty: blockchin_ctx.cumulative_difficulty + blockchin_ctx.next_difficulty,
238 block,
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use std::{
245 collections::VecDeque,
246 path::PathBuf,
247 slice,
248 sync::{Arc, LazyLock},
249 };
250
251 use proptest::proptest;
252
253 use cuprate_blockchain::{config::Config, service::BlockchainReadHandle};
254 use cuprate_p2p::block_downloader::ChainEntry;
255 use cuprate_p2p_core::{client::InternalPeerID, handles::HandleBuilder, ClearNet};
256
257 use crate::{fast_sync_stop_height, validate_entries, FAST_SYNC_BATCH_LEN};
258
259 static HASHES: LazyLock<&[[u8; 32]]> = LazyLock::new(|| {
260 (0..FAST_SYNC_BATCH_LEN * 2000)
261 .map(|i| {
262 let mut ret = [0; 32];
263 ret[..8].copy_from_slice(&i.to_le_bytes());
264 ret
265 })
266 .collect::<Vec<_>>()
267 .leak()
268 });
269
270 static FAST_SYNC_HASHES: LazyLock<&[[u8; 32]]> = LazyLock::new(|| {
271 HASHES
272 .chunks(FAST_SYNC_BATCH_LEN)
273 .map(|chunk| {
274 let len = chunk.len() * 32;
275 let bytes = chunk.as_ptr().cast::<u8>();
276
277 unsafe { blake3::hash(slice::from_raw_parts(bytes, len)).into() }
281 })
282 .collect::<Vec<_>>()
283 .leak()
284 });
285
286 fn test_db(path: PathBuf) -> BlockchainReadHandle {
287 let config = Config {
288 blob_dir: path.clone(),
289 index_dir: path.clone(),
290 ..Default::default()
291 };
292
293 let fjall = fjall::Database::builder(path).open().unwrap();
294
295 let thread_pool = Arc::new(rayon::ThreadPoolBuilder::new().build().unwrap());
296
297 let (blockchain_read_handle, _, _) =
298 cuprate_blockchain::service::init_with_pool(&config, fjall, thread_pool).unwrap();
299
300 blockchain_read_handle
301 }
302
303 proptest! {
304 #[test]
305 fn valid_entry(len in 0_usize..1_500_000) {
306 let mut ids = HASHES.to_vec();
307 ids.resize(len, [0_u8; 32]);
308
309 let handle = HandleBuilder::new().build();
310
311 let entry = ChainEntry {
312 ids,
313 peer: InternalPeerID::Unknown([1; 16]),
314 handle: handle.1
315 };
316
317 let data_dir = tempfile::tempdir().unwrap();
318
319 tokio_test::block_on(async move {
320 let mut blockchain_read_handle= test_db(data_dir.path().to_path_buf());
321
322 let ret = validate_entries::<ClearNet>(VecDeque::from([entry]), 0, &mut blockchain_read_handle, *FAST_SYNC_HASHES).await.unwrap();
323
324 let len_left = ret.0.iter().map(|e| e.ids.len()).sum::<usize>();
325 let len_right = ret.1.iter().map(|e| e.ids.len()).sum::<usize>();
326
327 assert_eq!(len_left + len_right, len);
328 assert!(len_left <= fast_sync_stop_height(*FAST_SYNC_HASHES));
329 assert!(len_right < FAST_SYNC_BATCH_LEN || len > fast_sync_stop_height(*FAST_SYNC_HASHES));
330 });
331 }
332
333 #[test]
334 fn single_hash_entries(len in 0_usize..1_500_000) {
335 let handle = HandleBuilder::new().build();
336 let entries = (0..len).map(|i| {
337 ChainEntry {
338 ids: vec![HASHES.get(i).copied().unwrap_or_default()],
339 peer: InternalPeerID::Unknown([1; 16]),
340 handle: handle.1.clone()
341 }
342 }).collect();
343
344 let data_dir = tempfile::tempdir().unwrap();
345
346 tokio_test::block_on(async move {
347 let mut blockchain_read_handle= test_db(data_dir.path().to_path_buf());
348
349 let ret = validate_entries::<ClearNet>(entries, 0, &mut blockchain_read_handle, *FAST_SYNC_HASHES).await.unwrap();
350
351 let len_left = ret.0.iter().map(|e| e.ids.len()).sum::<usize>();
352 let len_right = ret.1.iter().map(|e| e.ids.len()).sum::<usize>();
353
354 assert_eq!(len_left + len_right, len);
355 assert!(len_left <= fast_sync_stop_height(*FAST_SYNC_HASHES));
356 assert!(len_right < FAST_SYNC_BATCH_LEN || len > fast_sync_stop_height(*FAST_SYNC_HASHES));
357 });
358 }
359
360 #[test]
361 fn not_enough_hashes(len in 0_usize..FAST_SYNC_BATCH_LEN) {
362 let hashes_start_height = FAST_SYNC_BATCH_LEN * 1234;
363
364 let handle = HandleBuilder::new().build();
365 let entry = ChainEntry {
366 ids: HASHES[hashes_start_height..(hashes_start_height + len)].to_vec(),
367 peer: InternalPeerID::Unknown([1; 16]),
368 handle: handle.1
369 };
370
371 let data_dir = tempfile::tempdir().unwrap();
372
373 tokio_test::block_on(async move {
374 let mut blockchain_read_handle= test_db(data_dir.path().to_path_buf());
375
376 let ret = validate_entries::<ClearNet>(VecDeque::from([entry]), 0, &mut blockchain_read_handle, *FAST_SYNC_HASHES).await.unwrap();
377
378 let len_left = ret.0.iter().map(|e| e.ids.len()).sum::<usize>();
379 let len_right = ret.1.iter().map(|e| e.ids.len()).sum::<usize>();
380
381 assert_eq!(len_right, len);
382 assert_eq!(len_left, 0);
383 });
384 }
385 }
386}