Skip to main content

irontide_core/
hash_picker.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    reason = "M175: BEP 52 Merkle tree — base/index/count widths fixed by spec; piece arithmetic bounded by num_pieces (u32)"
6)]
7
8//! Hash request coordination for BEP 52 (Merkle tree hash downloading).
9//!
10//! `HashPicker` tracks which piece-layer and block-layer hashes are needed,
11//! generates `HashRequest`s for peers, and validates received hashes.
12//!
13//! **Priority:** block hash requests (reactive, for failed pieces) take
14//! precedence over piece-layer requests (proactive, 512-piece batches).
15//!
16//! **Limitation:** Currently assumes single-file v2 torrents (`file_index=0`).
17//! Multi-file piece-to-file mapping will be added in a future milestone.
18
19use std::time::Instant;
20
21use crate::hash_request::HashRequest;
22use crate::merkle_state::{MerkleTreeState, SetBlockResult};
23use crate::{Id32, MerkleTree};
24
25/// File info needed to initialize the hash picker.
26#[derive(Debug, Clone)]
27pub struct FileHashInfo {
28    /// Merkle root hash for this file.
29    pub root: Id32,
30    /// Total number of 16 KiB blocks in the file.
31    pub num_blocks: u32,
32    /// Total number of pieces in the file.
33    pub num_pieces: u32,
34}
35
36/// Result of adding received hashes to the picker.
37#[derive(Debug)]
38pub struct AddHashesResult {
39    /// Whether the hashes passed Merkle proof validation.
40    pub valid: bool,
41    /// Piece indices that passed deferred verification.
42    pub hash_passed: Vec<u32>,
43    /// Piece indices that failed deferred verification.
44    pub hash_failed: Vec<u32>,
45}
46
47struct PieceHashRequest {
48    last_request: Option<Instant>,
49    have: bool,
50}
51
52struct BlockHashRequest {
53    file_index: usize,
54    piece: u32,
55}
56
57/// Coordinates which Merkle hash requests to send to peers.
58///
59/// Priority: (1) block hashes for urgent/failed pieces,
60/// (2) piece-layer hashes in 512-chunk batches.
61pub struct HashPicker {
62    pub(crate) trees: Vec<MerkleTreeState>,
63    file_roots: Vec<Id32>,
64    piece_requests: Vec<Vec<PieceHashRequest>>,
65    block_requests: Vec<BlockHashRequest>,
66    piece_layer: u32,
67    blocks_per_piece: u32,
68}
69
70impl HashPicker {
71    /// Create a new hash picker for the given files.
72    ///
73    /// `blocks_per_piece` is `piece_length / 16384`.
74    #[must_use]
75    pub fn new(files: &[FileHashInfo], blocks_per_piece: u32) -> Self {
76        let piece_layer = blocks_per_piece.trailing_zeros();
77        let trees: Vec<_> = files
78            .iter()
79            .map(|f| MerkleTreeState::new(f.root, f.num_blocks, f.num_pieces))
80            .collect();
81
82        let piece_requests: Vec<Vec<PieceHashRequest>> = files
83            .iter()
84            .map(|f| {
85                let chunks = f.num_pieces.div_ceil(512) as usize;
86                (0..chunks)
87                    .map(|_| PieceHashRequest {
88                        last_request: None,
89                        have: false,
90                    })
91                    .collect()
92            })
93            .collect();
94
95        let file_roots = files.iter().map(|f| f.root).collect();
96
97        Self {
98            trees,
99            file_roots,
100            piece_requests,
101            block_requests: Vec::new(),
102            piece_layer,
103            blocks_per_piece,
104        }
105    }
106
107    /// Pick the next hash request to send.
108    ///
109    /// `has_piece` checks if the peer has a given piece index.
110    /// Priority: block requests (for failed/urgent pieces) > piece-layer requests.
111    pub fn pick_hashes(&self, has_piece: impl Fn(u32) -> bool) -> Option<HashRequest> {
112        // Priority 1: block hash requests (reactive, for piece failures)
113        if let Some(req) = self.block_requests.first() {
114            let first_block = req.piece * self.blocks_per_piece;
115            return Some(HashRequest {
116                file_root: self.file_roots[req.file_index],
117                base: 0,
118                index: first_block,
119                count: self.blocks_per_piece,
120                proof_layers: self.piece_layer + 1,
121            });
122        }
123
124        // Priority 2: piece-layer requests (512 pieces at a time)
125        for (file_idx, requests) in self.piece_requests.iter().enumerate() {
126            for (chunk_idx, req) in requests.iter().enumerate() {
127                if req.have {
128                    continue;
129                }
130
131                // Check if peer has any piece in this 512-chunk
132                let first_piece = chunk_idx as u32 * 512;
133                let has_relevant = (first_piece..)
134                    .take(512)
135                    .take_while(|&p| p < self.trees[file_idx].num_pieces())
136                    .any(&has_piece);
137
138                if !has_relevant {
139                    continue;
140                }
141
142                let remaining = self.trees[file_idx]
143                    .num_pieces()
144                    .saturating_sub(first_piece);
145                let count = 512.min(remaining.next_power_of_two());
146
147                // Gap 4 fix: calculate proof_layers from tree depth
148                let tree_depth = self.trees[file_idx]
149                    .num_blocks()
150                    .next_power_of_two()
151                    .trailing_zeros();
152                let proof_layers = tree_depth.saturating_sub(self.piece_layer);
153
154                return Some(HashRequest {
155                    file_root: self.file_roots[file_idx],
156                    base: self.piece_layer,
157                    index: first_piece,
158                    count,
159                    proof_layers,
160                });
161            }
162        }
163
164        None
165    }
166
167    /// Process received hashes from a peer.
168    ///
169    /// For piece-layer hashes, validates the uncle proof against the known file
170    /// root before accepting (Gap 1 fix). For block-layer hashes, stores them
171    /// directly in the tree state.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if the hash request references an unknown file root.
176    pub fn add_hashes(
177        &mut self,
178        req: &HashRequest,
179        hashes: &[Id32],
180    ) -> Result<AddHashesResult, crate::Error> {
181        let file_idx = self
182            .file_roots
183            .iter()
184            .position(|r| *r == req.file_root)
185            .ok_or_else(|| crate::Error::InvalidTorrent("unknown file root".into()))?;
186
187        if req.base == self.piece_layer {
188            // Piece-layer hashes — validate proof before accepting (Gap 1)
189            let base_count = req.count as usize;
190
191            if hashes.len() < base_count {
192                return Ok(AddHashesResult {
193                    valid: false,
194                    hash_passed: Vec::new(),
195                    hash_failed: Vec::new(),
196                });
197            }
198
199            let base_hashes = &hashes[..base_count];
200            let uncle_hashes = &hashes[base_count..];
201
202            // Verify the received hashes against the file root via uncle proof
203            if req.proof_layers > 0 && !uncle_hashes.is_empty() {
204                let sub_root = MerkleTree::root_from_hashes(base_hashes);
205                let leaf_index = req.index as usize / base_count;
206                if !MerkleTree::verify_proof(
207                    self.trees[file_idx].root(),
208                    sub_root,
209                    leaf_index,
210                    uncle_hashes,
211                ) {
212                    return Ok(AddHashesResult {
213                        valid: false,
214                        hash_passed: Vec::new(),
215                        hash_failed: Vec::new(),
216                    });
217                }
218            }
219
220            // Proof passed (or no proof needed) — accept the hashes
221            let verified = self.trees[file_idx]
222                .set_piece_hashes_and_verify(base_hashes.to_vec(), self.blocks_per_piece);
223
224            // Mark the 512-chunk as received
225            let chunk_idx = req.index as usize / 512;
226            if chunk_idx < self.piece_requests[file_idx].len() {
227                self.piece_requests[file_idx][chunk_idx].have = true;
228            }
229
230            Ok(AddHashesResult {
231                valid: true,
232                hash_passed: verified,
233                hash_failed: Vec::new(),
234            })
235        } else {
236            // Block-layer or other layer hashes — store directly
237            let mut hash_passed = Vec::new();
238            let mut hash_failed = Vec::new();
239
240            for (i, hash) in hashes.iter().enumerate() {
241                let block_index = req.index + i as u32;
242                match self.trees[file_idx].set_block_hash(block_index, *hash) {
243                    SetBlockResult::Ok => {
244                        let piece = block_index / self.blocks_per_piece;
245                        if !hash_passed.contains(&piece) {
246                            hash_passed.push(piece);
247                        }
248                    }
249                    SetBlockResult::HashFailed => {
250                        let piece = block_index / self.blocks_per_piece;
251                        if !hash_failed.contains(&piece) {
252                            hash_failed.push(piece);
253                        }
254                    }
255                    SetBlockResult::Unknown => {}
256                }
257            }
258
259            // Remove matching block request
260            self.block_requests.retain(|r| {
261                r.file_index != file_idx || r.piece != req.index / self.blocks_per_piece
262            });
263
264            Ok(AddHashesResult {
265                valid: true,
266                hash_passed,
267                hash_failed,
268            })
269        }
270    }
271
272    /// Record a computed block hash. Delegates to `MerkleTreeState`.
273    ///
274    /// Gap 10 fix: removed unused `offset` parameter from original plan.
275    pub fn set_block_hash(
276        &mut self,
277        file_index: usize,
278        block_index: u32,
279        hash: Id32,
280    ) -> SetBlockResult {
281        if file_index >= self.trees.len() {
282            return SetBlockResult::HashFailed;
283        }
284        self.trees[file_index].set_block_hash(block_index, hash)
285    }
286
287    /// Request block hashes for a piece that failed verification.
288    pub fn verify_block_hashes(&mut self, piece: u32) {
289        // Gap 6: single-file assumption — multi-file mapping deferred to M35
290        let file_index = 0;
291        if self.trees.is_empty() {
292            return;
293        }
294
295        let existing = self
296            .block_requests
297            .iter()
298            .any(|r| r.file_index == file_index && r.piece == piece);
299
300        if !existing {
301            self.block_requests
302                .push(BlockHashRequest { file_index, piece });
303        }
304    }
305
306    /// Mark a hash request as rejected (peer couldn't serve it).
307    pub fn hashes_rejected(&mut self, req: &HashRequest) {
308        if req.base == self.piece_layer {
309            let chunk_idx = req.index as usize / 512;
310            let file_idx = self.file_roots.iter().position(|r| *r == req.file_root);
311            if let Some(fi) = file_idx
312                && chunk_idx < self.piece_requests[fi].len()
313            {
314                self.piece_requests[fi][chunk_idx].last_request = None;
315            }
316        }
317    }
318
319    /// Do we have the piece-layer hash for a specific piece?
320    #[must_use]
321    pub fn have_piece_hash(&self, file_index: usize, piece: u32) -> bool {
322        self.trees
323            .get(file_index)
324            .is_some_and(|t| t.has_piece_hash(piece))
325    }
326
327    /// Are all blocks in a piece verified?
328    #[must_use]
329    pub fn piece_verified(&self, file_index: usize, piece: u32) -> bool {
330        self.trees
331            .get(file_index)
332            .is_some_and(|t| t.piece_verified(piece, self.blocks_per_piece))
333    }
334
335    /// Tree depth for a file (number of layers from root to block leaves).
336    #[must_use]
337    pub fn tree_depth(&self, file_index: usize) -> Option<u32> {
338        self.trees
339            .get(file_index)
340            .map(|t| t.num_blocks().next_power_of_two().trailing_zeros())
341    }
342
343    /// Pre-load piece-layer hashes from the `.torrent` file's `piece_layers` map.
344    ///
345    /// This is used when full metadata is available at torrent creation (not from
346    /// a magnet link). Each entry maps a file's Merkle root to the concatenated
347    /// piece-layer hashes (32 bytes each). After loading, any blocks that were
348    /// already hashed are retroactively verified.
349    ///
350    /// Returns the list of piece indices that were fully verified during loading.
351    pub fn load_piece_layers(
352        &mut self,
353        piece_layers: &std::collections::BTreeMap<Id32, Vec<u8>>,
354    ) -> Vec<u32> {
355        let mut verified = Vec::new();
356        for (file_idx, root) in self.file_roots.iter().enumerate() {
357            let Some(layer_bytes) = piece_layers.get(root) else {
358                continue;
359            };
360            // Each hash is 32 bytes (SHA-256)
361            if layer_bytes.len() % 32 != 0 {
362                continue;
363            }
364            let hashes: Vec<Id32> = layer_bytes
365                .chunks_exact(32)
366                .map(|chunk| {
367                    let mut arr = [0u8; 32];
368                    arr.copy_from_slice(chunk);
369                    Id32(arr)
370                })
371                .collect();
372
373            // Mark the corresponding piece-request chunks as received
374            let num_chunks = self.piece_requests.get(file_idx).map_or(0, Vec::len);
375            for chunk_idx in 0..num_chunks {
376                self.piece_requests[file_idx][chunk_idx].have = true;
377            }
378
379            // Load into tree state and retroactively verify stored blocks
380            if let Some(tree) = self.trees.get_mut(file_idx) {
381                let v = tree.set_piece_hashes_and_verify(hashes, self.blocks_per_piece);
382                verified.extend(v);
383            }
384        }
385        verified
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use crate::{Id32, MerkleTree, sha256};
393
394    fn make_file_info(root: Id32, num_blocks: u32, num_pieces: u32) -> FileHashInfo {
395        FileHashInfo {
396            root,
397            num_blocks,
398            num_pieces,
399        }
400    }
401
402    fn make_block_hashes(n: usize) -> Vec<Id32> {
403        (0..n).map(|i| sha256(&[i as u8])).collect()
404    }
405
406    #[test]
407    fn pick_piece_layer_first() {
408        let root = sha256(b"file1");
409        let files = vec![make_file_info(root, 1024, 64)];
410        let picker = HashPicker::new(&files, 16); // 16 blocks per piece
411
412        let req = picker.pick_hashes(|p| p == 0);
413        assert!(req.is_some());
414        let req = req.unwrap();
415        assert_eq!(req.file_root, root);
416        // Should request piece-layer hashes (base = piece_layer)
417        assert_eq!(req.base, 4); // log2(16) = 4
418    }
419
420    #[test]
421    fn pick_returns_none_when_complete() {
422        let block_hashes = make_block_hashes(4);
423        let tree = MerkleTree::from_leaves(&block_hashes);
424        let root = tree.root();
425        let files = vec![make_file_info(root, 4, 2)];
426        let mut picker = HashPicker::new(&files, 2);
427
428        // Simulate: we already have all piece hashes
429        let pieces = tree.piece_layer(2).to_vec();
430        picker.trees[0].set_piece_hashes(pieces);
431
432        // Mark all blocks verified
433        for (i, h) in block_hashes.iter().enumerate() {
434            picker.trees[0].set_block_hash(i as u32, *h);
435        }
436
437        // All piece hashes received → chunk marked as have
438        picker.piece_requests[0][0].have = true;
439
440        assert!(picker.pick_hashes(|_| true).is_none());
441    }
442
443    #[test]
444    fn add_hashes_populates_tree() {
445        let block_hashes = make_block_hashes(4);
446        let tree = MerkleTree::from_leaves(&block_hashes);
447        let root = tree.root();
448        let pieces = tree.piece_layer(2).to_vec();
449        let files = vec![make_file_info(root, 4, 2)];
450        let mut picker = HashPicker::new(&files, 2);
451
452        assert!(!picker.have_piece_hash(0, 0));
453
454        let req = HashRequest {
455            file_root: root,
456            base: 1, // piece layer for 2 blocks/piece
457            index: 0,
458            count: 2,
459            proof_layers: 0,
460        };
461        let result = picker.add_hashes(&req, &pieces);
462        assert!(result.is_ok());
463        assert!(result.unwrap().valid);
464        assert!(picker.have_piece_hash(0, 0));
465        assert!(picker.have_piece_hash(0, 1));
466    }
467
468    #[test]
469    fn set_block_hash_delegates_to_tree() {
470        let block_hashes = make_block_hashes(4);
471        let tree = MerkleTree::from_leaves(&block_hashes);
472        let root = tree.root();
473        let pieces = tree.piece_layer(2).to_vec();
474        let files = vec![make_file_info(root, 4, 2)];
475        let mut picker = HashPicker::new(&files, 2);
476
477        // Set piece hashes first
478        picker.trees[0].set_piece_hashes(pieces);
479
480        // Set block hashes — first returns Unknown, second completes piece → Ok
481        let r0 = picker.set_block_hash(0, 0, block_hashes[0]);
482        assert_eq!(r0, SetBlockResult::Unknown);
483
484        let r1 = picker.set_block_hash(0, 1, block_hashes[1]);
485        assert_eq!(r1, SetBlockResult::Ok);
486
487        assert!(picker.piece_verified(0, 0));
488    }
489
490    #[test]
491    fn verify_block_hashes_adds_request() {
492        let root = sha256(b"file1");
493        let files = vec![make_file_info(root, 1024, 64)];
494        let mut picker = HashPicker::new(&files, 16);
495
496        // Request block hashes for a failed piece
497        picker.verify_block_hashes(5);
498
499        let req = picker.pick_hashes(|_| true);
500        assert!(req.is_some());
501        let req = req.unwrap();
502        // Should be a block-layer request (base=0)
503        assert_eq!(req.base, 0);
504    }
505
506    #[test]
507    fn add_hashes_with_valid_proof_accepted() {
508        // Build a tree with 8 blocks, 2 per piece = 4 piece hashes
509        let block_hashes = make_block_hashes(8);
510        let tree = MerkleTree::from_leaves(&block_hashes);
511        let root = tree.root();
512        let piece_hashes = tree.piece_layer(2).to_vec();
513
514        let files = vec![make_file_info(root, 8, 4)];
515        let mut picker = HashPicker::new(&files, 2);
516
517        // Request first 2 piece hashes with uncle proof.
518        // Sub-root = hash(piece[0] || piece[1]) — one level above piece layer.
519        // Uncle = hash(piece[2] || piece[3]) — the sibling subtree root.
520        // verify_proof(root, sub_root, leaf_index=0, [uncle]) should pass.
521        let uncle = MerkleTree::root_from_hashes(&piece_hashes[2..]);
522
523        let mut hashes_with_proof = piece_hashes[0..2].to_vec();
524        hashes_with_proof.push(uncle);
525
526        let req = HashRequest {
527            file_root: root,
528            base: 1, // piece layer
529            index: 0,
530            count: 2,
531            proof_layers: 1,
532        };
533        let result = picker.add_hashes(&req, &hashes_with_proof).unwrap();
534        assert!(result.valid);
535        assert!(picker.have_piece_hash(0, 0));
536    }
537
538    #[test]
539    fn add_hashes_with_invalid_proof_rejected() {
540        let block_hashes = make_block_hashes(8);
541        let tree = MerkleTree::from_leaves(&block_hashes);
542        let root = tree.root();
543        let piece_hashes = tree.piece_layer(2).to_vec();
544
545        let files = vec![make_file_info(root, 8, 4)];
546        let mut picker = HashPicker::new(&files, 2);
547
548        // Send piece hashes with a wrong uncle hash
549        let wrong_uncle = sha256(b"wrong");
550        let mut hashes_with_bad_proof = piece_hashes[0..2].to_vec();
551        hashes_with_bad_proof.push(wrong_uncle);
552
553        let req = HashRequest {
554            file_root: root,
555            base: 1,
556            index: 0,
557            count: 2,
558            proof_layers: 1,
559        };
560        let result = picker.add_hashes(&req, &hashes_with_bad_proof).unwrap();
561        assert!(!result.valid);
562        // Piece hashes should NOT be set
563        assert!(!picker.have_piece_hash(0, 0));
564    }
565
566    #[test]
567    fn pick_hashes_with_callback() {
568        let root = sha256(b"file1");
569        let files = vec![make_file_info(root, 1024, 64)];
570        let picker = HashPicker::new(&files, 16);
571
572        // No pieces → no request
573        assert!(picker.pick_hashes(|_| false).is_none());
574
575        // Has piece 0 → request
576        assert!(picker.pick_hashes(|p| p == 0).is_some());
577    }
578}