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_group_id_v2(
97    strength: u8,
98    plot_pk: &G1Element,
99    pool_pk: Option<&G1Element>,
100    pool_contract: Option<&Bytes32>,
101) -> Bytes32 {
102    // plot_group_id = sha256( strength + plot_pk + (pool_pk | contract_ph) )
103    let mut group_ctx = Sha256::new();
104    strength.update_digest(&mut group_ctx);
105    plot_pk.update_digest(&mut group_ctx);
106    if let Some(pool_pk) = pool_pk {
107        pool_pk.update_digest(&mut group_ctx);
108    } else if let Some(contract_ph) = pool_contract {
109        contract_ph.update_digest(&mut group_ctx);
110    } else {
111        panic!(
112            "failed precondition of compute_plot_group_id_v2(). Either pool-public-key or pool-contract-hash must be specified"
113        );
114    }
115    group_ctx.finalize().into()
116}
117
118pub fn compute_plot_id_v2(
119    strength: u8,
120    plot_pk: &G1Element,
121    pool_pk: Option<&G1Element>,
122    pool_contract: Option<&Bytes32>,
123    plot_index: u16,
124    meta_group: u8,
125) -> Bytes32 {
126    let mut ctx = Sha256::new();
127    // plot_id = sha256( plot_group_id + plot_index + meta_group)
128    let plot_group_id = compute_plot_group_id_v2(strength, plot_pk, pool_pk, pool_contract);
129
130    plot_group_id.update_digest(&mut ctx);
131    plot_index.update_digest(&mut ctx);
132    meta_group.update_digest(&mut ctx);
133    ctx.finalize().into()
134}
135
136impl ProofOfSpace {
137    pub fn compute_plot_id(&self) -> Bytes32 {
138        if self.version == 0 {
139            // v1 proofs
140            compute_plot_id_v1(
141                &self.plot_public_key,
142                self.pool_public_key.as_ref(),
143                self.pool_contract_puzzle_hash.as_ref(),
144            )
145        } else if self.version == 1 {
146            // v2 proofs
147            compute_plot_id_v2(
148                self.strength,
149                &self.plot_public_key,
150                self.pool_public_key.as_ref(),
151                self.pool_contract_puzzle_hash.as_ref(),
152                self.plot_index,
153                self.meta_group,
154            )
155        } else {
156            panic!("unknown proof version: {}", self.version);
157        }
158    }
159
160    /// returns the quality string of the v2 proof of space.
161    /// returns None if this is a v1 proof or if the proof is invalid.
162    #[cfg(feature = "pos2")]
163    pub fn quality_string(&self) -> Option<Bytes32> {
164        if self.version != 1 {
165            return None;
166        }
167
168        let k_size = (self.proof.len() * 8 / 128) as u8;
169        let plot_id = self.compute_plot_id().to_bytes();
170        chia_pos2::quality_string_from_proof(&plot_id, k_size, self.strength, self.proof.as_slice())
171            .map(|quality| {
172                let mut sha256 = Sha256::new();
173                sha256.update(chia_pos2::serialize_quality(
174                    &quality.chain_links,
175                    self.strength,
176                ));
177                sha256.finalize().into()
178            })
179    }
180}
181
182#[cfg(feature = "py-bindings")]
183#[pymethods]
184impl ProofOfSpace {
185    #[pyo3(name = "param")]
186    fn py_param(&self) -> PyPlotParam {
187        match self.version {
188            0 => PyPlotParam {
189                size_v1: Some(self.size),
190                strength_v2: None,
191                plot_index: 0,
192                meta_group: 0,
193            },
194            1 => PyPlotParam {
195                size_v1: None,
196                strength_v2: Some(self.strength),
197                plot_index: self.plot_index,
198                meta_group: self.meta_group,
199            },
200            _ => {
201                panic!("invalid proof-of-space version {}", self.version);
202            }
203        }
204    }
205
206    #[pyo3(name = "compute_plot_id")]
207    pub fn py_compute_plot_id(&self) -> Bytes32 {
208        self.compute_plot_id()
209    }
210
211    #[pyo3(name = "quality_string")]
212    pub fn py_quality_string(&self) -> Option<Bytes32> {
213        self.quality_string()
214    }
215}
216
217// ProofOfSpace was updated in Chia 3.0 to support v2 proofs. In order to stay
218// backwards compatible with the network protocol and the block hashes of
219// previous versions, for v1 proofs, some care has to be taken.
220// Option fields are serialized with a 1-byte prefix indicating whether the
221// field is set or not. This byte is either 0 or 1. This leaves 7 unused bits.
222// We use bit 2 in the byte prefix for the pool_contract_puzzle_hash field to
223// indicate whether this is a v2 proof or not. v1 proofs leave this bit as 0,
224// and thus remain backwards compatible. V2 proofs set it to 1, which alters
225// which fields are serialized. e.g. we no longer include size (k) of the plot
226// since v2 plots have a fixed size.
227impl Streamable for ProofOfSpace {
228    fn update_digest(&self, digest: &mut Sha256) {
229        self.challenge.update_digest(digest);
230        self.pool_public_key.update_digest(digest);
231
232        if self.version == 0 {
233            self.pool_contract_puzzle_hash.update_digest(digest);
234            self.plot_public_key.update_digest(digest);
235            self.size.update_digest(digest);
236            self.proof.update_digest(digest);
237        } else if self.version == 1 {
238            if let Some(pool_contract) = self.pool_contract_puzzle_hash {
239                0b11_u8.update_digest(digest);
240                pool_contract.update_digest(digest);
241            } else {
242                0b10_u8.update_digest(digest);
243            }
244
245            self.plot_public_key.update_digest(digest);
246            self.plot_index.update_digest(digest);
247            self.meta_group.update_digest(digest);
248            self.strength.update_digest(digest);
249
250            // for v2 proofs, we don't hash the full proof directly. The full
251            // proof is the witness to this quality string commitment.
252            #[cfg(feature = "pos2")]
253            self.quality_string()
254                .expect("internal error. Can't compute hash of invalid ProofOfSpace")
255                .update_digest(digest);
256            #[cfg(not(feature = "pos2"))]
257            panic!("hashing a v2 ProofOfSpace requires the \"pos2\" feature");
258        } else {
259            panic!("version field must be 0 or 1, but it's {}", self.version);
260        }
261    }
262
263    fn stream(&self, out: &mut Vec<u8>) -> Result<()> {
264        self.challenge.stream(out)?;
265        self.pool_public_key.stream(out)?;
266
267        if self.version == 0 {
268            self.pool_contract_puzzle_hash.stream(out)?;
269            self.plot_public_key.stream(out)?;
270            self.size.stream(out)?;
271        } else if self.version == 1 {
272            if let Some(pool_contract) = self.pool_contract_puzzle_hash {
273                0b11_u8.stream(out)?;
274                pool_contract.stream(out)?;
275            } else {
276                0b10_u8.stream(out)?;
277            }
278
279            self.plot_public_key.stream(out)?;
280            self.plot_index.stream(out)?;
281            self.meta_group.stream(out)?;
282            self.strength.stream(out)?;
283        } else {
284            return Err(Error::InvalidPoS);
285        }
286
287        self.proof.stream(out)
288    }
289
290    fn parse<const TRUSTED: bool>(input: &mut Cursor<&[u8]>) -> Result<Self> {
291        let challenge = <Bytes32 as Streamable>::parse::<TRUSTED>(input)?;
292        let pool_public_key = <Option<G1Element> as Streamable>::parse::<TRUSTED>(input)?;
293
294        let prefix = <u8 as Streamable>::parse::<TRUSTED>(input)?;
295        let version = prefix >> 1;
296        let pool_contract_puzzle_hash = if (prefix & 1) != 0 {
297            Some(<Bytes32 as Streamable>::parse::<TRUSTED>(input)?)
298        } else {
299            None
300        };
301
302        let plot_public_key = <G1Element as Streamable>::parse::<TRUSTED>(input)?;
303
304        if version == 0 {
305            let size = <u8 as Streamable>::parse::<TRUSTED>(input)?;
306            let proof = <Bytes as Streamable>::parse::<TRUSTED>(input)?;
307
308            Ok(ProofOfSpace {
309                challenge,
310                pool_public_key,
311                pool_contract_puzzle_hash,
312                plot_public_key,
313                version,
314                plot_index: 0,
315                meta_group: 0,
316                strength: 0,
317                size,
318                proof,
319            })
320        } else if version == 1 {
321            let plot_index = <u16 as Streamable>::parse::<TRUSTED>(input)?;
322            let meta_group = <u8 as Streamable>::parse::<TRUSTED>(input)?;
323            let strength = <u8 as Streamable>::parse::<TRUSTED>(input)?;
324            let proof = <Bytes as Streamable>::parse::<TRUSTED>(input)?;
325
326            if pool_public_key.is_some() == pool_contract_puzzle_hash.is_some() {
327                return Err(Error::InvalidPoS);
328            }
329
330            Ok(ProofOfSpace {
331                challenge,
332                pool_public_key,
333                pool_contract_puzzle_hash,
334                plot_public_key,
335                version,
336                plot_index,
337                meta_group,
338                strength,
339                size: 0,
340                proof,
341            })
342        } else {
343            Err(Error::InvalidPoS)
344        }
345    }
346}
347
348#[cfg(test)]
349#[allow(clippy::needless_pass_by_value)]
350mod tests {
351    use super::*;
352    use hex_literal::hex;
353    use rstest::rstest;
354
355    fn plot_pk() -> G1Element {
356        const PLOT_PK_BYTES: [u8; 48] = hex!(
357            "96b35c22adf93068c9536e016e88251ad715a591d8deabb60917d9c495f45a220ca56b906793c27778d5f7f71fb50b94"
358        );
359        G1Element::from_bytes(&PLOT_PK_BYTES).expect("PLOT_PK_BYTES is valid")
360    }
361
362    fn pool_pk() -> G1Element {
363        const POOL_PK_BYTES: [u8; 48] = hex!(
364            "ac6e995e0f9c307853fa5c79e571de5ec2f2d45e5c2641c0847fef8041916e4d07d5a9200d5aa92ceac3b1bf41ce93b2"
365        );
366        G1Element::from_bytes(&POOL_PK_BYTES).expect("POOL_PK_BYTES is valid")
367    }
368
369    fn make_pos(version: u8, has_pool_pk: bool, has_contract: bool, size: u8) -> ProofOfSpace {
370        ProofOfSpace::new(
371            Bytes32::default(),
372            has_pool_pk.then(pool_pk),
373            has_contract.then(Bytes32::default),
374            plot_pk(),
375            version,
376            0,
377            0,
378            0,
379            size,
380            Bytes::from(vec![0x80]),
381        )
382    }
383
384    // Locate the pool_contract_puzzle_hash Option prefix byte (which also
385    // encodes the version) by finding where a contract-present and a
386    // contract-absent serialization first differ.
387    fn prefix_offset() -> usize {
388        let absent = make_pos(0, true, false, 32).to_bytes().unwrap();
389        let present = make_pos(0, true, true, 32).to_bytes().unwrap();
390        absent
391            .iter()
392            .zip(present.iter())
393            .position(|(a, b)| a != b)
394            .unwrap()
395    }
396
397    // these are regression tests and test vectors for plot ID computations
398    #[rstest]
399    #[case("pool_pk", hex!("e185d4ec721ec060eb5833ec07d802fc69a43ed45dd59d7f20c58494421e0270"))]
400    #[case("contract_ph", hex!("4e196e2fb1fc4c85fc48b30c1e585dc0bee08451895909b0ae2db63e2788ab82"))]
401    fn test_compute_plot_id_v1(#[case] variant: &str, #[case] expected: [u8; 32]) {
402        let (pool_pk, pool_contract) = match variant {
403            "pool_pk" => (Some(pool_pk()), None),
404            "contract_ph" => (None, Some(Bytes32::new([1u8; 32]))),
405            _ => panic!("unknown v1 variant: {variant}"),
406        };
407        let result = compute_plot_id_v1(&plot_pk(), pool_pk.as_ref(), pool_contract.as_ref());
408        assert_eq!(result, Bytes32::new(expected));
409    }
410
411    #[rstest]
412    #[case(0, "pool_pk", hex!("5457cccc4cd79900da4235cf5ca7d978a1993581376e76dfb089c274225419d1"))]
413    #[case(10, "pool_pk", hex!("e9d517de0ccfa94baf9e94b39dd0e8afce0451ec27635f43f2aa9b2f429d0501"))]
414    #[case(0, "contract_ph", hex!("210d1a307d26acb3fcfa02208061fc6b80e3fbb9ca5f3e4a596b7521d87ccd79"))]
415    #[case(5, "contract_ph", hex!("824d7b67ab4269c91eb0a2fe10cb48a1c1ad8cfa8a642387d49d5c3c3acbc3bd"))]
416    fn test_compute_plot_group_id_v2(
417        #[case] strength: u8,
418        #[case] variant: &str,
419        #[case] expected: [u8; 32],
420    ) {
421        let (pool_pk, pool_contract) = match variant {
422            "pool_pk" => (Some(pool_pk()), None),
423            "contract_ph" => (None, Some(Bytes32::new([1u8; 32]))),
424            _ => panic!("unknown v2 variant: {variant}"),
425        };
426        let result = compute_plot_group_id_v2(
427            strength,
428            &plot_pk(),
429            pool_pk.as_ref(),
430            pool_contract.as_ref(),
431        );
432        assert_eq!(result, Bytes32::new(expected));
433    }
434
435    #[rstest]
436    #[case(0, 0, 0, "pool_pk", hex!("d3692a5d4fbfe1061053d4afada80d8f0b58b87b46c170e7087716a72091def0"))]
437    #[case(10, 256, 7, "pool_pk", hex!("2316eadc21d38c4e8740eb9efd49a0c2014a5b1ef992f5ae0b2d1fda01a4b034"))]
438    #[case(0, 0, 0, "contract_ph", hex!("03b09cab4bfdbcd1e626d93888a72f002d3948459c23cde52e9dd8d72dd9ae04"))]
439    #[case(5, 100, 3, "contract_ph", hex!("d575860c249ace41a656fe0d97719127f839fae55e6c32ffd7743b5a8a2eae4d"))]
440    fn test_compute_plot_id_v2(
441        #[case] strength: u8,
442        #[case] plot_index: u16,
443        #[case] meta_group: u8,
444        #[case] variant: &str,
445        #[case] expected: [u8; 32],
446    ) {
447        let (pool_pk, pool_contract) = match variant {
448            "pool_pk" => (Some(pool_pk()), None),
449            "contract_ph" => (None, Some(Bytes32::new([1u8; 32]))),
450            _ => panic!("unknown v2 variant: {variant}"),
451        };
452        let result = compute_plot_id_v2(
453            strength,
454            &plot_pk(),
455            pool_pk.as_ref(),
456            pool_contract.as_ref(),
457            plot_index,
458            meta_group,
459        );
460        assert_eq!(result, Bytes32::new(expected));
461    }
462
463    // Regression tests for quality_string(). Vectors in quality-string-tests/*.txt:
464    // 7 lines (challenge hex, strength, plot_index, meta_group, pool hex, proof hex, expect_quality hex).
465    #[cfg(feature = "pos2")]
466    #[rstest]
467    #[case("pool-2-0-0")]
468    #[case("contract-2-0-0")]
469    #[case("contract-3-0-0")]
470    #[case("pool-3-0-0")]
471    #[case("pool-2-1-0")]
472    #[case("pool-2-0-1")]
473    #[case("pool-2-1000-7")]
474    fn test_quality_string(#[case] name: &str) {
475        let plot_pk = G1Element::from_bytes(&hex!(
476            "a9c96f979d895b9ded08907ecd775abf889d51219bb7776dd73fdbac6b0dcc063c72c9e10d96776f486bbd1416b54533"
477        ))
478        .unwrap();
479
480        let path = format!("quality-string-tests/{name}.txt");
481        let contents =
482            std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
483        let l: Vec<&str> = contents
484            .lines()
485            .map(|line| line.split('#').next().unwrap_or(line).trim())
486            .filter(|s| !s.is_empty())
487            .collect();
488        assert_eq!(l.len(), 7, "expected 7 lines");
489
490        let challenge: [u8; 32] = hex::decode(l[0])
491            .expect("challenge hex")
492            .try_into()
493            .unwrap();
494        let strength: u8 = l[1].parse().expect("strength");
495        let plot_index: u16 = l[2].parse().expect("plot_index");
496        let meta_group: u8 = l[3].parse().expect("meta_group");
497        let (pool_pk, pool_contract) = if l[4].len() == 96 {
498            let b = hex::decode(l[4]).expect("pool_pk hex");
499            (
500                Some(G1Element::from_bytes(b.as_slice().try_into().unwrap()).expect("pool_pk")),
501                None,
502            )
503        } else {
504            let ph: [u8; 32] = hex::decode(l[4])
505                .expect("pool_contract hex")
506                .try_into()
507                .unwrap();
508            (None, Some(Bytes32::new(ph)))
509        };
510        let proof = hex::decode(l[5]).expect("proof hex");
511        let expect_quality: [u8; 32] = hex::decode(l[6])
512            .expect("expect_quality hex")
513            .try_into()
514            .unwrap();
515
516        let pos = ProofOfSpace::new(
517            Bytes32::new(challenge),
518            pool_pk,
519            pool_contract,
520            plot_pk,
521            1,
522            plot_index,
523            meta_group,
524            strength,
525            22,
526            Bytes::from(proof),
527        );
528
529        let quality = pos
530            .quality_string()
531            .expect("quality_string should return Some");
532        assert_eq!(quality, Bytes32::new(expect_quality));
533    }
534
535    // Round-trip every accepted prefix-byte combination. For v0 (legacy v1
536    // proofs) the pool_contract Option is lenient and accepts any of
537    // {neither, pool_pk, contract, both}, exactly like the original plain
538    // Option<> derive. For v1 (v2 proofs) the size field is not stored and
539    // collapses to 0 on parse.
540    #[rstest]
541    #[case::v0_pool(0, true, false, 18, 18)]
542    #[case::v0_contract(0, false, true, 28, 28)]
543    #[case::v0_both_lenient(0, true, true, 38, 38)]
544    #[case::v0_neither_lenient(0, false, false, 10, 10)]
545    #[case::v1_pool(1, true, false, 18, 0)]
546    #[case::v1_contract(1, false, true, 28, 0)]
547    fn roundtrip(
548        #[case] version: u8,
549        #[case] has_pool_pk: bool,
550        #[case] has_contract: bool,
551        #[case] size: u8,
552        #[case] expected_size: u8,
553    ) {
554        let buf = make_pos(version, has_pool_pk, has_contract, size)
555            .to_bytes()
556            .unwrap();
557        let parsed = ProofOfSpace::parse::<false>(&mut Cursor::<&[u8]>::new(&buf)).unwrap();
558
559        assert_eq!(parsed.version, version);
560        assert_eq!(parsed.pool_public_key.is_some(), has_pool_pk);
561        assert_eq!(parsed.pool_contract_puzzle_hash.is_some(), has_contract);
562        assert_eq!(parsed.size, expected_size);
563        // re-serialization must be byte-for-byte stable
564        assert_eq!(parsed.to_bytes().unwrap(), buf);
565    }
566
567    // Inputs that must be rejected. The v2 (version 1) format requires exactly
568    // one of pool_public_key / pool_contract_puzzle_hash to be set, so both-set
569    // and neither-set are rejected at parse time. An unknown version (>= 2) is
570    // rejected at serialize time. The exact error code is unimportant, only
571    // that it fails.
572    #[rstest]
573    #[case::v1_both_set(1, true, true)]
574    #[case::v1_neither_set(1, false, false)]
575    #[case::unknown_version(2, true, false)]
576    fn parse_rejects(#[case] version: u8, #[case] has_pool_pk: bool, #[case] has_contract: bool) {
577        let pos = make_pos(version, has_pool_pk, has_contract, 0);
578        match pos.to_bytes() {
579            Ok(buf) => {
580                let err = ProofOfSpace::parse::<false>(&mut Cursor::<&[u8]>::new(&buf))
581                    .expect_err("must fail to parse");
582                assert_eq!(err, Error::InvalidPoS);
583            }
584            Err(e) => assert_eq!(e, Error::InvalidPoS),
585        }
586    }
587
588    // The version flag is packed into the pool_contract_puzzle_hash Option
589    // prefix byte. Only bit 0 (the Option flag) and bit 1 (the version) carry
590    // meaning. The high bits (2..=7) must be rejected for both versions,
591    // matching the strictness of a plain Option<> prefix in earlier protocol
592    // versions where this byte could only ever be 0 or 1.
593    #[rstest]
594    fn high_prefix_bits_rejected(#[values(0, 1)] version: u8, #[values(2, 3, 4, 5, 6, 7)] bit: u8) {
595        let offset = prefix_offset();
596        let size = if version == 0 { 32 } else { 0 };
597        let mut buf = make_pos(version, true, false, size).to_bytes().unwrap();
598
599        let expected_prefix = if version == 0 { 0b00 } else { 0b10 };
600        assert_eq!(buf[offset], expected_prefix);
601
602        buf[offset] |= 1u8 << bit;
603        let err = ProofOfSpace::parse::<false>(&mut Cursor::<&[u8]>::new(&buf))
604            .expect_err("high prefix bit must be rejected");
605        assert_eq!(err, Error::InvalidPoS);
606    }
607}