diff --git a/crates/chia-consensus/src/gen/mod.rs b/crates/chia-consensus/src/gen/mod.rs index a48e1022d..246d2bd15 100644 --- a/crates/chia-consensus/src/gen/mod.rs +++ b/crates/chia-consensus/src/gen/mod.rs @@ -17,4 +17,4 @@ pub mod validation_error; // unoptimized builds. Only run these with --release #[cfg(not(debug_assertions))] #[cfg(test)] -mod test_generators; +pub(crate) mod test_generators; diff --git a/crates/chia-consensus/src/gen/test_generators.rs b/crates/chia-consensus/src/gen/test_generators.rs index 6f1794745..fafcbac8c 100644 --- a/crates/chia-consensus/src/gen/test_generators.rs +++ b/crates/chia-consensus/src/gen/test_generators.rs @@ -12,7 +12,7 @@ use text_diff::Difference; use rstest::rstest; -fn print_conditions(a: &Allocator, c: &SpendBundleConditions) -> String { +pub(crate) fn print_conditions(a: &Allocator, c: &SpendBundleConditions) -> String { let mut ret = String::new(); if c.reserve_fee > 0 { ret += &format!("RESERVE_FEE: {}\n", c.reserve_fee); @@ -115,7 +115,7 @@ fn print_conditions(a: &Allocator, c: &SpendBundleConditions) -> String { ret } -fn print_diff(output: &str, expected: &str) { +pub(crate) fn print_diff(output: &str, expected: &str) { println!("\x1b[102m \x1b[0m - output from test"); println!("\x1b[101m \x1b[0m - expected output"); for diff in diff(expected, output, "\n").1 { diff --git a/crates/chia-consensus/src/spendbundle_conditions.rs b/crates/chia-consensus/src/spendbundle_conditions.rs index ebcef1ed8..55fd9051f 100644 --- a/crates/chia-consensus/src/spendbundle_conditions.rs +++ b/crates/chia-consensus/src/spendbundle_conditions.rs @@ -1,10 +1,8 @@ -use crate::allocator::make_allocator; use crate::consensus_constants::ConsensusConstants; use crate::gen::conditions::{ process_single_spend, validate_conditions, MempoolVisitor, ParseState, SpendBundleConditions, }; use crate::gen::flags::MEMPOOL_MODE; -use crate::gen::owned_conditions::OwnedSpendBundleConditions; use crate::gen::run_block_generator::subtract_cost; use crate::gen::validation_error::ValidationErr; use crate::spendbundle_validation::get_flags_for_height_and_constants; @@ -12,41 +10,40 @@ use chia_protocol::SpendBundle; use clvm_utils::tree_hash; use clvmr::allocator::Allocator; use clvmr::chia_dialect::ChiaDialect; -use clvmr::chia_dialect::LIMIT_HEAP; use clvmr::reduction::Reduction; use clvmr::run_program::run_program; use clvmr::serde::node_from_bytes; pub fn get_conditions_from_spendbundle( + a: &mut Allocator, spend_bundle: &SpendBundle, max_cost: u64, height: u32, constants: &ConsensusConstants, -) -> Result { +) -> Result { let flags = get_flags_for_height_and_constants(height, constants) | MEMPOOL_MODE; // below is an adapted version of the code from run_block_generators::run_block_generator2() // it assumes no block references are passed in let mut cost_left = max_cost; let dialect = ChiaDialect::new(flags); - let mut a: Allocator = make_allocator(LIMIT_HEAP); let mut ret = SpendBundleConditions::default(); let mut state = ParseState::default(); for coin_spend in &spend_bundle.coin_spends { // process the spend - let puz = node_from_bytes(&mut a, coin_spend.puzzle_reveal.as_slice())?; - let sol = node_from_bytes(&mut a, coin_spend.solution.as_slice())?; + let puz = node_from_bytes(a, coin_spend.puzzle_reveal.as_slice())?; + let sol = node_from_bytes(a, coin_spend.solution.as_slice())?; let parent = a.new_atom(coin_spend.coin.parent_coin_info.as_slice())?; let amount = a.new_number(coin_spend.coin.amount.into())?; - let Reduction(clvm_cost, conditions) = run_program(&mut a, &dialect, puz, sol, cost_left)?; + let Reduction(clvm_cost, conditions) = run_program(a, &dialect, puz, sol, cost_left)?; - subtract_cost(&a, &mut cost_left, clvm_cost)?; + subtract_cost(a, &mut cost_left, clvm_cost)?; - let buf = tree_hash(&a, puz); + let buf = tree_hash(a, puz); let puzzle_hash = a.new_atom(&buf)?; process_single_spend::( - &a, + a, &mut ret, &mut state, parent, @@ -59,11 +56,10 @@ pub fn get_conditions_from_spendbundle( )?; } - validate_conditions(&a, &ret, state, a.nil(), flags)?; + validate_conditions(a, &ret, state, a.nil(), flags)?; assert!(max_cost >= cost_left); ret.cost = max_cost - cost_left; - let osbc = OwnedSpendBundleConditions::from(&a, ret); - Ok(osbc) + Ok(ret) } #[cfg(test)] @@ -71,37 +67,260 @@ mod tests { use crate::consensus_constants::TEST_CONSTANTS; use super::*; + use crate::allocator::make_allocator; + use crate::gen::conditions::{ELIGIBLE_FOR_DEDUP, ELIGIBLE_FOR_FF}; use chia_bls::Signature; - use chia_protocol::{Coin, CoinSpend, Program}; - use hex_literal::hex; - - #[test] - fn test_get_conditions_from_spendbundle() { - let test_coin = Coin::new( - hex!("4444444444444444444444444444444444444444444444444444444444444444").into(), - hex!("3333333333333333333333333333333333333333333333333333333333333333").into(), - 1, - ); + use chia_protocol::CoinSpend; + use chia_traits::Streamable; + use clvmr::chia_dialect::LIMIT_HEAP; + use rstest::rstest; + use std::fs::read; + + #[rstest] + #[case("3000253", 8, 2, 13_344_870)] + #[case("1000101", 34, 15, 66_723_677)] + fn test_get_conditions_from_spendbundle( + #[case] filename: &str, + #[case] spends: usize, + #[case] additions: usize, + #[values(0, 1, 1000000, 5000000)] height: u32, + #[case] cost: u64, + ) { + let bundle = SpendBundle::from_bytes( + &read(format!("../../test-bundles/{filename}.bundle")).expect("read file"), + ) + .expect("parse bundle"); + + let mut a = make_allocator(LIMIT_HEAP); + let conditions = + get_conditions_from_spendbundle(&mut a, &bundle, cost, height, &TEST_CONSTANTS) + .expect("get_conditions_from_spendbundle"); + + assert_eq!(conditions.spends.len(), spends); + let create_coins = conditions + .spends + .iter() + .fold(0, |sum, spend| sum + spend.create_coin.len()); + assert_eq!(create_coins, additions); + assert_eq!(conditions.cost, cost); + } + + #[rstest] + #[case("bb13")] + #[case("e3c0")] + fn test_get_conditions_from_spendbundle_fast_forward( + #[case] filename: &str, + #[values(0, 1, 1_000_000, 5_000_000)] height: u32, + ) { + let cost = 2_125_866; + let spend = CoinSpend::from_bytes( + &read(format!("../../ff-tests/{filename}.spend")).expect("read file"), + ) + .expect("parse Spend"); + + let bundle = SpendBundle::new(vec![spend], Signature::default()); + + let mut a = make_allocator(LIMIT_HEAP); + let conditions = + get_conditions_from_spendbundle(&mut a, &bundle, cost, height, &TEST_CONSTANTS) + .expect("get_conditions_from_spendbundle"); + + assert_eq!(conditions.spends.len(), 1); + let spend = &conditions.spends[0]; + assert_eq!(spend.flags, ELIGIBLE_FOR_FF | ELIGIBLE_FOR_DEDUP); + assert_eq!(conditions.cost, cost); + } + + #[cfg(not(debug_assertions))] + use crate::gen::flags::{ALLOW_BACKREFS, ENABLE_MESSAGE_CONDITIONS}; + + #[cfg(not(debug_assertions))] + const DEFAULT_FLAGS: u32 = ALLOW_BACKREFS | ENABLE_MESSAGE_CONDITIONS | MEMPOOL_MODE; + + // given a block generator and block-refs, convert run the generator to + // produce the SpendBundle for the block without runningi, or validating, + // the puzzles. + #[cfg(not(debug_assertions))] + fn convert_block_to_bundle(generator: &[u8], block_refs: &[Vec]) -> SpendBundle { + use crate::gen::run_block_generator::extract_n; + use crate::gen::run_block_generator::setup_generator_args; + use crate::gen::validation_error::ErrorCode; + use chia_protocol::Coin; + use clvmr::op_utils::first; + use clvmr::serde::node_from_bytes_backrefs; + use clvmr::serde::node_to_bytes; + + let mut a = make_allocator(DEFAULT_FLAGS); + + let generator = node_from_bytes_backrefs(&mut a, generator).expect("node_from_bytes"); + let args = setup_generator_args(&mut a, block_refs).expect("setup_generator_args"); + let dialect = ChiaDialect::new(DEFAULT_FLAGS); + let Reduction(_, mut all_spends) = + run_program(&mut a, &dialect, generator, args, 11_000_000_000).expect("run_program"); - let solution = hex!("ffff31ffb0997cc43ed8788f841fcf3071f6f212b89ba494b6ebaf1bda88c3f9de9d968a61f3b7284a5ee13889399ca71a026549a2ff8568656c6c6f8080").to_vec(); - // ((49 0x997cc43ed8788f841fcf3071f6f212b89ba494b6ebaf1bda88c3f9de9d968a61f3b7284a5ee13889399ca71a026549a2 "hello")) + all_spends = first(&a, all_spends).expect("first"); - let spend = CoinSpend::new(test_coin, Program::new(vec![1_u8].into()), solution.into()); + let mut spends = Vec::::new(); - let coin_spends: Vec = vec![spend]; - let spend_bundle = SpendBundle { - coin_spends, - aggregated_signature: Signature::default(), + // at this point all_spends is a list of: + // (parent-coin-id puzzle-reveal amount solution . extra) + // where extra may be nil, or additional extension data + while let Some((spend, rest)) = a.next(all_spends) { + all_spends = rest; + // process the spend + let [parent_id, puzzle, amount, solution, _spend_level_extra] = + extract_n::<5>(&a, spend, ErrorCode::InvalidCondition).expect("extract_n"); + + spends.push(CoinSpend::new( + Coin::new( + a.atom(parent_id).as_ref().try_into().expect("parent_id"), + tree_hash(&a, puzzle).into(), + a.number(amount).try_into().expect("amount"), + ), + node_to_bytes(&a, puzzle).expect("node_to_bytes").into(), + node_to_bytes(&a, solution).expect("node_to_bytes").into(), + )); + } + SpendBundle::new(spends, Signature::default()) + } + + #[cfg(not(debug_assertions))] + #[rstest] + #[case("new-agg-sigs")] + #[case("infinity-g1")] + #[case("block-1ee588dc")] + #[case("block-6fe59b24")] + #[case("block-b45268ac")] + #[case("block-c2a8df0d")] + #[case("block-e5002df2")] + #[case("block-4671894")] + #[case("block-225758")] + #[case("assert-puzzle-announce-fail")] + #[case("block-834752")] + #[case("block-834752-compressed")] + #[case("block-834760")] + #[case("block-834761")] + #[case("block-834765")] + #[case("block-834766")] + #[case("block-834768")] + #[case("create-coin-different-amounts")] + #[case("create-coin-hint-duplicate-outputs")] + #[case("create-coin-hint")] + #[case("create-coin-hint2")] + #[case("deep-recursion-plus")] + #[case("double-spend")] + #[case("duplicate-coin-announce")] + #[case("duplicate-create-coin")] + #[case("duplicate-height-absolute-div")] + #[case("duplicate-height-absolute-substr-tail")] + #[case("duplicate-height-absolute-substr")] + #[case("duplicate-height-absolute")] + #[case("duplicate-height-relative")] + #[case("duplicate-outputs")] + #[case("duplicate-reserve-fee")] + #[case("duplicate-seconds-absolute")] + #[case("duplicate-seconds-relative")] + #[case("height-absolute-ladder")] + //#[case("infinite-recursion1")] + //#[case("infinite-recursion2")] + //#[case("infinite-recursion3")] + //#[case("infinite-recursion4")] + #[case("invalid-conditions")] + #[case("just-puzzle-announce")] + #[case("many-create-coin")] + #[case("many-large-ints-negative")] + #[case("many-large-ints")] + #[case("max-height")] + #[case("multiple-reserve-fee")] + #[case("negative-reserve-fee")] + //#[case("recursion-pairs")] + #[case("unknown-condition")] + #[case("duplicate-messages")] + fn run_generator(#[case] name: &str) { + use crate::gen::run_block_generator::run_block_generator; + use crate::gen::test_generators::{print_conditions, print_diff}; + use std::fs::read_to_string; + + let filename = format!("../../generator-tests/{name}.txt"); + println!("file: {filename}"); + let test_file = read_to_string(filename).expect("test file not found"); + let (generator, expected) = test_file.split_once('\n').expect("invalid test file"); + let generator_buffer = hex::decode(generator).expect("invalid hex encoded generator"); + + let expected = match expected.split_once("STRICT:\n") { + Some((_, m)) => m, + None => expected, + }; + + let mut block_refs = Vec::>::new(); + + let filename = format!("../../generator-tests/{name}.env"); + if let Ok(env_hex) = read_to_string(&filename) { + println!("block-ref file: {filename}"); + block_refs.push(hex::decode(env_hex).expect("hex decode env-file")); + } + + let bundle = convert_block_to_bundle(&generator_buffer, &block_refs); + + // run the whole block through run_block_generator() to ensure the + // output conditions match and update the cost. The cost + // of just the spend bundle will be lower + let (block_cost, block_output) = { + let mut a = make_allocator(DEFAULT_FLAGS); + let block_conds = run_block_generator::<_, MempoolVisitor, _>( + &mut a, + &generator_buffer, + &block_refs, + 11_000_000_000, + DEFAULT_FLAGS, + &TEST_CONSTANTS, + ); + match block_conds { + Ok(ref conditions) => (conditions.cost, print_conditions(&a, &conditions)), + Err(code) => { + println!("error: {code:?}"); + (0, format!("FAILED: {}\n", u32::from(code.1))) + } + } }; - let osbc = get_conditions_from_spendbundle( - &spend_bundle, - TEST_CONSTANTS.max_block_cost_clvm, - 236, + + let mut a = make_allocator(LIMIT_HEAP); + let conds = get_conditions_from_spendbundle( + &mut a, + &bundle, + 11_000_000_000, + 5_000_000, &TEST_CONSTANTS, - ) - .expect("test should pass"); + ); + + let output = match conds { + Ok(mut conditions) => { + // the cost of running the spend bundle should never be higher + // than the whole block but it's likely less. + println!("block_cost: {block_cost}"); + println!("bundle_cost: {}", conditions.cost); + assert!(conditions.cost <= block_cost); + assert!(conditions.cost > 0); + // update the cost we print here, just to be compatible with + // the test cases we have. We've already ensured the cost is + // lower + conditions.cost = block_cost; + print_conditions(&a, &conditions) + } + Err(code) => { + println!("error: {code:?}"); + format!("FAILED: {}\n", u32::from(code.1)) + } + }; + + if output != block_output { + print_diff(&output, &block_output); + panic!("run_block_generator produced a different result than get_conditions_from_spendbundle()"); + } - assert!(osbc.spends.len() == 1); - assert!(osbc.agg_sig_unsafe.len() == 1); + if output != expected { + print_diff(&output, expected); + panic!("mismatching condition output"); + } } } diff --git a/wheel/src/api.rs b/wheel/src/api.rs index f6127c8b0..7ab98ef3c 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -366,13 +366,16 @@ pub fn py_get_conditions_from_spendbundle( constants: &ConsensusConstants, height: u32, ) -> PyResult { - let osbc = get_conditions_from_spendbundle(spend_bundle, max_cost, height, constants).map_err( - |e| { - let error_code: u32 = e.1.into(); - PyErr::new::(error_code) - }, - )?; - Ok(osbc) + use chia_consensus::allocator::make_allocator; + use chia_consensus::gen::owned_conditions::OwnedSpendBundleConditions; + let mut a = make_allocator(LIMIT_HEAP); + let conditions = + get_conditions_from_spendbundle(&mut a, spend_bundle, max_cost, height, constants) + .map_err(|e| { + let error_code: u32 = e.1.into(); + PyErr::new::(error_code) + })?; + Ok(OwnedSpendBundleConditions::from(&a, conditions)) } #[pyfunction]