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 #[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
217impl 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 #[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 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 #[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 #[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 #[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 assert_eq!(parsed.to_bytes().unwrap(), buf);
565 }
566
567 #[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 #[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}