use crate::conditions::{
ELIGIBLE_FOR_DEDUP, MAX_SPENDS_PER_BLOCK, MempoolVisitor, ParseState, SpendBundleConditions,
process_single_spend, validate_conditions,
};
use crate::consensus_constants::ConsensusConstants;
use crate::flags::{ConsensusFlags, MEMPOOL_MODE};
use crate::puzzle_fingerprint::compute_puzzle_fingerprint;
use crate::run_block_generator::subtract_cost;
use crate::solution_generator::calculate_generator_length;
use crate::spend_visitor::SpendVisitor;
use crate::spendbundle_validation::get_flags_for_height_and_constants;
use crate::validation_error::ErrorCode;
use crate::validation_error::ValidationErr;
use chia_bls::PublicKey;
use chia_protocol::{Bytes, 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;
const QUOTE_BYTES: usize = 2;
pub fn get_conditions_from_spendbundle(
a: &mut Allocator,
spend_bundle: &SpendBundle,
max_cost: u64,
prev_tx_height: u32,
constants: &ConsensusConstants,
) -> Result<SpendBundleConditions, ValidationErr> {
let flags = get_flags_for_height_and_constants(prev_tx_height, constants);
Ok(run_spendbundle(
a,
spend_bundle,
max_cost,
flags | MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE,
constants,
)?
.0)
}
#[allow(clippy::type_complexity)]
pub fn run_spendbundle(
a: &mut Allocator,
spend_bundle: &SpendBundle,
max_cost: u64,
flags: ConsensusFlags,
constants: &ConsensusConstants,
) -> Result<(SpendBundleConditions, Vec<(PublicKey, Bytes)>), ValidationErr> {
let mut cost_left = max_cost;
let dialect = ChiaDialect::new(flags.to_clvm_flags());
let mut ret = SpendBundleConditions::default();
let mut state = ParseState::default();
let generator_length_without_quote =
calculate_generator_length(&spend_bundle.coin_spends) - QUOTE_BYTES;
let byte_cost = generator_length_without_quote as u64 * constants.cost_per_byte;
subtract_cost(a, &mut cost_left, byte_cost)?;
if flags.contains(ConsensusFlags::LIMIT_SPENDS)
&& spend_bundle.coin_spends.len() > MAX_SPENDS_PER_BLOCK
{
return Err(ValidationErr(a.nil(), ErrorCode::TooManySpends));
}
for coin_spend in &spend_bundle.coin_spends {
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)?;
ret.execution_cost += clvm_cost;
subtract_cost(a, &mut cost_left, clvm_cost)?;
let buf = tree_hash(a, puz);
if coin_spend.coin.puzzle_hash != buf.into() {
return Err(ValidationErr(puz, ErrorCode::WrongPuzzleHash));
}
let puzzle_hash = a.new_atom(&buf)?;
let spend = process_single_spend::<MempoolVisitor>(
a,
&mut ret,
&mut state,
parent,
puzzle_hash,
amount,
conditions,
flags,
&mut cost_left,
clvm_cost,
constants,
)?;
if (spend.flags & ELIGIBLE_FOR_DEDUP) != 0
&& flags.contains(ConsensusFlags::COMPUTE_FINGERPRINT)
{
spend.fingerprint = compute_puzzle_fingerprint(a, conditions)?;
}
}
MempoolVisitor::post_process(a, &state, &mut ret)?;
validate_conditions(a, &ret, &state, a.nil(), flags)?;
assert!(max_cost >= cost_left);
ret.cost = max_cost - cost_left;
Ok((ret, state.pkm_pairs))
}
#[cfg(test)]
mod tests {
use crate::consensus_constants::TEST_CONSTANTS;
use super::*;
use crate::allocator::make_allocator;
use crate::conditions::{ELIGIBLE_FOR_DEDUP, ELIGIBLE_FOR_FF};
use crate::run_block_generator::run_block_generator2;
use crate::solution_generator::solution_generator;
use chia_bls::Signature;
use chia_protocol::CoinSpend;
use chia_traits::Streamable;
use rstest::rstest;
use std::fs::read;
const QUOTE_EXECUTION_COST: u64 = 20;
const QUOTE_BYTES_COST: u64 = QUOTE_BYTES as u64 * TEST_CONSTANTS.cost_per_byte;
fn assert_run_spendbundle_matches_parse_spends(spend_bundle: &SpendBundle) {
use crate::conditions::parse_spends;
let flags = MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE;
let mut a1 = make_allocator(ConsensusFlags::LIMIT_HEAP);
let (sb_conds, _) = run_spendbundle(
&mut a1,
spend_bundle,
11_000_000_000,
flags,
&TEST_CONSTANTS,
)
.expect("run_spendbundle");
let mut a2 = make_allocator(ConsensusFlags::LIMIT_HEAP);
let dialect = ChiaDialect::new(flags.to_clvm_flags());
let mut spend_list = a2.nil();
for coin_spend in spend_bundle.coin_spends.iter().rev() {
let puz = node_from_bytes(&mut a2, coin_spend.puzzle_reveal.as_slice()).unwrap();
let sol = node_from_bytes(&mut a2, coin_spend.solution.as_slice()).unwrap();
let Reduction(_, conditions) =
run_program(&mut a2, &dialect, puz, sol, 11_000_000_000).unwrap();
let parent = a2
.new_atom(coin_spend.coin.parent_coin_info.as_slice())
.unwrap();
let ph_bytes = tree_hash(&a2, puz);
let puzzle_hash = a2.new_atom(&ph_bytes).unwrap();
let amount = a2.new_number(coin_spend.coin.amount.into()).unwrap();
let nil = a2.nil();
let tuple = a2.new_pair(conditions, nil).unwrap();
let tuple = a2.new_pair(amount, tuple).unwrap();
let tuple = a2.new_pair(puzzle_hash, tuple).unwrap();
let tuple = a2.new_pair(parent, tuple).unwrap();
spend_list = a2.new_pair(tuple, spend_list).unwrap();
}
let nil = a2.nil();
let spends_node = a2.new_pair(spend_list, nil).unwrap();
let ps_conds = parse_spends::<MempoolVisitor>(
&a2,
spends_node,
11_000_000_000,
0,
flags,
&Signature::default(),
None,
&TEST_CONSTANTS,
)
.expect("parse_spends");
assert_eq!(
sb_conds.spends.len(),
ps_conds.spends.len(),
"number of spends differ"
);
for (i, (s1, s2)) in sb_conds
.spends
.iter()
.zip(ps_conds.spends.iter())
.enumerate()
{
assert_eq!(
s1.flags, s2.flags,
"spend {i} flags differ: run_spendbundle={:#x}, parse_spends={:#x}",
s1.flags, s2.flags
);
}
}
#[rstest]
#[case("3000253", 8, 2, 51_216_870)]
#[case("1000101", 34, 15, 250_083_677)]
fn test_get_conditions_from_spendbundle(
#[case] filename: &str,
#[case] spends: usize,
#[case] additions: usize,
#[values(0, 1, 1_000_000, 5_000_000)] prev_tx_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(ConsensusFlags::LIMIT_HEAP);
let conditions =
get_conditions_from_spendbundle(&mut a, &bundle, cost, prev_tx_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);
let program_spends = bundle.coin_spends.iter().map(|coin_spend| {
(
coin_spend.coin,
&coin_spend.puzzle_reveal,
&coin_spend.solution,
)
});
let program = solution_generator(program_spends).expect("solution_generator failed");
let blocks: &[&[u8]] = &[];
let (_, block_conds) = run_block_generator2(
program.as_slice(),
blocks,
11_000_000_000,
MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE,
&Signature::default(),
None,
&TEST_CONSTANTS,
)
.expect("run_block_generator2 failed");
assert_eq!(
conditions.cost,
block_conds.cost - QUOTE_EXECUTION_COST - QUOTE_BYTES_COST
);
assert_eq!(
conditions.execution_cost,
block_conds.execution_cost - QUOTE_EXECUTION_COST
);
assert_eq!(conditions.condition_cost, block_conds.condition_cost);
assert_run_spendbundle_matches_parse_spends(&bundle);
}
#[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)] prev_tx_height: u32,
) {
let cost = 77_341_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(ConsensusFlags::LIMIT_HEAP);
let conditions =
get_conditions_from_spendbundle(&mut a, &bundle, cost, prev_tx_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);
assert_run_spendbundle_matches_parse_spends(&bundle);
}
fn make_list(a: &mut Allocator, items: &[clvmr::NodePtr]) -> clvmr::NodePtr {
let mut result = a.nil();
for &item in items.iter().rev() {
result = a.new_pair(item, result).unwrap();
}
result
}
fn condition_node(
a: &mut Allocator,
opcode: crate::opcodes::ConditionOpcode,
args: &[clvmr::NodePtr],
) -> clvmr::NodePtr {
let op = a.new_number(opcode.into()).unwrap();
let mut items = vec![op];
items.extend_from_slice(args);
make_list(a, &items)
}
const IDENTITY_PUZZLE: &[u8] = &[1];
fn identity_puzzle_hash() -> [u8; 32] {
use clvm_utils::tree_hash_atom;
tree_hash_atom(&[1]).to_bytes()
}
fn make_coin_spend(parent: [u8; 32], amount: u64, extra_conditions: &[&[u8]]) -> CoinSpend {
use crate::opcodes::{ASSERT_MY_AMOUNT, CREATE_COIN};
use chia_protocol::{Coin, Program};
use clvmr::serde::{node_from_bytes, node_to_bytes};
let mut a = Allocator::new();
let puzzle_hash = identity_puzzle_hash();
let ph_node = a.new_atom(&puzzle_hash).unwrap();
let amt_node = a.new_number(amount.into()).unwrap();
let create_coin = condition_node(&mut a, CREATE_COIN, &[ph_node, amt_node]);
let amt_node2 = a.new_number(amount.into()).unwrap();
let assert_my_amount = condition_node(&mut a, ASSERT_MY_AMOUNT, &[amt_node2]);
let mut all = vec![create_coin, assert_my_amount];
for extra in extra_conditions {
all.push(node_from_bytes(&mut a, extra).unwrap());
}
let conditions = make_list(&mut a, &all);
let solution = node_to_bytes(&a, conditions).unwrap();
CoinSpend::new(
Coin::new(parent.into(), puzzle_hash.into(), amount),
Program::from(IDENTITY_PUZZLE.to_vec()),
Program::from(solution),
)
}
fn serialize_condition(
opcode: crate::opcodes::ConditionOpcode,
args: &[clvmr::NodePtr],
a: &Allocator,
) -> Vec<u8> {
use clvmr::serde::node_to_bytes;
let mut a2 = Allocator::new();
let op = a2.new_number(opcode.into()).unwrap();
let mut items = vec![op];
for &arg in args {
let bytes = node_to_bytes(a, arg).unwrap();
items.push(node_from_bytes(&mut a2, &bytes).unwrap());
}
let list = make_list(&mut a2, &items);
node_to_bytes(&a2, list).unwrap()
}
#[test]
fn test_post_process_single_ff_eligible_spend() {
let spend_a = make_coin_spend([1u8; 32], 123, &[]);
let bundle = SpendBundle::new(vec![spend_a], Signature::default());
let mut alloc = make_allocator(ConsensusFlags::LIMIT_HEAP);
let flags = MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE;
let (conds, _) =
run_spendbundle(&mut alloc, &bundle, 11_000_000_000, flags, &TEST_CONSTANTS)
.expect("run_spendbundle");
assert_eq!(conds.spends.len(), 1);
assert_ne!(conds.spends[0].flags & ELIGIBLE_FOR_FF, 0);
assert_ne!(conds.spends[0].flags & ELIGIBLE_FOR_DEDUP, 0);
assert_run_spendbundle_matches_parse_spends(&bundle);
}
#[test]
fn test_post_process_assert_concurrent_spend_clears_ff() {
use chia_protocol::Coin;
let puzzle_hash = identity_puzzle_hash();
let spend_a = make_coin_spend([1u8; 32], 123, &[]);
let coin_a_id = Coin::new([1u8; 32].into(), puzzle_hash.into(), 123).coin_id();
let mut a = Allocator::new();
let coin_id_node = a.new_atom(coin_a_id.as_slice()).unwrap();
let assert_concurrent =
serialize_condition(crate::opcodes::ASSERT_CONCURRENT_SPEND, &[coin_id_node], &a);
let spend_b = make_coin_spend([2u8; 32], 123, &[&assert_concurrent]);
let bundle = SpendBundle::new(vec![spend_a, spend_b], Signature::default());
let mut alloc = make_allocator(ConsensusFlags::LIMIT_HEAP);
let flags = MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE;
let (conds, _) =
run_spendbundle(&mut alloc, &bundle, 11_000_000_000, flags, &TEST_CONSTANTS)
.expect("run_spendbundle");
assert_eq!(conds.spends.len(), 2);
assert_eq!(conds.spends[0].flags & ELIGIBLE_FOR_FF, 0);
assert_ne!(conds.spends[0].flags & ELIGIBLE_FOR_DEDUP, 0);
assert_run_spendbundle_matches_parse_spends(&bundle);
}
#[test]
fn test_post_process_ephemeral_output_clears_ff() {
use chia_protocol::{Coin, Program};
let puzzle_hash = identity_puzzle_hash();
let spend_a = make_coin_spend([1u8; 32], 123, &[]);
let coin_a_id = Coin::new([1u8; 32].into(), puzzle_hash.into(), 123).coin_id();
let ephemeral_coin = Coin::new(coin_a_id, puzzle_hash.into(), 123);
let a = Allocator::new();
let nil = a.nil();
let empty_solution = clvmr::serde::node_to_bytes(&a, nil).unwrap();
let spend_b = CoinSpend::new(
ephemeral_coin,
Program::from(IDENTITY_PUZZLE.to_vec()),
Program::from(empty_solution),
);
let bundle = SpendBundle::new(vec![spend_a, spend_b], Signature::default());
let mut alloc = make_allocator(ConsensusFlags::LIMIT_HEAP);
let flags = MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE;
let (conds, _) =
run_spendbundle(&mut alloc, &bundle, 11_000_000_000, flags, &TEST_CONSTANTS)
.expect("run_spendbundle");
assert_eq!(conds.spends.len(), 2);
assert_eq!(conds.spends[0].flags & ELIGIBLE_FOR_FF, 0);
assert_run_spendbundle_matches_parse_spends(&bundle);
}
fn convert_block_to_bundle(generator: &[u8], block_refs: &[Vec<u8>]) -> SpendBundle {
use crate::run_block_generator::setup_generator_args;
use chia_protocol::Bytes32;
use chia_protocol::Coin;
use chia_protocol::Program;
use clvm_traits::{FromClvm, destructure_tuple, match_tuple};
use clvm_utils::tree_hash_from_bytes;
use clvmr::NodePtr;
use clvmr::op_utils::first;
use clvmr::serde::node_from_bytes_backrefs;
let mut a = make_allocator(MEMPOOL_MODE);
let generator = node_from_bytes_backrefs(&mut a, generator).expect("node_from_bytes");
let args = setup_generator_args(&mut a, block_refs, ConsensusFlags::empty())
.expect("setup_generator_args");
let dialect = ChiaDialect::new(MEMPOOL_MODE.to_clvm_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();
while let Some((spend, rest)) = a.next(all_spends) {
all_spends = rest;
let destructure_tuple!(parent_id, puzzle, amount, solution, _) =
<match_tuple!(Bytes32, Program, u64, Program, NodePtr)>::from_clvm(&a, spend)
.expect("parsing CLVM");
spends.push(CoinSpend::new(
Coin::new(
parent_id,
tree_hash_from_bytes(puzzle.as_ref()).expect("hash").into(),
amount,
),
puzzle,
solution,
));
}
SpendBundle::new(spends, Signature::default())
}
#[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("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("unknown-condition")]
#[case("duplicate-messages")]
#[ignore = "expensive test, only run in release mode (--include-ignored)"]
fn run_generator(#[case] name: &str) {
use crate::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);
let (execution_cost, block_cost, block_output, block_conds) = {
let block_conds = run_block_generator2(
&generator_buffer,
&block_refs,
11_000_000_000,
MEMPOOL_MODE | ConsensusFlags::DONT_VALIDATE_SIGNATURE,
&Signature::default(),
None,
&TEST_CONSTANTS,
);
match &block_conds {
Ok((a2, conditions)) => (
conditions.execution_cost,
conditions.cost,
print_conditions(a2, conditions, a2),
block_conds,
),
Err(code) => {
println!("error: {code:?}");
(
0,
0,
format!("FAILED: {}\n", u32::from(code.1)),
block_conds,
)
}
}
};
let mut a1 = make_allocator(MEMPOOL_MODE);
let conds = get_conditions_from_spendbundle(
&mut a1,
&bundle,
11_000_000_000,
5_000_000,
&TEST_CONSTANTS,
);
let output = match conds {
Ok(mut conditions) => {
let block_byte_cost = generator_buffer.len() as u64 * TEST_CONSTANTS.cost_per_byte;
let program_spends = bundle.coin_spends.iter().map(|coin_spend| {
(
coin_spend.coin,
&coin_spend.puzzle_reveal,
&coin_spend.solution,
)
});
let generator_length_without_quote = solution_generator(program_spends)
.expect("solution_generator failed")
.len()
- QUOTE_BYTES;
let bundle_byte_cost =
generator_length_without_quote as u64 * TEST_CONSTANTS.cost_per_byte;
println!(
" block_cost: {block_cost} bytes: {block_byte_cost} exe-cost: {execution_cost}"
);
println!("bundle_cost: {} bytes: {bundle_byte_cost}", conditions.cost);
println!("execution_cost: {}", conditions.execution_cost);
println!("condition_cost: {}", conditions.condition_cost);
assert!(conditions.cost - bundle_byte_cost <= block_cost - block_byte_cost);
assert!(conditions.cost > 0);
assert!(conditions.execution_cost > 0);
assert_eq!(
conditions.cost,
conditions.condition_cost + conditions.execution_cost + bundle_byte_cost
);
conditions.cost = block_cost;
conditions.execution_cost = execution_cost;
let (a2, _) = block_conds.as_ref().unwrap();
print_conditions(&a1, &conditions, a2)
}
Err(code) => {
println!("error: {code:?}");
format!("FAILED: {}\n", u32::from(code.1))
}
};
if output != block_output {
print_diff(&output, &block_output);
panic!(
"run_block_generator2 produced a different result than get_conditions_from_spendbundle()"
);
}
if output != expected {
print_diff(&output, expected);
panic!("mismatching condition output");
}
}
fn make_bare_coin_spend(parent: [u8; 32], amount: u64) -> CoinSpend {
use chia_protocol::{Coin, Program};
let puzzle_hash = identity_puzzle_hash();
let a = Allocator::new();
let nil = a.nil();
let solution = clvmr::serde::node_to_bytes(&a, nil).unwrap();
CoinSpend::new(
Coin::new(parent.into(), puzzle_hash.into(), amount),
Program::from(IDENTITY_PUZZLE.to_vec()),
Program::from(solution),
)
}
#[rstest]
#[case(MAX_SPENDS_PER_BLOCK, ConsensusFlags::LIMIT_SPENDS, None)]
#[case(MAX_SPENDS_PER_BLOCK + 1, ConsensusFlags::LIMIT_SPENDS, Some(ErrorCode::TooManySpends))]
#[case(MAX_SPENDS_PER_BLOCK + 1, ConsensusFlags::empty(), None)]
fn test_limit_spends_run_spendbundle(
#[case] num_spends: usize,
#[case] extra_flags: ConsensusFlags,
#[case] expected_err: Option<ErrorCode>,
) {
let coin_spends: Vec<CoinSpend> = (0..num_spends)
.map(|i| {
let mut parent = [0u8; 32];
parent[0..4].copy_from_slice(&(i as u32).to_be_bytes());
make_bare_coin_spend(parent, 0)
})
.collect();
let bundle = SpendBundle::new(coin_spends, Signature::default());
let mut alloc = make_allocator(ConsensusFlags::LIMIT_HEAP);
let flags = ConsensusFlags::DONT_VALIDATE_SIGNATURE | extra_flags;
let result = run_spendbundle(&mut alloc, &bundle, u64::MAX, flags, &TEST_CONSTANTS);
match (expected_err, result) {
(Some(err), Err(e)) => {
assert_eq!(e.1, err);
}
(None, Ok((conds, _))) => {
assert_eq!(conds.spends.len(), num_spends);
}
_ => {
panic!("mismatch");
}
}
}
}