Skip to main content

chia_query/peer/
block.rs

1//! Block parsing utilities.
2//!
3//! Uses `chia_consensus` to extract additions, removals, and coin spends from
4//! a `FullBlock`'s CLVM generator.  Follows the same patterns as the
5//! chia-block-listener / chia-generator-parser crates.
6
7use chia_bls::Signature;
8use chia_consensus::additions_and_removals::additions_and_removals;
9use chia_consensus::consensus_constants::ConsensusConstants;
10use chia_consensus::flags::DONT_VALIDATE_SIGNATURE;
11use chia_consensus::get_puzzle_and_solution::get_puzzle_and_solution_for_coin;
12use chia_consensus::run_block_generator::run_block_generator2;
13use chia_consensus::{allocator::make_allocator, validation_error};
14use chia_protocol::{Bytes32, FullBlock};
15
16use clvmr::serde::{node_from_bytes_backrefs, node_to_bytes};
17
18use crate::types::*;
19
20// ---------------------------------------------------------------------------
21// Additions & removals from a FullBlock
22// ---------------------------------------------------------------------------
23
24/// Extract coin additions and removals from a full block's generator.
25pub fn block_additions_and_removals(
26    block: &FullBlock,
27    height: u32,
28    constants: &ConsensusConstants,
29) -> Result<AdditionsAndRemovals, ChiaQueryError> {
30    let generator = match &block.transactions_generator {
31        Some(g) => g,
32        None => {
33            return Ok(AdditionsAndRemovals {
34                additions: reward_coins(block, height),
35                removals: Vec::new(),
36            });
37        }
38    };
39
40    let timestamp = block_timestamp(block);
41    let block_refs: Vec<&[u8]> = Vec::new();
42    let flags = DONT_VALIDATE_SIGNATURE;
43
44    let (raw_additions, raw_removals) =
45        additions_and_removals(generator.as_ref(), block_refs, flags, constants)
46            .map_err(|e| ChiaQueryError::PeerConnection(format!("CLVM execution failed: {e:?}")))?;
47
48    let mut additions: Vec<CoinRecord> = raw_additions
49        .iter()
50        .map(|(coin, _hint)| CoinRecord {
51            coin: Coin::from_protocol(coin),
52            confirmed_block_index: height,
53            spent_block_index: 0,
54            spent: false,
55            coinbase: false,
56            timestamp,
57        })
58        .collect();
59
60    additions.extend(reward_coins(block, height));
61
62    // `additions_and_removals` hands back each removal already paired with its
63    // pre-computed coin id; the id is redundant here because `CoinRecord`
64    // derives it from the coin itself.
65    let removals: Vec<CoinRecord> = raw_removals
66        .iter()
67        .map(|(_coin_id, coin)| CoinRecord {
68            coin: Coin::from_protocol(coin),
69            confirmed_block_index: 0,
70            spent_block_index: height,
71            spent: true,
72            coinbase: false,
73            timestamp: 0,
74        })
75        .collect();
76
77    Ok(AdditionsAndRemovals {
78        additions,
79        removals,
80    })
81}
82
83// ---------------------------------------------------------------------------
84// Block spends (puzzle_reveal + solution for every spent coin)
85// ---------------------------------------------------------------------------
86
87/// Run the block generator and extract all coin spends with their
88/// puzzle_reveal and solution.
89pub fn block_spends(
90    block: &FullBlock,
91    constants: &ConsensusConstants,
92) -> Result<Vec<CoinSpend>, ChiaQueryError> {
93    let generator = match &block.transactions_generator {
94        Some(g) => g,
95        None => return Ok(Vec::new()),
96    };
97
98    let flags = DONT_VALIDATE_SIGNATURE;
99    let block_refs: Vec<&[u8]> = Vec::new();
100    let mut allocator = make_allocator(flags);
101
102    let conds = run_block_generator2(
103        &mut allocator,
104        generator.as_ref(),
105        &block_refs,
106        constants.max_block_cost_clvm,
107        flags,
108        &Signature::default(),
109        None,
110        constants,
111    )
112    .map_err(|e| ChiaQueryError::PeerConnection(format!("run_block_generator2 failed: {e:?}")))?;
113
114    let program = node_from_bytes_backrefs(&mut allocator, generator.as_ref())
115        .map_err(|e| ChiaQueryError::PeerConnection(format!("parse generator: {e:?}")))?;
116
117    let args = chia_consensus::run_block_generator::setup_generator_args(
118        &mut allocator,
119        &block_refs,
120        flags,
121    )
122    .map_err(|e| ChiaQueryError::PeerConnection(format!("setup args: {e:?}")))?;
123
124    let dialect = clvmr::chia_dialect::ChiaDialect::new(flags);
125    let reduction = clvmr::run_program::run_program(
126        &mut allocator,
127        &dialect,
128        program,
129        args,
130        constants.max_block_cost_clvm,
131    )
132    .map_err(|e| ChiaQueryError::PeerConnection(format!("run_program: {e:?}")))?;
133
134    let generator_output = reduction.1;
135
136    let mut spends = Vec::new();
137    for sc in &conds.spends {
138        let parent_id: Bytes32 = allocator.atom(sc.parent_id).as_ref().try_into().unwrap();
139        let puzzle_hash: Bytes32 = allocator.atom(sc.puzzle_hash).as_ref().try_into().unwrap();
140        let removal = chia_protocol::Coin {
141            parent_coin_info: parent_id,
142            puzzle_hash,
143            amount: sc.coin_amount,
144        };
145
146        if let Ok((puzzle_node, solution_node)) =
147            get_puzzle_and_solution_for_coin(&allocator, generator_output, &removal)
148        {
149            let puzzle_bytes = node_to_bytes(&allocator, puzzle_node).unwrap_or_default();
150            let solution_bytes = node_to_bytes(&allocator, solution_node).unwrap_or_default();
151
152            spends.push(CoinSpend {
153                coin: Coin::from_protocol(&removal),
154                puzzle_reveal: format!("0x{}", hex::encode(&puzzle_bytes)),
155                solution: format!("0x{}", hex::encode(&solution_bytes)),
156            });
157        }
158    }
159
160    Ok(spends)
161}
162
163// ---------------------------------------------------------------------------
164// Block spends WITH parsed conditions
165// ---------------------------------------------------------------------------
166
167/// Same as `block_spends` but also runs each puzzle against its solution to
168/// extract the CLVM output conditions.
169pub fn block_spends_with_conditions(
170    block: &FullBlock,
171    constants: &ConsensusConstants,
172) -> Result<Vec<CoinSpendWithConditions>, ChiaQueryError> {
173    let generator = match &block.transactions_generator {
174        Some(g) => g,
175        None => return Ok(Vec::new()),
176    };
177
178    let flags = DONT_VALIDATE_SIGNATURE;
179    let block_refs: Vec<&[u8]> = Vec::new();
180    let mut allocator = make_allocator(flags);
181
182    let conds = run_block_generator2(
183        &mut allocator,
184        generator.as_ref(),
185        &block_refs,
186        constants.max_block_cost_clvm,
187        flags,
188        &Signature::default(),
189        None,
190        constants,
191    )
192    .map_err(|e| ChiaQueryError::PeerConnection(format!("run_block_generator2 failed: {e:?}")))?;
193
194    let program = node_from_bytes_backrefs(&mut allocator, generator.as_ref())
195        .map_err(|e| ChiaQueryError::PeerConnection(format!("parse generator: {e:?}")))?;
196
197    let args = chia_consensus::run_block_generator::setup_generator_args(
198        &mut allocator,
199        &block_refs,
200        flags,
201    )
202    .map_err(|e| ChiaQueryError::PeerConnection(format!("setup args: {e:?}")))?;
203
204    let dialect = clvmr::chia_dialect::ChiaDialect::new(flags);
205    let reduction = clvmr::run_program::run_program(
206        &mut allocator,
207        &dialect,
208        program,
209        args,
210        constants.max_block_cost_clvm,
211    )
212    .map_err(|e| ChiaQueryError::PeerConnection(format!("run_program: {e:?}")))?;
213
214    let generator_output = reduction.1;
215
216    let mut result = Vec::new();
217    for sc in &conds.spends {
218        let parent_id: Bytes32 = allocator.atom(sc.parent_id).as_ref().try_into().unwrap();
219        let puzzle_hash: Bytes32 = allocator.atom(sc.puzzle_hash).as_ref().try_into().unwrap();
220        let removal = chia_protocol::Coin {
221            parent_coin_info: parent_id,
222            puzzle_hash,
223            amount: sc.coin_amount,
224        };
225
226        if let Ok((puzzle_node, solution_node)) =
227            get_puzzle_and_solution_for_coin(&allocator, generator_output, &removal)
228        {
229            let puzzle_bytes = node_to_bytes(&allocator, puzzle_node).unwrap_or_default();
230            let solution_bytes = node_to_bytes(&allocator, solution_node).unwrap_or_default();
231
232            // Run puzzle(solution) to extract conditions.
233            let conditions = match clvmr::run_program::run_program(
234                &mut allocator,
235                &dialect,
236                puzzle_node,
237                solution_node,
238                constants.max_block_cost_clvm,
239            ) {
240                Ok(clvmr::reduction::Reduction(_, output)) => parse_conditions(&allocator, output),
241                Err(_) => Vec::new(),
242            };
243
244            result.push(CoinSpendWithConditions {
245                coin_spend: CoinSpend {
246                    coin: Coin::from_protocol(&removal),
247                    puzzle_reveal: format!("0x{}", hex::encode(&puzzle_bytes)),
248                    solution: format!("0x{}", hex::encode(&solution_bytes)),
249                },
250                conditions,
251            });
252        }
253    }
254
255    Ok(result)
256}
257
258// ---------------------------------------------------------------------------
259// Parse raw CLVM conditions output into our Condition type
260// ---------------------------------------------------------------------------
261
262/// The output of `run_program(puzzle, solution)` is a list of conditions.
263/// Each condition is `(opcode . (arg1 arg2 ...))`.
264fn parse_conditions(allocator: &clvmr::Allocator, output: clvmr::NodePtr) -> Vec<Condition> {
265    parse_conditions_public(allocator, output)
266}
267
268/// Public version of condition parsing for use from `router.rs`.
269pub fn parse_conditions_public(
270    allocator: &clvmr::Allocator,
271    mut output: clvmr::NodePtr,
272) -> Vec<Condition> {
273    let mut conditions = Vec::new();
274
275    while let Ok(Some((cond, rest))) = validation_error::next(allocator, output) {
276        output = rest;
277
278        // cond = (opcode . args_list)
279        let Ok(opcode_node) = validation_error::first(allocator, cond) else {
280            continue;
281        };
282
283        let opcode_bytes = match allocator.sexp(opcode_node) {
284            clvmr::allocator::SExp::Atom => allocator.atom(opcode_node).as_ref().to_vec(),
285            clvmr::allocator::SExp::Pair(_, _) => continue,
286        };
287        let opcode = serde_json::Value::String(format!("0x{}", hex::encode(&opcode_bytes)));
288
289        // Collect args
290        let mut vars = Vec::new();
291        let Ok(mut args_iter) = validation_error::rest(allocator, cond) else {
292            continue;
293        };
294
295        while let Ok(Some((arg, rest))) = validation_error::next(allocator, args_iter) {
296            args_iter = rest;
297            // Args can be atoms or pairs (e.g., hint lists).
298            // Serialize pairs as CLVM bytes for fidelity.
299            let arg_hex = match allocator.sexp(arg) {
300                clvmr::allocator::SExp::Atom => {
301                    format!("0x{}", hex::encode(allocator.atom(arg).as_ref()))
302                }
303                clvmr::allocator::SExp::Pair(_, _) => {
304                    match clvmr::serde::node_to_bytes(allocator, arg) {
305                        Ok(bytes) => format!("0x{}", hex::encode(&bytes)),
306                        Err(_) => continue,
307                    }
308                }
309            };
310            vars.push(arg_hex);
311        }
312
313        conditions.push(Condition { opcode, vars });
314    }
315
316    conditions
317}
318
319// ---------------------------------------------------------------------------
320// Helpers
321// ---------------------------------------------------------------------------
322
323fn reward_coins(block: &FullBlock, height: u32) -> Vec<CoinRecord> {
324    let Some(ref ti) = block.transactions_info else {
325        return Vec::new();
326    };
327    let timestamp = block_timestamp(block);
328    ti.reward_claims_incorporated
329        .iter()
330        .map(|c| CoinRecord {
331            coin: Coin::from_protocol(c),
332            confirmed_block_index: height,
333            spent_block_index: 0,
334            spent: false,
335            coinbase: true,
336            timestamp,
337        })
338        .collect()
339}
340
341fn block_timestamp(block: &FullBlock) -> u64 {
342    block
343        .foliage_transaction_block
344        .as_ref()
345        .map(|ft| ft.timestamp)
346        .unwrap_or(0)
347}