diff --git a/fuzz/fuzz_targets/parse-cond-args.rs b/fuzz/fuzz_targets/parse-cond-args.rs index c4b3df7aa..b3d5b5138 100644 --- a/fuzz/fuzz_targets/parse-cond-args.rs +++ b/fuzz/fuzz_targets/parse-cond-args.rs @@ -5,7 +5,7 @@ use clvmr::allocator::Allocator; use chia::fuzzing_utils::{BitCursor, make_tree}; use chia::gen::conditions::parse_args; -use chia::gen::flags::{COND_ARGS_NIL, STRICT_ARGS_COUNT}; +use chia::gen::flags::{COND_ARGS_NIL, STRICT_ARGS_COUNT, ENABLE_ASSERT_BEFORE}; use chia::gen::opcodes::{ AGG_SIG_ME, AGG_SIG_UNSAFE, ALWAYS_TRUE, @@ -18,7 +18,7 @@ use chia::gen::opcodes::{ fuzz_target!(|data: &[u8]| { let mut a = Allocator::new(); let input = make_tree(&mut a, &mut BitCursor::new(data), false); - for flags in &[0, COND_ARGS_NIL, STRICT_ARGS_COUNT] { + for flags in &[0, ENABLE_ASSERT_BEFORE | COND_ARGS_NIL, ENABLE_ASSERT_BEFORE | STRICT_ARGS_COUNT] { for op in &[AGG_SIG_ME, AGG_SIG_UNSAFE, ALWAYS_TRUE, ASSERT_COIN_ANNOUNCEMENT, ASSERT_HEIGHT_ABSOLUTE, ASSERT_HEIGHT_RELATIVE, ASSERT_MY_AMOUNT, ASSERT_MY_COIN_ID, ASSERT_MY_PARENT_ID, ASSERT_MY_PUZZLEHASH, ASSERT_PUZZLE_ANNOUNCEMENT, diff --git a/fuzz/fuzz_targets/parse-conditions.rs b/fuzz/fuzz_targets/parse-conditions.rs index fb01e8ba2..7fc69ff46 100644 --- a/fuzz/fuzz_targets/parse-conditions.rs +++ b/fuzz/fuzz_targets/parse-conditions.rs @@ -5,12 +5,12 @@ use clvmr::allocator::Allocator; use chia::fuzzing_utils::{BitCursor, make_tree}; use chia::gen::conditions::parse_spends; -use chia::gen::flags::{COND_ARGS_NIL, STRICT_ARGS_COUNT, NO_UNKNOWN_CONDS}; +use chia::gen::flags::{COND_ARGS_NIL, STRICT_ARGS_COUNT, ENABLE_ASSERT_BEFORE, NO_UNKNOWN_CONDS}; fuzz_target!(|data: &[u8]| { let mut a = Allocator::new(); let input = make_tree(&mut a, &mut BitCursor::new(data), false); - for flags in &[0, COND_ARGS_NIL, STRICT_ARGS_COUNT, NO_UNKNOWN_CONDS] { + for flags in &[0, ENABLE_ASSERT_BEFORE | COND_ARGS_NIL, ENABLE_ASSERT_BEFORE | STRICT_ARGS_COUNT, NO_UNKNOWN_CONDS] { let _ret = parse_spends(&a, input, 33000000000, *flags); } }); diff --git a/src/gen/conditions.rs b/src/gen/conditions.rs index e707f89ff..7aa4d7e4f 100644 --- a/src/gen/conditions.rs +++ b/src/gen/conditions.rs @@ -6,11 +6,11 @@ use super::condition_sanitizers::{ use super::opcodes::{ parse_opcode, ConditionOpcode, AGG_SIG_COST, AGG_SIG_ME, AGG_SIG_UNSAFE, ALWAYS_TRUE, ASSERT_BEFORE_HEIGHT_ABSOLUTE, ASSERT_BEFORE_HEIGHT_RELATIVE, ASSERT_BEFORE_SECONDS_ABSOLUTE, - ASSERT_BEFORE_SECONDS_RELATIVE, ASSERT_COIN_ANNOUNCEMENT, ASSERT_CONCURRENT_SPEND, - ASSERT_HEIGHT_ABSOLUTE, ASSERT_HEIGHT_RELATIVE, ASSERT_MY_AMOUNT, ASSERT_MY_COIN_ID, - ASSERT_MY_PARENT_ID, ASSERT_MY_PUZZLEHASH, ASSERT_PUZZLE_ANNOUNCEMENT, ASSERT_SECONDS_ABSOLUTE, - ASSERT_SECONDS_RELATIVE, CREATE_COIN, CREATE_COIN_ANNOUNCEMENT, CREATE_COIN_COST, - CREATE_PUZZLE_ANNOUNCEMENT, RESERVE_FEE, + ASSERT_BEFORE_SECONDS_RELATIVE, ASSERT_COIN_ANNOUNCEMENT, ASSERT_CONCURRENT_PUZZLE, + ASSERT_CONCURRENT_SPEND, ASSERT_HEIGHT_ABSOLUTE, ASSERT_HEIGHT_RELATIVE, ASSERT_MY_AMOUNT, + ASSERT_MY_COIN_ID, ASSERT_MY_PARENT_ID, ASSERT_MY_PUZZLEHASH, ASSERT_PUZZLE_ANNOUNCEMENT, + ASSERT_SECONDS_ABSOLUTE, ASSERT_SECONDS_RELATIVE, CREATE_COIN, CREATE_COIN_ANNOUNCEMENT, + CREATE_COIN_COST, CREATE_PUZZLE_ANNOUNCEMENT, RESERVE_FEE, }; use super::sanitize_int::sanitize_uint; use super::validation_error::{first, next, rest, ErrorCode, ValidationErr}; @@ -74,6 +74,9 @@ pub enum Condition { AssertPuzzleAnnouncement(NodePtr), // ensure the specified coin ID is also being spent (hash, 32 bytes) AssertConcurrentSpend(NodePtr), + // ensure that the specified puzzle hash is used by at least one spend + // (hash, 32 bytes) + AssertConcurrentPuzzle(NodePtr), // ID (hash, 32 bytes) AssertMyCoinId(NodePtr), AssertMyParentId(NodePtr), @@ -218,6 +221,11 @@ pub fn parse_args( let id = sanitize_hash(a, first(a, c)?, 32, ErrorCode::AssertConcurrentSpendFailed)?; Ok(Condition::AssertConcurrentSpend(id)) } + ASSERT_CONCURRENT_PUZZLE => { + maybe_check_args_terminator(a, c, flags)?; + let id = sanitize_hash(a, first(a, c)?, 32, ErrorCode::AssertConcurrentPuzzleFailed)?; + Ok(Condition::AssertConcurrentPuzzle(id)) + } ASSERT_MY_COIN_ID => { maybe_check_args_terminator(a, c, flags)?; let id = sanitize_hash(a, first(a, c)?, 32, ErrorCode::AssertMyCoinIdFailed)?; @@ -400,11 +408,21 @@ struct ParseState { // checked once everything has been parsed. assert_concurrent_spend: HashSet, + // the assert concurrent puzzle hashes are inserted into this set and + // checked once everything has been parsed. + assert_concurrent_puzzle: HashSet, + // all coin IDs that have been spent so far. When we parse a spend we also // compute the coin ID, and stick it in this set. It's reference counted // since it may also be referenced by announcements spent_coins: HashSet>, + // for every coin spent, we also store all the puzzle hashes that were + // spent. Note that these are just the node pointers into the allocator, so + // there may still be duplicates here. We defer creating a hash set of the + // actual hashes until the end, and only if there are any puzzle assertions + spent_puzzles: HashSet, + // the sum of all values of all spent coins removal_amount: u128, @@ -420,7 +438,9 @@ impl ParseState { assert_coin: HashSet::new(), assert_puzzle: HashSet::new(), assert_concurrent_spend: HashSet::new(), + assert_concurrent_puzzle: HashSet::new(), spent_coins: HashSet::new(), + spent_puzzles: HashSet::new(), removal_amount: 0, addition_amount: 0, } @@ -450,6 +470,8 @@ fn parse_spend_conditions( return Err(ValidationErr(spend, ErrorCode::DoubleSpend)); } + state.spent_puzzles.insert(puzzle_hash); + state.removal_amount += my_amount as u128; let mut spend = Spend { @@ -601,6 +623,9 @@ fn parse_spend_conditions( Condition::AssertConcurrentSpend(id) => { state.assert_concurrent_spend.insert(id); } + Condition::AssertConcurrentPuzzle(id) => { + state.assert_concurrent_puzzle.insert(id); + } Condition::AggSigMe(pk, msg) => { spend.agg_sig_me.push((pk, msg)); spend.flags &= !ELIGIBLE_FOR_DEDUP; @@ -673,6 +698,25 @@ pub fn parse_spends( } } + if !state.assert_concurrent_puzzle.is_empty() { + let mut spent_phs = HashSet::::new(); + + // expand all the spent puzzle hashes into a set, to allow + // fast lookups of all assertions + for ph in state.spent_puzzles { + spent_phs.insert(a.atom(ph).into()); + } + + for puzzle_assert in state.assert_concurrent_puzzle { + if !spent_phs.contains(&a.atom(puzzle_assert).into()) { + return Err(ValidationErr( + puzzle_assert, + ErrorCode::AssertConcurrentPuzzleFailed, + )); + } + } + } + // check all the assert announcements // if there are no asserts, there is no need to hash all the announcements if !state.assert_coin.is_empty() { @@ -1130,6 +1174,7 @@ fn test_invalid_spend_list_terminator() { #[case(AGG_SIG_UNSAFE, "{pubkey} ({msg1}")] #[case(AGG_SIG_ME, "{pubkey} ({msg1}")] #[case(ASSERT_CONCURRENT_SPEND, "{coin12}")] +#[case(ASSERT_CONCURRENT_PUZZLE, "{h2}")] fn test_extra_arg_mempool(#[case] condition: ConditionOpcode, #[case] arg: &str) { // extra args are disallowed in mempool mode assert_eq!( @@ -1166,6 +1211,7 @@ fn test_extra_arg_mempool(#[case] condition: ConditionOpcode, #[case] arg: &str) #[case(ASSERT_MY_PARENT_ID, "{h1}", "", |_: &SpendBundleConditions, _: &Spend| {})] #[case(ASSERT_MY_PUZZLEHASH, "{h2}", "", |_: &SpendBundleConditions, _: &Spend| {})] #[case(ASSERT_CONCURRENT_SPEND, "{coin12}", "", |_: &SpendBundleConditions, _: &Spend| {})] +#[case(ASSERT_CONCURRENT_PUZZLE, "{h2}", "", |_: &SpendBundleConditions, _: &Spend| {})] fn test_extra_arg( #[case] condition: ConditionOpcode, #[case] arg: &str, @@ -1245,6 +1291,7 @@ fn test_height_exceed_max(#[case] condition: ConditionOpcode, #[case] expected_e #[case(ASSERT_MY_PARENT_ID, "{h1}", |_: &SpendBundleConditions, _: &Spend| {})] #[case(ASSERT_MY_PUZZLEHASH, "{h2}", |_: &SpendBundleConditions, _: &Spend| {})] #[case(ASSERT_CONCURRENT_SPEND, "{coin12}", |_: &SpendBundleConditions, _: &Spend| {})] +#[case(ASSERT_CONCURRENT_PUZZLE, "{h2}", |_: &SpendBundleConditions, _: &Spend| {})] fn test_single_condition( #[case] condition: ConditionOpcode, #[case] arg: &str, @@ -1304,6 +1351,11 @@ fn test_single_condition( "{coin12}", Some(ErrorCode::InvalidConditionOpcode) )] +#[case( + ASSERT_CONCURRENT_PUZZLE, + "{coin12}", + Some(ErrorCode::InvalidConditionOpcode) +)] fn test_disable_assert_before( #[case] condition: ConditionOpcode, #[case] arg: &str, @@ -1387,6 +1439,7 @@ fn test_multiple_conditions( #[case(AGG_SIG_UNSAFE)] #[case(AGG_SIG_ME)] #[case(ASSERT_CONCURRENT_SPEND)] +#[case(ASSERT_CONCURRENT_PUZZLE)] fn test_missing_arg(#[case] condition: ConditionOpcode) { // extra args are disallowed in mempool mode assert_eq!( @@ -2602,3 +2655,125 @@ fn test_assert_concurrent_spend_self() { assert_eq!(spend.agg_sig_me.len(), 0); assert_eq!(spend.flags, ELIGIBLE_FOR_DEDUP); } + +#[test] +fn test_concurrent_puzzle() { + // ASSERT_CONCURRENT_PUZZLE + // this spends the coin (h1, h2, 123) + // and (h2, h2, 123). + + // three cases are tested: + // 1. the first spend asserts second's puzzle hash + // 2. the second asserts the first's puzzle hash + // 3. the second asserts its own puzzle hash + // the result is the same in all cases, and all are expected to pass + + let test_cases = [ + "(\ + (({h1} ({h1} (123 (((65 ({h2} )))\ + (({h2} ({h2} (123 ())\ + ))", + "(\ + (({h1} ({h1} (123 ())\ + (({h2} ({h2} (123 (((65 ({h1} )))\ + ))", + "(\ + (({h1} ({h1} (123 ())\ + (({h2} ({h2} (123 (((65 ({h2} )))\ + ))", + ]; + + for test in test_cases { + let (a, conds) = cond_test(test).unwrap(); + + // just make sure there are no constraints + assert_eq!(conds.agg_sig_unsafe.len(), 0); + assert_eq!(conds.reserve_fee, 0); + assert_eq!(conds.height_absolute, 0); + assert_eq!(conds.seconds_absolute, 0); + assert_eq!(conds.cost, 0); + + // there are two spends + assert_eq!(conds.spends.len(), 2); + let spend = &conds.spends[0]; + assert_eq!(*spend.coin_id, test_coin_id(H1, H1, 123)); + assert_eq!(a.atom(spend.puzzle_hash), H1); + assert_eq!(spend.agg_sig_me.len(), 0); + assert_eq!(spend.flags, ELIGIBLE_FOR_DEDUP); + + let spend = &conds.spends[1]; + assert_eq!(*spend.coin_id, test_coin_id(H2, H2, 123)); + assert_eq!(a.atom(spend.puzzle_hash), H2); + assert_eq!(spend.agg_sig_me.len(), 0); + assert_eq!(spend.flags, ELIGIBLE_FOR_DEDUP); + } +} + +#[test] +fn test_concurrent_puzzle_fail() { + // ASSERT_CONCURRENT_PUZZLE + // this spends the coin (h1, h2, 123) + // and (h2, h2, 123). + + // this test ensures that asserting a puzzle hash that's not being spent + // causes a failure. + + let test_cases = [ + "(\ + (({h1} ({h2} (123 (((65 ({h1} )))\ + (({h2} ({h2} (123 ())\ + ))", + "(\ + (({h1} ({h2} (123 ())\ + (({h2} ({h2} (123 (((65 ({h1} )))\ + ))", + // msg1 has an invalid length for a sha256 hash + "(\ + (({h1} ({h2} (123 ())\ + (({h2} ({h2} (123 (((65 ({msg1} )))\ + ))", + // in this case we *create* coin ((coin12 h2 42)) + // i.e. paid to puzzle hash h2. And we assert the puzzle hash h2 is + // being spent. This should not pass, i.e. make sure we don't "cross the + // beams" on created coins and spent puzzles + "(\ + (({h1} ({h1} (123 (((51 ({h2} (42 )))\ + (({h2} ({h1} (123 (((65 ({h2} )))\ + ))", + ]; + + for test in test_cases { + assert_eq!( + cond_test(test).unwrap_err().1, + ErrorCode::AssertConcurrentPuzzleFailed + ); + } +} + +#[test] +fn test_assert_concurrent_puzzle_self() { + // ASSERT_CONCURRENT_PUZZLE + // asserting ones own puzzle hash is always true + + let (a, conds) = cond_test( + "(\ + (({h1} ({h2} (123 (((65 ({h2} )))\ + ))", + ) + .unwrap(); + + // just make sure there are no constraints + assert_eq!(conds.agg_sig_unsafe.len(), 0); + assert_eq!(conds.reserve_fee, 0); + assert_eq!(conds.height_absolute, 0); + assert_eq!(conds.seconds_absolute, 0); + assert_eq!(conds.cost, 0); + + // there are two spends + assert_eq!(conds.spends.len(), 1); + let spend = &conds.spends[0]; + assert_eq!(*spend.coin_id, test_coin_id(H1, H2, 123)); + assert_eq!(a.atom(spend.puzzle_hash), H2); + assert_eq!(spend.agg_sig_me.len(), 0); + assert_eq!(spend.flags, ELIGIBLE_FOR_DEDUP); +} diff --git a/src/gen/opcodes.rs b/src/gen/opcodes.rs index 31cb34629..11a8449b4 100644 --- a/src/gen/opcodes.rs +++ b/src/gen/opcodes.rs @@ -19,6 +19,7 @@ pub const ASSERT_COIN_ANNOUNCEMENT: ConditionOpcode = 61; pub const CREATE_PUZZLE_ANNOUNCEMENT: ConditionOpcode = 62; pub const ASSERT_PUZZLE_ANNOUNCEMENT: ConditionOpcode = 63; pub const ASSERT_CONCURRENT_SPEND: ConditionOpcode = 64; +pub const ASSERT_CONCURRENT_PUZZLE: ConditionOpcode = 65; // the conditions below let coins inquire about themselves pub const ASSERT_MY_COIN_ID: ConditionOpcode = 70; @@ -83,7 +84,8 @@ pub fn parse_opcode(a: &Allocator, op: NodePtr, flags: u32) -> Option Some(buf[0]), + | ASSERT_CONCURRENT_SPEND + | ASSERT_CONCURRENT_PUZZLE => Some(buf[0]), _ => None, } } else { @@ -180,6 +182,7 @@ fn test_parse_opcode() { None ); assert_eq!(opcode_tester(&mut a, &[ASSERT_CONCURRENT_SPEND]), None); + assert_eq!(opcode_tester(&mut a, &[ASSERT_CONCURRENT_PUZZLE]), None); assert_eq!(opcode_tester(&mut a, &[ALWAYS_TRUE]), Some(ALWAYS_TRUE)); // leading zeros are not allowed, it makes it a different value @@ -273,6 +276,10 @@ fn test_parse_opcode() { opcode_tester_with_assert_before(&mut a, &[ASSERT_CONCURRENT_SPEND]), Some(ASSERT_CONCURRENT_SPEND) ); + assert_eq!( + opcode_tester_with_assert_before(&mut a, &[ASSERT_CONCURRENT_PUZZLE]), + Some(ASSERT_CONCURRENT_PUZZLE) + ); assert_eq!( opcode_tester_with_assert_before(&mut a, &[ALWAYS_TRUE]), diff --git a/src/gen/validation_error.rs b/src/gen/validation_error.rs index 65f989039..86c51aebb 100644 --- a/src/gen/validation_error.rs +++ b/src/gen/validation_error.rs @@ -30,6 +30,7 @@ pub enum ErrorCode { AssertPuzzleAnnouncementFailed, AssertCoinAnnouncementFailed, AssertConcurrentSpendFailed, + AssertConcurrentPuzzleFailed, ReserveFeeConditionFailed, DuplicateOutput, DoubleSpend, @@ -91,6 +92,7 @@ impl From for u32 { ErrorCode::AssertPuzzleAnnouncementFailed => 12, ErrorCode::AssertCoinAnnouncementFailed => 12, ErrorCode::AssertConcurrentSpendFailed => 132, + ErrorCode::AssertConcurrentPuzzleFailed => 133, ErrorCode::ReserveFeeConditionFailed => 48, ErrorCode::DuplicateOutput => 4, ErrorCode::DoubleSpend => 5,