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