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#[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 version: u8,
24
25 plot_index: u16,
27 meta_group: u8,
28 strength: u8,
29
30 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 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 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 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 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 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 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
216impl 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 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 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 #[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 #[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 #[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 assert_eq!(parsed.to_bytes().unwrap(), buf);
560 }
561
562 #[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 #[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}