Skip to content

Commit

Permalink
Add functions get_flags_for_height_and_constants() and `get_conditi…
Browse files Browse the repository at this point in the history
…ons_from_spendbundle()` (#634)

* initial commit

* add to lib

* add api info for pybindings

* cargo fmt

* use uncached tree_hash()

* fmt

* add test for get_conditions_from_spendbundle()

* add regression test for get_flags

* use underscored numbers for readability

* make get_conditions_from_spendbundle() return a non-owned SpendBundleConditions (to enable reusing existing test facilities). Extend the tests to cover the block-generator test cases

* paramterise test and use exact fork values

* use named flags for case definitions

* remove unnecessary 0

* fix typestub incorrect name

---------

Co-authored-by: Arvid Norberg <[email protected]>
  • Loading branch information
matt-o-how and arvidn authored Aug 6, 2024
1 parent b7d070a commit 5f920ce
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 5 deletions.
2 changes: 1 addition & 1 deletion crates/chia-consensus/src/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 5 additions & 1 deletion crates/chia-consensus/src/gen/run_block_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions crates/chia-consensus/src/gen/test_generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/chia-consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
326 changes: 326 additions & 0 deletions crates/chia-consensus/src/spendbundle_conditions.rs
Original file line number Diff line number Diff line change
@@ -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<SpendBundleConditions, ValidationErr> {
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::<MempoolVisitor>(
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<u8>]) -> 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::<CoinSpend>::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::<Vec<u8>>::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");
}
}
}
Loading

0 comments on commit 5f920ce

Please sign in to comment.