packx/
lib.rs

1use bytemuck::{Pod, Zeroable};
2
3pub const SOLUTION_SIZE: usize = 145; // 1 (bump) + 16 (seeds) + 128 (nonces)
4
5#[repr(C)]
6#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
7pub struct Solution {
8    pub bump: u8,          // single-byte bump
9    pub seeds: [u8; 16],   // 16 seeds, one per 8-byte group
10    pub nonces: [u8; 128], // 128 nonces (u8), one per byte
11}
12
13impl Solution {
14    pub fn new(bump: u8, seeds: [u8; 16], nonces: [u8; 128]) -> Self {
15        Solution {
16            bump,
17            seeds,
18            nonces,
19        }
20    }
21
22    /// Leading-zero bits in BLAKE3(serialize(solution)).
23    #[inline]
24    pub fn difficulty(&self) -> u32 {
25        let bytes = serialize(self);
26        let h = compute_hash(&[&bytes]);
27        get_difficulty(h)
28    }
29
30    /// Serialize to 145 bytes.
31    pub fn to_bytes(&self) -> [u8; SOLUTION_SIZE] {
32        serialize(self)
33    }
34
35    /// Deserialize from 145 bytes.
36    pub fn from_bytes(data: &[u8; SOLUTION_SIZE]) -> Self {
37        deserialize(data)
38    }
39
40    /// Reconstruct data using H(pubkey, bump, seed, nonce).
41    pub fn unpack(&self, pubkey: &[u8; 32]) -> [u8; 128] {
42        unpack(pubkey, self)
43    }
44}
45
46/// Per-bump table allocated on the heap. Large fields are boxed slices.
47#[repr(C)]
48pub struct SeedTable {
49    /// [seed][target] -> nonce
50    pub nonces: Box<[[u8; 256]]>,   // len = 256
51    /// [seed] -> 256-bit bitset of achievable targets
52    pub present: Box<[[u8; 32]]>,   // len = 256
53}
54
55/// All bumps for one pubkey (heap allocated).
56pub struct SolverMemory {
57    pub tables: Box<[Box<SeedTable>]>,
58}
59
60#[inline(always)]
61fn bit_test(bits: &[u8; 32], target: u8) -> bool {
62    let idx = (target >> 3) as usize;
63    let mask = 1u8 << (target & 7);
64    (bits[idx] & mask) != 0
65}
66
67#[inline(always)]
68fn bit_set(bits: &mut [u8; 32], target: u8) {
69    let idx = (target >> 3) as usize;
70    let mask = 1u8 << (target & 7);
71    bits[idx] |= mask;
72}
73
74#[inline(always)]
75fn h0(pubkey: &[u8; 32], bump: u8, seed: u8, nonce: u8) -> u8 {
76    let bump_b = [bump];
77    let seed_b = [seed];
78    let nonce_b = [nonce];
79    compute_hash(&[pubkey, &bump_b, &seed_b, &nonce_b])[0]
80}
81
82#[inline(always)]
83fn compute_hash(inputs: &[&[u8]]) -> [u8; 32] {
84    #[cfg(feature = "solana")]
85    {
86        solana_program::blake3::hashv(inputs).to_bytes()
87    }
88    #[cfg(not(feature = "solana"))]
89    {
90        let mut hasher = blake3::Hasher::new();
91        for input in inputs {
92            hasher.update(input);
93        }
94        hasher.finalize().into()
95    }
96}
97
98#[inline]
99fn get_difficulty(hash: [u8; 32]) -> u32 {
100    let mut count = 0u32;
101    for &b in &hash {
102        let lz = b.leading_zeros();
103        count += lz;
104        if lz < 8 {
105            break;
106        }
107    }
108    count
109}
110
111#[inline]
112pub fn serialize(solution: &Solution) -> [u8; SOLUTION_SIZE] {
113    let mut out = [0u8; SOLUTION_SIZE];
114    out.copy_from_slice(bytemuck::bytes_of(solution));
115    out
116}
117
118#[inline]
119pub fn deserialize(bytes_in: &[u8; SOLUTION_SIZE]) -> Solution {
120    let mut s = Solution {
121        bump: 0,
122        seeds: [0; 16],
123        nonces: [0; 128],
124    };
125    bytemuck::bytes_of_mut(&mut s).copy_from_slice(bytes_in);
126    s
127}
128
129/// Build one bump table on the heap. No large stack locals.
130pub fn build_one_bump(pubkey: &[u8; 32], bump: u8) -> Box<SeedTable> {
131    // Use boxed slices so the large storage is on the heap.
132    let mut table = Box::new(SeedTable {
133        nonces: vec![[0u8; 256]; 256].into_boxed_slice(),
134        present: vec![[0u8; 32]; 256].into_boxed_slice(),
135    });
136
137    for seed in 0u8..=u8::MAX {
138        let present_row: &mut [u8; 32] = &mut table.present[seed as usize];
139        let nonces_row: &mut [u8; 256] = &mut table.nonces[seed as usize];
140
141        for nonce in 0u8..=u8::MAX {
142            let t = h0(pubkey, bump, seed, nonce);
143            if !bit_test(present_row, t) {
144                bit_set(present_row, t);
145                nonces_row[t as usize] = nonce;
146            }
147        }
148    }
149
150    table
151}
152
153/// Build all 256 bump tables on the heap.
154pub fn build_memory(pubkey: &[u8; 32]) -> SolverMemory {
155    let mut vec_tables: Vec<Box<SeedTable>> = Vec::with_capacity(256);
156    for bump in 0u8..=u8::MAX {
157        vec_tables.push(build_one_bump(pubkey, bump));
158    }
159    SolverMemory {
160        tables: vec_tables.into_boxed_slice(),
161    }
162}
163
164/// Seed that can cover a group, with the 8 nonces to use.
165#[derive(Clone, Copy)]
166struct SeedCandidate {
167    seed: u8,
168    nonces8: [u8; 8],
169}
170
171/// Build candidates for group g using table.
172fn build_group_candidates(data: &[u8; 128], g: usize, table: &SeedTable) -> Vec<SeedCandidate> {
173    let cs = g * 8;
174    let need = [
175        data[cs + 0], data[cs + 1], data[cs + 2], data[cs + 3],
176        data[cs + 4], data[cs + 5], data[cs + 6], data[cs + 7],
177    ];
178
179    let mut out = Vec::with_capacity(8);
180
181    for seed in 0u8..=u8::MAX {
182        let present = &table.present[seed as usize];
183        if !(bit_test(present, need[0]) &&
184             bit_test(present, need[1]) &&
185             bit_test(present, need[2]) &&
186             bit_test(present, need[3]) &&
187             bit_test(present, need[4]) &&
188             bit_test(present, need[5]) &&
189             bit_test(present, need[6]) &&
190             bit_test(present, need[7])) {
191            continue;
192        }
193
194        let row = &table.nonces[seed as usize];
195        out.push(SeedCandidate {
196            seed,
197            nonces8: [
198                row[need[0] as usize],
199                row[need[1] as usize],
200                row[need[2] as usize],
201                row[need[3] as usize],
202                row[need[4] as usize],
203                row[need[5] as usize],
204                row[need[6] as usize],
205                row[need[7] as usize],
206            ],
207        });
208    }
209
210    out
211}
212
213/// Iterator over the cartesian product of candidate lists.
214struct MixedRadix {
215    radices: [usize; 16],
216    idx: [usize; 16],
217    first: bool,
218    done: bool,
219}
220
221impl MixedRadix {
222    fn new(radices: [usize; 16]) -> Option<Self> {
223        if radices.iter().any(|&r| r == 0) {
224            return None;
225        }
226        Some(Self {
227            radices,
228            idx: [0; 16],
229            first: true,
230            done: false,
231        })
232    }
233}
234
235impl Iterator for MixedRadix {
236    type Item = [usize; 16];
237    fn next(&mut self) -> Option<Self::Item> {
238        if self.done {
239            return None;
240        }
241        if self.first {
242            self.first = false;
243            return Some(self.idx);
244        }
245        for pos in 0..16 {
246            self.idx[pos] += 1;
247            if self.idx[pos] < self.radices[pos] {
248                return Some(self.idx);
249            } else {
250                self.idx[pos] = 0;
251            }
252        }
253        self.done = true;
254        None
255    }
256}
257
258/// Solve for one bump using its table by scanning per-group candidates and trying combinations.
259pub fn solve_one_bump(
260    data: &[u8; 128],
261    bump: u8,
262    table: &SeedTable,
263    difficulty: u32,
264) -> Option<Solution> {
265    let mut cands: [Vec<SeedCandidate>; 16] = core::array::from_fn(|_| Vec::new());
266    for g in 0..16 {
267        cands[g] = build_group_candidates(data, g, table);
268        if cands[g].is_empty() {
269            return None;
270        }
271    }
272
273    let mut order: [usize; 16] = core::array::from_fn(|i| i);
274    order.sort_by_key(|&g| cands[g].len());
275
276    let radices_ordered: [usize; 16] = core::array::from_fn(|i| cands[order[i]].len());
277    let Some(iter) = MixedRadix::new(radices_ordered) else { return None; };
278
279    for idxs_ordered in iter {
280        let mut seeds_out = [0u8; 16];
281        let mut nonces_out = [0u8; 128];
282
283        for (pos, &g) in order.iter().enumerate() {
284            let choice = cands[g][idxs_ordered[pos]];
285            seeds_out[g] = choice.seed;
286            let cs = g * 8;
287            nonces_out[cs..cs + 8].copy_from_slice(&choice.nonces8);
288        }
289
290        let solution = Solution { bump, seeds: seeds_out, nonces: nonces_out };
291        if solution.difficulty() >= difficulty {
292            return Some(solution);
293        }
294    }
295
296    None
297}
298
299/// Solve using a precomputed all-bumps table.
300pub fn solve_with_memory(
301    data: &[u8; 128],
302    mem: &SolverMemory,
303    difficulty: u32,
304) -> Option<Solution> {
305    for bump in 0u8..=u8::MAX {
306        let table: &SeedTable = &mem.tables[bump as usize];
307        if let Some(solution) = solve_one_bump(data, bump, table, difficulty) {
308            return Some(solution);
309        }
310    }
311    None
312}
313
314/// Solve by first building the precompute for this pubkey, then searching.
315pub fn solve(
316    pubkey: &[u8; 32],
317    data: &[u8; 128],
318    difficulty: u32,
319) -> Option<Solution> {
320    let mem = build_memory(pubkey);
321    solve_with_memory(data, &mem, difficulty)
322}
323
324/// Reconstruct data using H(pubkey, bump, seed, nonce).
325pub fn unpack(pubkey: &[u8; 32], solution: &Solution) -> [u8; 128] {
326    let mut data = [0u8; 128];
327    for g in 0..16 {
328        let seed = solution.seeds[g];
329        let cs = g * 8;
330        for i in 0..8 {
331            let nonce = solution.nonces[cs + i];
332            data[cs + i] = h0(pubkey, solution.bump, seed, nonce);
333        }
334    }
335    data
336}
337
338/// Check reconstruction and difficulty.
339pub fn verify(pubkey: &[u8; 32], data: &[u8; 128], solution: &Solution, difficulty: u32) -> bool {
340    if unpack(pubkey, solution) != *data {
341        return false;
342    }
343    solution.difficulty() >= difficulty
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use rand::RngCore;
350
351    const TEST_DIFFICULTY: u32 = 1;
352    const TEST_BUMP_TRIES: u8 = 7;
353
354    fn solve_lightweight(pubkey: &[u8; 32], data: &[u8; 128], difficulty: u32) -> Option<Solution> {
355        for bump in 0u8..=TEST_BUMP_TRIES {
356            let table = build_one_bump(pubkey, bump);
357            if let Some(solution) = solve_one_bump(data, bump, &table, difficulty) {
358                return Some(solution);
359            }
360        }
361        None
362    }
363
364    #[test]
365    fn test_solve_and_verify() {
366        let mut rng = rand::thread_rng();
367        let mut pubkey = [0u8; 32];
368        let mut data = [0u8; 128];
369        rng.fill_bytes(&mut pubkey);
370        rng.fill_bytes(&mut data);
371
372        let solution = solve_lightweight(&pubkey, &data, TEST_DIFFICULTY).expect("solve failed");
373        assert!(verify(&pubkey, &data, &solution, TEST_DIFFICULTY));
374    }
375
376    #[test]
377    fn test_serialize_deserialize_roundtrip() {
378        let solution = Solution { bump: 3, seeds: [9; 16], nonces: [7; 128] };
379        let ser = serialize(&solution);
380        let de = deserialize(&ser);
381        assert_eq!(solution.bump, de.bump);
382        assert_eq!(solution.seeds, de.seeds);
383        assert_eq!(solution.nonces, de.nonces);
384    }
385
386    #[test]
387    fn test_verify_failure_wrong_data() {
388        let mut rng = rand::thread_rng();
389        let mut pubkey = [0u8; 32];
390        let mut data = [0u8; 128];
391        rng.fill_bytes(&mut pubkey);
392        rng.fill_bytes(&mut data);
393
394        let solution = solve_lightweight(&pubkey, &data, TEST_DIFFICULTY).expect("solve failed");
395        let mut wrong = data;
396        wrong[0] = wrong[0].wrapping_add(1);
397        assert!(!verify(&pubkey, &wrong, &solution, TEST_DIFFICULTY));
398    }
399
400    #[test]
401    fn test_verify_failure_wrong_pubkey() {
402        let mut rng = rand::thread_rng();
403        let mut pubkey = [0u8; 32];
404        let mut data = [0u8; 128];
405        rng.fill_bytes(&mut pubkey);
406        rng.fill_bytes(&mut data);
407
408        let solution = solve_lightweight(&pubkey, &data, TEST_DIFFICULTY).expect("solve failed");
409        let mut other = pubkey;
410        other[0] ^= 1;
411        assert!(!verify(&other, &data, &solution, TEST_DIFFICULTY));
412    }
413
414    #[test]
415    fn test_unpack_roundtrip() {
416        let mut rng = rand::thread_rng();
417        let mut pubkey = [0u8; 32];
418        let mut data = [0u8; 128];
419        rng.fill_bytes(&mut pubkey);
420        rng.fill_bytes(&mut data);
421
422        let solution = solve_lightweight(&pubkey, &data, TEST_DIFFICULTY).expect("solve failed");
423        assert_eq!(unpack(&pubkey, &solution), data);
424    }
425}