Skip to main content

chia_protocol/
proof_of_space.rs

1use crate::bytes::{Bytes, Bytes32};
2use chia_bls::G1Element;
3use chia_sha2::Sha256;
4use chia_streamable_macro::streamable;
5use chia_traits::{Error, Result, Streamable};
6use std::io::Cursor;
7
8// This structure was updated for v2 proof-of-space, in a backwards compatible
9// way. The Option types are serialized as 1 byte to indicate whether the value
10// is set or not. Only 1 bit out of 8 are used. The byte prefix for
11// pool_contract_puzzle_hash is used to indicate whether this is a v1 or v2
12// proof.
13#[streamable(no_streamable)]
14pub struct ProofOfSpace {
15    challenge: Bytes32,
16    pool_public_key: Option<G1Element>,
17    pool_contract_puzzle_hash: Option<Bytes32>,
18    plot_public_key: G1Element,
19
20    // this is 0 for v1 proof-of-space and 1 for v2. The version is encoded as
21    // part of the 8 bits prefix, indicating whether pool_contract_puzzle_hash
22    // is set or not
23    version: u8,
24
25    // These are set for v2 proofs and all zero for v1 proofs
26    plot_index: u16,
27    meta_group: u8,
28    strength: u8,
29
30    // this is set for v1 proofs, and zero for v2 proofs
31    size: u8,
32
33    proof: Bytes,
34}
35
36#[cfg(feature = "py-bindings")]
37use pyo3::prelude::*;
38
39#[cfg(feature = "py-bindings")]
40#[pyclass(name = "PlotParam")]
41pub struct PyPlotParam {
42    #[pyo3(get)]
43    pub size_v1: Option<u8>,
44    #[pyo3(get)]
45    pub strength_v2: Option<u8>,
46    #[pyo3(get)]
47    pub plot_index: u16,
48    #[pyo3(get)]
49    pub meta_group: u8,
50}
51
52#[cfg(feature = "py-bindings")]
53#[pymethods]
54impl PyPlotParam {
55    #[staticmethod]
56    fn make_v1(s: u8) -> Self {
57        assert!(s < 64);
58        Self {
59            size_v1: Some(s),
60            strength_v2: None,
61            plot_index: 0,
62            meta_group: 0,
63        }
64    }
65
66    #[staticmethod]
67    fn make_v2(plot_index: u16, meta_group: u8, strength: u8) -> Self {
68        assert!(strength < 64);
69        Self {
70            size_v1: None,
71            strength_v2: Some(strength),
72            plot_index,
73            meta_group,
74        }
75    }
76}
77
78pub fn compute_plot_id_v1(
79    plot_pk: &G1Element,
80    pool_pk: Option<&G1Element>,
81    pool_contract: Option<&Bytes32>,
82) -> Bytes32 {
83    let mut ctx = Sha256::new();
84    // plot_id = sha256( ( pool_pk | contract_ph) + plot_pk)
85    if let Some(pool_pk) = pool_pk {
86        pool_pk.update_digest(&mut ctx);
87    } else if let Some(contract_ph) = pool_contract {
88        contract_ph.update_digest(&mut ctx);
89    } else {
90        panic!("invalid proof of space. Neither pool pk nor contract puzzle hash set");
91    }
92    plot_pk.update_digest(&mut ctx);
93    ctx.finalize().into()
94}
95
96pub fn compute_plot_id_v2(
97    strength: u8,
98    plot_pk: &G1Element,
99    pool_pk: Option<&G1Element>,
100    pool_contract: Option<&Bytes32>,
101    plot_index: u16,
102    meta_group: u8,
103) -> Bytes32 {
104    let mut ctx = Sha256::new();
105    // plot_group_id = sha256( strength + plot_pk + (pool_pk | contract_ph) )
106    // plot_id = sha256( plot_group_id + plot_index + meta_group)
107    let mut group_ctx = Sha256::new();
108    strength.update_digest(&mut group_ctx);
109    plot_pk.update_digest(&mut group_ctx);
110    if let Some(pool_pk) = pool_pk {
111        pool_pk.update_digest(&mut group_ctx);
112    } else if let Some(contract_ph) = pool_contract {
113        contract_ph.update_digest(&mut group_ctx);
114    } else {
115        panic!(
116            "failed precondition of compute_plot_id_2(). Either pool-public-key or pool-contract-hash must be specified"
117        );
118    }
119    let plot_group_id: Bytes32 = group_ctx.finalize().into();
120
121    plot_group_id.update_digest(&mut ctx);
122    plot_index.update_digest(&mut ctx);
123    meta_group.update_digest(&mut ctx);
124    ctx.finalize().into()
125}
126
127impl ProofOfSpace {
128    pub fn compute_plot_id(&self) -> Bytes32 {
129        if self.version == 0 {
130            // v1 proofs
131            compute_plot_id_v1(
132                &self.plot_public_key,
133                self.pool_public_key.as_ref(),
134                self.pool_contract_puzzle_hash.as_ref(),
135            )
136        } else if self.version == 1 {
137            // v2 proofs
138            compute_plot_id_v2(
139                self.strength,
140                &self.plot_public_key,
141                self.pool_public_key.as_ref(),
142                self.pool_contract_puzzle_hash.as_ref(),
143                self.plot_index,
144                self.meta_group,
145            )
146        } else {
147            panic!("unknown proof version: {}", self.version);
148        }
149    }
150
151    /// returns the quality string of the v2 proof of space.
152    /// returns None if this is a v1 proof or if the proof is invalid.
153    pub fn quality_string(&self) -> Option<Bytes32> {
154        if self.version != 1 {
155            return None;
156        }
157
158        let k_size = (self.proof.len() * 8 / 128) as u8;
159        let plot_id = self.compute_plot_id().to_bytes();
160        chia_pos2::quality_string_from_proof(&plot_id, k_size, self.strength, self.proof.as_slice())
161            .map(|quality| {
162                let mut sha256 = Sha256::new();
163                sha256.update(chia_pos2::serialize_quality(
164                    &quality.chain_links,
165                    self.strength,
166                ));
167                sha256.finalize().into()
168            })
169    }
170}
171
172#[cfg(feature = "py-bindings")]
173#[pymethods]
174impl ProofOfSpace {
175    #[pyo3(name = "param")]
176    fn py_param(&self) -> PyPlotParam {
177        match self.version {
178            0 => PyPlotParam {
179                size_v1: Some(self.size),
180                strength_v2: None,
181                plot_index: 0,
182                meta_group: 0,
183            },
184            1 => PyPlotParam {
185                size_v1: None,
186                strength_v2: Some(self.strength),
187                plot_index: self.plot_index,
188                meta_group: self.meta_group,
189            },
190            _ => {
191                panic!("invalid proof-of-space version {}", self.version);
192            }
193        }
194    }
195
196    #[pyo3(name = "compute_plot_id")]
197    pub fn py_compute_plot_id(&self) -> Bytes32 {
198        self.compute_plot_id()
199    }
200
201    #[pyo3(name = "quality_string")]
202    pub fn py_quality_string(&self) -> Option<Bytes32> {
203        self.quality_string()
204    }
205}
206
207// ProofOfSpace was updated in Chia 3.0 to support v2 proofs. In order to stay
208// backwards compatible with the network protocol and the block hashes of
209// previous versions, for v1 proofs, some care has to be taken.
210// Optional fields are serialized with a 1-byte prefix indicating whether the
211// field is set or not. This byte is either 0 or 1. This leaves 7 unused bits.
212// We use bit 2 in the byte prefix for the pool_contract_puzzle_hash field to
213// indicate whether this is a v2 proof or not. v1 proofs leave this bit as 0,
214// and thus remain backwards compatible. V2 proofs set it to 1, which alters
215// which fields are serialized. e.g. we no longer include size (k) of the plot
216// since v2 plots have a fixed size.
217impl Streamable for ProofOfSpace {
218    fn update_digest(&self, digest: &mut Sha256) {
219        self.challenge.update_digest(digest);
220        self.pool_public_key.update_digest(digest);
221
222        if self.version == 0 {
223            self.pool_contract_puzzle_hash.update_digest(digest);
224            self.plot_public_key.update_digest(digest);
225            self.size.update_digest(digest);
226            self.proof.update_digest(digest);
227        } else if self.version == 1 {
228            if let Some(pool_contract) = self.pool_contract_puzzle_hash {
229                0b11_u8.update_digest(digest);
230                pool_contract.update_digest(digest);
231            } else {
232                0b10_u8.update_digest(digest);
233            }
234
235            self.plot_public_key.update_digest(digest);
236            self.plot_index.update_digest(digest);
237            self.meta_group.update_digest(digest);
238            self.strength.update_digest(digest);
239
240            // for v2 proofs, we don't hash the full proof directly. The full
241            // proof is the witness to this quality string commitment.
242            self.quality_string()
243                .expect("internal error. Can't compute hash of invalid ProofOfSpace")
244                .update_digest(digest);
245        } else {
246            panic!("version field must be 0 or 1, but it's {}", self.version);
247        }
248    }
249
250    fn stream(&self, out: &mut Vec<u8>) -> Result<()> {
251        self.challenge.stream(out)?;
252        self.pool_public_key.stream(out)?;
253
254        if self.version == 0 {
255            self.pool_contract_puzzle_hash.stream(out)?;
256            self.plot_public_key.stream(out)?;
257            self.size.stream(out)?;
258        } else if self.version == 1 {
259            if let Some(pool_contract) = self.pool_contract_puzzle_hash {
260                0b11_u8.stream(out)?;
261                pool_contract.stream(out)?;
262            } else {
263                0b10_u8.stream(out)?;
264            }
265
266            self.plot_public_key.stream(out)?;
267            self.plot_index.stream(out)?;
268            self.meta_group.stream(out)?;
269            self.strength.stream(out)?;
270        } else {
271            return Err(Error::InvalidPoS);
272        }
273
274        self.proof.stream(out)
275    }
276
277    fn parse<const TRUSTED: bool>(input: &mut Cursor<&[u8]>) -> Result<Self> {
278        let challenge = <Bytes32 as Streamable>::parse::<TRUSTED>(input)?;
279        let pool_public_key = <Option<G1Element> as Streamable>::parse::<TRUSTED>(input)?;
280
281        let prefix = <u8 as Streamable>::parse::<TRUSTED>(input)?;
282        let version = u8::from((prefix & 0b10) != 0);
283        let pool_contract_puzzle_hash = if (prefix & 1) != 0 {
284            Some(<Bytes32 as Streamable>::parse::<TRUSTED>(input)?)
285        } else {
286            None
287        };
288
289        let plot_public_key = <G1Element as Streamable>::parse::<TRUSTED>(input)?;
290
291        if version == 0 {
292            let size = <u8 as Streamable>::parse::<TRUSTED>(input)?;
293            let proof = <Bytes as Streamable>::parse::<TRUSTED>(input)?;
294
295            Ok(ProofOfSpace {
296                challenge,
297                pool_public_key,
298                pool_contract_puzzle_hash,
299                plot_public_key,
300                version,
301                plot_index: 0,
302                meta_group: 0,
303                strength: 0,
304                size,
305                proof,
306            })
307        } else if version == 1 {
308            let plot_index = <u16 as Streamable>::parse::<TRUSTED>(input)?;
309            let meta_group = <u8 as Streamable>::parse::<TRUSTED>(input)?;
310            let strength = <u8 as Streamable>::parse::<TRUSTED>(input)?;
311            let proof = <Bytes as Streamable>::parse::<TRUSTED>(input)?;
312
313            if pool_public_key.is_some() == pool_contract_puzzle_hash.is_some() {
314                return Err(Error::InvalidPoS);
315            }
316
317            Ok(ProofOfSpace {
318                challenge,
319                pool_public_key,
320                pool_contract_puzzle_hash,
321                plot_public_key,
322                version,
323                plot_index,
324                meta_group,
325                strength,
326                size: 0,
327                proof,
328            })
329        } else {
330            Err(Error::InvalidPoS)
331        }
332    }
333}
334
335#[cfg(test)]
336#[allow(clippy::needless_pass_by_value)]
337mod tests {
338    use super::*;
339    use hex_literal::hex;
340    use rstest::rstest;
341
342    fn plot_pk() -> G1Element {
343        const PLOT_PK_BYTES: [u8; 48] = hex!(
344            "96b35c22adf93068c9536e016e88251ad715a591d8deabb60917d9c495f45a220ca56b906793c27778d5f7f71fb50b94"
345        );
346        G1Element::from_bytes(&PLOT_PK_BYTES).expect("PLOT_PK_BYTES is valid")
347    }
348
349    fn pool_pk() -> G1Element {
350        const POOL_PK_BYTES: [u8; 48] = hex!(
351            "ac6e995e0f9c307853fa5c79e571de5ec2f2d45e5c2641c0847fef8041916e4d07d5a9200d5aa92ceac3b1bf41ce93b2"
352        );
353        G1Element::from_bytes(&POOL_PK_BYTES).expect("POOL_PK_BYTES is valid")
354    }
355
356    // these are regression tests and test vectors for plot ID computations
357    #[rstest]
358    #[case("pool_pk", hex!("e185d4ec721ec060eb5833ec07d802fc69a43ed45dd59d7f20c58494421e0270"))]
359    #[case("contract_ph", hex!("4e196e2fb1fc4c85fc48b30c1e585dc0bee08451895909b0ae2db63e2788ab82"))]
360    fn test_compute_plot_id_v1(#[case] variant: &str, #[case] expected: [u8; 32]) {
361        let (pool_pk, pool_contract) = match variant {
362            "pool_pk" => (Some(pool_pk()), None),
363            "contract_ph" => (None, Some(Bytes32::new([1u8; 32]))),
364            _ => panic!("unknown v1 variant: {variant}"),
365        };
366        let result = compute_plot_id_v1(&plot_pk(), pool_pk.as_ref(), pool_contract.as_ref());
367        assert_eq!(result, Bytes32::new(expected));
368    }
369
370    #[rstest]
371    #[case(0, 0, 0, "pool_pk", hex!("d3692a5d4fbfe1061053d4afada80d8f0b58b87b46c170e7087716a72091def0"))]
372    #[case(10, 256, 7, "pool_pk", hex!("2316eadc21d38c4e8740eb9efd49a0c2014a5b1ef992f5ae0b2d1fda01a4b034"))]
373    #[case(0, 0, 0, "contract_ph", hex!("03b09cab4bfdbcd1e626d93888a72f002d3948459c23cde52e9dd8d72dd9ae04"))]
374    #[case(5, 100, 3, "contract_ph", hex!("d575860c249ace41a656fe0d97719127f839fae55e6c32ffd7743b5a8a2eae4d"))]
375    fn test_compute_plot_id_v2(
376        #[case] strength: u8,
377        #[case] plot_index: u16,
378        #[case] meta_group: u8,
379        #[case] variant: &str,
380        #[case] expected: [u8; 32],
381    ) {
382        let (pool_pk, pool_contract) = match variant {
383            "pool_pk" => (Some(pool_pk()), None),
384            "contract_ph" => (None, Some(Bytes32::new([1u8; 32]))),
385            _ => panic!("unknown v2 variant: {variant}"),
386        };
387        let result = compute_plot_id_v2(
388            strength,
389            &plot_pk(),
390            pool_pk.as_ref(),
391            pool_contract.as_ref(),
392            plot_index,
393            meta_group,
394        );
395        assert_eq!(result, Bytes32::new(expected));
396    }
397
398    // Regression tests for quality_string(). Vectors in quality-string-tests/*.txt:
399    // 7 lines (challenge hex, strength, plot_index, meta_group, pool hex, proof hex, expect_quality hex).
400    #[rstest]
401    #[case("pool-2-0-0")]
402    #[case("contract-2-0-0")]
403    #[case("contract-3-0-0")]
404    #[case("pool-3-0-0")]
405    #[case("pool-2-1-0")]
406    #[case("pool-2-0-1")]
407    #[case("pool-2-1000-7")]
408    fn test_quality_string(#[case] name: &str) {
409        let plot_pk = G1Element::from_bytes(&hex!(
410            "a9c96f979d895b9ded08907ecd775abf889d51219bb7776dd73fdbac6b0dcc063c72c9e10d96776f486bbd1416b54533"
411        ))
412        .unwrap();
413
414        let path = format!("quality-string-tests/{name}.txt");
415        let contents =
416            std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
417        let l: Vec<&str> = contents
418            .lines()
419            .map(|line| line.split('#').next().unwrap_or(line).trim())
420            .filter(|s| !s.is_empty())
421            .collect();
422        assert_eq!(l.len(), 7, "expected 7 lines");
423
424        let challenge: [u8; 32] = hex::decode(l[0])
425            .expect("challenge hex")
426            .try_into()
427            .unwrap();
428        let strength: u8 = l[1].parse().expect("strength");
429        let plot_index: u16 = l[2].parse().expect("plot_index");
430        let meta_group: u8 = l[3].parse().expect("meta_group");
431        let (pool_pk, pool_contract) = if l[4].len() == 96 {
432            let b = hex::decode(l[4]).expect("pool_pk hex");
433            (
434                Some(G1Element::from_bytes(b.as_slice().try_into().unwrap()).expect("pool_pk")),
435                None,
436            )
437        } else {
438            let ph: [u8; 32] = hex::decode(l[4])
439                .expect("pool_contract hex")
440                .try_into()
441                .unwrap();
442            (None, Some(Bytes32::new(ph)))
443        };
444        let proof = hex::decode(l[5]).expect("proof hex");
445        let expect_quality: [u8; 32] = hex::decode(l[6])
446            .expect("expect_quality hex")
447            .try_into()
448            .unwrap();
449
450        let pos = ProofOfSpace::new(
451            Bytes32::new(challenge),
452            pool_pk,
453            pool_contract,
454            plot_pk,
455            1,
456            plot_index,
457            meta_group,
458            strength,
459            22,
460            Bytes::from(proof),
461        );
462
463        let quality = pos
464            .quality_string()
465            .expect("quality_string should return Some");
466        assert_eq!(quality, Bytes32::new(expect_quality));
467    }
468
469    #[rstest]
470    #[case(0, 18, Ok(18))]
471    #[case(0, 28, Ok(28))]
472    #[case(0, 38, Ok(38))]
473    #[case(1, 18, Ok(0))]
474    #[case(1, 28, Ok(0))]
475    #[case(1, 38, Ok(0))]
476    #[case(2, 18, Err(Error::InvalidPoS))]
477    fn proof_of_space_size(#[case] version: u8, #[case] size: u8, #[case] expect: Result<u8>) {
478        let pos = ProofOfSpace::new(
479            Bytes32::from(b"abababababababababababababababab"),
480            Some(G1Element::default()),
481            None,
482            G1Element::default(),
483            version,
484            0,
485            0,
486            0,
487            size,
488            Bytes::from(vec![]),
489        );
490
491        match pos.to_bytes() {
492            Ok(buf) => {
493                let new_pos =
494                    ProofOfSpace::parse::<false>(&mut Cursor::<&[u8]>::new(&buf)).expect("parse()");
495                assert_eq!(new_pos.size, expect.unwrap());
496            }
497            Err(e) => {
498                assert_eq!(e, expect.unwrap_err());
499            }
500        }
501    }
502}