diff --git a/crates/chia-consensus/fuzz/fuzz_targets/parse-spends.rs b/crates/chia-consensus/fuzz/fuzz_targets/parse-spends.rs index 7e9dbdcf4..a0be1b18c 100644 --- a/crates/chia-consensus/fuzz/fuzz_targets/parse-spends.rs +++ b/crates/chia-consensus/fuzz/fuzz_targets/parse-spends.rs @@ -7,14 +7,21 @@ use chia_fuzz::{make_list, BitCursor}; use clvmr::{Allocator, NodePtr}; use chia_consensus::consensus_constants::TEST_CONSTANTS; -use chia_consensus::gen::flags::{NO_UNKNOWN_CONDS, STRICT_ARGS_COUNT}; +use chia_consensus::gen::flags::{ + ENABLE_SHA256TREE_CONDITIONS, NO_UNKNOWN_CONDS, STRICT_ARGS_COUNT, +}; fuzz_target!(|data: &[u8]| { let mut a = Allocator::new(); let input = make_list(&mut a, &mut BitCursor::new(data)); // spends is a list of spends let input = a.new_pair(input, NodePtr::NIL).unwrap(); - for flags in &[0, STRICT_ARGS_COUNT, NO_UNKNOWN_CONDS] { + for flags in &[ + 0, + STRICT_ARGS_COUNT, + NO_UNKNOWN_CONDS, + ENABLE_SHA256TREE_CONDITIONS, + ] { let _ret = parse_spends::( &a, input, diff --git a/crates/chia-consensus/src/gen/conditions.rs b/crates/chia-consensus/src/gen/conditions.rs index 39213398a..c39a1c129 100644 --- a/crates/chia-consensus/src/gen/conditions.rs +++ b/crates/chia-consensus/src/gen/conditions.rs @@ -10,14 +10,16 @@ use super::opcodes::{ ASSERT_COIN_ANNOUNCEMENT, ASSERT_CONCURRENT_PUZZLE, ASSERT_CONCURRENT_SPEND, ASSERT_EPHEMERAL, ASSERT_HEIGHT_ABSOLUTE, ASSERT_HEIGHT_RELATIVE, ASSERT_MY_AMOUNT, ASSERT_MY_BIRTH_HEIGHT, ASSERT_MY_BIRTH_SECONDS, 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, RECEIVE_MESSAGE, - REMARK, RESERVE_FEE, SEND_MESSAGE, SOFTFORK, + ASSERT_PUZZLE_ANNOUNCEMENT, ASSERT_SECONDS_ABSOLUTE, ASSERT_SECONDS_RELATIVE, + ASSERT_SHA256_TREE, CREATE_COIN, CREATE_COIN_ANNOUNCEMENT, CREATE_COIN_COST, + CREATE_PUZZLE_ANNOUNCEMENT, RECEIVE_MESSAGE, REMARK, RESERVE_FEE, SEND_MESSAGE, SOFTFORK, }; use super::sanitize_int::{sanitize_uint, SanitizedUint}; use super::validation_error::{first, next, rest, ErrorCode, ValidationErr}; use crate::consensus_constants::ConsensusConstants; -use crate::gen::flags::{DONT_VALIDATE_SIGNATURE, NO_UNKNOWN_CONDS, STRICT_ARGS_COUNT}; +use crate::gen::flags::{ + DONT_VALIDATE_SIGNATURE, ENABLE_SHA256TREE_CONDITIONS, NO_UNKNOWN_CONDS, STRICT_ARGS_COUNT, +}; use crate::gen::make_aggsig_final_message::u64_to_bytes; use crate::gen::messages::{Message, SpendId}; use crate::gen::spend_visitor::SpendVisitor; @@ -235,6 +237,9 @@ pub enum Condition { // this means the condition is unconditionally true and can be skipped Skip, SkipRelativeCondition, + + // assert sha256tree (SExp, 32 bytes) + AssertSha256Tree(NodePtr, [u8; 32]), } fn check_agg_sig_unsafe_message( @@ -581,6 +586,21 @@ pub fn parse_args( // this condition is always true, we always ignore arguments Ok(Condition::Skip) } + ASSERT_SHA256_TREE => { + if flags & ENABLE_SHA256TREE_CONDITIONS == 0 { + return Err(ValidationErr(c, ErrorCode::InvalidConditionOpcode)); + } + let sexp = first(a, c)?; + c = rest(a, c)?; + maybe_check_args_terminator(a, c, flags)?; + let id = sanitize_hash(a, first(a, c)?, 32, ErrorCode::InvalidHashValue)?; + let hash: [u8; 32] = a + .atom(id) + .as_ref() + .try_into() + .expect("we already sanitised this"); + Ok(Condition::AssertSha256Tree(sexp, hash)) + } _ => Err(ValidationErr(c, ErrorCode::InvalidConditionOpcode)), } } @@ -792,6 +812,9 @@ pub struct ParseState { // TODO: We would probably save heap allocations by turning this into a // blst_pairing object. pub pkm_pairs: Vec<(PublicKey, Bytes)>, + + // shatree asserts + sha256tree_asserts: Vec<(NodePtr, [u8; 32])>, } // returns (parent-id, puzzle-hash, amount, condition-list) @@ -1257,6 +1280,15 @@ pub fn parse_conditions( Condition::SkipRelativeCondition => { assert_not_ephemeral(&mut spend.flags, state, ret.spends.len()); } + Condition::AssertSha256Tree(sexp, hash) => { + if flags & ENABLE_SHA256TREE_CONDITIONS != 0 { + // the raison d'etre for this condition is to pay extra for guaranteed caching, to make it cheaper overall + // if clvm_utils::tree_hash(a, sexp).to_bytes() != a.atom(hash).as_ref() { + // return Err(ValidationErr(c, ErrorCode::AssertSha256TreeFailed)); + // } + state.sha256tree_asserts.push((sexp, hash)); + } + } Condition::Skip => {} } } @@ -1296,7 +1328,7 @@ fn is_ephemeral( // condition op-code pub fn parse_spends( a: &Allocator, - spends: NodePtr, + spends: NodePtr, // list of ((parent_id, puzzle_hash, amount, conditions)... ) max_cost: Cost, flags: u32, aggregate_signature: &Signature, @@ -1459,6 +1491,13 @@ pub fn validate_conditions( } } + for (sexp, hash) in &state.sha256tree_asserts { + // TODO: add caching here + if clvm_utils::tree_hash(a, *sexp).to_bytes() != *hash { + return Err(ValidationErr(*sexp, ErrorCode::AssertSha256TreeFailed)); + } + } + if !state.assert_puzzle.is_empty() { let mut announcements = HashSet::::new(); @@ -1968,6 +2007,7 @@ fn test_invalid_spend_list_terminator() { #[case(AGG_SIG_PARENT_AMOUNT, "{pubkey} ({msg1}")] #[case(ASSERT_CONCURRENT_SPEND, "{coin12}")] #[case(ASSERT_CONCURRENT_PUZZLE, "{h2}")] +#[case(ASSERT_SHA256_TREE, "{h1} ({h2}")] fn test_strict_args_count( #[case] condition: ConditionOpcode, #[case] arg: &str, @@ -1979,10 +2019,10 @@ fn test_strict_args_count( "((({{h1}} ({{h2}} (123 ((({} ({} ( 1337 )))))", condition as u8, arg ), - flags | DONT_VALIDATE_SIGNATURE, + flags | DONT_VALIDATE_SIGNATURE | ENABLE_SHA256TREE_CONDITIONS, ); if flags == 0 { - // two of the cases won't pass, even when garbage at the end is allowed. + // three of the cases won't pass, even when garbage at the end is allowed. if condition == ASSERT_COIN_ANNOUNCEMENT { assert_eq!(ret.unwrap_err().1, ErrorCode::AssertCoinAnnouncementFailed,); } else if condition == ASSERT_PUZZLE_ANNOUNCEMENT { @@ -1990,6 +2030,8 @@ fn test_strict_args_count( ret.unwrap_err().1, ErrorCode::AssertPuzzleAnnouncementFailed, ); + } else if condition == ASSERT_SHA256_TREE { + assert_eq!(ret.unwrap_err().1, ErrorCode::AssertSha256TreeFailed,); } else { assert!(ret.is_ok()); } @@ -2352,12 +2394,13 @@ fn test_multiple_conditions( #[case(AGG_SIG_PARENT_AMOUNT)] #[case(ASSERT_CONCURRENT_SPEND)] #[case(ASSERT_CONCURRENT_PUZZLE)] +#[case(ASSERT_SHA256_TREE)] fn test_missing_arg(#[case] condition: ConditionOpcode) { // extra args are disallowed in mempool mode assert_eq!( cond_test_flag( &format!("((({{h1}} ({{h2}} (123 ((({} )))))", condition as u8), - 0 + ENABLE_SHA256TREE_CONDITIONS ) .unwrap_err() .1, @@ -3951,6 +3994,57 @@ fn test_concurrent_puzzle_fail() { } } +#[cfg(test)] +#[rstest] +#[case( + "0x1000", + "0x21df504fc8e0a0c53f8da8728a6ce0b2c6911db03184ee59eda9a6a108b008e4", + true +)] +#[case( + "0x1000", + "0x21df504fc8e0a0c53f8da8728a6ce0b2c6911db03184ee59eda9a6a108b008e5", + false +)] +#[case( + "(0x1000 0x2000 ", + "0x52beefa879dfaab96e4e42d202da06853e0f64f07e5144fcb1e46cb2de3eb7dc", + true +)] // (0x1000 . 0x2000) +#[case( + "(0x1000 0x2000 ", + "0x52beefa879dfaab96e4e42d202da06853e0f64f07e5144fcb1e46cb2de3eb7dd", + false +)] // (0x1000 . 0x2000) +#[case( + "(0x1000 (0x2000 (0x3000 ) ", + "0x02fa79e6a347b47ddd95a785b045de89f87d9c0f0cafec012d127ae4ebc1dc9b", + true +)] // (0x1000 0x2000 0x300) +#[case( + "(0x1000 (0x2000 (0x3000 ) ", + "0x02fa79e6a347b47ddd95a785b045de89f87d9c0f0cafec012d127ae4ebc1dc9c", + false +)] // (0x1000 0x2000 0x300) +fn test_sha256tree(#[case] sexp: &str, #[case] hash: &str, #[case] expected: bool) { + let input = format!( + "(\ + (({{h1}} ({{h2}} (123 (((91 ({sexp} ({hash} )))\ + ))" + ); + assert_eq!( + cond_test_cb( + &input, + MEMPOOL_MODE | ENABLE_SHA256TREE_CONDITIONS, + None, + &Signature::default(), + None, + ) + .is_ok(), + expected + ); +} + #[test] fn test_assert_concurrent_puzzle_self() { // ASSERT_CONCURRENT_PUZZLE diff --git a/crates/chia-consensus/src/gen/flags.rs b/crates/chia-consensus/src/gen/flags.rs index b7c243298..1c9ca0902 100644 --- a/crates/chia-consensus/src/gen/flags.rs +++ b/crates/chia-consensus/src/gen/flags.rs @@ -19,3 +19,5 @@ pub const STRICT_ARGS_COUNT: u32 = 0x8_0000; pub const DONT_VALIDATE_SIGNATURE: u32 = 0x1_0000; pub const MEMPOOL_MODE: u32 = CLVM_MEMPOOL_MODE | NO_UNKNOWN_CONDS | STRICT_ARGS_COUNT; + +pub const ENABLE_SHA256TREE_CONDITIONS: u32 = 0x4_0000; diff --git a/crates/chia-consensus/src/gen/opcodes.rs b/crates/chia-consensus/src/gen/opcodes.rs index 30916086a..83435d856 100644 --- a/crates/chia-consensus/src/gen/opcodes.rs +++ b/crates/chia-consensus/src/gen/opcodes.rs @@ -55,6 +55,8 @@ pub const ASSERT_BEFORE_SECONDS_ABSOLUTE: ConditionOpcode = 85; pub const ASSERT_BEFORE_HEIGHT_RELATIVE: ConditionOpcode = 86; pub const ASSERT_BEFORE_HEIGHT_ABSOLUTE: ConditionOpcode = 87; +pub const ASSERT_SHA256_TREE: ConditionOpcode = 91; + // no-op condition pub const REMARK: ConditionOpcode = 1; @@ -155,7 +157,8 @@ pub fn parse_opcode(a: &Allocator, op: NodePtr, _flags: u32) -> Option Some(b0), + | RECEIVE_MESSAGE + | ASSERT_SHA256_TREE => Some(b0), _ => None, } } else { diff --git a/crates/chia-consensus/src/gen/validation_error.rs b/crates/chia-consensus/src/gen/validation_error.rs index a7cc9dfa2..47aafe3ab 100644 --- a/crates/chia-consensus/src/gen/validation_error.rs +++ b/crates/chia-consensus/src/gen/validation_error.rs @@ -163,6 +163,8 @@ pub enum ErrorCode { InvalidMessageMode, InvalidCoinId, MessageNotSentOrReceived, + AssertSha256TreeFailed, + InvalidHashValue, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] @@ -359,6 +361,8 @@ impl From for u32 { ErrorCode::InvalidMessageMode => 145, ErrorCode::InvalidCoinId => 146, ErrorCode::MessageNotSentOrReceived => 147, + ErrorCode::AssertSha256TreeFailed => 148, + ErrorCode::InvalidHashValue => 149, } } } diff --git a/crates/chia-tools/src/bin/run-spend.rs b/crates/chia-tools/src/bin/run-spend.rs index dddcc19ad..eebf2ab45 100644 --- a/crates/chia-tools/src/bin/run-spend.rs +++ b/crates/chia-tools/src/bin/run-spend.rs @@ -127,6 +127,13 @@ impl DebugPrint for Condition { } Self::Skip => "[Skip] REMARK ...".to_string(), Self::SkipRelativeCondition => "[SkipRelativeCondition]".to_string(), + Self::AssertSha256Tree(sexp, hash) => { + format!( + "ASSERT_SHA256_TREE {} {}", + sexp.debug_print(a), + hash.debug_print(a) + ) + } } } }