diff --git a/Cargo.lock b/Cargo.lock index 1dc360bc3..b35c72096 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", @@ -303,8 +307,10 @@ dependencies = [ "chia", "chia-protocol", "chia-traits", + "clvm-traits", "clvm-utils", "clvmr", + "hex-literal", "libfuzzer-sys", ] @@ -344,11 +350,13 @@ dependencies = [ "chia", "chia-protocol", "chia-traits", + "chia-wallet", "clap", "clvm-traits", "clvm-utils", "clvmr", "hex", + "hex-literal", "sqlite", "threadpool", "zstd", 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/chia-tools/Cargo.toml b/chia-tools/Cargo.toml index 43300c4b3..34e1193e5 100644 --- a/chia-tools/Cargo.toml +++ b/chia-tools/Cargo.toml @@ -13,6 +13,7 @@ chia-protocol = { version = "0.2.7", path = "../chia-protocol" } chia-traits = { path = "../chia-traits" } clvm-utils = { path = "../clvm-utils" } clvm-traits = { path = "../clvm-traits" } +chia-wallet = { version = "=0.1.0", path = "../chia-wallet" } clvmr = { version = "=0.3.0", features = ["counters"] } chia = { version = "0.2.9", path = ".." } sqlite = "=0.31.0" @@ -20,6 +21,7 @@ clap = { version = "=4.3.9", features = ["derive"] } zstd = "=0.12.3" threadpool = "=1.8.1" hex = "=0.4.3" +hex-literal = "=0.4.1" [lib] name = "chia_tools" @@ -44,3 +46,13 @@ bench = false name = "gen-corpus" test = false bench = false + +[[bin]] +name = "run-spend" +test = false +bench = false + +[[bin]] +name = "fast-forward-spend" +test = false +bench = false diff --git a/chia-tools/src/bin/fast-forward-spend.rs b/chia-tools/src/bin/fast-forward-spend.rs new file mode 100644 index 000000000..fa17c8062 --- /dev/null +++ b/chia-tools/src/bin/fast-forward-spend.rs @@ -0,0 +1,77 @@ +use clap::Parser; +use std::fs; +use std::io::Cursor; + +use chia::fast_forward::fast_forward_singleton; +use chia_protocol::bytes::Bytes32; +use chia_protocol::{coin::Coin, coin_spend::CoinSpend, program::Program}; +use chia_traits::streamable::Streamable; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::tree_hash; +use clvmr::allocator::Allocator; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Path to CoinSpend file + #[arg(short, long)] + spend: String, + + /// fast-forward the CoinSpend to spend a coin whose parent's parent is this + /// coin ID. + #[arg(short, long)] + new_parents_parent: String, + + /// Save resulting CoinSpend to this file + #[arg(short, long)] + output_file: String, +} + +fn main() { + let args = Args::parse(); + + let spend_bytes = fs::read(args.spend).expect("read file"); + let spend = CoinSpend::parse(&mut Cursor::new(&spend_bytes)).expect("parse CoinSpend"); + + let new_parents_parent: Bytes32 = hex::decode(args.new_parents_parent) + .expect("invalid hex") + .as_slice() + .try_into() + .expect("parent_id"); + + 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, + 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, + }; + + let new_solution = fast_forward_singleton( + &mut a, + puzzle, + solution, + &spend.coin, + &new_coin, + &new_parent_coin, + ) + .expect("fast-forward"); + + let new_spend = CoinSpend { + coin: new_parent_coin, + puzzle_reveal: spend.puzzle_reveal, + solution: Program::from_clvm(&a, new_solution).expect("new solution"), + }; + let mut bytes = Vec::::new(); + new_spend.stream(&mut bytes).expect("stream CoinSpend"); + fs::write(args.output_file, bytes).expect("write"); +} diff --git a/chia-tools/src/bin/gen-corpus.rs b/chia-tools/src/bin/gen-corpus.rs index 2f2b6ad7b..906dbf0e8 100644 --- a/chia-tools/src/bin/gen-corpus.rs +++ b/chia-tools/src/bin/gen-corpus.rs @@ -10,6 +10,7 @@ use chia_traits::streamable::Streamable; use chia_protocol::bytes::Bytes32; use chia_protocol::{coin::Coin, coin_spend::CoinSpend, program::Program}; +use chia_wallet::singleton::SINGLETON_TOP_LAYER_PUZZLE_HASH; use clvm_traits::FromClvm; use clvm_utils::{tree_hash, CurriedProgram}; use clvmr::allocator::NodePtr; @@ -48,6 +49,7 @@ fn main() { use std::collections::HashSet; use std::sync::{Arc, Mutex}; let seen_puzzles = Arc::new(Mutex::new(HashSet::::new())); + let seen_singletons = Arc::new(Mutex::new(HashSet::::new())); iterate_tx_blocks( &args.file, @@ -59,6 +61,7 @@ fn main() { let prg = block.transactions_generator.unwrap(); let seen_puzzles = seen_puzzles.clone(); + let seen_singletons = seen_singletons.clone(); pool.execute(move || { let mut a = Allocator::new_limited(500000000, 62500000, 62500000); @@ -76,10 +79,13 @@ fn main() { _ => puzzle_hash, }; - if !seen_puzzles.lock().unwrap().insert(mod_hash) { + let run_puzzle = seen_puzzles.lock().unwrap().insert(mod_hash); + let fast_forward = (mod_hash == SINGLETON_TOP_LAYER_PUZZLE_HASH) + && seen_singletons.lock().unwrap().insert(puzzle_hash); + + if !run_puzzle && !fast_forward { return; } - use std::fs::write; let puzzle_reveal = Program::from_clvm(a, puzzle).expect("puzzle reveal"); @@ -97,10 +103,20 @@ fn main() { let mut bytes = Vec::::new(); spend.stream(&mut bytes).expect("stream CoinSpend"); - let directory = "../fuzz/corpus/run-puzzle"; - let _ = std::fs::create_dir_all(directory); - write(format!("{directory}/{mod_hash}.spend"), bytes).expect("write"); - println!("{height}: {mod_hash}"); + if run_puzzle { + let directory = "../fuzz/corpus/run-puzzle"; + let _ = std::fs::create_dir_all(directory); + write(format!("{directory}/{mod_hash}.spend"), &bytes).expect("write"); + println!("{height}: {mod_hash}"); + } + + if fast_forward { + let directory = "../fuzz/corpus/fast-forward"; + let _ = std::fs::create_dir_all(directory); + write(format!("{directory}/{puzzle_hash}.spend"), bytes) + .expect("write"); + println!("{height}: {puzzle_hash}"); + } }, ) .expect("failed to run block generator"); diff --git a/chia-tools/src/bin/run-spend.rs b/chia-tools/src/bin/run-spend.rs new file mode 100644 index 000000000..a3f73991c --- /dev/null +++ b/chia-tools/src/bin/run-spend.rs @@ -0,0 +1,285 @@ +use chia::gen::conditions::Condition; +use chia_protocol::Bytes32; +use chia_traits::Streamable; +use clap::Parser; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::tree_hash; +use clvm_utils::CurriedProgram; +use clvmr::{allocator::NodePtr, Allocator}; +use hex_literal::hex; +use std::io::Cursor; + +/// Run a puzzle given a solution and print the resulting conditions +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Path to CoinSpend (serialized binary file) + spend: String, +} + +trait DebugPrint { + fn debug_print(&self, a: &Allocator) -> String; +} + +impl DebugPrint for NodePtr { + fn debug_print(&self, a: &Allocator) -> String { + hex::encode(a.atom(*self)) + } +} + +impl DebugPrint for Condition { + // TODO: it would be nice if this was a macro + fn debug_print(&self, a: &Allocator) -> String { + match self { + Self::AggSigUnsafe(pk, msg) => format!( + "AGG_SIG_UNSAFE {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::AggSigMe(pk, msg) => { + format!("AGG_SIG_ME {} {}", pk.debug_print(a), msg.debug_print(a)) + } + Self::AggSigParent(pk, msg) => format!( + "AGG_SIG_PARENT {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::AggSigPuzzle(pk, msg) => format!( + "AGG_SIG_PUZZLE {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::AggSigAmount(pk, msg) => format!( + "AGG_SIG_AMOUNT {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::AggSigPuzzleAmount(pk, msg) => format!( + "AGG_SIG_PUZZLE_AMOUNT {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::AggSigParentAmount(pk, msg) => format!( + "AGG_SIG_PARENT_AMOUNT {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::AggSigParentPuzzle(pk, msg) => format!( + "AGG_SIG_PARENT_PUZZLE {} {}", + pk.debug_print(a), + msg.debug_print(a) + ), + Self::CreateCoin(ph, amount, hint) => format!( + "CRATE_COIN {} {} {}", + ph.debug_print(a), + amount, + hint.debug_print(a) + ), + Self::ReserveFee(amount) => format!("RESERVE_FEE {}", amount), + Self::CreateCoinAnnouncement(msg) => { + format!("CREATE_COIN_ANNOUNCEMENT {}", msg.debug_print(a)) + } + Self::CreatePuzzleAnnouncement(msg) => { + format!("CREATE_PUZZLE_ANNOUNCEMENT {}", msg.debug_print(a)) + } + Self::AssertCoinAnnouncement(msg) => { + format!("ASSERT_COIN_ANNOUNCEMENT {}", msg.debug_print(a)) + } + Self::AssertPuzzleAnnouncement(msg) => { + format!("ASSERT_PUZZLE_ANNOUNCEMENT {}", msg.debug_print(a)) + } + Self::AssertConcurrentSpend(coinid) => { + format!("ASSERT_CONCURRENT_SPEND {}", coinid.debug_print(a)) + } + Self::AssertConcurrentPuzzle(ph) => { + format!("ASSERT_CONCURRENT_PUZZLE {}", ph.debug_print(a)) + } + Self::AssertMyCoinId(coinid) => format!("ASSERT_MY_COINID {}", coinid.debug_print(a)), + Self::AssertMyParentId(coinid) => { + format!("ASSERT_MY_PARENT_ID {}", coinid.debug_print(a)) + } + Self::AssertMyPuzzlehash(ph) => format!("ASSERT_MY_PUZZLE_HASH {}", ph.debug_print(a)), + Self::AssertMyAmount(amount) => format!("ASSERT_MY_AMOUNT {amount}"), + Self::AssertMyBirthSeconds(s) => format!("ASSERT_MY_BIRTH_SECONDS {s}"), + Self::AssertMyBirthHeight(h) => format!("ASSERT_MY_BIRTH_HEIGHT {h}"), + Self::AssertSecondsRelative(s) => format!("ASSERT_SECONDS_RELATIVE {s}"), + Self::AssertSecondsAbsolute(s) => format!("ASSERT_SECONDS_ABSOLUTE {s}"), + Self::AssertHeightRelative(h) => format!("ASSERT_HEIGHT_RELATIVE {h}"), + Self::AssertHeightAbsolute(h) => format!("ASSERT_HEIGHT_ABSOLUTE {h}"), + Self::AssertBeforeSecondsRelative(s) => format!("ASSERT_BEFORE_SECONDS_RELATIVE {s}"), + Self::AssertBeforeSecondsAbsolute(s) => format!("ASSERT_BEFORE_SECONDS_ABSOLUTE {s}"), + Self::AssertBeforeHeightRelative(h) => format!("ASSERT_BEFORE_HEIGHT_RELATIVE {h}"), + Self::AssertBeforeHeightAbsolute(h) => format!("ASSERT_BEFORE_HEIGHT_ABSOLUTE {h}"), + Self::AssertEphemeral => "ASSERT_EPHEMERAL".to_string(), + Self::Softfork(cost) => format!("SOFTFORK {cost}"), + Self::Skip => "[Skip] REMARK ...".to_string(), + Self::SkipRelativeCondition => "[SkipRelativeCondition]".to_string(), + } + } +} + +const SINGLETON_MOD_HASH: [u8; 32] = + hex!("7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f"); + +#[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 EveProof { + pub parent_parent_coin_id: 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, +} + +#[derive(FromClvm, ToClvm, Debug)] +#[clvm(proper_list)] +pub struct EveSingletonSolution { + pub lineage_proof: EveProof, + pub amount: u64, + pub inner_solution: NodePtr, +} + +fn print_puzzle_info(a: &Allocator, puzzle: NodePtr, solution: NodePtr) { + println!("Puzzle: {}", hex::encode(tree_hash(a, puzzle))); + // exit if this puzzle is not curried + let Ok(uncurried) = CurriedProgram::::from_clvm(a, puzzle) else { + println!(" puzzle has no curried parameters"); + return; + }; + + match tree_hash(a, uncurried.program) { + SINGLETON_MOD_HASH => { + println!("singleton_top_layer_1_1.clsp"); + let Ok(uncurried) = CurriedProgram::::from_clvm(a, puzzle) else { + println!("failed to uncurry singleton"); + return; + }; + println!(" singleton-struct:"); + println!( + " mod-hash: {:?}", + uncurried.args.singleton_struct.mod_hash + ); + println!( + " launcher-id: {:?}", + uncurried.args.singleton_struct.launcher_id + ); + println!( + " launcher-puzzle-hash: {:?}", + uncurried.args.singleton_struct.launcher_puzzle_hash + ); + + let inner_solution = if let Ok(sol) = SingletonSolution::from_clvm(a, solution) { + println!(" solution"); + println!(" lineage-proof: {:?}", sol.lineage_proof); + println!(" amount: {}", sol.amount); + sol.inner_solution + } else if let Ok(sol) = EveSingletonSolution::from_clvm(a, solution) { + println!(" eve-solution:"); + println!(" lineage-proof:: {:?}", sol.lineage_proof); + println!(" amount: {}", sol.amount); + sol.inner_solution + } else { + println!("-- failed to parse singleton solution"); + return; + }; + + println!("\nInner Puzzle:\n"); + print_puzzle_info(a, uncurried.args.inner_puzzle, inner_solution); + } + + // Unknown puzzle + n => { + println!(" Unknown puzzle {}", &hex::encode(n)); + } + } +} +fn main() { + use chia::gen::conditions::parse_args; + use chia::gen::flags::ENABLE_SOFTFORK_CONDITION; + use chia::gen::opcodes::parse_opcode; + use chia::gen::validation_error::{first, rest}; + use chia_protocol::coin_spend::CoinSpend; + use clvmr::reduction::{EvalErr, Reduction}; + use clvmr::{run_program, ChiaDialect}; + use std::fs::read; + + let args = Args::parse(); + + let mut a = Allocator::new(); + let spend = read(args.spend).expect("spend file not found"); + let spend = CoinSpend::parse(&mut Cursor::new(spend.as_slice())).expect("parse CoinSpend"); + + let puzzle = spend + .puzzle_reveal + .to_clvm(&mut a) + .expect("deserialize puzzle"); + let solution = spend + .solution + .to_clvm(&mut a) + .expect("deserialize solution"); + + println!("Spending {:?}", &spend.coin); + println!(" coin-id: {}\n", hex::encode(spend.coin.coin_id())); + let dialect = ChiaDialect::new(0); + let Reduction(_clvm_cost, conditions) = + match run_program(&mut a, &dialect, puzzle, solution, 11000000000) { + Ok(r) => r, + Err(EvalErr(_, e)) => { + println!("Eval Error: {e:?}"); + return; + } + }; + + println!("Conditions\n"); + let mut iter = conditions; + + while let Some((mut c, next)) = a.next(iter) { + iter = next; + let op_ptr = first(&a, c).expect("parsing conditions"); + let op = match parse_opcode(&a, op_ptr, ENABLE_SOFTFORK_CONDITION) { + None => { + println!(" UNKNOWN CONDITION [{}]", &hex::encode(a.atom(op_ptr))); + continue; + } + Some(v) => v, + }; + + c = rest(&a, c).expect("parsing conditions"); + + let condition = parse_args(&a, c, op, 0).expect("parse condition args"); + println!(" [{op:?}] {}", condition.debug_print(&a)); + } + + // look for known puzzles to display more information + + println!("\nPuzzle Info\n"); + print_puzzle_info(&a, puzzle, solution); +} 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/fuzz/Cargo.toml b/fuzz/Cargo.toml index ffcd3bd36..c1690af2f 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -12,9 +12,11 @@ cargo-fuzz = true libfuzzer-sys = "0.4" clvmr = "=0.3.0" clvm-utils = { path = "../clvm-utils" } +clvm-traits = { path = "../clvm-traits" } chia-protocol = { path = "../chia-protocol" } chia-traits = { path = "../chia-traits" } chia = { path = ".." } +hex-literal = "=0.4.1" [lib] name = "fuzzing_utils" @@ -89,3 +91,10 @@ path = "fuzz_targets/run-puzzle.rs" test = false doc = false bench = false + +[[bin]] +name = "fast-forward" +path = "fuzz_targets/fast-forward.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fast-forward.rs b/fuzz/fuzz_targets/fast-forward.rs new file mode 100644 index 000000000..d7dbe9816 --- /dev/null +++ b/fuzz/fuzz_targets/fast-forward.rs @@ -0,0 +1,95 @@ +#![no_main] +use chia::fast_forward::fast_forward_singleton; +use chia::gen::run_puzzle::run_puzzle; +use chia::gen::validation_error::ValidationErr; +use chia_protocol::Bytes32; +use chia_protocol::Coin; +use chia_protocol::CoinSpend; +use chia_traits::streamable::Streamable; +use clvm_traits::ToClvm; +use clvm_utils::tree_hash; +use clvmr::serde::node_to_bytes; +use clvmr::Allocator; +use hex_literal::hex; +use libfuzzer_sys::fuzz_target; +use std::io::Cursor; + +fuzz_target!(|data: &[u8]| { + let Ok(spend) = CoinSpend::parse(&mut Cursor::new(data)) else { + return; + }; + let new_parents_parent = + hex!("abababababababababababababababababababababababababababababababab"); + + let mut a = Allocator::new_limited(500000000, 62500000, 62500000); + let Ok(puzzle) = spend.puzzle_reveal.to_clvm(&mut a) else { + return; + }; + let Ok(solution) = spend.solution.to_clvm(&mut a) else { + return; + }; + 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 Ok(new_solution) = fast_forward_singleton( + &mut a, + puzzle, + solution, + &spend.coin, + &new_coin, + &new_parent_coin, + ) else { + return; + }; + 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, + ); + + // 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, + ); + + match (conditions1, conditions2) { + (Err(ValidationErr(n1, msg1)), Err(ValidationErr(n2, msg2))) => { + assert_eq!(msg1, msg2); + assert_eq!( + node_to_bytes(&a, n1).unwrap(), + node_to_bytes(&a, n2).unwrap() + ); + } + (Ok(conditions1), Ok(conditions2)) => { + assert!(conditions1.spends[0].create_coin == conditions2.spends[0].create_coin); + } + _ => { + panic!("unexpected"); + } + } +}); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..807d44d24 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,51 @@ +use crate::gen::validation_error::ValidationErr; +use clvmr::reduction::EvalErr; +use thiserror::Error; + +#[cfg(feature = "py-bindings")] +use pyo3::PyErr; + +#[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), +} + +#[cfg(feature = "py-bindings")] +impl std::convert::From for PyErr { + fn from(err: Error) -> PyErr { + pyo3::exceptions::PyValueError::new_err(err.to_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; diff --git a/tests/test_fast_forward.py b/tests/test_fast_forward.py new file mode 100644 index 000000000..38ed79c23 --- /dev/null +++ b/tests/test_fast_forward.py @@ -0,0 +1,23 @@ +from chia_rs import Spend, CoinSpend, Coin, supports_fast_forward, fast_forward_singleton +import pytest + +@pytest.mark.parametrize("file", ["bb13", "e3c0"]) +def test_supports_fast_forward(file: str) -> None: + with open(f"ff-tests/{file}.spend", "rb") as f: + spend = CoinSpend.from_bytes(f.read()) + assert supports_fast_forward(spend) + +@pytest.mark.parametrize("file", ["bb13", "e3c0"]) +def test_fast_forward_singleton(file: str) -> None: + with open(f"ff-tests/{file}.spend", "rb") as f: + spend = CoinSpend.from_bytes(f.read()) + + parents_parent = bytes([0] * 32) + new_parent = Coin(parents_parent, spend.coin.puzzle_hash, spend.coin.amount) + new_coin = Coin(new_parent.name(), new_parent.puzzle_hash, new_parent.amount) + new_solution = fast_forward_singleton(spend, new_coin, new_parent) + + expected = bytearray(bytes(spend.solution)) + # this is where the parent's parent coin ID lives in the solution + expected[3:35] = parents_parent + assert expected == new_solution diff --git a/wheel/chia_rs.pyi b/wheel/chia_rs.pyi index 637f2f075..6ec0596c3 100644 --- a/wheel/chia_rs.pyi +++ b/wheel/chia_rs.pyi @@ -13,6 +13,9 @@ def solution_generator_backrefs(spends: Sequence[Tuple[Coin, bytes, bytes]]) -> def compute_merkle_set_root(items: Sequence[bytes]) -> bytes: ... +def supports_fast_forward(spend: CoinSpend) -> bool : ... +def fast_forward_singleton(spend: CoinSpend, new_coin: Coin, new_parent: Coin) -> bytes: ... + def run_block_generator( program: ReadableBuffer, args: List[ReadableBuffer], max_cost: int, flags: int ) -> Tuple[Optional[int], Optional[SpendBundleConditions]]: ... @@ -42,6 +45,7 @@ ENABLE_FIXED_DIV: int = ... ALLOW_BACKREFS: int = ... ELIGIBLE_FOR_DEDUP: int = ... +ELIGIBLE_FOR_FF: int = ... NO_UNKNOWN_OPS: int = ... diff --git a/wheel/generate_type_stubs.py b/wheel/generate_type_stubs.py index c34e1bc4a..c97f7a8ae 100644 --- a/wheel/generate_type_stubs.py +++ b/wheel/generate_type_stubs.py @@ -196,6 +196,9 @@ def solution_generator_backrefs(spends: Sequence[Tuple[Coin, bytes, bytes]]) -> def compute_merkle_set_root(items: Sequence[bytes]) -> bytes: ... +def supports_fast_forward(spend: CoinSpend) -> bool : ... +def fast_forward_singleton(spend: CoinSpend, new_coin: Coin, new_parent: Coin) -> bytes: ... + def run_block_generator( program: ReadableBuffer, args: List[ReadableBuffer], max_cost: int, flags: int ) -> Tuple[Optional[int], Optional[SpendBundleConditions]]: ... @@ -225,6 +228,7 @@ def run_puzzle( ALLOW_BACKREFS: int = ... ELIGIBLE_FOR_DEDUP: int = ... +ELIGIBLE_FOR_FF: int = ... NO_UNKNOWN_OPS: int = ... diff --git a/wheel/src/api.rs b/wheel/src/api.rs index 0d22352af..009fe3021 100644 --- a/wheel/src/api.rs +++ b/wheel/src/api.rs @@ -50,6 +50,7 @@ use std::iter::zip; use crate::run_program::{run_chia_program, serialized_length}; use crate::adapt_response::eval_err_to_pyresult; +use chia::fast_forward::fast_forward_singleton as native_ff; use chia::gen::get_puzzle_and_solution::get_puzzle_and_solution_for_coin as parse_puzzle_solution; use chia::gen::validation_error::ValidationErr; use clvmr::allocator::NodePtr; @@ -274,6 +275,57 @@ impl AugSchemeMPL { } } +#[pyfunction] +fn supports_fast_forward(spend: &CoinSpend) -> bool { + // the test function just attempts the rebase onto a dummy parent coin + let new_parent = Coin { + parent_coin_info: [0_u8; 32].into(), + puzzle_hash: spend.coin.puzzle_hash, + amount: spend.coin.amount, + }; + let new_coin = Coin { + parent_coin_info: new_parent.coin_id().into(), + puzzle_hash: spend.coin.puzzle_hash, + amount: spend.coin.amount, + }; + + let mut a = make_allocator(LIMIT_HEAP); + let Ok(puzzle) = node_from_bytes(&mut a, spend.puzzle_reveal.as_slice()) else { + return false; + }; + let Ok(solution) = node_from_bytes(&mut a, spend.solution.as_slice()) else { + return false; + }; + + native_ff( + &mut a, + puzzle, + solution, + &spend.coin, + &new_coin, + &new_parent, + ) + .is_ok() +} + +#[pyfunction] +fn fast_forward_singleton<'p>( + py: Python<'p>, + spend: &CoinSpend, + new_coin: &Coin, + new_parent: &Coin, +) -> PyResult<&'p PyBytes> { + let mut a = make_allocator(LIMIT_HEAP); + let puzzle = node_from_bytes(&mut a, spend.puzzle_reveal.as_slice())?; + let solution = node_from_bytes(&mut a, spend.solution.as_slice())?; + + let new_solution = native_ff(&mut a, puzzle, solution, &spend.coin, new_coin, new_parent)?; + Ok(PyBytes::new( + py, + node_to_bytes(&a, new_solution)?.as_slice(), + )) +} + #[pymodule] pub fn chia_rs(py: Python, m: &PyModule) -> PyResult<()> { // generator functions @@ -282,6 +334,8 @@ pub fn chia_rs(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(run_puzzle, m)?)?; m.add_function(wrap_pyfunction!(solution_generator, m)?)?; m.add_function(wrap_pyfunction!(solution_generator_backrefs, m)?)?; + m.add_function(wrap_pyfunction!(supports_fast_forward, m)?)?; + m.add_function(wrap_pyfunction!(fast_forward_singleton, m)?)?; m.add_class::()?; m.add( "ELIGIBLE_FOR_DEDUP",