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 000000000..974d84374 Binary files /dev/null and b/ff-tests/bb13.spend differ diff --git a/ff-tests/e3c0.spend b/ff-tests/e3c0.spend new file mode 100644 index 000000000..18e314ac1 Binary files /dev/null and b/ff-tests/e3c0.spend differ 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;