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_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 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 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 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 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
207impl 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 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 #[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 #[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}