From 11a5937adcb21a7a542f524201856fb76a75c9f1 Mon Sep 17 00:00:00 2001 From: arvidn Date: Tue, 30 Jul 2024 11:03:45 +0200 Subject: [PATCH 1/3] factor out function to prepar arguments to be passed to block generator --- .../chia-consensus/benches/run-generator.rs | 4 +- .../fuzz/fuzz_targets/run-generator.rs | 8 +-- .../src/gen/run_block_generator.rs | 62 ++++++++++++------- .../chia-consensus/src/gen/test_generators.rs | 4 +- crates/chia-tools/src/bin/analyze-chain.rs | 4 +- .../src/bin/test-block-generators.rs | 6 +- wheel/src/run_generator.rs | 8 +-- 7 files changed, 58 insertions(+), 38 deletions(-) diff --git a/crates/chia-consensus/benches/run-generator.rs b/crates/chia-consensus/benches/run-generator.rs index a815b38c0..a37b9cb9e 100644 --- a/crates/chia-consensus/benches/run-generator.rs +++ b/crates/chia-consensus/benches/run-generator.rs @@ -51,7 +51,7 @@ fn run(c: &mut Criterion) { let mut a = Allocator::new(); let start = Instant::now(); - let conds = run_block_generator::<_, MempoolVisitor>( + let conds = run_block_generator::<_, MempoolVisitor, _>( &mut a, gen, &block_refs, @@ -69,7 +69,7 @@ fn run(c: &mut Criterion) { let mut a = Allocator::new(); let start = Instant::now(); - let conds = run_block_generator2::<_, MempoolVisitor>( + let conds = run_block_generator2::<_, MempoolVisitor, _>( &mut a, gen, &block_refs, diff --git a/crates/chia-consensus/fuzz/fuzz_targets/run-generator.rs b/crates/chia-consensus/fuzz/fuzz_targets/run-generator.rs index d92aaa8b0..0f539a177 100644 --- a/crates/chia-consensus/fuzz/fuzz_targets/run-generator.rs +++ b/crates/chia-consensus/fuzz/fuzz_targets/run-generator.rs @@ -10,10 +10,10 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { let mut a1 = make_allocator(LIMIT_HEAP); - let r1 = run_block_generator::<&[u8], MempoolVisitor>( + let r1 = run_block_generator::<&[u8], MempoolVisitor, _>( &mut a1, data, - &[], + [], 110_000_000, ALLOW_BACKREFS, &TEST_CONSTANTS, @@ -21,10 +21,10 @@ fuzz_target!(|data: &[u8]| { drop(a1); let mut a2 = make_allocator(LIMIT_HEAP); - let r2 = run_block_generator2::<&[u8], MempoolVisitor>( + let r2 = run_block_generator2::<&[u8], MempoolVisitor, _>( &mut a2, data, - &[], + [], 110_000_000, ALLOW_BACKREFS, &TEST_CONSTANTS, diff --git a/crates/chia-consensus/src/gen/run_block_generator.rs b/crates/chia-consensus/src/gen/run_block_generator.rs index 128ac4321..8a0360d4f 100644 --- a/crates/chia-consensus/src/gen/run_block_generator.rs +++ b/crates/chia-consensus/src/gen/run_block_generator.rs @@ -24,6 +24,31 @@ fn subtract_cost(a: &Allocator, cost_left: &mut Cost, subtract: Cost) -> Result< } } +/// Prepares the arguments passed to the block generator. They are in the form: +/// (DESERIALIZER_MOD (block1 block2 block3 ...)) +pub fn setup_generator_args, I: IntoIterator>( + a: &mut Allocator, + block_refs: I, +) -> Result +where + ::IntoIter: DoubleEndedIterator, +{ + let clvm_deserializer = node_from_bytes(a, &CLVM_DESERIALIZER)?; + + // iterate in reverse order since we're building a linked list from + // the tail + let mut blocks = NodePtr::NIL; + for g in block_refs.into_iter().rev() { + let ref_gen = a.new_atom(g.as_ref())?; + blocks = a.new_pair(ref_gen, blocks)?; + } + + // the first argument to the generator is the serializer, followed by a list + // of the blocks it requested. + let args = a.new_pair(blocks, NodePtr::NIL)?; + Ok(a.new_pair(clvm_deserializer, args)?) +} + // Runs the generator ROM and passes in the program (transactions generator). // The program is expected to return a list of spends. Each item being: @@ -38,14 +63,17 @@ fn subtract_cost(a: &Allocator, cost_left: &mut Cost, subtract: Cost) -> Result< // the only reason we need to pass in the allocator is because the returned // SpendBundleConditions contains NodePtr fields. If that's changed, we could // create the allocator inside this functions as well. -pub fn run_block_generator, V: SpendVisitor>( +pub fn run_block_generator, V: SpendVisitor, I: IntoIterator>( a: &mut Allocator, program: &[u8], - block_refs: &[GenBuf], + block_refs: I, max_cost: u64, flags: u32, constants: &ConsensusConstants, -) -> Result { +) -> Result +where + ::IntoIter: DoubleEndedIterator, +{ let mut cost_left = max_cost; let byte_cost = program.len() as u64 * constants.cost_per_byte; @@ -58,10 +86,12 @@ pub fn run_block_generator, V: SpendVisitor>( node_from_bytes(a, program)? }; + // this is setting up the arguments to be passed to the generator ROM, + // not the actual generator (the ROM does that). // iterate in reverse order since we're building a linked list from // the tail let mut args = a.nil(); - for g in block_refs.iter().rev() { + for g in block_refs.into_iter().rev() { let ref_gen = a.new_atom(g.as_ref())?; args = a.new_pair(ref_gen, args)?; } @@ -112,39 +142,29 @@ pub fn extract_n( // you only pay cost for the generator, the puzzles and the conditions). // it also does not apply the stack depth or object allocation limits the same, // as each puzzle run in its own environment. -pub fn run_block_generator2, V: SpendVisitor>( +pub fn run_block_generator2, V: SpendVisitor, I: IntoIterator>( a: &mut Allocator, program: &[u8], - block_refs: &[GenBuf], + block_refs: I, max_cost: u64, flags: u32, constants: &ConsensusConstants, -) -> Result { +) -> Result +where + ::IntoIter: DoubleEndedIterator, +{ let byte_cost = program.len() as u64 * constants.cost_per_byte; let mut cost_left = max_cost; subtract_cost(a, &mut cost_left, byte_cost)?; - let clvm_deserializer = node_from_bytes(a, &CLVM_DESERIALIZER)?; let (program, backrefs) = if (flags & ALLOW_BACKREFS) != 0 { node_from_bytes_backrefs_record(a, program)? } else { (node_from_bytes(a, program)?, HashSet::::new()) }; - // iterate in reverse order since we're building a linked list from - // the tail - let mut blocks = a.nil(); - for g in block_refs.iter().rev() { - let ref_gen = a.new_atom(g.as_ref())?; - blocks = a.new_pair(ref_gen, blocks)?; - } - - // the first argument to the generator is the serializer, followed by a list - // of the blocks it requested. - let mut args = a.new_pair(blocks, a.nil())?; - args = a.new_pair(clvm_deserializer, args)?; - + let args = setup_generator_args(a, block_refs)?; let dialect = ChiaDialect::new(flags); let Reduction(clvm_cost, mut all_spends) = run_program(a, &dialect, program, args, cost_left)?; diff --git a/crates/chia-consensus/src/gen/test_generators.rs b/crates/chia-consensus/src/gen/test_generators.rs index 10fd98ca9..6f1794745 100644 --- a/crates/chia-consensus/src/gen/test_generators.rs +++ b/crates/chia-consensus/src/gen/test_generators.rs @@ -231,7 +231,7 @@ fn run_generator(#[case] name: &str) { for (flags, expected) in zip(&[DEFAULT_FLAGS, DEFAULT_FLAGS | MEMPOOL_MODE], expected) { println!("flags: {flags:x}"); let mut a = make_allocator(*flags); - let conds = run_block_generator::<_, MempoolVisitor>( + let conds = run_block_generator::<_, MempoolVisitor, _>( &mut a, &generator, &block_refs, @@ -246,7 +246,7 @@ fn run_generator(#[case] name: &str) { }; let mut a = make_allocator(*flags); - let conds = run_block_generator2::<_, MempoolVisitor>( + let conds = run_block_generator2::<_, MempoolVisitor, _>( &mut a, &generator, &block_refs, diff --git a/crates/chia-tools/src/bin/analyze-chain.rs b/crates/chia-tools/src/bin/analyze-chain.rs index 853ffa053..485f0f2de 100644 --- a/crates/chia-tools/src/bin/analyze-chain.rs +++ b/crates/chia-tools/src/bin/analyze-chain.rs @@ -51,9 +51,9 @@ fn main() { // after the hard fork, we run blocks without paying for the // CLVM generator ROM let block_runner = if height >= 5_496_000 { - run_block_generator2::<_, EmptyVisitor> + run_block_generator2::<_, EmptyVisitor, _> } else { - run_block_generator::<_, EmptyVisitor> + run_block_generator::<_, EmptyVisitor, _> }; let generator = block diff --git a/crates/chia-tools/src/bin/test-block-generators.rs b/crates/chia-tools/src/bin/test-block-generators.rs index 17e2863a1..862bd3ede 100644 --- a/crates/chia-tools/src/bin/test-block-generators.rs +++ b/crates/chia-tools/src/bin/test-block-generators.rs @@ -159,9 +159,9 @@ fn main() { // after the hard fork, we run blocks without paying for the // CLVM generator ROM let block_runner = if height >= 5_496_000 { - run_block_generator2::<_, EmptyVisitor> + run_block_generator2::<_, EmptyVisitor, _> } else { - run_block_generator::<_, EmptyVisitor> + run_block_generator::<_, EmptyVisitor, _> }; let mut conditions = @@ -181,7 +181,7 @@ fn main() { } if args.validate { - let mut baseline = run_block_generator::<_, EmptyVisitor>( + let mut baseline = run_block_generator::<_, EmptyVisitor, _>( &mut a, generator.as_ref(), &block_refs, diff --git a/wheel/src/run_generator.rs b/wheel/src/run_generator.rs index 60437400d..c43db5e04 100644 --- a/wheel/src/run_generator.rs +++ b/wheel/src/run_generator.rs @@ -45,9 +45,9 @@ pub fn run_block_generator( unsafe { std::slice::from_raw_parts(program.buf_ptr() as *const u8, program.len_bytes()) }; let run_block = if (flags & ANALYZE_SPENDS) == 0 { - native_run_block_generator::<_, EmptyVisitor> + native_run_block_generator::<_, EmptyVisitor, _> } else { - native_run_block_generator::<_, MempoolVisitor> + native_run_block_generator::<_, MempoolVisitor, _> }; Ok( @@ -99,9 +99,9 @@ pub fn run_block_generator2( unsafe { std::slice::from_raw_parts(program.buf_ptr() as *const u8, program.len_bytes()) }; let run_block = if (flags & ANALYZE_SPENDS) == 0 { - native_run_block_generator2::<_, EmptyVisitor> + native_run_block_generator2::<_, EmptyVisitor, _> } else { - native_run_block_generator2::<_, MempoolVisitor> + native_run_block_generator2::<_, MempoolVisitor, _> }; Ok( From 28404dcd5ff56057b7f68a6c6f45e6364b9530f0 Mon Sep 17 00:00:00 2001 From: arvidn Date: Tue, 30 Jul 2024 12:08:46 +0200 Subject: [PATCH 2/3] factor out py_to_slice() function, to simplify the python bindings --- wheel/src/api.rs | 23 +++---- wheel/src/run_generator.rs | 126 +++++++++++++++---------------------- 2 files changed, 58 insertions(+), 91 deletions(-) diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 81e6f65a8..2d5de0f6d 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -1,4 +1,4 @@ -use crate::run_generator::{run_block_generator, run_block_generator2}; +use crate::run_generator::{py_to_slice, run_block_generator, run_block_generator2}; use chia_consensus::allocator::make_allocator; use chia_consensus::consensus_constants::ConsensusConstants; use chia_consensus::gen::conditions::MempoolVisitor; @@ -107,20 +107,15 @@ pub fn confirm_not_included_already_hashed( } #[pyfunction] -pub fn tree_hash(py: Python<'_>, blob: PyBuffer) -> PyResult> { - assert!( - blob.is_c_contiguous(), - "tree_hash() must be called with a contiguous buffer" - ); - let slice = - unsafe { std::slice::from_raw_parts(blob.buf_ptr() as *const u8, blob.len_bytes()) }; +pub fn tree_hash<'a>(py: Python<'a>, blob: PyBuffer) -> PyResult> { + let slice = py_to_slice::<'a>(blob); Ok(PyBytes::new_bound(py, &tree_hash_from_bytes(slice)?)) } #[allow(clippy::too_many_arguments)] #[pyfunction] -pub fn get_puzzle_and_solution_for_coin( - py: Python<'_>, +pub fn get_puzzle_and_solution_for_coin<'a>( + py: Python<'a>, program: PyBuffer, args: PyBuffer, max_cost: Cost, @@ -131,12 +126,8 @@ pub fn get_puzzle_and_solution_for_coin( ) -> PyResult<(Bound<'_, PyBytes>, Bound<'_, PyBytes>)> { let mut allocator = make_allocator(LIMIT_HEAP); - assert!(program.is_c_contiguous(), "program must be contiguous"); - let program = - unsafe { std::slice::from_raw_parts(program.buf_ptr() as *const u8, program.len_bytes()) }; - - assert!(args.is_c_contiguous(), "args must be contiguous"); - let args = unsafe { std::slice::from_raw_parts(args.buf_ptr() as *const u8, args.len_bytes()) }; + let program = py_to_slice::<'a>(program); + let args = py_to_slice::<'a>(args); let deserialize = if (flags & ALLOW_BACKREFS) != 0 { node_from_bytes_backrefs diff --git a/wheel/src/run_generator.rs b/wheel/src/run_generator.rs index c43db5e04..bc364728b 100644 --- a/wheel/src/run_generator.rs +++ b/wheel/src/run_generator.rs @@ -13,110 +13,86 @@ use pyo3::buffer::PyBuffer; use pyo3::prelude::*; use pyo3::types::PyList; +pub fn py_to_slice<'a>(buf: PyBuffer) -> &'a [u8] { + assert!(buf.is_c_contiguous(), "buffer must be contiguous"); + unsafe { std::slice::from_raw_parts(buf.buf_ptr() as *const u8, buf.len_bytes()) } +} + #[pyfunction] -pub fn run_block_generator( - _py: Python<'_>, +pub fn run_block_generator<'a>( + _py: Python<'a>, program: PyBuffer, block_refs: &Bound<'_, PyList>, max_cost: Cost, flags: u32, constants: &ConsensusConstants, -) -> PyResult<(Option, Option)> { +) -> (Option, Option) { let mut allocator = make_allocator(flags); - let mut refs = Vec::<&[u8]>::new(); - for g in block_refs { - let buf = g.extract::>()?; - - assert!( - buf.is_c_contiguous(), - "block_refs buffers must be contiguous" - ); - let slice = - unsafe { std::slice::from_raw_parts(buf.buf_ptr() as *const u8, buf.len_bytes()) }; - refs.push(slice); - } - - assert!( - program.is_c_contiguous(), - "program buffer must be contiguous" - ); - let program = - unsafe { std::slice::from_raw_parts(program.buf_ptr() as *const u8, program.len_bytes()) }; - + let refs = block_refs.into_iter().map(|b| { + let buf = b + .extract::>() + .expect("block_refs should be a list of buffers"); + py_to_slice::<'a>(buf) + }); + let program = py_to_slice::<'a>(program); let run_block = if (flags & ANALYZE_SPENDS) == 0 { native_run_block_generator::<_, EmptyVisitor, _> } else { native_run_block_generator::<_, MempoolVisitor, _> }; - Ok( - match run_block(&mut allocator, program, &refs, max_cost, flags, constants) { - Ok(spend_bundle_conds) => ( - None, - Some(OwnedSpendBundleConditions::from( - &allocator, - spend_bundle_conds, - )), - ), - Err(ValidationErr(_, error_code)) => { - // a validation error occurred - (Some(error_code.into()), None) - } - }, - ) + match run_block(&mut allocator, program, refs, max_cost, flags, constants) { + Ok(spend_bundle_conds) => ( + None, + Some(OwnedSpendBundleConditions::from( + &allocator, + spend_bundle_conds, + )), + ), + Err(ValidationErr(_, error_code)) => { + // a validation error occurred + (Some(error_code.into()), None) + } + } } #[pyfunction] -pub fn run_block_generator2( - _py: Python<'_>, +pub fn run_block_generator2<'a>( + _py: Python<'a>, program: PyBuffer, block_refs: &Bound<'_, PyList>, max_cost: Cost, flags: u32, constants: &ConsensusConstants, -) -> PyResult<(Option, Option)> { +) -> (Option, Option) { let mut allocator = make_allocator(flags); - let mut refs = Vec::<&[u8]>::new(); - for g in block_refs { - let buf = g.extract::>()?; - - assert!( - buf.is_c_contiguous(), - "block_refs buffers must be contiguous" - ); - let slice = - unsafe { std::slice::from_raw_parts(buf.buf_ptr() as *const u8, buf.len_bytes()) }; - refs.push(slice); - } - - assert!( - program.is_c_contiguous(), - "program buffer must be contiguous" - ); - let program = - unsafe { std::slice::from_raw_parts(program.buf_ptr() as *const u8, program.len_bytes()) }; + let refs = block_refs.into_iter().map(|b| { + let buf = b + .extract::>() + .expect("block_refs must be list of buffers"); + py_to_slice::<'a>(buf) + }); + let program = py_to_slice::<'a>(program); let run_block = if (flags & ANALYZE_SPENDS) == 0 { native_run_block_generator2::<_, EmptyVisitor, _> } else { native_run_block_generator2::<_, MempoolVisitor, _> }; - Ok( - match run_block(&mut allocator, program, &refs, max_cost, flags, constants) { - Ok(spend_bundle_conds) => ( - None, - Some(OwnedSpendBundleConditions::from( - &allocator, - spend_bundle_conds, - )), - ), - Err(ValidationErr(_, error_code)) => { - // a validation error occurred - (Some(error_code.into()), None) - } - }, - ) + match run_block(&mut allocator, program, refs, max_cost, flags, constants) { + Ok(spend_bundle_conds) => ( + None, + Some(OwnedSpendBundleConditions::from( + &allocator, + spend_bundle_conds, + )), + ), + Err(ValidationErr(_, error_code)) => { + // a validation error occurred + (Some(error_code.into()), None) + } + } } From f6c19821ed3eff1b6477a0b52715fd8f538cb44f Mon Sep 17 00:00:00 2001 From: arvidn Date: Tue, 30 Jul 2024 12:57:09 +0200 Subject: [PATCH 3/3] simplify error handling in run_chia_program() and get_puzzle_and_solution_for_coin() and remove eval_err_to_pyresult() --- wheel/src/adapt_response.rs | 11 ----------- wheel/src/api.rs | 33 +++++++++++++++++---------------- wheel/src/lib.rs | 1 - wheel/src/run_program.rs | 23 ++++++++++++----------- 4 files changed, 29 insertions(+), 39 deletions(-) delete mode 100644 wheel/src/adapt_response.rs diff --git a/wheel/src/adapt_response.rs b/wheel/src/adapt_response.rs deleted file mode 100644 index 263aeae99..000000000 --- a/wheel/src/adapt_response.rs +++ /dev/null @@ -1,11 +0,0 @@ -use clvmr::allocator::Allocator; -use clvmr::reduction::EvalErr; -use clvmr::serde::node_to_bytes; - -use pyo3::exceptions::*; -use pyo3::prelude::*; - -pub fn eval_err_to_pyresult(eval_err: EvalErr, allocator: &Allocator) -> PyResult { - let blob = node_to_bytes(allocator, eval_err.0).ok().map(hex::encode); - Err(PyValueError::new_err((eval_err.1, blob))) -} diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 2d5de0f6d..43c7e88d3 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -52,7 +52,6 @@ use std::iter::zip; use crate::run_program::{run_chia_program, serialized_length}; -use crate::adapt_response::eval_err_to_pyresult; use chia_consensus::fast_forward::fast_forward_singleton as native_ff; use chia_consensus::gen::get_puzzle_and_solution::get_puzzle_and_solution_for_coin as parse_puzzle_solution; use chia_consensus::gen::validation_error::ValidationErr; @@ -138,14 +137,19 @@ pub fn get_puzzle_and_solution_for_coin<'a>( let args = deserialize(&mut allocator, args)?; let dialect = &ChiaDialect::new(flags); - let r = py.allow_threads(|| -> Result<(NodePtr, NodePtr), EvalErr> { - let Reduction(_cost, result) = - run_program(&mut allocator, dialect, program, args, max_cost)?; - match parse_puzzle_solution(&allocator, result, find_parent, find_amount, find_ph) { - Err(ValidationErr(n, _)) => Err(EvalErr(n, "coin not found".to_string())), - Ok(pair) => Ok(pair), - } - }); + let (puzzle, solution) = py + .allow_threads(|| -> Result<(NodePtr, NodePtr), EvalErr> { + let Reduction(_cost, result) = + run_program(&mut allocator, dialect, program, args, max_cost)?; + match parse_puzzle_solution(&allocator, result, find_parent, find_amount, find_ph) { + Err(ValidationErr(n, _)) => Err(EvalErr(n, "coin not found".to_string())), + Ok(pair) => Ok(pair), + } + }) + .map_err(|e| { + let blob = node_to_bytes(&allocator, e.0).ok().map(hex::encode); + PyValueError::new_err((e.1, blob)) + })?; // keep serializing normally, until wallets support backrefs let serialize = node_to_bytes; @@ -156,13 +160,10 @@ pub fn get_puzzle_and_solution_for_coin<'a>( node_to_bytes }; */ - match r { - Err(eval_err) => eval_err_to_pyresult(eval_err, &allocator), - Ok((puzzle, solution)) => Ok(( - PyBytes::new_bound(py, &serialize(&allocator, puzzle)?), - PyBytes::new_bound(py, &serialize(&allocator, solution)?), - )), - } + Ok(( + PyBytes::new_bound(py, &serialize(&allocator, puzzle)?), + PyBytes::new_bound(py, &serialize(&allocator, solution)?), + )) } #[pyfunction] diff --git a/wheel/src/lib.rs b/wheel/src/lib.rs index 3b42df578..b9d7c6c9d 100644 --- a/wheel/src/lib.rs +++ b/wheel/src/lib.rs @@ -1,6 +1,5 @@ #![allow(unsafe_code, clippy::needless_pass_by_value)] -mod adapt_response; mod api; mod run_generator; mod run_program; diff --git a/wheel/src/run_program.rs b/wheel/src/run_program.rs index 659cbaf40..8abceeb76 100644 --- a/wheel/src/run_program.rs +++ b/wheel/src/run_program.rs @@ -1,4 +1,3 @@ -use super::adapt_response::eval_err_to_pyresult; use chia_consensus::allocator::make_allocator; use chia_consensus::gen::flags::ALLOW_BACKREFS; use chia_protocol::LazyNode; @@ -6,8 +5,11 @@ use clvmr::chia_dialect::ChiaDialect; use clvmr::cost::Cost; use clvmr::reduction::Response; use clvmr::run_program::run_program; -use clvmr::serde::{node_from_bytes, node_from_bytes_backrefs, serialized_length_from_bytes}; +use clvmr::serde::{ + node_from_bytes, node_from_bytes_backrefs, node_to_bytes, serialized_length_from_bytes, +}; use pyo3::buffer::PyBuffer; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use std::rc::Rc; @@ -31,7 +33,7 @@ pub fn run_chia_program( ) -> PyResult<(Cost, LazyNode)> { let mut allocator = make_allocator(flags); - let r: Response = (|| -> PyResult { + let reduction = (|| -> PyResult { let deserialize = if (flags & ALLOW_BACKREFS) != 0 { node_from_bytes_backrefs } else { @@ -42,12 +44,11 @@ pub fn run_chia_program( let dialect = ChiaDialect::new(flags); Ok(py.allow_threads(|| run_program(&mut allocator, &dialect, program, args, max_cost))) - })()?; - match r { - Ok(reduction) => { - let val = LazyNode::new(Rc::new(allocator), reduction.1); - Ok((reduction.0, val)) - } - Err(eval_err) => eval_err_to_pyresult(eval_err, &allocator), - } + })()? + .map_err(|e| { + let blob = node_to_bytes(&allocator, e.0).ok().map(hex::encode); + PyValueError::new_err((e.1, blob)) + })?; + let val = LazyNode::new(Rc::new(allocator), reduction.1); + Ok((reduction.0, val)) }