From 68921438b63187294ccbeaf9fdadb0bf0a1c2e6f Mon Sep 17 00:00:00 2001 From: arvidn Date: Fri, 18 Aug 2023 16:16:40 +0200 Subject: [PATCH] introduce fast_forward_singleton(), to change the coin of a singleton spend --- Cargo.lock | 4 + Cargo.toml | 4 + chia-protocol/src/bytes.rs | 6 + ff-tests/bb13.spend | Bin 0 -> 6297 bytes ff-tests/e3c0.spend | Bin 0 -> 6297 bytes src/error.rs | 41 +++ src/fast_forward.rs | 509 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 8 files changed, 566 insertions(+) create mode 100644 ff-tests/bb13.spend create mode 100644 ff-tests/e3c0.spend create mode 100644 src/error.rs create mode 100644 src/fast_forward.rs diff --git a/Cargo.lock b/Cargo.lock index 1dc360bc3..33ae00c69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,10 @@ name = "chia" version = "0.2.9" dependencies = [ "chia-protocol", + "chia-traits", + "chia-wallet", + "clvm-derive", + "clvm-traits", "clvm-utils", "clvmr", "hex", diff --git a/Cargo.toml b/Cargo.toml index 0a5c81f2d..445fb7652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,11 @@ clvmr = "=0.3.0" hex = "=0.4.3" pyo3 = { version = ">=0.19.0", optional = true } clvm-utils = { version = "=0.2.7", path = "clvm-utils" } +chia-traits = { version = "=0.1.0", path = "chia-traits" } +clvm-traits = { version = "=0.1.0", path = "clvm-traits" } +clvm-derive = { version = "=0.1.0", path = "clvm-derive" } chia-protocol = { version = "=0.2.7", path = "chia-protocol" } +chia-wallet = { version = "=0.1.0", path = "chia-wallet" } hex-literal = "=0.4.1" thiserror = "1.0.44" diff --git a/chia-protocol/src/bytes.rs b/chia-protocol/src/bytes.rs index 635e7289b..569f6b2b2 100644 --- a/chia-protocol/src/bytes.rs +++ b/chia-protocol/src/bytes.rs @@ -260,6 +260,12 @@ impl<'a, const N: usize> From<&'a BytesImpl> for &'a [u8; N] { } } +impl From<&BytesImpl> for [u8; N] { + fn from(v: &BytesImpl) -> [u8; N] { + v.0 + } +} + impl<'a, const N: usize> From<&'a BytesImpl> for &'a [u8] { fn from(v: &'a BytesImpl) -> &'a [u8] { &v.0 diff --git a/ff-tests/bb13.spend b/ff-tests/bb13.spend new file mode 100644 index 0000000000000000000000000000000000000000..974d84374404ce768334590241c9361065130984 GIT binary patch literal 6297 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 literal 0 HcmV?d00001 diff --git a/ff-tests/e3c0.spend b/ff-tests/e3c0.spend new file mode 100644 index 0000000000000000000000000000000000000000..18e314ac15bd2d386eaa1e0ce1e871d492e43d26 GIT binary patch literal 6297 zcmds5eN>j^8RvOgq^6>DNWqpTpf$tU!MRj8+R?SnqI0a#%IW*O zC(^K`MD(K!v8Jzg z{p^u>HAVlvnYOkz880p`*cADndloJ!IanE?{tWUEIZY(dP`Z(F0!;K0erep?NPz%I zAb2RBpAhm=l%{0aluS*bNUBt@W?4UFgiK zEUC0bJe21`Q%3r!jKMUI0Mj?{(9j0quegvrVZ#iNYWrW$^wR?sPf2!V*Dr~qX^Ncv#leiEfXw+Wls1V1eCQ;PJp z<$?eek}hP*LnCb~6U9|$E=`Lntv)ihGxlLOj;hVekl*YEa$cO=G6DoIG@@<85E`wi9y-ceNfUU;fjaSt)tP z=XUK__zcx;9J2E2OM%bNjw;#ERNC6ycx3+R*GK&P{%;RE6P=U2bW3{Or{^BZC@v}A zJFmB5%u9cMeoIApUk7_r=dzdjVjT8Y*rnZ7u?FN+K3fM)W*3m*0>~G)?Com;OVl)tJg0wfS(aV=2Mv0 zqi8aFolz8-jm`*#@;7G`N#@he2zGtwj0`dxtWkh&v$}kKlDXeCyiDfHuHhvzKXDB& zlKBVg@?jN&da^%~)UO{8U2X|D-VsV4XX*bpK2q7@pp$qhFYH>@1tp2*%hia-uA)Ny z@IH{sf%ur@N~ykr3QNSGMlqdT?p6*?_!I&Pr2E&&87bbukCOxNgq z0cLc|YMo#E>INXbfa&B|DZ&m*hF{!FU^k6#s#3saY>l}`hEISoscvzWysH-2LKp=j z58B?Vqf9IVrz)a>(?I2AiqPqclnUaO0Wd~j5=|^nPIzdNYaX#s!PGYojLdeqfMY{z zkzg{nT!Yxe%eHJ8_n5TFCh0>{YRAe$0PiXJ!ic?^Z?mB&a=TC#h~{j#W_Vs zcaUIa9IR*2i!N>8)PX|zs>q^RAj@iaU5K(%wuI)99A%DhbC##UrWmvsU^IO&G0P$^rOKfk9y^)L&!Jac@S&fhfYQ)JH@1$AlvGGcB$yl| zCnbOh!ce)FvK+@^Acu949h}3u`G)6EH@$(_^70)A++qn6>47Im@V)P*FG#UC=V1&oAE!p}w zGWxTKDx3`GtBCmyCShlAUn0Tn>LrERl)^?=0ows!M zCM0OiTe>TfA+PD!;N5TO5-Bfh1kTyea5=}c^({!5E0yfwV@n4YpFmVOI15rH<@=0# z;jxeUs~o3LpVGmI08PT11XX*QKA1YEx;yI^EmR#dckK=1a_?JkRaf=+wP`b_JhrdC?WuKlkYW{i0ti$P*Jq17h$*}k1+itEs{`9&TsbB1D zuNm2L#JhD$wb77J`@yMucdyB&YNP1Y&NsS-&imD))ka3L_wI_B2iA4NS|cv_YUR6A z-aE2&nL+3Dhsg+Cf;zKOAwh(f-Strrq0&9nyNZExSiNLNRKkZ);e!M3Nc;c@OCr!9 zM=lmJ9zb-+xNkBy9zl6EmC9u;>i!|J=05}k>HauWoh)#jE$Aq-bQuL{ z9brunU%fP(orW7B)fU9+WXD2@sGfmDYy}{i)dHh;8Px}|Vs&}(BFai--J9)}0{1@P`d&4+?4vpRzl`lpc`jpBQ`7^~8+MH-zhzPBtnLX_ ztGoJI+h_!Ke4za=KW)|J?m1unNtHRaas7;+t@vf@EaR1u-1VEM&TgJsy=&p{tVfhZ zbq88Hx@X?8p<|-IC$0JIA2mIgH}3cEeE96rL+j4Ge=+^tGny@H$eye(QkNzVees&R zJ$pw#zacm;ySRQ_%3s#*{>|{TQCXIMo8NrGdv#@I$IkP&-1N#vMH7On^bRXcdLnawXa4!aRaBR> v;>45bD;~c7d~n&mmDAs9y7j%}yxccGzQ5UAT^m@n>Qi13d>pIg$ngIKSEl)} literal 0 HcmV?d00001 diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..bf6609523 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,41 @@ +use crate::gen::validation_error::ValidationErr; +use clvmr::reduction::EvalErr; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum Error { + #[error("CLVM {0}")] + Clvm(#[from] clvm_traits::Error), + + #[error("Eval {0}")] + Eval(#[from] EvalErr), + + #[error("Validation {0}")] + Validation(#[from] ValidationErr), + + #[error("not a singleton mod hash")] + NotSingletonModHash, + + #[error("inner puzzle hash mismatch")] + InnerPuzzleHashMismatch, + + #[error("puzzle hash mismatch")] + PuzzleHashMismatch, + + #[error("coin amount mismatch")] + CoinAmountMismatch, + + #[error("coin amount is even")] + CoinAmountEven, + + #[error("parent coin mismatch")] + ParentCoinMismatch, + + #[error("coin mismatch")] + CoinMismatch, + + #[error("{0}")] + Custom(String), +} + +pub type Result = std::result::Result; diff --git a/src/fast_forward.rs b/src/fast_forward.rs new file mode 100644 index 000000000..3efe6d980 --- /dev/null +++ b/src/fast_forward.rs @@ -0,0 +1,509 @@ +use crate::error::{Error, Result}; +use chia_protocol::Bytes32; +use chia_protocol::Coin; +use chia_wallet::singleton::SINGLETON_TOP_LAYER_PUZZLE_HASH; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::CurriedProgram; +use clvm_utils::{tree_hash, tree_hash_atom, tree_hash_pair}; +use clvmr::allocator::{Allocator, NodePtr}; + +#[derive(FromClvm, ToClvm, Debug)] +#[clvm(tuple)] +pub struct SingletonStruct { + pub mod_hash: Bytes32, + pub launcher_id: Bytes32, + pub launcher_puzzle_hash: Bytes32, +} + +#[derive(FromClvm, ToClvm, Debug)] +#[clvm(curried_args)] +pub struct SingletonArgs { + pub singleton_struct: SingletonStruct, + pub inner_puzzle: NodePtr, +} + +#[derive(FromClvm, ToClvm, Debug)] +#[clvm(proper_list)] +pub struct LineageProof { + pub parent_parent_coin_id: Bytes32, + pub parent_inner_puzzle_hash: Bytes32, + pub parent_amount: u64, +} + +#[derive(FromClvm, ToClvm, Debug)] +#[clvm(proper_list)] +pub struct SingletonSolution { + pub lineage_proof: LineageProof, + pub amount: u64, + pub inner_solution: NodePtr, +} + +// TODO: replace this with a generic function to compute the hash of curried +// puzzles +const OP_QUOTE: u8 = 1; +const OP_APPLY: u8 = 2; +const OP_CONS: u8 = 4; +fn curry_single_arg(arg_hash: [u8; 32], rest: [u8; 32]) -> [u8; 32] { + tree_hash_pair( + tree_hash_atom(&[OP_CONS]), + tree_hash_pair( + tree_hash_pair(tree_hash_atom(&[OP_QUOTE]), arg_hash), + tree_hash_pair(rest, tree_hash_atom(&[])), + ), + ) +} + +fn curry_and_treehash(inner_puzzle_hash: &Bytes32, singleton_struct: &SingletonStruct) -> Bytes32 { + let singleton_struct_hash = tree_hash_pair( + tree_hash_atom(&singleton_struct.mod_hash), + tree_hash_pair( + tree_hash_atom(&singleton_struct.launcher_id), + tree_hash_atom(&singleton_struct.launcher_puzzle_hash), + ), + ); + + let args_hash = tree_hash_atom(&[OP_QUOTE]); + let args_hash = curry_single_arg(inner_puzzle_hash.into(), args_hash); + let args_hash = curry_single_arg(singleton_struct_hash, args_hash); + + tree_hash_pair( + tree_hash_atom(&[OP_APPLY]), + tree_hash_pair( + tree_hash_pair( + tree_hash_atom(&[OP_QUOTE]), + (&singleton_struct.mod_hash).into(), + ), + tree_hash_pair(args_hash, tree_hash_atom(&[])), + ), + ) + .into() +} + +// given a puzzle, solution and new coin of a singleton +// this function validates the lineage proof and returns a new +// solution spending a new coin ID. +// The existing coin to be spent and the new coin's parent must also be passed in +// for validation. +pub fn fast_forward_singleton( + a: &mut Allocator, + puzzle: NodePtr, + solution: NodePtr, + coin: &Coin, // the current coin being spent (for validation) + new_coin: &Coin, // the new coin to spend + new_parent: &Coin, // the parent coin of the new coin being spent +) -> Result { + // a coin with an even amount is not a valid singleton + // as defined by singleton_top_layer_v1_1.clsp + if (coin.amount & 1) == 0 || (new_parent.amount & 1) == 0 || (new_coin.amount & 1) == 0 { + return Err(Error::CoinAmountEven); + } + + // in the case of fast-forwarding a spend, we require the amount to remain + // unchanged + if coin.amount != new_coin.amount || coin.amount != new_parent.amount { + return Err(Error::CoinAmountMismatch); + } + + // we can only fast-forward spends of singletons whose puzzle hash doesn't + // change + if coin.puzzle_hash != new_parent.puzzle_hash || coin.puzzle_hash != new_coin.puzzle_hash { + return Err(Error::PuzzleHashMismatch); + } + + let singleton = CurriedProgram::::from_clvm(a, puzzle)?; + let mut new_solution = SingletonSolution::from_clvm(a, solution)?; + + // this is the tree hash of the singleton top layer puzzle + // the tree hash of singleton_top_layer_v1_1.clsp + if singleton.args.singleton_struct.mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH { + return Err(Error::NotSingletonModHash); + } + + // also make sure the actual mod-hash of this puzzle matches the + // singleton_top_layer_v1_1.clsp + let mod_hash = tree_hash(a, singleton.program); + if mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH { + return Err(Error::NotSingletonModHash); + } + + // we can only fast-forward if the coin amount stay the same + // this is to minimize the risk of producing an invalid spend, after + // fast-forward. e.g. we might end up attempting to spend more that the + // amount of the coin + if coin.amount != new_solution.lineage_proof.parent_amount || coin.amount != new_parent.amount { + return Err(Error::CoinAmountMismatch); + } + + // given the parent's parent, the parent's inner puzzle and parent's amount, + // we can compute the hash of the curried inner puzzle for our parent coin + let parent_puzzle_hash = curry_and_treehash( + &new_solution.lineage_proof.parent_inner_puzzle_hash, + &singleton.args.singleton_struct, + ); + + // now that we know the parent coin's puzzle hash, we have all the pieces to + // compute the coin being spent (before the fast-forward). + let parent_coin = Coin { + parent_coin_info: new_solution.lineage_proof.parent_parent_coin_id, + puzzle_hash: parent_puzzle_hash, + amount: new_solution.lineage_proof.parent_amount, + }; + + if parent_coin.coin_id() != coin.parent_coin_info { + return Err(Error::ParentCoinMismatch); + } + + let inner_puzzle_hash = tree_hash(a, singleton.args.inner_puzzle); + if inner_puzzle_hash != new_solution.lineage_proof.parent_inner_puzzle_hash { + return Err(Error::InnerPuzzleHashMismatch); + } + + let puzzle_hash = tree_hash(a, puzzle); + + if puzzle_hash != new_parent.puzzle_hash || puzzle_hash != coin.puzzle_hash { + // we can only fast-forward if the puzzle hash match the new coin + // the spend is assumed to be valied already, so we don't check it + // against the original coin being spent + return Err(Error::PuzzleHashMismatch); + } + + // update the solution to use the new parent coin's information + new_solution.lineage_proof.parent_parent_coin_id = new_parent.parent_coin_info; + + let expected_new_parent = new_parent.coin_id(); + + if new_coin.parent_coin_info != expected_new_parent { + return Err(Error::CoinMismatch); + } + + Ok(new_solution.to_clvm(a)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::gen::run_puzzle::run_puzzle; + use chia_protocol::CoinSpend; + use chia_traits::streamable::Streamable; + use clvmr::serde::{node_from_bytes, node_to_bytes}; + use hex_literal::hex; + use rstest::rstest; + use std::fs; + use std::io::Cursor; + + // this test loads CoinSpends from file (Coin, puzzle, solution)-triples + // and "fast-forwards" the spend onto a few different parent-parent coins + // and ensures the spends are still valid + #[rstest] + #[case("e3c0")] + #[case("bb13")] + fn test_fast_forward( + #[case] spend_file: &str, + #[values( + "abababababababababababababababababababababababababababababababab", + "0000000000000000000000000000000000000000000000000000000000000000", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + )] + new_parents_parent: &str, + ) { + let spend_bytes = fs::read(format!("ff-tests/{spend_file}.spend")).expect("read file"); + let spend = + CoinSpend::parse(&mut Cursor::new(spend_bytes.as_slice())).expect("parse CoinSpend"); + let new_parents_parent = hex::decode(new_parents_parent).unwrap(); + + let mut a = Allocator::new_limited(500000000, 62500000, 62500000); + let puzzle = spend.puzzle_reveal.to_clvm(&mut a).expect("to_clvm"); + let solution = spend.solution.to_clvm(&mut a).expect("to_clvm"); + let puzzle_hash = Bytes32::from(tree_hash(&a, puzzle)); + + let new_parent_coin = Coin { + parent_coin_info: new_parents_parent.as_slice().into(), + puzzle_hash, + amount: spend.coin.amount, + }; + + let new_coin = Coin { + parent_coin_info: new_parent_coin.coin_id().into(), + puzzle_hash, + amount: spend.coin.amount, + }; + + // perform fast-forward + let new_solution = fast_forward_singleton( + &mut a, + puzzle, + solution, + &spend.coin, + &new_coin, + &new_parent_coin, + ) + .expect("fast-forward"); + let new_solution = node_to_bytes(&a, new_solution).expect("serialize new solution"); + + // run original spend + let conditions1 = run_puzzle( + &mut a, + spend.puzzle_reveal.as_slice(), + spend.solution.as_slice(), + &spend.coin.parent_coin_info, + spend.coin.amount, + 11000000000, + 0, + ) + .expect("run_puzzle"); + + // run new spend + let conditions2 = run_puzzle( + &mut a, + spend.puzzle_reveal.as_slice(), + new_solution.as_slice(), + &new_coin.parent_coin_info, + new_coin.amount, + 11000000000, + 0, + ) + .expect("run_puzzle"); + + assert!(conditions1.spends[0].create_coin == conditions2.spends[0].create_coin); + } + + fn run_ff_test( + mutate: fn(&mut Allocator, &mut Coin, &mut Coin, &mut Coin, &mut Vec, &mut Vec), + expected_err: Error, + ) { + let spend_bytes = fs::read(format!("ff-tests/e3c0.spend")).expect("read file"); + let mut spend = + CoinSpend::parse(&mut Cursor::new(spend_bytes.as_slice())).expect("parse CoinSpend"); + let new_parents_parent: &[u8] = + &hex!("abababababababababababababababababababababababababababababababab"); + + let mut a = Allocator::new_limited(500000000, 62500000, 62500000); + let puzzle = spend.puzzle_reveal.to_clvm(&mut a).expect("to_clvm"); + let puzzle_hash = Bytes32::from(tree_hash(&a, puzzle)); + + let mut new_parent_coin = Coin { + parent_coin_info: new_parents_parent.into(), + puzzle_hash, + amount: spend.coin.amount, + }; + + let mut new_coin = Coin { + parent_coin_info: new_parent_coin.coin_id().into(), + puzzle_hash, + amount: spend.coin.amount, + }; + + let mut puzzle = spend.puzzle_reveal.as_slice().to_vec(); + let mut solution = spend.solution.as_slice().to_vec(); + mutate( + &mut a, + &mut spend.coin, + &mut new_coin, + &mut new_parent_coin, + &mut puzzle, + &mut solution, + ); + + let puzzle = node_from_bytes(&mut a, puzzle.as_slice()).expect("to_clvm"); + let solution = node_from_bytes(&mut a, solution.as_slice()).expect("to_clvm"); + + // attempt fast-forward + assert_eq!( + fast_forward_singleton( + &mut a, + puzzle, + solution, + &spend.coin, + &new_coin, + &new_parent_coin + ) + .unwrap_err(), + expected_err + ); + } + + #[test] + fn test_even_amount() { + run_ff_test( + |_a, coin, _new_coin, _new_parent, _puzzle, _solution| { + coin.amount = 2; + }, + Error::CoinAmountEven, + ); + + run_ff_test( + |_a, _coin, new_coin, _new_parent, _puzzle, _solution| { + new_coin.amount = 2; + }, + Error::CoinAmountEven, + ); + + run_ff_test( + |_a, _coin, _new_coin, new_parent, _puzzle, _solution| { + new_parent.amount = 2; + }, + Error::CoinAmountEven, + ); + } + + #[test] + fn test_amount_mismatch() { + run_ff_test( + |_a, coin, _new_coin, _new_parent, _puzzle, _solution| { + coin.amount = 3; + }, + Error::CoinAmountMismatch, + ); + + run_ff_test( + |_a, _coin, new_coin, _new_parent, _puzzle, _solution| { + new_coin.amount = 3; + }, + Error::CoinAmountMismatch, + ); + + run_ff_test( + |_a, _coin, _new_coin, new_parent, _puzzle, _solution| { + new_parent.amount = 3; + }, + Error::CoinAmountMismatch, + ); + } + + fn parse_solution(a: &mut Allocator, solution: &[u8]) -> SingletonSolution { + let new_solution = node_from_bytes(a, solution).expect("parse solution"); + SingletonSolution::from_clvm(&a, new_solution).expect("parse solution") + } + + fn serialize_solution(a: &mut Allocator, solution: &SingletonSolution) -> Vec { + let new_solution = solution.to_clvm(a).expect("to_clvm"); + node_to_bytes(&a, new_solution).expect("serialize solution") + } + + fn parse_singleton(a: &mut Allocator, puzzle: &[u8]) -> CurriedProgram { + let puzzle = node_from_bytes(a, puzzle).expect("parse puzzle"); + CurriedProgram::::from_clvm(a, puzzle).expect("uncurry") + } + + fn serialize_singleton( + a: &mut Allocator, + singleton: &CurriedProgram, + ) -> Vec { + let puzzle = singleton.to_clvm(a).expect("to_clvm"); + node_to_bytes(a, puzzle).expect("serialize puzzle") + } + + #[test] + fn test_invalid_lineage_proof_parent() { + run_ff_test( + |a, _coin, _new_coin, _new_parent, _puzzle, solution| { + let mut new_solution = parse_solution(a, &solution); + + // corrupt the lineage proof + new_solution.lineage_proof.parent_parent_coin_id = Bytes32::from(hex!( + "fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + )); + + *solution = serialize_solution(a, &new_solution); + }, + Error::ParentCoinMismatch, + ); + } + + #[test] + fn test_invalid_lineage_proof_parent_amount() { + run_ff_test( + |a, _coin, _new_coin, _new_parent, _puzzle, solution| { + let mut new_solution = parse_solution(a, &solution); + + // corrupt the lineage proof + new_solution.lineage_proof.parent_amount = 11; + + *solution = serialize_solution(a, &new_solution); + }, + Error::CoinAmountMismatch, + ); + } + + #[test] + fn test_invalid_lineage_proof_parent_inner_ph() { + run_ff_test( + |a, _coin, _new_coin, _new_parent, _puzzle, solution| { + let mut new_solution = parse_solution(a, &solution); + + // corrupt the lineage proof + new_solution.lineage_proof.parent_inner_puzzle_hash = Bytes32::from(hex!( + "fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + )); + + *solution = serialize_solution(a, &new_solution); + }, + Error::ParentCoinMismatch, + ); + } + + #[test] + fn test_invalid_lineage_proof_parent_inner_ph_with_coin() { + run_ff_test( + |a, coin, new_coin, new_parent, puzzle, solution| { + let mut new_solution = parse_solution(a, &solution); + let singleton = parse_singleton(a, puzzle); + + // corrupt the lineage proof + new_solution.lineage_proof.parent_inner_puzzle_hash = Bytes32::from(hex!( + "fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + )); + + // adjust the coins puzzle hashes to match + let parent_puzzle_hash = curry_and_treehash( + &new_solution.lineage_proof.parent_inner_puzzle_hash, + &singleton.args.singleton_struct, + ); + + *solution = serialize_solution(a, &new_solution); + + *new_parent = Coin { + parent_coin_info: new_solution.lineage_proof.parent_parent_coin_id, + puzzle_hash: parent_puzzle_hash, + amount: new_solution.lineage_proof.parent_amount, + }; + + new_coin.puzzle_hash = parent_puzzle_hash.into(); + + coin.parent_coin_info = new_parent.coin_id().into(); + coin.puzzle_hash = parent_puzzle_hash; + }, + Error::InnerPuzzleHashMismatch, + ); + } + + #[test] + fn test_invalid_puzzle_hash() { + run_ff_test( + |a, _coin, _new_coin, _new_parent, puzzle, _solution| { + let mut singleton = parse_singleton(a, puzzle); + + singleton.program = a.null(); + + *puzzle = serialize_singleton(a, &singleton); + }, + Error::NotSingletonModHash, + ); + } + + #[test] + fn test_invalid_singleton_struct_puzzle_hash() { + run_ff_test( + |a, _coin, _new_coin, _new_parent, puzzle, _solution| { + let mut singleton = parse_singleton(a, puzzle); + + singleton.args.singleton_struct.mod_hash = Bytes32::from(hex!( + "fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + )); + + *puzzle = serialize_singleton(a, &singleton); + }, + Error::NotSingletonModHash, + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 34d255a53..e942f4f0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod allocator; pub mod compression; +pub mod error; +pub mod fast_forward; pub mod gen; pub mod generator_rom; pub mod merkle_set;