Skip to main content

dig_clvm/consensus/
block.rs

1//! Block generator construction and validation.
2//!
3//! `build_block_generator()` assembles spend bundles into a compressed block
4//! generator using CLVM back-references. `validate_block()` executes a block
5//! generator and validates all conditions.
6
7use chia_bls::{BlsCache, Signature};
8use chia_consensus::allocator::make_allocator;
9use chia_consensus::flags::DONT_VALIDATE_SIGNATURE;
10use chia_consensus::owned_conditions::OwnedSpendBundleConditions;
11use chia_consensus::run_block_generator::run_block_generator2;
12use chia_consensus::solution_generator::solution_generator_backrefs;
13use chia_consensus::spendbundle_conditions::run_spendbundle;
14use chia_protocol::{Coin, SpendBundle};
15use clvmr::cost::Cost;
16use clvmr::LIMIT_HEAP;
17
18use super::config::ValidationConfig;
19use super::context::ValidationContext;
20use super::error::ValidationError;
21use super::result::{BlockGeneratorResult, SpendResult};
22
23/// Build a block generator from a set of spend bundles.
24///
25/// Bundles are added in order until `max_cost` is reached. The caller should
26/// pre-sort bundles by fee/cost ratio (highest first) to maximize fee revenue.
27///
28/// Mirrors L1's `create_block_generator()` at `mempool.py:505`.
29pub fn build_block_generator(
30    bundles: &[SpendBundle],
31    context: &ValidationContext,
32    max_cost: Cost,
33) -> Result<BlockGeneratorResult, ValidationError> {
34    let consensus = context.constants.consensus();
35    let mut cost_remaining = max_cost;
36    let mut included_spends: Vec<(Coin, Vec<u8>, Vec<u8>)> = Vec::new();
37    let mut all_additions: Vec<Coin> = Vec::new();
38    let mut all_removals: Vec<Coin> = Vec::new();
39    let mut signatures: Vec<Signature> = Vec::new();
40    let mut total_cost: Cost = 0;
41    let mut bundles_included: usize = 0;
42
43    for bundle in bundles {
44        // Run CLVM to compute cost for this bundle (skip sig verification)
45        let mut a = make_allocator(LIMIT_HEAP);
46        let result = run_spendbundle(
47            &mut a,
48            bundle,
49            cost_remaining,
50            context.height,
51            DONT_VALIDATE_SIGNATURE,
52            consensus,
53        );
54
55        let (sbc, _pkm_pairs) = match result {
56            Ok(r) => r,
57            Err(_) => continue, // Skip bundles that fail (cost exceeded, invalid, etc.)
58        };
59
60        let conditions = OwnedSpendBundleConditions::from(&a, sbc);
61        let bundle_cost = conditions.cost;
62
63        if bundle_cost > cost_remaining {
64            continue; // Skip if this bundle would exceed remaining budget
65        }
66
67        // Collect spends for generator construction
68        for cs in &bundle.coin_spends {
69            included_spends.push((cs.coin, cs.puzzle_reveal.to_vec(), cs.solution.to_vec()));
70            all_removals.push(cs.coin);
71        }
72
73        // Extract additions from conditions
74        for spend in &conditions.spends {
75            let parent_id = spend.coin_id;
76            for cc in &spend.create_coin {
77                all_additions.push(Coin::new(parent_id, cc.0, cc.1));
78            }
79        }
80
81        signatures.push(bundle.aggregated_signature.clone());
82        total_cost += bundle_cost;
83        cost_remaining -= bundle_cost;
84        bundles_included += 1;
85    }
86
87    // Build the compressed generator using CLVM back-references
88    let spends_iter = included_spends
89        .iter()
90        .map(|(coin, puz, sol)| (*coin, puz.as_slice(), sol.as_slice()));
91
92    let generator = solution_generator_backrefs(spends_iter)
93        .map_err(|e| ValidationError::Clvm(format!("solution_generator_backrefs: {}", e)))?;
94
95    // Aggregate all signatures
96    let aggregated_signature = if signatures.is_empty() {
97        Signature::default()
98    } else {
99        let mut agg = signatures[0].clone();
100        for sig in &signatures[1..] {
101            agg += sig;
102        }
103        agg
104    };
105
106    Ok(BlockGeneratorResult {
107        generator,
108        block_refs: Vec::new(), // No cross-block refs for now
109        aggregated_signature,
110        additions: all_additions,
111        removals: all_removals,
112        cost: total_cost,
113        bundles_included,
114    })
115}
116
117/// Validate a block generator and return the combined additions + removals.
118///
119/// Executes the block-level CLVM program, validates all conditions, and
120/// returns the aggregate SpendResult.
121///
122/// Mirrors L1's `_run_block()` at `multiprocess_validation.py:62`.
123pub fn validate_block(
124    generator: &[u8],
125    generator_refs: &[Vec<u8>],
126    context: &ValidationContext,
127    config: &ValidationConfig,
128    bls_cache: Option<&mut BlsCache>,
129    aggregated_signature: &Signature,
130) -> Result<SpendResult, ValidationError> {
131    let consensus = context.constants.consensus();
132    let mut a = make_allocator(LIMIT_HEAP);
133
134    // Execute the block generator via run_block_generator2
135    // This runs the CLVM program that produces all spends + conditions
136    let sbc = run_block_generator2(
137        &mut a,
138        generator,
139        generator_refs.iter().map(|r| r.as_slice()),
140        config.max_cost_per_block,
141        config.flags,
142        aggregated_signature,
143        bls_cache.map(|c| &*c),
144        consensus,
145    )
146    .map_err(|e| ValidationError::Clvm(format!("{:?}", e)))?;
147
148    let conditions = OwnedSpendBundleConditions::from(&a, sbc);
149
150    // Cost enforcement
151    if conditions.cost > config.max_cost_per_block {
152        return Err(ValidationError::CostExceeded {
153            limit: config.max_cost_per_block,
154            consumed: conditions.cost,
155        });
156    }
157
158    // Extract additions from conditions
159    let additions: Vec<Coin> = conditions
160        .spends
161        .iter()
162        .flat_map(|spend| {
163            let parent_id = spend.coin_id;
164            spend
165                .create_coin
166                .iter()
167                .map(move |cc| Coin::new(parent_id, cc.0, cc.1))
168        })
169        .collect();
170
171    // Extract removals from conditions (spent coin IDs)
172    let removals: Vec<Coin> = conditions
173        .spends
174        .iter()
175        .map(|spend| Coin::new(spend.parent_id, spend.puzzle_hash, spend.coin_amount))
176        .collect();
177
178    // Conservation check
179    let total_input: u64 = removals.iter().map(|c| c.amount).sum();
180    let total_output: u64 = additions.iter().map(|c| c.amount).sum();
181
182    if total_input < total_output {
183        return Err(ValidationError::ConservationViolation {
184            input: total_input,
185            output: total_output,
186        });
187    }
188
189    let fee = total_input - total_output;
190
191    Ok(SpendResult {
192        additions,
193        removals,
194        fee,
195        conditions,
196    })
197}