diff --git a/src/gen/conditions.rs b/src/gen/conditions.rs index 4fe82f7df..d7f74d7a1 100644 --- a/src/gen/conditions.rs +++ b/src/gen/conditions.rs @@ -15,6 +15,7 @@ use super::validation_error::{first, next, rest, ErrorCode, ValidationErr}; use crate::gen::flags::COND_ARGS_NIL; use crate::gen::flags::NO_UNKNOWN_CONDS; use crate::gen::flags::STRICT_ARGS_COUNT; +use crate::gen::flags::NO_RELATIVE_CONDITIONS_ON_EPHEMERAL; use crate::gen::validation_error::check_nil; use chia_protocol::bytes::Bytes32; use clvmr::allocator::{Allocator, NodePtr, SExp}; @@ -520,6 +521,18 @@ pub struct ParseState { // created in this same block // each item is the index into the SpendBundleConditions::spends vector assert_ephemeral: HashSet, + + // spends that use relative height- or time conditions are disallowed on + // ephemeral coins. They are recorded in this set to be be checked once all + // spends have been parsed. These conditions are: + // ASSERT_HEIGHT_RELATIVE + // ASSERT_SECONDS_RELATIVE + // ASSERT_BEFORE_HEIGHT_RELATIVE + // ASSERT_BEFORE_SECONDS_RELATIVE + // ASSERT_MY_BIRTH_SECONDS + // ASSERT_MY_BIRTH_HEIGHT + // each item is the index into the SpendBundleConditions::spends vector + assert_not_ephemeral: HashSet, } pub(crate) fn parse_single_spend( @@ -574,6 +587,15 @@ pub(crate) fn parse_single_spend( parse_conditions(a, ret, state, coin_spend, iter, flags, max_cost) } +fn assert_not_ephemeral(spend_flags: &mut u32, state: &mut ParseState, idx: usize) { + if (*spend_flags & HAS_RELATIVE_CONDITION) != 0 { + return; + } + + state.assert_not_ephemeral.insert(idx); + *spend_flags |= HAS_RELATIVE_CONDITION; +} + pub fn parse_conditions( a: &Allocator, ret: &mut SpendBundleConditions, @@ -653,7 +675,7 @@ pub fn parse_conditions( )); } } - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::AssertSecondsAbsolute(s) => { // keep the most strict condition. i.e. the highest limit @@ -677,7 +699,7 @@ pub fn parse_conditions( )); } } - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::AssertHeightAbsolute(h) => { // keep the most strict condition. i.e. the highest limit @@ -701,7 +723,7 @@ pub fn parse_conditions( )); } } - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::AssertBeforeSecondsAbsolute(s) => { // keep the most strict condition. i.e. the lowest limit @@ -729,7 +751,7 @@ pub fn parse_conditions( )); } } - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::AssertBeforeHeightAbsolute(h) => { // keep the most strict condition. i.e. the lowest limit @@ -757,7 +779,7 @@ pub fn parse_conditions( return Err(ValidationErr(c, ErrorCode::AssertMyBirthSecondsFailed)); } spend.birth_seconds = Some(s); - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::AssertMyBirthHeight(h) => { // if this spend already has a birth_height assertion, it's an @@ -767,7 +789,7 @@ pub fn parse_conditions( return Err(ValidationErr(c, ErrorCode::AssertMyBirthHeightFailed)); } spend.birth_height = Some(h); - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::AssertEphemeral => { state.assert_ephemeral.insert(ret.spends.len()); @@ -809,7 +831,7 @@ pub fn parse_conditions( spend.flags &= !ELIGIBLE_FOR_DEDUP; } Condition::SkipRelativeCondition => { - spend.flags |= HAS_RELATIVE_CONDITION; + assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } Condition::Skip => {} } @@ -970,6 +992,18 @@ pub fn parse_spends( } } + if (flags & NO_RELATIVE_CONDITIONS_ON_EPHEMERAL) != 0 { + for spend_idx in state.assert_not_ephemeral { + // make sure this coin was NOT created in this block + if is_ephemeral(&a, spend_idx, &state.spent_coins, &ret.spends) { + return Err(ValidationErr( + ret.spends[spend_idx].parent_id, + ErrorCode::EphemeralRelativeCondition, + )); + } + } + } + if !state.assert_puzzle.is_empty() { let mut announcements = HashSet::::new(); @@ -3533,3 +3567,92 @@ fn test_assert_ephemeral_wrong_parent() { ErrorCode::AssertEphemeralFailed ); } + +#[cfg(test)] +#[rstest] +// the default expected errors are post soft-fork, when both new rules are +// activated +#[case(ASSERT_HEIGHT_ABSOLUTE, None)] +#[case(ASSERT_HEIGHT_RELATIVE, Some(ErrorCode::EphemeralRelativeCondition))] +#[case(ASSERT_SECONDS_ABSOLUTE, None)] +#[case(ASSERT_SECONDS_RELATIVE, Some(ErrorCode::EphemeralRelativeCondition))] +#[case(ASSERT_MY_BIRTH_HEIGHT, Some(ErrorCode::EphemeralRelativeCondition))] +#[case(ASSERT_MY_BIRTH_SECONDS, Some(ErrorCode::EphemeralRelativeCondition))] +#[case(ASSERT_BEFORE_HEIGHT_ABSOLUTE, None)] +#[case(ASSERT_BEFORE_HEIGHT_RELATIVE, Some(ErrorCode::EphemeralRelativeCondition))] +#[case(ASSERT_BEFORE_SECONDS_ABSOLUTE, None)] +#[case(ASSERT_BEFORE_SECONDS_RELATIVE, Some(ErrorCode::EphemeralRelativeCondition))] +fn test_relative_condition_on_ephemeral( + #[case] condition: ConditionOpcode, + #[case] mut expect_error: Option, + #[values(0, ENABLE_ASSERT_BEFORE)] enable_assert_before: u32, + #[values(0, NO_RELATIVE_CONDITIONS_ON_EPHEMERAL)] no_rel_conds_on_ephemeral: u32, +) { + // this test ensures that we disallow relative conditions (including + // assert-my-birth conditions) on ephemeral coin spends. + // We run these test cases for every combination of enabling/disabling + // assert-before conditions as well as disallowing relative conditions on + // ephemeral coins + + let cond = condition as u8; + + if no_rel_conds_on_ephemeral == 0 { + // if we allow relative conditions, all cases should pass + expect_error = None; + } + + if enable_assert_before == 0 && [ASSERT_MY_BIRTH_HEIGHT, + ASSERT_MY_BIRTH_SECONDS, + ASSERT_BEFORE_HEIGHT_ABSOLUTE, + ASSERT_BEFORE_HEIGHT_RELATIVE, + ASSERT_BEFORE_SECONDS_ABSOLUTE, + ASSERT_BEFORE_SECONDS_RELATIVE, + ].contains(&condition) + { + // if new conditions aren't enabled, they are just ignored + expect_error = None; + } + + // the coin11 value is the coinID computed from (H1, H1, 123). + // coin11 is the first coin we spend in this case. + // 51 is CREATE_COIN + let test = format!("(\ + (({{h1}} ({{h1}} (123 (\ + ((51 ({{h2}} (123 ) \ + ))\ + (({{coin11}} ({{h2}} (123 (\ + (({} (1000 ) \ + ))\ + ))", cond); + + let flags = enable_assert_before | no_rel_conds_on_ephemeral; + + match expect_error { + Some(err) => { + assert_eq!(cond_test_flag(&test, flags).unwrap_err().1, err); + }, + None => { + // we don't expect any error + let (a, conds) = cond_test_flag(&test, flags).unwrap(); + + assert_eq!(conds.reserve_fee, 0); + assert_eq!(conds.cost, CREATE_COIN_COST); + + 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((&(*conds.spends[0].coin_id)).into(), H2, 123) + ); + assert_eq!(a.atom(spend.puzzle_hash), H2); + assert_eq!(spend.agg_sig_me.len(), 0); + assert!((spend.flags & ELIGIBLE_FOR_DEDUP) != 0); + } + } +} diff --git a/src/gen/flags.rs b/src/gen/flags.rs index fc91556fd..ee548185c 100644 --- a/src/gen/flags.rs +++ b/src/gen/flags.rs @@ -17,5 +17,11 @@ pub const STRICT_ARGS_COUNT: u32 = 0x80000; // When set, support the new ASSERT_BEFORE_* conditions pub const ENABLE_ASSERT_BEFORE: u32 = 0x100000; -pub const MEMPOOL_MODE: u32 = - CLVM_MEMPOOL_MODE | NO_UNKNOWN_CONDS | COND_ARGS_NIL | STRICT_ARGS_COUNT; +// disallow relative height- and time conditions on ephemeral spends +pub const NO_RELATIVE_CONDITIONS_ON_EPHEMERAL: u32 = 0x200000; + +pub const MEMPOOL_MODE: u32 = CLVM_MEMPOOL_MODE + | NO_UNKNOWN_CONDS + | COND_ARGS_NIL + | STRICT_ARGS_COUNT + | NO_RELATIVE_CONDITIONS_ON_EPHEMERAL; diff --git a/src/gen/validation_error.rs b/src/gen/validation_error.rs index cc4a63af0..6c78fd68d 100644 --- a/src/gen/validation_error.rs +++ b/src/gen/validation_error.rs @@ -48,6 +48,7 @@ pub enum ErrorCode { ImpossibleHeightRelativeConstraints, ImpossibleHeightAbsoluteConstraints, AssertEphemeralFailed, + EphemeralRelativeCondition, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -124,6 +125,7 @@ impl From for u32 { ErrorCode::AssertMyBirthSecondsFailed => 138, ErrorCode::AssertMyBirthHeightFailed => 139, ErrorCode::AssertEphemeralFailed => 140, + ErrorCode::EphemeralRelativeCondition => 141, } } } diff --git a/wheel/chia_rs.pyi b/wheel/chia_rs.pyi index 7574e0710..3c3b60c6b 100644 --- a/wheel/chia_rs.pyi +++ b/wheel/chia_rs.pyi @@ -29,6 +29,8 @@ LIMIT_HEAP: int = ... LIMIT_STACK: int = ... ENABLE_ASSERT_BEFORE: int = ... MEMPOOL_MODE: int = ... +NO_RELATIVE_CONDITIONS_ON_EPHEMERAL: int = ... + ELIGIBLE_FOR_DEDUP: int = ... NO_UNKNOWN_OPS: int = ... diff --git a/wheel/generate_type_stubs.py b/wheel/generate_type_stubs.py index 114b49bc4..9f61c10e2 100644 --- a/wheel/generate_type_stubs.py +++ b/wheel/generate_type_stubs.py @@ -173,6 +173,8 @@ def run_puzzle( LIMIT_STACK: int = ... ENABLE_ASSERT_BEFORE: int = ... MEMPOOL_MODE: int = ... +NO_RELATIVE_CONDITIONS_ON_EPHEMERAL: int = ... + ELIGIBLE_FOR_DEDUP: int = ... NO_UNKNOWN_OPS: int = ... diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 41544d3f4..c94c5d6d0 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -6,6 +6,7 @@ use chia::gen::flags::NO_UNKNOWN_CONDS; use chia::gen::flags::STRICT_ARGS_COUNT; use chia::gen::flags::MEMPOOL_MODE; use chia::gen::flags::ENABLE_ASSERT_BEFORE; +use chia::gen::flags::NO_RELATIVE_CONDITIONS_ON_EPHEMERAL; use chia::merkle_set::compute_merkle_set_root as compute_merkle_root_impl; use chia::allocator::make_allocator; use chia_protocol::Bytes32; @@ -141,6 +142,7 @@ pub fn chia_rs(py: Python, m: &PyModule) -> PyResult<()> { m.add("NO_UNKNOWN_CONDS", NO_UNKNOWN_CONDS)?; m.add("STRICT_ARGS_COUNT", STRICT_ARGS_COUNT)?; m.add("ENABLE_ASSERT_BEFORE", ENABLE_ASSERT_BEFORE)?; + m.add("NO_RELATIVE_CONDITIONS_ON_EPHEMERAL", NO_RELATIVE_CONDITIONS_ON_EPHEMERAL)?; m.add("MEMPOOL_MODE", MEMPOOL_MODE)?; // Chia classes