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