1use crate::consensus_constants::ConsensusConstants;
2use crate::error::Result;
3use chia_bls::Signature;
4use chia_protocol::SpendBundle;
5use clvmr::allocator::{Allocator, NodePtr};
6use clvmr::serde::{Serializer, node_from_bytes_backrefs};
7use std::borrow::Borrow;
8
9#[cfg(feature = "py-bindings")]
10use pyo3::prelude::*;
11#[cfg(feature = "py-bindings")]
12use pyo3::types::PyList;
13
14const MAX_SKIPPED_ITEMS: u32 = 6;
18
19const MIN_COST_THRESHOLD: u64 = 6_000_000;
22
23#[derive(PartialEq)]
26pub enum BuildBlockResult {
27 KeepGoing,
29 Done,
31}
32
33#[cfg_attr(feature = "py-bindings", pyclass)]
42pub struct BlockBuilder {
43 allocator: Allocator,
44 signature: Signature,
45 sentinel: NodePtr,
46
47 block_cost: u64,
50
51 byte_cost: u64,
53
54 num_skipped: u32,
57
58 ser: Serializer,
60}
61
62fn result(num_skipped: u32) -> BuildBlockResult {
63 if num_skipped > MAX_SKIPPED_ITEMS {
64 BuildBlockResult::Done
65 } else {
66 BuildBlockResult::KeepGoing
67 }
68}
69
70impl BlockBuilder {
71 pub fn new() -> Result<Self> {
72 let mut a = Allocator::new();
73
74 let sentinel = a.new_pair(NodePtr::NIL, NodePtr::NIL)?;
77
78 let spend_list = a.new_pair(sentinel, a.nil())?;
84 let quoted_list = a.new_pair(a.one(), spend_list)?;
85
86 let mut ser = Serializer::new(Some(sentinel));
87 ser.add(&a, quoted_list)?;
88
89 Ok(Self {
90 allocator: a,
91 signature: Signature::default(),
92 sentinel,
93 block_cost: 20,
96 byte_cost: 0,
97 num_skipped: 0,
98 ser,
99 })
100 }
101
102 pub fn add_spend_bundles<T, S>(
111 &mut self,
112 bundles: T,
113 cost: u64,
114 constants: &ConsensusConstants,
115 ) -> Result<(bool, BuildBlockResult)>
116 where
117 T: IntoIterator<Item = S>,
118 S: Borrow<SpendBundle>,
119 {
120 if self.byte_cost + self.block_cost + MIN_COST_THRESHOLD > constants.max_block_cost_clvm {
123 self.num_skipped += 1;
124 return Ok((false, BuildBlockResult::Done));
125 }
126
127 if self.byte_cost + self.block_cost + cost > constants.max_block_cost_clvm {
128 self.num_skipped += 1;
129 return Ok((false, result(self.num_skipped)));
130 }
131
132 let a = &mut self.allocator;
133
134 let mut spend_list = self.sentinel;
135 let mut cumulative_signature = Signature::default();
136 for bundle in bundles {
137 for spend in &bundle.borrow().coin_spends {
138 let solution = node_from_bytes_backrefs(a, spend.solution.as_ref())?;
140 let item = a.new_pair(solution, NodePtr::NIL)?;
141 let amount = a.new_number(spend.coin.amount.into())?;
143 let item = a.new_pair(amount, item)?;
144 let puzzle = node_from_bytes_backrefs(a, spend.puzzle_reveal.as_ref())?;
146 let item = a.new_pair(puzzle, item)?;
147 let parent_id = a.new_atom(&spend.coin.parent_coin_info)?;
149 let item = a.new_pair(parent_id, item)?;
150
151 spend_list = a.new_pair(item, spend_list)?;
152 }
153 cumulative_signature.aggregate(&bundle.borrow().aggregated_signature);
154 }
155
156 let (done, state) = self.ser.add(a, spend_list)?;
157
158 self.byte_cost = (self.ser.size() + 2) * constants.cost_per_byte;
160 if self.byte_cost + self.block_cost + cost > constants.max_block_cost_clvm {
161 self.ser.restore(state);
168 self.byte_cost = (self.ser.size() + 2) * constants.cost_per_byte;
169 self.num_skipped += 1;
170 return Ok((false, result(self.num_skipped)));
171 }
172 self.block_cost += cost;
173 self.signature.aggregate(&cumulative_signature);
174
175 let result = if done
178 || self.byte_cost + self.block_cost + MIN_COST_THRESHOLD > constants.max_block_cost_clvm
179 {
180 BuildBlockResult::Done
181 } else {
182 BuildBlockResult::KeepGoing
183 };
184 Ok((true, result))
185 }
186
187 pub fn cost(&self) -> u64 {
188 self.byte_cost + self.block_cost
189 }
190
191 pub fn finalize(mut self, constants: &ConsensusConstants) -> Result<(Vec<u8>, Signature, u64)> {
193 let (done, _) = self.ser.add(&self.allocator, self.allocator.nil())?;
194 assert!(done);
195
196 self.block_cost += self.ser.size() * constants.cost_per_byte;
198
199 assert!(self.block_cost <= constants.max_block_cost_clvm);
200 Ok((self.ser.into_inner(), self.signature, self.block_cost))
201 }
202}
203
204#[cfg(feature = "py-bindings")]
205#[pymethods]
206impl BlockBuilder {
207 #[new]
208 pub fn py_new() -> PyResult<Self> {
209 Ok(Self::new()?)
210 }
211
212 #[pyo3(name = "add_spend_bundles")]
215 pub fn py_add_spend_bundle(
216 &mut self,
217 bundles: &Bound<'_, PyList>,
218 cost: u64,
219 constants: &ConsensusConstants,
220 ) -> PyResult<(bool, bool)> {
221 let (added, result) = self.add_spend_bundles(
222 bundles.iter().map(|item| {
223 item.extract::<Bound<'_, SpendBundle>>()
229 .expect("spend bundle")
230 .get()
231 .clone()
232 }),
233 cost,
234 constants,
235 )?;
236 let done = matches!(result, BuildBlockResult::Done);
237 Ok((added, done))
238 }
239
240 #[pyo3(name = "cost")]
241 pub fn py_cost(&self) -> u64 {
242 self.cost()
243 }
244
245 #[pyo3(name = "finalize")]
247 pub fn py_finalize(
248 &mut self,
249 constants: &ConsensusConstants,
250 ) -> PyResult<(Vec<u8>, Signature, u64)> {
251 let mut temp = BlockBuilder::new()?;
252 std::mem::swap(self, &mut temp);
253 let (generator, sig, cost) = temp.finalize(constants)?;
254 Ok((generator, sig, cost))
255 }
256}
257
258#[cfg(not(debug_assertions))]
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::consensus_constants::TEST_CONSTANTS;
264 use crate::flags::ConsensusFlags;
265 use crate::flags::MEMPOOL_MODE;
266 use crate::owned_conditions::OwnedSpendBundleConditions;
267 use crate::run_block_generator::run_block_generator2;
268 use crate::solution_generator::calculate_generator_length;
269 use crate::spendbundle_conditions::run_spendbundle;
270 use chia_protocol::{Bytes, Coin};
271 use chia_traits::Streamable;
272 use rand::rngs::StdRng;
273 use rand::{SeedableRng, prelude::SliceRandom};
274 use std::collections::HashSet;
275 use std::fs;
276 use std::time::Instant;
277
278 #[test]
279 fn test_build_block() {
280 let mut all_bundles = vec![];
281 println!("loading spend bundles from disk");
282 let mut seen_spends = HashSet::new();
283 for entry in fs::read_dir("../../test-bundles").expect("listing test-bundles directory") {
284 let file = entry.expect("list dir").path();
285 if file.extension().map(|s| s.to_str()) != Some(Some("bundle")) {
286 continue;
287 }
288 if file.file_stem().map(std::ffi::OsStr::len) != Some(64_usize) {
290 continue;
291 }
292 let buf = fs::read(file.clone()).expect("read bundle file");
293 let bundle = SpendBundle::from_bytes(buf.as_slice()).expect("parsing SpendBundle");
294
295 let mut a = Allocator::new();
296 let conds = run_spendbundle(
297 &mut a,
298 &bundle,
299 11_000_000_000,
300 ConsensusFlags::empty(),
301 &TEST_CONSTANTS,
302 )
303 .expect("run_spendbundle")
304 .0;
305
306 if conds
307 .spends
308 .iter()
309 .any(|s| seen_spends.contains(&*s.coin_id))
310 {
311 panic!(
315 "conflict in {}",
316 file.file_name().unwrap().to_str().unwrap()
317 );
318 }
319 if conds.spends.iter().any(|s| {
320 s.create_coin.iter().any(|c| {
321 seen_spends.contains(&Coin::new(*s.coin_id, c.puzzle_hash, c.amount).coin_id())
322 })
323 }) {
324 panic!(
332 "conflict in {}",
333 file.file_name().unwrap().to_str().unwrap()
334 );
335 }
336 for spend in &conds.spends {
337 seen_spends.insert(*spend.coin_id);
338 for coin in &spend.create_coin {
339 seen_spends
340 .insert(Coin::new(*spend.coin_id, coin.puzzle_hash, coin.amount).coin_id());
341 }
342 }
343
344 let cost = conds.cost
347 - (calculate_generator_length(&bundle.coin_spends) as u64 - 2)
348 * TEST_CONSTANTS.cost_per_byte;
349
350 let mut conds = OwnedSpendBundleConditions::from(&a, conds);
351 for s in &mut conds.spends {
352 s.flags = 0;
357 s.fingerprint = Bytes::default();
358 s.create_coin.sort();
363 }
364 all_bundles.push(Box::new((bundle, cost, conds)));
365 }
366 all_bundles.sort_by_key(|x| x.1);
367 println!("loaded {} spend bundles", all_bundles.len());
380
381 let num_cores: usize = std::thread::available_parallelism().unwrap().into();
382 let pool = blocking_threadpool::Builder::new()
383 .num_threads(num_cores)
384 .queue_len(num_cores + 1)
385 .build();
386
387 for seed in 0..30 {
388 let mut bundles = all_bundles.clone();
389 let mut rng = StdRng::seed_from_u64(seed);
390 pool.execute(move || {
391 bundles.shuffle(&mut rng);
392
393 let start = Instant::now();
394 let mut builder = BlockBuilder::new().expect("BlockBuilder");
395 let mut skipped = 0;
396 let mut num_tx = 0;
397 let mut max_call_time = 0.0f32;
398 let mut spends = vec![];
399 for entry in &bundles {
400 let (bundle, cost, conds) = entry.as_ref();
401 let start_call = Instant::now();
402 let (added, result) = builder
403 .add_spend_bundles([bundle].into_iter(), *cost, &TEST_CONSTANTS)
404 .expect("add_spend_bundle");
405
406 max_call_time = f32::max(max_call_time, start_call.elapsed().as_secs_f32());
407 if added {
408 num_tx += 1;
409 spends.extend(conds.spends.iter());
410 } else {
411 skipped += 1;
412 }
413 if result == BuildBlockResult::Done {
414 break;
415 }
416 }
417 let (generator, signature, cost) =
418 builder.finalize(&TEST_CONSTANTS).expect("finalize()");
419
420 println!(
421 "idx: {seed:3} built block in {:>5.2} seconds, cost: {cost:11} skipped: {skipped:2} longest-call: {max_call_time:>5.2}s TX: {num_tx}",
422 start.elapsed().as_secs_f32()
423 );
424
425 let mut a = Allocator::new();
429 let conds = run_block_generator2::<&[u8], _>(
430 &mut a,
431 generator.as_slice(),
432 [],
433 TEST_CONSTANTS.max_block_cost_clvm,
434 MEMPOOL_MODE,
435 &signature,
436 None,
437 &TEST_CONSTANTS,
438 )
439 .expect("run_block_generator2");
440 assert_eq!(conds.cost, cost);
441 let mut conds = OwnedSpendBundleConditions::from(&a, conds);
442
443 assert_eq!(conds.spends.len(), spends.len());
444 conds.spends.sort_by_key(|s| s.coin_id);
445 spends.sort_by_key(|s| s.coin_id);
446 for (mut generator, tx) in conds.spends.into_iter().zip(spends) {
447 generator.create_coin.sort();
448 generator.flags = 0;
449 generator.fingerprint = Bytes::default();
450 assert_eq!(&generator, tx);
451 }
452 });
453 assert_eq!(pool.panic_count(), 0);
454 }
455 pool.join();
456 assert_eq!(pool.panic_count(), 0);
457 }
458}