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/run_block_generator.rs b/crates/chia-consensus/src/gen/run_block_generator.rs index 8a0360d4f..e73ebf01d 100644 --- a/crates/chia-consensus/src/gen/run_block_generator.rs +++ b/crates/chia-consensus/src/gen/run_block_generator.rs @@ -15,7 +15,11 @@ use clvmr::run_program::run_program; use clvmr::serde::{node_from_bytes, node_from_bytes_backrefs, node_from_bytes_backrefs_record}; use std::collections::{HashMap, HashSet}; -fn subtract_cost(a: &Allocator, cost_left: &mut Cost, subtract: Cost) -> Result<(), ValidationErr> { +pub fn subtract_cost( + a: &Allocator, + cost_left: &mut Cost, + subtract: Cost, +) -> Result<(), ValidationErr> { if subtract > *cost_left { Err(ValidationErr(a.nil(), ErrorCode::CostExceeded)) } else { 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/lib.rs b/crates/chia-consensus/src/lib.rs index b7e99a193..018eac02b 100644 --- a/crates/chia-consensus/src/lib.rs +++ b/crates/chia-consensus/src/lib.rs @@ -8,3 +8,5 @@ pub mod gen; pub mod generator_rom; pub mod merkle_set; pub mod merkle_tree; +pub mod spendbundle_conditions; +pub mod spendbundle_validation; diff --git a/crates/chia-consensus/src/spendbundle_conditions.rs b/crates/chia-consensus/src/spendbundle_conditions.rs new file mode 100644 index 000000000..55fd9051f --- /dev/null +++ b/crates/chia-consensus/src/spendbundle_conditions.rs @@ -0,0 +1,326 @@ +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::run_block_generator::subtract_cost; +use crate::gen::validation_error::ValidationErr; +use crate::spendbundle_validation::get_flags_for_height_and_constants; +use chia_protocol::SpendBundle; +use clvm_utils::tree_hash; +use clvmr::allocator::Allocator; +use clvmr::chia_dialect::ChiaDialect; +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 { + 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 ret = SpendBundleConditions::default(); + let mut state = ParseState::default(); + + for coin_spend in &spend_bundle.coin_spends { + // process the spend + 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(a, &dialect, puz, sol, cost_left)?; + + subtract_cost(a, &mut cost_left, clvm_cost)?; + + let buf = tree_hash(a, puz); + let puzzle_hash = a.new_atom(&buf)?; + process_single_spend::( + a, + &mut ret, + &mut state, + parent, + puzzle_hash, + amount, + conditions, + flags, + &mut cost_left, + constants, + )?; + } + + validate_conditions(a, &ret, state, a.nil(), flags)?; + assert!(max_cost >= cost_left); + ret.cost = max_cost - cost_left; + Ok(ret) +} + +#[cfg(test)] +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::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"); + + all_spends = first(&a, all_spends).expect("first"); + + let mut spends = Vec::::new(); + + // 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 mut a = make_allocator(LIMIT_HEAP); + let conds = get_conditions_from_spendbundle( + &mut a, + &bundle, + 11_000_000_000, + 5_000_000, + &TEST_CONSTANTS, + ); + + 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()"); + } + + if output != expected { + print_diff(&output, expected); + panic!("mismatching condition output"); + } + } +} diff --git a/crates/chia-consensus/src/spendbundle_validation.rs b/crates/chia-consensus/src/spendbundle_validation.rs new file mode 100644 index 000000000..6f4d939e9 --- /dev/null +++ b/crates/chia-consensus/src/spendbundle_validation.rs @@ -0,0 +1,54 @@ +use clvmr::{ENABLE_BLS_OPS_OUTSIDE_GUARD, ENABLE_FIXED_DIV}; + +use crate::consensus_constants::ConsensusConstants; +use crate::gen::flags::{ALLOW_BACKREFS, DISALLOW_INFINITY_G1, ENABLE_MESSAGE_CONDITIONS}; + +pub fn get_flags_for_height_and_constants(height: u32, constants: &ConsensusConstants) -> u32 { + let mut flags: u32 = 0; + + if height >= constants.soft_fork4_height { + flags |= ENABLE_MESSAGE_CONDITIONS; + } + + if height >= constants.soft_fork5_height { + flags |= DISALLOW_INFINITY_G1; + } + + if height >= constants.hard_fork_height { + // the hard-fork initiated with 2.0. To activate June 2024 + // * costs are ascribed to some unknown condition codes, to allow for + // soft-forking in new conditions with cost + // * a new condition, SOFTFORK, is added which takes a first parameter to + // specify its cost. This allows soft-forks similar to the softfork + // operator + // * BLS operators introduced in the soft-fork (behind the softfork + // guard) are made available outside of the guard. + // * division with negative numbers are allowed, and round toward + // negative infinity + // * AGG_SIG_* conditions are allowed to have unknown additional + // arguments + // * Allow the block generator to be serialized with the improved clvm + // serialization format (with back-references) + flags = flags | ENABLE_BLS_OPS_OUTSIDE_GUARD | ENABLE_FIXED_DIV | ALLOW_BACKREFS; + } + flags +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus_constants::TEST_CONSTANTS; + use rstest::rstest; + + #[rstest] + #[case(0, 0)] + #[case(TEST_CONSTANTS.hard_fork_height, ENABLE_BLS_OPS_OUTSIDE_GUARD | ENABLE_FIXED_DIV | ALLOW_BACKREFS)] + #[case(TEST_CONSTANTS.soft_fork4_height, ENABLE_BLS_OPS_OUTSIDE_GUARD | ENABLE_FIXED_DIV | ALLOW_BACKREFS | ENABLE_MESSAGE_CONDITIONS)] + #[case(TEST_CONSTANTS.soft_fork5_height, ENABLE_BLS_OPS_OUTSIDE_GUARD | ENABLE_FIXED_DIV | ALLOW_BACKREFS | ENABLE_MESSAGE_CONDITIONS | DISALLOW_INFINITY_G1)] + fn test_get_flags(#[case] height: u32, #[case] expected_value: u32) { + assert_eq!( + get_flags_for_height_and_constants(height, &TEST_CONSTANTS), + expected_value + ); + } +} diff --git a/wheel/generate_type_stubs.py b/wheel/generate_type_stubs.py index 008b8ecdb..8c8ee653f 100644 --- a/wheel/generate_type_stubs.py +++ b/wheel/generate_type_stubs.py @@ -305,6 +305,19 @@ def confirm_not_included_already_hashed( proof: bytes, ) -> bool: ... +def get_conditions_from_spendbundle( + spend_bundle: SpendBundle, + max_cost: int, + constants: ConsensusConstants, + height: int, +) -> SpendBundleConditions: ... + +def get_flags_for_height_and_constants( + height: int, + constants: ConsensusConstants +) -> int: ... + + NO_UNKNOWN_CONDS: int = ... STRICT_ARGS_COUNT: int = ... LIMIT_HEAP: int = ... diff --git a/wheel/python/chia_rs/chia_rs.pyi b/wheel/python/chia_rs/chia_rs.pyi index 3a543d342..4fc806c61 100644 --- a/wheel/python/chia_rs/chia_rs.pyi +++ b/wheel/python/chia_rs/chia_rs.pyi @@ -49,6 +49,19 @@ def confirm_not_included_already_hashed( proof: bytes, ) -> bool: ... +def get_conditions_from_spendbundle( + spend_bundle: SpendBundle, + max_cost: int, + constants: ConsensusConstants, + height: int, +) -> SpendBundleConditions: ... + +def get_flags_for_height_and_constants( + height: int, + constants: ConsensusConstants +) -> int: ... + + NO_UNKNOWN_CONDS: int = ... STRICT_ARGS_COUNT: int = ... LIMIT_HEAP: int = ... diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 43c7e88d3..7ab98ef3c 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -12,6 +12,8 @@ use chia_consensus::gen::solution_generator::solution_generator as native_soluti use chia_consensus::gen::solution_generator::solution_generator_backrefs as native_solution_generator_backrefs; use chia_consensus::merkle_set::compute_merkle_set_root as compute_merkle_root_impl; use chia_consensus::merkle_tree::{validate_merkle_proof, MerkleSet}; +use chia_consensus::spendbundle_conditions::get_conditions_from_spendbundle; +use chia_consensus::spendbundle_validation::get_flags_for_height_and_constants; use chia_protocol::{ BlockRecord, Bytes32, ChallengeBlockInfo, ChallengeChainSubSlot, ClassgroupElement, Coin, CoinSpend, CoinState, CoinStateFilters, CoinStateUpdate, EndOfSubSlotBundle, Foliage, @@ -41,7 +43,7 @@ use chia_protocol::{ use clvm_utils::tree_hash_from_bytes; use clvmr::{ENABLE_BLS_OPS_OUTSIDE_GUARD, ENABLE_FIXED_DIV, LIMIT_HEAP, NO_UNKNOWN_OPS}; use pyo3::buffer::PyBuffer; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyRuntimeError, PyTypeError, PyValueError}; use pyo3::prelude::*; use pyo3::pybacked::PyBackedBytes; use pyo3::types::PyBytes; @@ -356,6 +358,32 @@ fn fast_forward_singleton<'p>( )) } +#[pyfunction] +#[pyo3(name = "get_conditions_from_spendbundle")] +pub fn py_get_conditions_from_spendbundle( + spend_bundle: &SpendBundle, + max_cost: u64, + constants: &ConsensusConstants, + height: u32, +) -> PyResult { + 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] +#[pyo3(name = "get_flags_for_height_and_constants")] +pub fn py_get_flags_for_height_and_constants(height: u32, constants: &ConsensusConstants) -> u32 { + get_flags_for_height_and_constants(height, constants) +} + #[pymodule] pub fn chia_rs(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // generator functions @@ -385,6 +413,10 @@ pub fn chia_rs(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(confirm_included_already_hashed, m)?)?; m.add_function(wrap_pyfunction!(confirm_not_included_already_hashed, m)?)?; + // multithread validattion + m.add_function(wrap_pyfunction!(py_get_conditions_from_spendbundle, m)?)?; + m.add_function(wrap_pyfunction!(py_get_flags_for_height_and_constants, m)?)?; + // clvm functions m.add("NO_UNKNOWN_CONDS", NO_UNKNOWN_CONDS)?; m.add("STRICT_ARGS_COUNT", STRICT_ARGS_COUNT)?;