Skip to main content

chia_pos2/
lib.rs

1use std::ffi::{CString, c_char};
2use std::fs::File;
3use std::io::{Error, Read, Result};
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8mod bits;
9
10pub const NUM_CHAIN_LINKS: usize = 16;
11
12#[repr(C)]
13#[derive(Default, Clone)]
14#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
15/// This object contains a quality proof along with metadata required to look
16/// up the remaining proof fragments from the plot, to form a partial proof
17pub struct QualityChain {
18    pub chain_links: [u64; NUM_CHAIN_LINKS],
19}
20
21unsafe extern "C" {
22    // these C functions are defined in src/api.cpp
23
24    fn validate_proof(
25        plot_id: *const u8,
26        k_size: u8,
27        strength: u8,
28        challenge: *const u8,
29        proof: *const u32,
30        quality: *mut QualityChain,
31    ) -> bool;
32
33    fn qualities_for_challenge(
34        plot_file: *const c_char,
35        challenge: *const u8,
36        output: *mut QualityChain,
37        num_outputs: u32,
38    ) -> u32;
39
40    // proof must point to exactly 16 proof fragments (each a uint64_t)
41    // plot ID must point to exactly 32 bytes
42    // output must point to exactly 512 32 bit integers
43    fn solve_partial_proof(
44        quality: *const QualityChain,
45        plot_id: *const u8,
46        k: u8,
47        strength: u8,
48        output: *mut u32,
49    ) -> bool;
50
51    // Converts full proof to quality string (does not validate).
52    // plot_id must point to 32 bytes
53    // proof to TOTAL_XS_IN_PROOF (128) uint32_t
54    // quality is output
55    fn proof_to_quality_string(
56        plot_id: *const u8,
57        k: u8,
58        strength: u8,
59        proof: *const u32,
60        quality: *mut QualityChain,
61    ) -> bool;
62
63    fn create_plot(
64        filename: *const c_char,
65        k: u8,
66        strength: u8,
67        plot_id: *const u8,
68        index: u16,
69        meta_group: u8,
70        memo: *const u8,
71        memo_length: u8,
72    ) -> bool;
73}
74
75pub type Bytes32 = [u8; 32];
76
77pub fn solve_proof(
78    quality_proof: &QualityChain,
79    plot_id: &Bytes32,
80    k: u8,
81    strength: u8,
82) -> Vec<u8> {
83    let mut proof = [0_u32; 128];
84    // SAFETY: Calling into pos2 C++ library. See src/api.cpp for requirements
85    // proof must point to exactly 128 x-values (each a uint32_t)
86    // plot ID must point to exactly 32 bytes
87    // output must point to exactly 512 32-bit integers
88    if !unsafe {
89        solve_partial_proof(
90            quality_proof,
91            plot_id.as_ptr(),
92            k,
93            strength,
94            proof.as_mut_ptr(),
95        )
96    } {
97        return vec![];
98    }
99
100    bits::compact_bits(&proof, k)
101}
102
103pub fn validate_proof_v2(
104    plot_id: &Bytes32,
105    size: u8,
106    challenge: &Bytes32,
107    strength: u8,
108    proof: &[u8],
109) -> Option<QualityChain> {
110    let x_values = bits::expand_bits(proof, size)?;
111
112    if x_values.len() != NUM_CHAIN_LINKS * 8 {
113        // a full proof has exactly 128 x-values. This is invalid or incomplete
114        return None;
115    }
116
117    let mut quality = QualityChain::default();
118    // SAFETY: Calling into pos2 C++ library. See src/api.cpp for requirements
119    // plot_id must point to 32 bytes
120    // challenge must point to 32 bytes
121    // proof must point to 512 uint32_t
122    let valid = unsafe {
123        validate_proof(
124            plot_id.as_ptr(),
125            size,
126            strength,
127            challenge.as_ptr(),
128            x_values.as_ptr(),
129            &mut quality,
130        )
131    };
132    if valid { Some(quality) } else { None }
133}
134
135/// Converts full proof bytes to quality string (does not validate the proof).
136/// Returns `Some(quality)` on success, `None` if proof format is invalid or conversion fails.
137pub fn quality_string_from_proof(
138    plot_id: &Bytes32,
139    k: u8,
140    strength: u8,
141    proof: &[u8],
142) -> Option<QualityChain> {
143    let x_values = bits::expand_bits(proof, k)?;
144
145    if x_values.len() != NUM_CHAIN_LINKS * 8 {
146        return None;
147    }
148
149    let mut quality = QualityChain::default();
150    // SAFETY: plot_id 32 bytes, proof 128 u32s, quality is output. See src/api.cpp.
151    let ok = unsafe {
152        // Call the C API (extern declared above); avoid name shadowing via alias.
153        proof_to_quality_string(
154            plot_id.as_ptr(),
155            k,
156            strength,
157            x_values.as_ptr(),
158            &mut quality,
159        )
160    };
161    if ok { Some(quality) } else { None }
162}
163
164pub fn create_v2_plot(
165    filename: &Path,
166    k: u8,
167    strength: u8,
168    plot_id: &Bytes32,
169    index: u16,
170    meta_group: u8,
171    memo: &[u8],
172) -> Result<()> {
173    let Some(filename) = filename.to_str() else {
174        return Err(Error::other("invalid path"));
175    };
176
177    if memo.len() > 255 {
178        return Err(Error::other("invalid memo"));
179    };
180
181    let filename = CString::new(filename)?;
182    // SAFETY: Calling into pos2 C++ library. See src/api.cpp for requirements
183    // filename is the full path, null terminated
184    // plot_id must point to 32 bytes of plot ID
185    // memo must point to bytes containing:
186    // * pool contract puzzle hash or pool public key
187    // * farmer public key
188    // * plot secret key
189    // returns true on success
190    let success: bool = unsafe {
191        create_plot(
192            filename.as_ptr(),
193            k,
194            strength,
195            plot_id.as_ptr(),
196            index,
197            meta_group,
198            memo.as_ptr(),
199            memo.len() as u8,
200        )
201    };
202    if success {
203        Ok(())
204    } else {
205        Err(Error::other("failed to create plot file"))
206    }
207}
208
209/// out must point to exactly 129 bytes
210/// serializes the QualityProof into the form that will be hashed together with
211/// the challenge to determine the quality of ths proof. The quality is used to
212/// check if it passes the current difficulty. The format is:
213/// 1 byte: plot strength
214/// repeat 16 times:
215///   8 bytes: little-endian proof fragment
216pub fn serialize_quality(
217    fragments: &[u64; NUM_CHAIN_LINKS],
218    strength: u8,
219) -> [u8; NUM_CHAIN_LINKS * 8 + 1] {
220    let mut ret = [0_u8; 129];
221
222    ret[0] = strength;
223    let mut idx = 1;
224    for cl in fragments {
225        ret[idx..(idx + 8)].clone_from_slice(&cl.to_le_bytes());
226        idx += 8;
227    }
228    ret
229}
230
231/// Farmer wide state for prover
232#[derive(Serialize, Deserialize)]
233pub struct Prover {
234    path: PathBuf,
235    plot_id: Bytes32,
236    memo: Vec<u8>,
237    strength: u8,
238    index: u16,
239    meta_group: u8,
240    size: u8,
241}
242
243impl Prover {
244    pub fn new(plot_path: &Path) -> Result<Prover> {
245        let mut file = File::open(plot_path)?;
246
247        // Read PlotData from a binary file. The v2 plot header format is as
248        // follows:
249        // 4 bytes:  "pos2"
250        // 1 byte:   version. 0=invalid, 1=fat plots, 2=benesh plots (compressed)
251        // 32 bytes: plot ID
252        // 1 byte:   k-size
253        // 1 byte:   strength, defaults to 2
254        // 2 bytes:  index
255        // 1 byte:   meta group
256        // 1 byte:   memo length (either 112 or 128)
257        // varies:   memo
258        let mut header = [0_u8; 4 + 1 + 32 + 1 + 1 + 1 + 2 + 1 + 128];
259        file.read_exact(&mut header)?;
260
261        let mut offset: usize = 0;
262        if &header[offset..(offset + 4)] != b"pos2" {
263            return Err(Error::other("Not a plotfile"));
264        }
265        offset += 4;
266        if header[offset] != 1 {
267            return Err(Error::other("unsupported plot version"));
268        }
269        offset += 1;
270        let plot_id: [u8; 32] = header[offset..(offset + 32)].try_into().unwrap();
271        offset += 32;
272        let size = header[offset];
273        if !(18..=32).contains(&size) || (size % 2) != 0 {
274            return Err(Error::other("invalid k-size"));
275        }
276        offset += 1;
277
278        let strength = header[offset];
279        if strength < 2 {
280            return Err(Error::other("invalid strength"));
281        }
282        offset += 1;
283
284        let index = u16::from_le_bytes(header[offset..offset + 2].try_into().unwrap());
285        offset += 2;
286
287        let meta_group = header[offset];
288        offset += 1;
289
290        let memo_len = header[offset];
291        offset += 1;
292
293        let memo: &[u8] = &header[offset..(offset + memo_len as usize)];
294
295        Ok(Prover {
296            path: plot_path.to_path_buf(),
297            plot_id,
298            memo: memo.to_vec(),
299            strength,
300            index,
301            meta_group,
302            size,
303        })
304    }
305
306    pub fn get_qualities_for_challenge(&self, challenge: &Bytes32) -> Result<Vec<QualityChain>> {
307        let Some(plot_path) = self.path.to_str() else {
308            return Err(Error::other("invalid path"));
309        };
310
311        let plot_path = CString::new(plot_path)?;
312
313        let mut results = Vec::<QualityChain>::with_capacity(10);
314        // SAFETY: Calling into pos2 C++ library. See src/api.cpp for requirements
315        // find quality proofs for a challenge.
316        // challenge must point to 32 bytes
317        // plot_file must be a null-terminated string
318        // output must point to "num_outputs" objects
319        unsafe {
320            let num_results = qualities_for_challenge(
321                plot_path.as_ptr(),
322                challenge.as_ptr(),
323                results.as_mut_ptr(),
324                10,
325            );
326            results.set_len(num_results as usize);
327        }
328        Ok(results)
329    }
330
331    pub fn size(&self) -> u8 {
332        self.size
333    }
334
335    pub fn plot_id(&self) -> &Bytes32 {
336        &self.plot_id
337    }
338
339    pub fn get_strength(&self) -> u8 {
340        self.strength
341    }
342
343    pub fn get_filename(&self) -> String {
344        // This conversion should be safe because the path is constructed from a
345        // string
346        self.path.to_string_lossy().into_owned()
347    }
348
349    pub fn get_memo(&self) -> &[u8] {
350        &self.memo
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use rstest::rstest;
358
359    /// Creates a v2 plot if missing, runs 100 challenges, solves proofs, validates,
360    /// and round-trips proof -> quality_string and checks it matches the original quality.
361    #[test]
362    fn test_plot_roundtrip() {
363        let k = 20u8;
364        let strength = 2u8;
365        let index = 0u16;
366        let meta_group = 0u8;
367        let plot_id: Bytes32 = [0xab; 32];
368        let memo = [0u8; 112];
369        let plot_path = std::env::temp_dir().join("pos2_chia_test_k20.plot");
370
371        if !plot_path.exists() {
372            create_v2_plot(&plot_path, k, strength, &plot_id, index, meta_group, &memo)
373                .expect("create_v2_plot");
374        }
375
376        let prover = Prover::new(&plot_path).expect("open prover");
377        assert_eq!(prover.size(), k);
378        assert_eq!(prover.get_strength(), strength);
379        let plot_id = *prover.plot_id();
380
381        let mut num_proofs = 0;
382        let mut challenge = [0u8; 32];
383        for challenge_idx in 0..300u32 {
384            challenge[0..4].copy_from_slice(&challenge_idx.to_le_bytes());
385
386            let qualities = prover
387                .get_qualities_for_challenge(&challenge)
388                .expect("get_qualities_for_challenge");
389
390            for quality in qualities {
391                let proof = solve_proof(&quality, &plot_id, k, strength);
392                assert!(!proof.is_empty(), "failed to solve proof");
393                num_proofs += 1;
394                let validated = validate_proof_v2(&plot_id, k, &challenge, strength, &proof);
395                assert!(
396                    validated.is_some(),
397                    "proof should validate for challenge {challenge_idx}",
398                );
399                let recovered = quality_string_from_proof(&plot_id, k, strength, &proof);
400                let recovered = recovered.expect("quality_string_from_proof");
401                assert_eq!(
402                    quality.chain_links, recovered.chain_links,
403                    "challenge {challenge_idx}: quality roundtrip must match",
404                );
405            }
406        }
407        assert_eq!(num_proofs, 9);
408    }
409
410    #[rstest]
411    fn test_serialize_quality(
412        #[values(1, 0xff00, 0x777777)] step_size: u64,
413        #[values(0, 0xffffffff00000000, 0xff00ff00ff00ff00)] fragment_start: u64,
414        #[values(
415            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 23, 26, 28, 33, 63, 64, 100, 200, 240, 255
416        )]
417        strength: u8,
418    ) {
419        let mut quality = QualityChain::default();
420
421        let mut idx = fragment_start;
422        for link in &mut quality.chain_links {
423            *link = idx;
424            idx += step_size;
425        }
426
427        let quality_str = serialize_quality(&quality.chain_links, strength);
428        assert_eq!(quality_str[0], strength);
429        idx = fragment_start;
430        for i in (1..(NUM_CHAIN_LINKS * 8 + 1)).step_by(8) {
431            assert_eq!(
432                u64::from_le_bytes(quality_str[i..(i + 8)].try_into().unwrap()),
433                idx
434            );
435            idx += step_size;
436        }
437    }
438}