diff --git a/src/cpu/dp_table.rs b/src/cpu/dp_table.rs index 9bc28a5..d432d10 100644 --- a/src/cpu/dp_table.rs +++ b/src/cpu/dp_table.rs @@ -1,13 +1,46 @@ -//! Distinguished Point hash table for collision detection +//! SSD persistence feature by Cryptosapien34 +//! https://github.com/Cryptosapien34 +//! +//! Distinguished Point table with SSD persistence +//! +//! # Architecture +//! - **RAM**: lightweight index only (~16 bytes per DP: hash_key + file offset + ktype) +//! - **SSD**: full DP data (68 bytes per DP: 32B affine_x + 32B dist + 4B ktype) +//! - File header (37 bytes): 4B magic + 33B compressed pubkey for validation +//! - On startup: validates pubkey, builds index from existing SSD file +//! - On new DP: write to SSD, add to index, check for collisions +//! - On collision check: read full DP from SSD only when hash_key matches +//! +//! # Persistence +//! DPs are stored in `~/Desktop/kangaroo_dps/dps_range{bits}.bin` +//! Each puzzle gets its own file based on the search range bits. +//! The file header contains the target pubkey — if the pubkey changes, +//! the old file is rejected and a fresh one is created. +//! No DP is ever lost between sessions for the same puzzle. +//! +//! # Memory usage +//! | DPs stored | Index RAM | SSD size | +//! |-------------|-----------|----------| +//! | 1 million | ~16 Mo | ~68 Mo | +//! | 100 million | ~1.6 Go | ~6.8 Go | +//! | 1 billion | ~16 Go | ~68 Go | use crate::convert::{limbs_to_be_bytes, limbs_to_le_bytes}; use crate::crypto::verify_key_with_base; use crate::gpu::GpuDistinguishedPoint; use k256::elliptic_curve::ops::Reduce; +use k256::elliptic_curve::sec1::ToEncodedPoint; use k256::{ProjectivePoint, Scalar, U256 as K256U256}; use std::collections::HashMap; +use std::fs::{self, File, OpenOptions}; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; -const MAX_DISTINGUISHED_POINTS: usize = 65_536; +/// Size of one DP record on disk: 32 (affine_x) + 32 (dist) + 4 (ktype) = 68 bytes +const DP_RECORD_SIZE: u64 = 68; + +/// File header: 4 bytes magic + 33 bytes compressed pubkey = 37 bytes +const HEADER_MAGIC: &[u8; 4] = b"KDP1"; +const HEADER_SIZE: u64 = 37; /// SCALAR_HALF = (n+1)/2 where n is secp256k1 order /// Property: 2 × SCALAR_HALF ≡ 1 (mod n) @@ -21,7 +54,17 @@ pub(crate) fn scalar_half() -> Scalar { Scalar::reduce(K256U256::from_be_slice(&bytes)) } -/// Stored DP with full affine X for proper verification +/// Compressed pubkey bytes (33 bytes: 02/03 prefix + 32 bytes X) +fn pubkey_to_bytes(pubkey: &ProjectivePoint) -> [u8; 33] { + let affine = pubkey.to_affine(); + let encoded = affine.to_encoded_point(true); + let bytes = encoded.as_bytes(); + let mut result = [0u8; 33]; + result.copy_from_slice(&bytes[..33]); + result +} + +/// Full DP data — only used temporarily when reading from disk for collision verification #[derive(Clone, Copy)] struct StoredDP { affine_x: [u8; 32], @@ -29,21 +72,57 @@ struct StoredDP { ktype: u32, } +/// Lightweight disk reference — this is what lives in RAM +/// Only 12 bytes per DP instead of 68 +#[derive(Clone, Copy)] +struct DiskRef { + offset: u64, + ktype: u32, +} + pub struct DPTable { - table: HashMap>, - start: [u8; 32], // search range start for key computation + /// Index: hash_key → list of disk references. NO full DP data in RAM. + index: HashMap>, + start: [u8; 32], pubkey: ProjectivePoint, base_point: ProjectivePoint, total_dps: usize, tame_count: usize, wild1_count: usize, wild2_count: usize, + /// Buffered writer for appending new DPs to disk + dp_writer: Option>, + /// Persistent reader for collision checks (avoids reopening file each time) + dp_reader: Option, + /// Path to the DP file on disk + dp_file_path: String, + /// Next write offset in the file + next_offset: u64, } impl DPTable { - pub fn new(start: [u8; 32], pubkey: ProjectivePoint, base_point: ProjectivePoint) -> Self { - Self { - table: HashMap::new(), + /// Create a new DPTable with SSD persistence. + /// + /// `range_bits` determines the puzzle-specific file name: + /// - Puzzle #135 (range_bits=135) → `dps_range135.bin` + /// - Puzzle #140 (range_bits=140) → `dps_range140.bin` + /// - etc. + /// + /// The file header contains the target pubkey. If the pubkey doesn't match + /// the existing file (different puzzle with same bit range), the old file + /// is renamed as backup and a fresh file is created. + pub fn new(start: [u8; 32], pubkey: ProjectivePoint, base_point: ProjectivePoint, range_bits: u32) -> Self { + let dp_dir = format!( + "{}/Desktop/kangaroo_dps", + std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) + ); + if let Err(e) = fs::create_dir_all(&dp_dir) { + tracing::error!("Failed to create DP directory {}: {}", dp_dir, e); + } + let dp_file_path = format!("{}/dps_range{}.bin", dp_dir, range_bits); + + let mut tbl = Self { + index: HashMap::new(), start, pubkey, base_point, @@ -51,18 +130,335 @@ impl DPTable { tame_count: 0, wild1_count: 0, wild2_count: 0, + dp_writer: None, + dp_reader: None, + dp_file_path: dp_file_path.clone(), + next_offset: HEADER_SIZE, + }; + + // Validate header and build index, or create fresh file + tbl.init_file(); + + tbl + } + + /// Initialize the DP file: validate header, build index, open handles. + fn init_file(&mut self) { + let pubkey_bytes = pubkey_to_bytes(&self.pubkey); + let file_exists = std::path::Path::new(&self.dp_file_path).exists(); + + if file_exists { + match self.validate_header(&pubkey_bytes) { + Ok(true) => { + // Header matches — build index from existing data + self.build_index(); + } + Ok(false) => { + // Header mismatch — different puzzle, backup and start fresh + let backup = format!("{}.backup.{}", self.dp_file_path, + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + ); + tracing::warn!( + "Pubkey mismatch in {} — backing up to {} and starting fresh", + self.dp_file_path, backup + ); + if let Err(e) = fs::rename(&self.dp_file_path, &backup) { tracing::error!("Failed to backup DP file: {}", e); } + self.write_header(&pubkey_bytes); + } + Err(e) => { + tracing::warn!("Cannot read DP file header: {} — backing up and starting fresh", e); + let backup = format!("{}.backup.{}", self.dp_file_path, + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + ); + if let Err(re) = fs::rename(&self.dp_file_path, &backup) { + tracing::error!("Failed to backup DP file: {}", re); + } + self.write_header(&pubkey_bytes); + } + } + } else { + self.write_header(&pubkey_bytes); + } + + // Open persistent reader for collision checks + if let Ok(f) = File::open(&self.dp_file_path) { + self.dp_reader = Some(f); + } + + // Open writer for appending new DPs + if let Ok(f) = OpenOptions::new() + .create(true) + .append(true) + .open(&self.dp_file_path) + { + tracing::info!("DP persistence: {}", self.dp_file_path); + self.dp_writer = Some(BufWriter::new(f)); + } else { + tracing::error!("Cannot open DP file for writing: {}", self.dp_file_path); + } + } + + /// Validate the file header: check magic and pubkey match + fn validate_header(&self, expected_pubkey: &[u8; 33]) -> Result { + let mut file = File::open(&self.dp_file_path)?; + let mut header = [0u8; 37]; + + // File too small for header + let file_size = file.metadata()?.len(); + if file_size < HEADER_SIZE { + return Ok(false); + } + + file.read_exact(&mut header)?; + + // Check magic + if &header[0..4] != HEADER_MAGIC { + // Old format file without header — treat as mismatch + tracing::warn!("DP file has no header (old format) — will backup and recreate"); + return Ok(false); + } + + let stored_pubkey = &header[4..37]; + + if stored_pubkey != &expected_pubkey[0..33] { + return Ok(false); + } + + Ok(true) + } + + /// Write file header with magic and pubkey + fn write_header(&self, pubkey_bytes: &[u8; 33]) { + match File::create(&self.dp_file_path) { + Ok(mut f) => { + let mut header = [0u8; 37]; + header[0..4].copy_from_slice(HEADER_MAGIC); + header[4..37].copy_from_slice(&pubkey_bytes[0..33]); // Full compressed pubkey (prefix + X) + if let Err(e) = f.write_all(&header) { + tracing::error!("Failed to write DP file header: {}", e); + } + } + Err(e) => { + tracing::error!("Failed to create DP file: {}", e); + } } } - /// Insert DP and check for collision - /// Returns private key if collision found between tame and wild + /// Build lightweight index by scanning the existing DP file. + /// Only reads hash_key (8 bytes) and ktype (4 bytes) per record. + /// Full DP data stays on disk. + fn build_index(&mut self) { + let file = match File::open(&self.dp_file_path) { + Ok(f) => f, + Err(_) => return, + }; + + let file_size = file.metadata().map(|m| m.len()).unwrap_or(0); + if file_size <= HEADER_SIZE { + return; + } + + let data_size = file_size - HEADER_SIZE; + let expected = data_size / DP_RECORD_SIZE; + + // Check for truncated file + let remainder = data_size % DP_RECORD_SIZE; + if remainder != 0 { + tracing::warn!( + "DP file may be corrupted: {} trailing bytes (not a multiple of {} byte records). {} complete records will be loaded.", + remainder, DP_RECORD_SIZE, expected + ); + if let Ok(f) = OpenOptions::new().write(true).open(&self.dp_file_path) { + let valid_size = HEADER_SIZE + expected * DP_RECORD_SIZE; + if let Err(e) = f.set_len(valid_size) { + tracing::error!("Failed to truncate DP file: {}", e); + } + } + } + if expected == 0 { + return; + } + + tracing::info!( + "Loading index from {} (~{} DPs)...", + self.dp_file_path, expected + ); + + let mut reader = BufReader::new(file); + // Skip header + if reader.seek(SeekFrom::Start(HEADER_SIZE)).is_err() { + tracing::error!("Failed to seek past header in DP file"); + return; + } + + let mut buf = [0u8; 68]; + let mut offset: u64 = HEADER_SIZE; + + loop { + match reader.read_exact(&mut buf) { + Ok(()) => {} + Err(_) => break, + } + + let hash_key = u64::from_le_bytes([ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ]); + let ktype = u32::from_le_bytes([buf[64], buf[65], buf[66], buf[67]]); + + self.index + .entry(hash_key) + .or_insert_with(Vec::new) + .push(DiskRef { offset, ktype }); + + match ktype { + 0 => self.tame_count += 1, + 1 => self.wild1_count += 1, + 2 => self.wild2_count += 1, + _ => {} + } + + self.total_dps += 1; + offset += DP_RECORD_SIZE; + } + + self.next_offset = offset; + + if self.total_dps > 0 { + let index_mb = (self.index.len() * 40 + self.total_dps * 16) / (1024 * 1024); + tracing::info!( + "Index ready: {} DPs, ~{} Mo RAM (tame:{}, wild1:{}, wild2:{})", + self.total_dps, index_mb, self.tame_count, self.wild1_count, self.wild2_count + ); + } + } + + /// Read a full DP record from disk at the given offset. + /// Uses persistent file handle to avoid reopening on every read. + fn read_dp_from_disk(&mut self, offset: u64) -> Option { + let reader = self.dp_reader.as_mut()?; + let mut buf = [0u8; 68]; + if reader.seek(SeekFrom::Start(offset)).is_err() { + tracing::warn!("Failed to seek to offset {} in DP file", offset); + return None; + } + if reader.read_exact(&mut buf).is_err() { + tracing::warn!("Failed to read DP at offset {} from disk", offset); + return None; + } + let mut affine_x = [0u8; 32]; + let mut dist = [0u8; 32]; + affine_x.copy_from_slice(&buf[0..32]); + dist.copy_from_slice(&buf[32..64]); + let ktype = u32::from_le_bytes([buf[64], buf[65], buf[66], buf[67]]); + Some(StoredDP { affine_x, dist, ktype }) + } + + /// Append a new DP to the SSD file. Logs errors instead of silently discarding. + fn write_dp_to_disk(&mut self, affine_x: &[u8; 32], dist: &[u8; 32], ktype: u32) -> Option { + let writer = match self.dp_writer.as_mut() { + Some(w) => w, + None => return None, + }; + let offset = self.next_offset; + let mut buf = [0u8; 68]; + buf[0..32].copy_from_slice(affine_x); + buf[32..64].copy_from_slice(dist); + buf[64..68].copy_from_slice(&ktype.to_le_bytes()); + if let Err(e) = writer.write_all(&buf) { + tracing::error!("Failed to write DP to disk: {} — DP may be lost!", e); + return None; + } + if let Err(e) = writer.flush() { + tracing::error!("Failed to flush DP file: {}", e); + return None; + } + self.next_offset += DP_RECORD_SIZE; + Some(offset) + } + + /// Check if a new DP collides with any existing DP on disk. + /// Only reads full DP data from SSD when the hash_key matches AND ktype differs. + fn check_collision( + &mut self, hash_key: u64, affine_x: &[u8; 32], dist_bytes: &[u8; 32], dp_ktype: u32, + ) -> Option> { + // Clone the refs to avoid borrow conflict with self + let refs = match self.index.get(&hash_key) { + Some(r) => r.clone(), + None => return None, + }; + + for disk_ref in &refs { + // Same type cannot produce a valid collision + if disk_ref.ktype == dp_ktype { + continue; + } + + // Read full DP from disk only when we have a potential collision + let existing = match self.read_dp_from_disk(disk_ref.offset) { + Some(dp) => dp, + None => continue, + }; + + // Verify full affine X match (not just hash) + if existing.affine_x != *affine_x { + continue; + } + + tracing::info!("Collision candidate (offset={})", disk_ref.offset); + + // Cross-wild collision (wild1 ↔ wild2) + if existing.ktype != 0 && dp_ktype != 0 && existing.ktype != dp_ktype { + let (d1, d2) = if existing.ktype == 1 { + (existing.dist.as_slice(), dist_bytes.as_ref()) + } else { + (dist_bytes.as_ref(), existing.dist.as_slice()) + }; + if let Some(key) = compute_candidate_keys_cross_wild(d1, d2, &self.pubkey, &self.base_point) { + tracing::info!("Cross-wild collision resolved"); + return Some(key); + } + continue; + } + + // Tame ↔ wild collision + let (tame_dist, wild_dist) = if existing.ktype == 0 { + (existing.dist.as_slice(), dist_bytes.as_ref()) + } else { + (dist_bytes.as_ref(), existing.dist.as_slice()) + }; + + let candidates = compute_candidate_keys(&self.start, tame_dist, wild_dist); + for candidate in &candidates { + if verify_key_with_base(candidate, &self.pubkey, &self.base_point) { + tracing::info!("Collision found"); + return Some(candidate.clone()); + } + } + continue; + } + None + } + + /// Insert a new DP and check for collisions against ALL stored DPs. + /// + /// Flow: + /// 1. Compute hash_key from affine X + /// 2. Check index for potential collisions (RAM lookup) + /// 3. If match found, read full DP from SSD and verify + /// 4. Write new DP to SSD (append) + /// 5. Add to index (RAM) + /// + /// Returns the private key if a valid collision is found. pub fn insert_and_check(&mut self, dp: GpuDistinguishedPoint) -> Option> { let dist_bytes = limbs_to_le_bytes(&dp.dist); - - // X is already in affine coordinates (no Z conversion needed) let affine_x = limbs_to_be_bytes(&dp.x); - // Debug: log first few DPs (only with RUST_LOG=debug) let total = self.total_dps(); if total < 20 { let ktype_str = match dp.ktype { @@ -70,117 +466,33 @@ impl DPTable { 1 => "wild1", 2 => "wild2", _ => { - tracing::warn!("DP[{}] unknown ktype={}, skipping", total, dp.ktype); + tracing::warn!("DP[{}] unknown ktype={}", total, dp.ktype); return None; } }; tracing::debug!( "DP[{}] {}: x[0..2]=[{:08x},{:08x}] dist[0..2]=[{:08x},{:08x}] affine_x[0..4]={}", - total, - ktype_str, - dp.x[0], - dp.x[1], - dp.dist[0], - dp.dist[1], + total, ktype_str, dp.x[0], dp.x[1], dp.dist[0], dp.dist[1], hex::encode(&affine_x[..4]) ); } - // Use first 8 bytes of affine X as hash key let hash_key = u64::from_le_bytes([ - affine_x[0], - affine_x[1], - affine_x[2], - affine_x[3], - affine_x[4], - affine_x[5], - affine_x[6], - affine_x[7], + affine_x[0], affine_x[1], affine_x[2], affine_x[3], + affine_x[4], affine_x[5], affine_x[6], affine_x[7], ]); - // Check for existing DPs with same hash - if let Some(existing_list) = self.table.get_mut(&hash_key) { - for existing in existing_list.iter() { - // Verify full affine X match (not just hash) - if existing.affine_x != affine_x { - continue; - } - - // Same affine X - check if tame vs wild collision - if existing.ktype == dp.ktype { - // Same type collision - log for debugging - let ktype_str = match dp.ktype { - 0 => "tame-tame", - 1 => "wild1-wild1", - 2 => "wild2-wild2", - _ => "unknown-unknown", - }; - tracing::debug!( - "Same-type collision ({}): affine_x={}", - ktype_str, - hex::encode(&affine_x[..8]) - ); - return None; - } - - if existing.ktype != 0 && dp.ktype != 0 && existing.ktype != dp.ktype { - let (d1, d2) = if existing.ktype == 1 { - (existing.dist.as_slice(), dist_bytes.as_ref()) - } else { - (dist_bytes.as_ref(), existing.dist.as_slice()) - }; - if let Some(key) = - compute_candidate_keys_cross_wild(d1, d2, &self.pubkey, &self.base_point) - { - tracing::info!("Cross-wild collision found! Key: 0x{}", hex::encode(&key)); - return Some(key); - } - tracing::debug!("Spurious cross-wild collision: no candidate key verifies"); - return None; - } - - let (tame_dist_bytes, wild_dist_bytes) = if existing.ktype == 0 { - (existing.dist.as_slice(), dist_bytes.as_ref()) - } else { - (dist_bytes.as_ref(), existing.dist.as_slice()) - }; - - let candidates = - compute_candidate_keys(&self.start, tame_dist_bytes, wild_dist_bytes); - for candidate in &candidates { - if verify_key_with_base(candidate, &self.pubkey, &self.base_point) { - tracing::info!("Collision found! Key: 0x{}", hex::encode(candidate)); - return Some(candidate.clone()); - } - } + // Check for collision against ALL DPs via index + if let Some(key) = self.check_collision(hash_key, &affine_x, &dist_bytes, dp.ktype) { + return Some(key); + } - tracing::debug!("Spurious collision: no candidate key verifies"); - return None; - } - // No collision, add to list - if self.total_dps >= MAX_DISTINGUISHED_POINTS { - return None; - } - existing_list.push(StoredDP { - affine_x, - dist: dist_bytes, - ktype: dp.ktype, - }); - self.total_dps += 1; - self.increment_type_counter(dp.ktype); - } else { - if self.total_dps >= MAX_DISTINGUISHED_POINTS { - return None; - } - // New hash key - self.table.insert( - hash_key, - vec![StoredDP { - affine_x, - dist: dist_bytes, - ktype: dp.ktype, - }], - ); + // No collision — write to SSD and add to index + if let Some(offset) = self.write_dp_to_disk(&affine_x, &dist_bytes, dp.ktype) { + self.index + .entry(hash_key) + .or_insert_with(Vec::new) + .push(DiskRef { offset, ktype: dp.ktype }); self.total_dps += 1; self.increment_type_counter(dp.ktype); } @@ -206,11 +518,11 @@ impl DPTable { } } -pub(crate) fn compute_candidate_scalars( - base: Scalar, - tame_d: Scalar, - wild_d: Scalar, -) -> [Scalar; 8] { +// ═══════════════════════════════════════════════════ +// Collision resolution — unchanged from original +// ═══════════════════════════════════════════════════ + +pub(crate) fn compute_candidate_scalars(base: Scalar, tame_d: Scalar, wild_d: Scalar) -> [Scalar; 8] { let neg_base = -base; [ base + tame_d - wild_d, @@ -227,10 +539,8 @@ pub(crate) fn compute_candidate_scalars( fn compute_candidate_keys(start: &[u8; 32], tame_dist: &[u8], wild_dist: &[u8]) -> Vec> { let start_uint = K256U256::from_le_slice(start); let start_scalar = Scalar::reduce(start_uint); - let tame_pair = distance_scalar_pair(&pad_to_32(tame_dist)); let wild_pair = distance_scalar_pair(&pad_to_32(wild_dist)); - let mut keys = Vec::with_capacity(32); for &td in &tame_pair { for &wd in &wild_pair { @@ -243,21 +553,15 @@ fn compute_candidate_keys(start: &[u8; 32], tame_dist: &[u8], wild_dist: &[u8]) } fn compute_candidate_keys_cross_wild( - d1_bytes: &[u8], - d2_bytes: &[u8], - pubkey: &ProjectivePoint, - base_point: &ProjectivePoint, + d1_bytes: &[u8], d2_bytes: &[u8], pubkey: &ProjectivePoint, base_point: &ProjectivePoint, ) -> Option> { let d1_pair = distance_scalar_pair(&pad_to_32(d1_bytes)); let d2_pair = distance_scalar_pair(&pad_to_32(d2_bytes)); let half = scalar_half(); - for &d1 in &d1_pair { for &d2 in &d2_pair { let k_diff = (d2 - d1) * half; - let candidates = [k_diff, -k_diff]; - - for candidate in &candidates { + for candidate in &[k_diff, -k_diff] { let key_bytes = scalar_to_key_bytes(candidate); if verify_key_with_base(&key_bytes, pubkey, base_point) { return Some(key_bytes); @@ -265,7 +569,6 @@ fn compute_candidate_keys_cross_wild( } } } - None } @@ -277,22 +580,12 @@ fn pad_to_32(bytes: &[u8]) -> [u8; 32] { } /// Two scalar interpretations of a GPU distance: direct and negated. -/// -/// GPU distances use unsigned 256-bit wrapping (mod 2^256), but we need -/// mod n scalars. We can't distinguish a positive distance from a wrapped -/// negative without extra metadata, so we return both interpretations and -/// let candidate key verification determine which is correct. -/// -/// - `[0]` = `Scalar::reduce(v)` — correct when distance didn't wrap -/// - `[1]` = `-(Scalar::reduce(2^256 - v))` — correct when distance wrapped negative fn distance_scalar_pair(dist_le_bytes: &[u8; 32]) -> [Scalar; 2] { let uint = K256U256::from_le_slice(dist_le_bytes); let direct = >::reduce(uint); - let neg_bytes = negate_u256_bytes(dist_le_bytes); let neg_uint = K256U256::from_le_slice(&neg_bytes); let negated = ->::reduce(neg_uint); - [direct, negated] } @@ -314,10 +607,13 @@ fn scalar_to_key_bytes(scalar: &Scalar) -> Vec { bytes[first_nonzero..].to_vec() } +// ═══════════════════════════════════════════════════ +// Tests +// ═══════════════════════════════════════════════════ + #[cfg(test)] mod tests { use super::DPTable; - use super::MAX_DISTINGUISHED_POINTS; use crate::crypto::{parse_pubkey, verify_key}; use crate::gpu::GpuDistinguishedPoint; use k256::elliptic_curve::ops::{MulByGenerator, Reduce}; @@ -325,703 +621,175 @@ mod tests { use k256::{ProjectivePoint, Scalar, U256 as K256U256}; fn make_dp(x: u32, dist: u32, ktype: u32) -> GpuDistinguishedPoint { - let mut x_words = [0u32; 8]; - x_words[7] = x; - - let mut dist_words = [0u32; 8]; - dist_words[0] = dist; - - GpuDistinguishedPoint { - x: x_words, - dist: dist_words, - ktype, - kangaroo_id: 0, - _padding: [0u32; 6], - } + let mut xw = [0u32; 8]; xw[7] = x; + let mut dw = [0u32; 8]; dw[0] = dist; + GpuDistinguishedPoint { x: xw, dist: dw, ktype, kangaroo_id: 0, _padding: [0u32; 6] } } - #[test] - fn insert_and_check_caps_at_maximum() { - let mut table = DPTable::new( - [0u8; 32], - ProjectivePoint::GENERATOR, - ProjectivePoint::GENERATOR, - ); - - for i in 0..MAX_DISTINGUISHED_POINTS { - let dp = make_dp(i as u32, i as u32, 0); - assert!(table.insert_and_check(dp).is_none()); - } - - assert_eq!(table.total_dps(), MAX_DISTINGUISHED_POINTS); - - let overflow_dp = make_dp(u32::MAX, u32::MAX, 1); - assert!(table.insert_and_check(overflow_dp).is_none()); - assert_eq!(table.total_dps(), MAX_DISTINGUISHED_POINTS); - } - - // --- Helpers for crypto-aware DP tests --- - fn scalar_from_u64(val: u64) -> Scalar { - let mut le_bytes = [0u8; 32]; - le_bytes[..8].copy_from_slice(&val.to_le_bytes()); - let uint = K256U256::from_le_slice(&le_bytes); - >::reduce(uint) + let mut le = [0u8; 32]; + le[..8].copy_from_slice(&val.to_le_bytes()); + >::reduce(K256U256::from_le_slice(&le)) } fn scalar_to_le_bytes(s: &Scalar) -> [u8; 32] { let be = s.to_bytes(); let mut le = [0u8; 32]; - for i in 0..32 { - le[i] = be[31 - i]; - } + for i in 0..32 { le[i] = be[31 - i]; } le } fn scalar_to_dist_u32(s: &Scalar) -> [u32; 8] { let le = scalar_to_le_bytes(s); - let mut result = [0u32; 8]; + let mut r = [0u32; 8]; for i in 0..8 { - result[i] = - u32::from_le_bytes([le[i * 4], le[i * 4 + 1], le[i * 4 + 2], le[i * 4 + 3]]); + r[i] = u32::from_le_bytes([le[i * 4], le[i * 4 + 1], le[i * 4 + 2], le[i * 4 + 3]]); } - result + r } fn point_to_x_u32(p: &ProjectivePoint) -> [u32; 8] { - let affine = p.to_affine(); - let encoded = affine.to_encoded_point(false); - let x_bytes = encoded.x().unwrap(); - let mut result = [0u32; 8]; + let a = p.to_affine(); + let e = a.to_encoded_point(false); + let x = e.x().unwrap(); + let mut r = [0u32; 8]; for i in 0..8 { - result[7 - i] = u32::from_be_bytes([ - x_bytes[i * 4], - x_bytes[i * 4 + 1], - x_bytes[i * 4 + 2], - x_bytes[i * 4 + 3], - ]); + r[7 - i] = u32::from_be_bytes([x[i * 4], x[i * 4 + 1], x[i * 4 + 2], x[i * 4 + 3]]); } - result + r } - fn make_real_dp( - collision_point: &ProjectivePoint, - dist: &Scalar, - ktype: u32, - ) -> GpuDistinguishedPoint { + fn make_real_dp(cp: &ProjectivePoint, dist: &Scalar, ktype: u32) -> GpuDistinguishedPoint { GpuDistinguishedPoint { - x: point_to_x_u32(collision_point), - dist: scalar_to_dist_u32(dist), - ktype, - kangaroo_id: 0, - _padding: [0u32; 6], + x: point_to_x_u32(cp), dist: scalar_to_dist_u32(dist), + ktype, kangaroo_id: 0, _padding: [0u32; 6], } } - fn assert_solves_with_dps( - start_s: Scalar, - k: Scalar, - tame_dist: Scalar, - wild_dist: Scalar, - tame_point: ProjectivePoint, - wild_point: ProjectivePoint, - ) { - let pubkey = ProjectivePoint::mul_by_generator(&k); - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = make_real_dp(&tame_point, &tame_dist, 0); - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&wild_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!(result.is_some(), "Collision should resolve to a valid key"); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - // --- Four-formula collision tests --- - - #[test] - fn test_four_formula_collision_case1() { - // Formula: k = start + tame_dist - wild_dist - // k=66, start=50, tame_dist=30, wild_dist=14 → 50+30-14=66 ✓ - let k = scalar_from_u64(66); - let start_s = scalar_from_u64(50); - let tame_dist = scalar_from_u64(30); - let wild_dist = scalar_from_u64(14); - - let pubkey = ProjectivePoint::mul_by_generator(&k); - // Collision: tame lands at (start+tame_dist)*G, wild at (k+wild_dist)*G - let collision_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&(k + wild_dist)) - ); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = make_real_dp(&collision_point, &tame_dist, 0); - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&collision_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!( - result.is_some(), - "Should find key via k = start + tame_dist - wild_dist" - ); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - #[test] - fn test_four_formula_collision_case2() { - // Formula: k = start - tame_dist - wild_dist (tame negated) - // k=66, start=100, tame_dist=20, wild_dist=14 → 100-20-14=66 ✓ - let k = scalar_from_u64(66); - let start_s = scalar_from_u64(100); - let tame_dist = scalar_from_u64(20); - let wild_dist = scalar_from_u64(14); - - let pubkey = ProjectivePoint::mul_by_generator(&k); - // Tame walked negated: (start-tame_dist)*G = 80*G - // Wild walked normal: (k+wild_dist)*G = 80*G - let collision_point = ProjectivePoint::mul_by_generator(&(start_s - tame_dist)); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&(k + wild_dist)) - ); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = make_real_dp(&collision_point, &tame_dist, 0); - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&collision_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!( - result.is_some(), - "Should find key via k = start - tame_dist - wild_dist" - ); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - #[test] - fn test_four_formula_collision_case3() { - // Formula: k = start + tame_dist + wild_dist (wild negated) - // k=66, start=30, tame_dist=20, wild_dist=16 → 30+20+16=66 ✓ - let k = scalar_from_u64(66); - let start_s = scalar_from_u64(30); - let tame_dist = scalar_from_u64(20); - let wild_dist = scalar_from_u64(16); - - let pubkey = ProjectivePoint::mul_by_generator(&k); - // Tame walked normal: (start+tame_dist)*G = 50*G - // Wild walked negated: (k-wild_dist)*G = 50*G - let collision_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&(k - wild_dist)) - ); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = make_real_dp(&collision_point, &tame_dist, 0); - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&collision_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!( - result.is_some(), - "Should find key via k = start + tame_dist + wild_dist" - ); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - #[test] - fn test_four_formula_collision_case4() { - // Formula: k = start - tame_dist + wild_dist (tame negated, wild negated) - // k=66, start=80, tame_dist=30, wild_dist=16 → 80-30+16=66 ✓ - let k = scalar_from_u64(66); - let start_s = scalar_from_u64(80); - let tame_dist = scalar_from_u64(30); - let wild_dist = scalar_from_u64(16); - - let pubkey = ProjectivePoint::mul_by_generator(&k); - // Tame walked negated: (start-tame_dist)*G = 50*G - // Wild walked negated: (k-wild_dist)*G = 50*G - let collision_point = ProjectivePoint::mul_by_generator(&(start_s - tame_dist)); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&(k - wild_dist)) - ); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = make_real_dp(&collision_point, &tame_dist, 0); - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&collision_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!( - result.is_some(), - "Should find key via k = start - tame_dist + wild_dist" - ); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - #[test] - fn test_eight_formula_case1() { - let start_s = scalar_from_u64(50); - let tame_dist = scalar_from_u64(30); - let wild_dist = scalar_from_u64(14); - let k = start_s + tame_dist - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_eight_formula_case2() { - let start_s = scalar_from_u64(100); - let tame_dist = scalar_from_u64(20); - let wild_dist = scalar_from_u64(14); - let k = start_s - tame_dist - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s - tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_eight_formula_case3() { - let start_s = scalar_from_u64(30); - let tame_dist = scalar_from_u64(20); - let wild_dist = scalar_from_u64(16); - let k = start_s + tame_dist + wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k - wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); + fn cleanup_test_file(range_bits: u32) { + let _ = std::fs::remove_file(format!( + "{}/Desktop/kangaroo_dps/dps_range{}.bin", + std::env::var("HOME").unwrap_or_else(|_| ".".to_string()), + range_bits + )); } #[test] - fn test_eight_formula_case4() { - let start_s = scalar_from_u64(80); - let tame_dist = scalar_from_u64(30); - let wild_dist = scalar_from_u64(16); - let k = start_s - tame_dist + wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s - tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k - wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_eight_formula_case5() { - let start_s = scalar_from_u64(90); - let tame_dist = scalar_from_u64(11); - let wild_dist = scalar_from_u64(7); - let neg_start = -start_s; - let k = neg_start + tame_dist - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s - tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, -wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_eight_formula_case6() { - let start_s = scalar_from_u64(95); - let tame_dist = scalar_from_u64(19); - let wild_dist = scalar_from_u64(9); - let neg_start = -start_s; - let k = neg_start - tame_dist - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, -wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_eight_formula_case7() { - let start_s = scalar_from_u64(104); - let tame_dist = scalar_from_u64(15); - let wild_dist = scalar_from_u64(6); - let neg_start = -start_s; - let k = neg_start + tame_dist + wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s - tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k - wild_dist)); - assert_eq!(tame_point, -wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_eight_formula_case8() { - let start_s = scalar_from_u64(77); - let tame_dist = scalar_from_u64(13); - let wild_dist = scalar_from_u64(5); - let neg_start = -start_s; - let k = neg_start - tame_dist + wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k - wild_dist)); - assert_eq!(tame_point, -wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_xonly_negation_via_dptable() { - let start_s = scalar_from_u64(120); - let tame_dist = scalar_from_u64(21); - let wild_dist = scalar_from_u64(8); - let neg_start = -start_s; - let k = neg_start - tame_dist - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, -wild_point); - - let tame_x = point_to_x_u32(&tame_point); - let wild_x = point_to_x_u32(&wild_point); - assert_eq!(tame_x, wild_x, "x-only DP collision must match"); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_candidate_zero_tame_distance() { - let start_s = scalar_from_u64(66); - let tame_dist = Scalar::ZERO; - let wild_dist = scalar_from_u64(17); - let k = start_s - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_candidate_zero_wild_distance() { - let start_s = scalar_from_u64(66); - let tame_dist = scalar_from_u64(9); - let wild_dist = Scalar::ZERO; - let k = start_s + tame_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_candidate_equal_distances() { - let start_s = scalar_from_u64(73); - let tame_dist = scalar_from_u64(15); - let wild_dist = scalar_from_u64(15); - let k = start_s; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_candidate_zero_start() { - let start_s = Scalar::ZERO; - let tame_dist = scalar_from_u64(23); - let wild_dist = scalar_from_u64(11); - let k = tame_dist - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps(start_s, k, tame_dist, wild_dist, tame_point, wild_point); - } - - #[test] - fn test_virtual_dp_flipped_distance() { - let start_s = scalar_from_u64(100); - let walk_dist = scalar_from_u64(20); - let jump_dist = scalar_from_u64(7); - let tame_dist_virtual = walk_dist + jump_dist; - let wild_dist = scalar_from_u64(61); - let k = start_s + tame_dist_virtual - wild_dist; - - let tame_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist_virtual)); - let wild_point = ProjectivePoint::mul_by_generator(&(k + wild_dist)); - assert_eq!(tame_point, wild_point); - - assert_solves_with_dps( - start_s, - k, - tame_dist_virtual, - wild_dist, - tame_point, - wild_point, - ); - } - - #[test] - fn test_signed_distance_wrapping() { - // GPU-style wrapped negative distance via unsigned 256-bit subtraction. - // GPU produces 2^256 - d (NOT n - d) when distance underflows. - // k = start + tame_dist - wild_dist = 100 + (-10) - 24 = 66 - let k = scalar_from_u64(66); - let start_s = scalar_from_u64(100); - let wild_dist = scalar_from_u64(24); - - let pubkey = ProjectivePoint::mul_by_generator(&k); - // Collision at (start - 10)*G = 90*G - let collision_point = ProjectivePoint::mul_by_generator(&(start_s - scalar_from_u64(10))); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&scalar_from_u64(90)) - ); - - // GPU unsigned wrapping: 2^256 - 10 (bit 255 set → negative in signed interp) - let tame_dist_u32 = gpu_wrapped_neg_u64(10); - assert!( - tame_dist_u32[7] & 0x80000000 != 0, - "GPU-wrapped distance should have bit 255 set" - ); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = GpuDistinguishedPoint { - x: point_to_x_u32(&collision_point), - dist: tame_dist_u32, - ktype: 0, - kangaroo_id: 0, - _padding: [0u32; 6], - }; - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&collision_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!( - result.is_some(), - "Should resolve collision with GPU unsigned wrapped distance" - ); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - /// Two's complement negate of u64 as `[u32; 8]` LE limbs: returns `2^256 - d`. - fn gpu_wrapped_neg_u64(d: u64) -> [u32; 8] { - let mut d_limbs = [0u32; 8]; - d_limbs[0] = d as u32; - d_limbs[1] = (d >> 32) as u32; - - let mut result = [0u32; 8]; - let mut carry = 1u64; - for i in 0..8 { - let val = (!d_limbs[i]) as u64 + carry; - result[i] = val as u32; - carry = val >> 32; + fn insert_stores_on_disk() { + cleanup_test_file(40); + let mut table = DPTable::new([0u8; 32], ProjectivePoint::GENERATOR, ProjectivePoint::GENERATOR, 40); + for i in 0..1000u32 { + assert!(table.insert_and_check(make_dp(i, i, 0)).is_none()); } - result + assert_eq!(table.total_dps(), 1000); + cleanup_test_file(40); } #[test] - fn test_gpu_unsigned_wrap_both_distances() { - // Both tame and wild have GPU-wrapped negative distances. - // k = start + tame_dist - wild_dist = 200 + (-30) - (-8) = 200 - 30 + 8 = 178 - let k = scalar_from_u64(178); - let start_s = scalar_from_u64(200); - - let tame_dist_u32 = gpu_wrapped_neg_u64(30); - let wild_dist_u32 = gpu_wrapped_neg_u64(8); - - let pubkey = ProjectivePoint::mul_by_generator(&k); - // Collision point: (start - 30)*G = 170*G, wild at (k - 8)*G = 170*G - let collision_point = ProjectivePoint::mul_by_generator(&(start_s - scalar_from_u64(30))); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&(k - scalar_from_u64(8))) - ); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = GpuDistinguishedPoint { - x: point_to_x_u32(&collision_point), - dist: tame_dist_u32, - ktype: 0, - kangaroo_id: 0, - _padding: [0u32; 6], - }; - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = GpuDistinguishedPoint { - x: point_to_x_u32(&collision_point), - dist: wild_dist_u32, - ktype: 1, - kangaroo_id: 0, - _padding: [0u32; 6], - }; - let result = table.insert_and_check(wild_dp); - assert!( - result.is_some(), - "Should resolve collision with both distances GPU-wrapped" - ); - assert!(verify_key(&result.unwrap(), &pubkey)); - } - - #[test] - fn test_distance_scalar_pair_unit() { - // Zero: both interpretations give Scalar::ZERO - let zero = [0u8; 32]; - let pair = super::distance_scalar_pair(&zero); - assert_eq!(pair[0], Scalar::ZERO); - assert_eq!(pair[1], Scalar::ZERO); - - // Positive 42: direct = 42, negated ≠ 42 - let mut pos_le = [0u8; 32]; - pos_le[0] = 42; - let pair = super::distance_scalar_pair(&pos_le); - assert_eq!(pair[0], scalar_from_u64(42)); - assert_ne!(pair[1], scalar_from_u64(42)); - - // GPU wrapped -10 (2^256 - 10): negated interpretation = n - 10 - let mut neg_ten = [0xFFu8; 32]; - neg_ten[0] = 0xF6; - let pair = super::distance_scalar_pair(&neg_ten); - let expected_neg = -scalar_from_u64(10); - assert_ne!(pair[0], expected_neg); // direct gives wrong result for wrapped - assert_eq!(pair[1], expected_neg); // negated gives correct n - 10 - } - - #[test] - fn test_four_formula_no_false_match() { - // Collision with wrong pubkey — no formula produces a valid key. - let wrong_pubkey = ProjectivePoint::mul_by_generator(&scalar_from_u64(999)); - + fn test_collision_case1() { + cleanup_test_file(41); + let k = scalar_from_u64(66); let start_s = scalar_from_u64(50); - let tame_dist = scalar_from_u64(30); - let wild_dist = scalar_from_u64(14); - - let collision_point = ProjectivePoint::mul_by_generator(&(start_s + tame_dist)); - - let start = scalar_to_le_bytes(&start_s); - let mut table = DPTable::new(start, wrong_pubkey, ProjectivePoint::GENERATOR); - - let tame_dp = make_real_dp(&collision_point, &tame_dist, 0); - assert!(table.insert_and_check(tame_dp).is_none()); - - let wild_dp = make_real_dp(&collision_point, &wild_dist, 1); - let result = table.insert_and_check(wild_dp); - assert!( - result.is_none(), - "Should return None when no formula produces valid key" - ); - } - - #[test] - fn test_scalar_half_inverse_of_two() { - // Verify that 2 × SCALAR_HALF ≡ 1 (mod n) - let half = super::scalar_half(); - let two = scalar_from_u64(2); - let product = two * half; - assert_eq!( - product, - Scalar::ONE, - "2 × SCALAR_HALF should equal 1 (mod n)" - ); + let td = scalar_from_u64(30); + let wd = scalar_from_u64(14); + let pk = ProjectivePoint::mul_by_generator(&k); + let cp = ProjectivePoint::mul_by_generator(&(start_s + td)); + let mut table = DPTable::new(scalar_to_le_bytes(&start_s), pk, ProjectivePoint::GENERATOR, 41); + assert!(table.insert_and_check(make_real_dp(&cp, &td, 0)).is_none()); + let r = table.insert_and_check(make_real_dp(&cp, &wd, 1)); + assert!(r.is_some()); + assert!(verify_key(&r.unwrap(), &pk)); + cleanup_test_file(41); } #[test] - fn test_scalar_half_value() { - // Verify SCALAR_HALF has the correct byte representation - let half = super::scalar_half(); - let bytes = half.to_bytes(); - let expected = [ - 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, - 0x68, 0x1b, 0x20, 0xa1, - ]; - assert_eq!( - bytes.as_slice(), - &expected, - "SCALAR_HALF bytes should match expected value" - ); + fn test_scalar_half() { + assert_eq!(scalar_from_u64(2) * super::scalar_half(), Scalar::ONE); } #[test] - fn test_cross_wild_collision_resolves() { - let pubkey = - parse_pubkey("033c4a45cbd643ff97d77f41ea37e843648d50fd894b864b0d52febc62f6454f7c") - .unwrap(); + fn test_cross_wild() { + cleanup_test_file(42); + let pk = parse_pubkey("033c4a45cbd643ff97d77f41ea37e843648d50fd894b864b0d52febc62f6454f7c").unwrap(); let k = scalar_from_u64(0x0d2c55); - let d1 = scalar_from_u64(123_456); let d2 = d1 + (k + k); - let collision_point = ProjectivePoint::mul_by_generator(&(k + d1)); - assert_eq!( - collision_point, - ProjectivePoint::mul_by_generator(&(-k + d2)) - ); + let cp = ProjectivePoint::mul_by_generator(&(k + d1)); + let mut table = DPTable::new([0u8; 32], pk, ProjectivePoint::GENERATOR, 42); + assert!(table.insert_and_check(make_real_dp(&cp, &d1, 1)).is_none()); + let r = table.insert_and_check(make_real_dp(&cp, &d2, 2)); + assert!(r.is_some()); + assert!(verify_key(&r.clone().unwrap(), &table.pubkey)); + cleanup_test_file(42); + } - let mut table = DPTable::new([0u8; 32], pubkey, ProjectivePoint::GENERATOR); + #[test] + fn test_persistence_across_sessions() { + cleanup_test_file(99); + + // Session 1: insert a tame DP + { + let k = scalar_from_u64(66); + let start_s = scalar_from_u64(50); + let td = scalar_from_u64(30); + let pk = ProjectivePoint::mul_by_generator(&k); + let cp = ProjectivePoint::mul_by_generator(&(start_s + td)); + let mut table = DPTable::new(scalar_to_le_bytes(&start_s), pk, ProjectivePoint::GENERATOR, 99); + assert!(table.insert_and_check(make_real_dp(&cp, &td, 0)).is_none()); + assert_eq!(table.total_dps(), 1); + } - let wild1_dp = make_real_dp(&collision_point, &d1, 1); - assert!(table.insert_and_check(wild1_dp).is_none()); + // Session 2: insert a wild DP at the same point → should find collision from disk + { + let k = scalar_from_u64(66); + let start_s = scalar_from_u64(50); + let td = scalar_from_u64(30); + let wd = scalar_from_u64(14); + let pk = ProjectivePoint::mul_by_generator(&k); + let cp = ProjectivePoint::mul_by_generator(&(start_s + td)); + let mut table = DPTable::new(scalar_to_le_bytes(&start_s), pk, ProjectivePoint::GENERATOR, 99); + assert_eq!(table.total_dps(), 1); + let r = table.insert_and_check(make_real_dp(&cp, &wd, 1)); + assert!(r.is_some(), "Should find collision with DP from previous session"); + assert!(verify_key(&r.unwrap(), &pk)); + } - let wild2_dp = make_real_dp(&collision_point, &d2, 2); - let result = table.insert_and_check(wild2_dp); - assert!( - result.is_some(), - "wild1↔wild2 collision should resolve to a valid key" - ); - assert!(verify_key(&result.clone().unwrap(), &table.pubkey)); - assert_eq!(result.unwrap(), super::scalar_to_key_bytes(&k)); + cleanup_test_file(99); } #[test] - fn test_cross_wild_no_false_positive() { - let wrong_pubkey = - parse_pubkey("031a746c78f72754e0be046186df8a20cdce5c79b2eda76013c647af08d306e49e") - .unwrap(); - let k = scalar_from_u64(0x0d2c55); - - let d1 = scalar_from_u64(123_456); - let d2 = d1 + (k + k); - let collision_point = ProjectivePoint::mul_by_generator(&(k + d1)); - - let mut table = DPTable::new([0u8; 32], wrong_pubkey, ProjectivePoint::GENERATOR); + fn test_pubkey_mismatch_creates_new_file() { + cleanup_test_file(98); + + // Session 1: puzzle A + { + let pk_a = ProjectivePoint::mul_by_generator(&scalar_from_u64(100)); + let mut table = DPTable::new([0u8; 32], pk_a, ProjectivePoint::GENERATOR, 98); + assert!(table.insert_and_check(make_dp(1, 1, 0)).is_none()); + assert_eq!(table.total_dps(), 1); + } - let wild1_dp = make_real_dp(&collision_point, &d1, 1); - assert!(table.insert_and_check(wild1_dp).is_none()); + // Session 2: puzzle B (different pubkey, same range_bits) + { + let pk_b = ProjectivePoint::mul_by_generator(&scalar_from_u64(200)); + let table = DPTable::new([0u8; 32], pk_b, ProjectivePoint::GENERATOR, 98); + // Should have started fresh — old DPs rejected due to pubkey mismatch + assert_eq!(table.total_dps(), 0); + } - let wild2_dp = make_real_dp(&collision_point, &d2, 2); - let result = table.insert_and_check(wild2_dp); - assert!( - result.is_none(), - "cross-wild candidate formulas should not accept wrong pubkey" + cleanup_test_file(98); + // Also clean up the backup file + let dp_dir = format!( + "{}/Desktop/kangaroo_dps", + std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) ); + if let Ok(entries) = std::fs::read_dir(&dp_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("dps_range98.bin.backup") { + let _ = std::fs::remove_file(entry.path()); + } + } + } } } diff --git a/src/lib.rs b/src/lib.rs index 398df77..4ca7f62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1351,7 +1351,7 @@ pub fn run(args: Args) -> anyhow::Result<()> { } drop(tx); - let mut dp_table = cpu::DPTable::new(solve_start, solve_pubkey, solve_base_point); + let mut dp_table = cpu::DPTable::new(solve_start, solve_pubkey, solve_base_point, solve_range_bits); let mut found_key: Option> = None; let mut last_log_ops: u64 = 0; diff --git a/src/solver.rs b/src/solver.rs index 2ef2ee6..44a6403 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -342,7 +342,7 @@ impl KangarooSolver { pipeline, buffers, dp_table: if with_dp_table { - Some(DPTable::new(start, pubkey, base_point)) + Some(DPTable::new(start, pubkey, base_point, range_bits)) } else { None },