Skip to main content

cuprate_fast_sync/
fast_sync.rs

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
23/// The size of a batch of block hashes to hash to create a fast sync hash.
24pub const FAST_SYNC_BATCH_LEN: usize = 512;
25
26/// Returns the height of the first block not included in the given hashes.
27pub const fn fast_sync_stop_height(hashes: &[[u8; 32]]) -> usize {
28    hashes.len() * FAST_SYNC_BATCH_LEN
29}
30
31/// Validates that the given [`ChainEntry`]s are in the fast-sync hashes.
32///
33/// `entries` should be a list of sequential entries.
34/// `start_height` should be the height of the first block in the first entry.
35/// `fast_sync_hashes` should be the hashes to validate against (empty to skip validation).
36///
37/// Returns a tuple, the first element being the entries that are valid* the second
38/// the entries we do not know are valid and should be passed in again when we have more entries.
39///
40/// *once we are passed the fast sync blocks all entries will be returned as valid as
41/// we can not check their validity here.
42///
43/// There may be more entries returned than passed in as entries could be split.
44pub 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 we are past the top fast sync block return all entries as valid.
53    if start_height >= stop_height {
54        return Ok((entries, VecDeque::new()));
55    }
56
57    /*
58       The algorithm used here needs to preserve which peer told us about which blocks, so we cannot
59       simply join all the hashes together return all the ones that can be validated and the ones that
60       can't, we need to keep the batches separate.
61
62       The first step is to calculate how many hashes we need from the blockchain to make up the first
63       fast-sync hash.
64
65       Then will take out all the batches at the end for which we cannot make up a full fast-sync hash
66       for, we will split a batch if it can only be partially validated.
67
68       With the remaining hashes from the blockchain and the hashes in the batches we can validate we
69       work on calculating the fast sync hashes and comparing them to the ones provided.
70    */
71
72    // First calculate the start and stop for this range of hashes.
73    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        // get the hashes we are missing to create the first fast-sync hash.
88        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 we don't have enough hashes to make up a batch we can't validate any.
104    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    // start moving from the back of the batches taking enough hashes out so we are only left with hashes
111    // that can be verified.
112    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            // This batch is partially valid so split it.
117            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        // Add this batch to the front of the unknowns, we do not know its validity.
130        let back = entries.pop_back().unwrap();
131        hashes_stop_diff_last_height -= back.ids.len();
132        unknown.push_front(back);
133    }
134
135    // Start verifying the hashes.
136    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    // Make sure we actually checked all hashes.
157    assert_eq!(last_i % FAST_SYNC_BATCH_LEN, 0);
158
159    Ok((entries, unknown))
160}
161
162/// Get the index of the hash that contains this block in the fast sync hashes.
163const fn get_hash_index_for_height(height: usize) -> usize {
164    height / FAST_SYNC_BATCH_LEN
165}
166
167/// Creates a [`VerifiedBlockInformation`] from a block known to be valid.
168///
169/// # Panics
170///
171/// This may panic if used on an invalid block.
172pub 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                // SAFETY:
278                // We are casting a valid [[u8; 32]] to a [u8], no alignment requirements and we are using it
279                // within the [[u8; 32]]'s lifetime.
280                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}