Skip to content

Commit

Permalink
add support for ASSERT_CONCURRENT_PUZZLE
Browse files Browse the repository at this point in the history
  • Loading branch information
arvidn committed Feb 1, 2023
1 parent ae0f25d commit 94cfc7d
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 10 deletions.
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/parse-cond-args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/parse-conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand Down
185 changes: 180 additions & 5 deletions src/gen/conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -400,11 +408,21 @@ struct ParseState {
// checked once everything has been parsed.
assert_concurrent_spend: HashSet<NodePtr>,

// the assert concurrent puzzle hashes are inserted into this set and
// checked once everything has been parsed.
assert_concurrent_puzzle: HashSet<NodePtr>,

// 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<Arc<Bytes32>>,

// 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<NodePtr>,

// the sum of all values of all spent coins
removal_amount: u128,

Expand All @@ -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,
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -673,6 +698,25 @@ pub fn parse_spends(
}
}

if !state.assert_concurrent_puzzle.is_empty() {
let mut spent_phs = HashSet::<Bytes32>::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() {
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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);
}
9 changes: 8 additions & 1 deletion src/gen/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,7 +84,8 @@ pub fn parse_opcode(a: &Allocator, op: NodePtr, flags: u32) -> Option<ConditionO
| ASSERT_BEFORE_SECONDS_ABSOLUTE
| ASSERT_BEFORE_HEIGHT_RELATIVE
| ASSERT_BEFORE_HEIGHT_ABSOLUTE
| ASSERT_CONCURRENT_SPEND => Some(buf[0]),
| ASSERT_CONCURRENT_SPEND
| ASSERT_CONCURRENT_PUZZLE => Some(buf[0]),
_ => None,
}
} else {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]),
Expand Down
2 changes: 2 additions & 0 deletions src/gen/validation_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum ErrorCode {
AssertPuzzleAnnouncementFailed,
AssertCoinAnnouncementFailed,
AssertConcurrentSpendFailed,
AssertConcurrentPuzzleFailed,
ReserveFeeConditionFailed,
DuplicateOutput,
DoubleSpend,
Expand Down Expand Up @@ -91,6 +92,7 @@ impl From<ErrorCode> for u32 {
ErrorCode::AssertPuzzleAnnouncementFailed => 12,
ErrorCode::AssertCoinAnnouncementFailed => 12,
ErrorCode::AssertConcurrentSpendFailed => 132,
ErrorCode::AssertConcurrentPuzzleFailed => 133,
ErrorCode::ReserveFeeConditionFailed => 48,
ErrorCode::DuplicateOutput => 4,
ErrorCode::DoubleSpend => 5,
Expand Down

0 comments on commit 94cfc7d

Please sign in to comment.