From 933baf66263edd770298fac396def094989c34a9 Mon Sep 17 00:00:00 2001 From: arvidn Date: Wed, 27 Dec 2023 01:19:36 +0100 Subject: [PATCH] add member functions to SpendBundle, to run puzzles and gather added coins (additions()). In the python bindings, also add removals() and debug() --- Cargo.lock | 1 + chia-protocol/fuzz/Cargo.toml | 1 + chia-protocol/src/spend_bundle.rs | 249 ++++++++++++++++++++++++++++++ test-bundles/1000101.bundle | Bin 0 -> 16295 bytes test-bundles/3000253.bundle | Bin 0 -> 3469 bytes tests/test_spend_bundle.py | 212 +++++++++++++++++++++++++ wheel/chia_rs.pyi | 6 + wheel/generate_type_stubs.py | 7 + 8 files changed, 476 insertions(+) create mode 100644 test-bundles/1000101.bundle create mode 100644 test-bundles/3000253.bundle create mode 100644 tests/test_spend_bundle.py diff --git a/Cargo.lock b/Cargo.lock index 656e6a93b..4e589ec1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,6 +384,7 @@ dependencies = [ "arbitrary", "chia-protocol", "chia-traits 0.3.3", + "clvm-traits", "clvmr", "hex", "libfuzzer-sys", diff --git a/chia-protocol/fuzz/Cargo.toml b/chia-protocol/fuzz/Cargo.toml index 72de911b0..335b06d72 100644 --- a/chia-protocol/fuzz/Cargo.toml +++ b/chia-protocol/fuzz/Cargo.toml @@ -12,6 +12,7 @@ cargo-fuzz = true libfuzzer-sys = "0.4" clvmr = "0.3.3" chia-traits = { path = "../../chia-traits" } +clvm-traits = { path = "../../clvm-traits" } arbitrary = { version = "=1.3.0" } sha2 = "0.10.8" hex = "0.4.3" diff --git a/chia-protocol/src/spend_bundle.rs b/chia-protocol/src/spend_bundle.rs index 860e5fb54..b6feedf09 100644 --- a/chia-protocol/src/spend_bundle.rs +++ b/chia-protocol/src/spend_bundle.rs @@ -1,9 +1,258 @@ use crate::coin_spend::CoinSpend; use crate::streamable_struct; +use crate::Bytes32; +use crate::Coin; use chia_bls::G2Element; use chia_streamable_macro::Streamable; +use chia_traits::Streamable; +use clvm_traits::FromClvm; +use clvmr::allocator::{NodePtr, SExp}; +use clvmr::cost::Cost; +use clvmr::op_utils::{first, rest}; +use clvmr::reduction::EvalErr; +use clvmr::Allocator; +use std::result::Result; + +#[cfg(feature = "py-bindings")] +use pyo3::prelude::*; streamable_struct! (SpendBundle { coin_spends: Vec, aggregated_signature: G2Element, }); + +impl SpendBundle { + pub fn aggregate(spend_bundles: &[SpendBundle]) -> SpendBundle { + let mut coin_spends = Vec::::new(); + let mut aggregated_signature = G2Element::default(); + for sb in spend_bundles { + coin_spends.extend_from_slice(&sb.coin_spends[..]); + aggregated_signature.aggregate(&sb.aggregated_signature); + } + SpendBundle { + coin_spends, + aggregated_signature, + } + } + + pub fn name(&self) -> Bytes32 { + self.hash().into() + } + + pub fn additions(&self) -> Result, EvalErr> { + const CREATE_COIN_COST: Cost = 1800000; + const CREATE_COIN: u8 = 51; + + let mut ret = Vec::::new(); + let mut cost_left = 11000000000; + let mut a = Allocator::new(); + let checkpoint = a.checkpoint(); + use clvmr::ENABLE_FIXED_DIV; + + for cs in &self.coin_spends { + a.restore_checkpoint(&checkpoint); + let (cost, mut conds) = + cs.puzzle_reveal + .run(&mut a, ENABLE_FIXED_DIV, cost_left, &cs.solution)?; + if cost > cost_left { + return Err(EvalErr(a.null(), "cost exceeded".to_string())); + } + cost_left -= cost; + let parent_coin_info: Bytes32 = cs.coin.coin_id().into(); + + while let Some((c, tail)) = a.next(conds) { + conds = tail; + let op = first(&a, c)?; + let c = rest(&a, c)?; + let buf = match a.sexp(op) { + SExp::Atom => a.atom(op), + _ => { + return Err(EvalErr(op, "invalid condition".to_string())); + } + }; + if buf.len() != 1 { + continue; + } + if buf[0] == CREATE_COIN { + let (puzzle_hash, (amount, _)) = <(Bytes32, (u64, NodePtr))>::from_clvm(&a, c) + .map_err(|_| EvalErr(c, "failed to parse spend".to_string()))?; + ret.push(Coin { + parent_coin_info, + puzzle_hash, + amount, + }); + if CREATE_COIN_COST > cost_left { + return Err(EvalErr(a.null(), "cost exceeded".to_string())); + } + cost_left -= CREATE_COIN_COST; + } + } + } + Ok(ret) + } +} + +#[cfg(feature = "py-bindings")] +#[pymethods] +impl SpendBundle { + #[staticmethod] + #[pyo3(name = "aggregate")] + fn py_aggregate(spend_bundles: Vec) -> SpendBundle { + SpendBundle::aggregate(&spend_bundles) + } + + #[pyo3(name = "name")] + fn py_name(&self) -> Bytes32 { + self.name() + } + + fn removals(&self) -> Vec { + let mut ret = Vec::::with_capacity(self.coin_spends.len()); + for cs in &self.coin_spends { + ret.push(cs.coin.clone()); + } + ret + } + + #[pyo3(name = "additions")] + fn py_additions(&self) -> PyResult> { + self.additions() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.1)) + } + + fn debug(&self, py: Python<'_>) -> PyResult<()> { + use pyo3::types::PyDict; + let ctx: &PyDict = PyDict::new(py); + ctx.set_item("self", self.clone().into_py(py))?; + py.run( + "from chia.wallet.util.debug_spend_bundle import debug_spend_bundle\n\ + debug_spend_bundle(self)\n", + None, + Some(ctx), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Program; + use rstest::rstest; + use std::fs; + + #[rstest] + #[case( + "e3c0", + "fd65e4b0f21322f78d1025e8a8ff7a1df77cd40b86885b851f4572e5ce06e4ff", + "e3c000a395f8f69d5e263a9548f13bffb1c4b701ab8f3faa03f7647c8750d077" + )] + #[case( + "bb13", + "6b2aaee962cb1de3fdeb1f0506c02df4b9e162e2af3dd1db22048454b5122a87", + "bb13d1e13438736c7ba0217c7b82ee4db56a7f4fb9d22c703c2152362b2314ee" + )] + fn test_additions_ff( + #[case] spend_file: &str, + #[case] expect_parent: &str, + #[case] expect_ph: &str, + ) { + let spend_bytes = fs::read(format!("../ff-tests/{spend_file}.spend")).expect("read file"); + let spend = CoinSpend::from_bytes(&spend_bytes).expect("parse CoinSpend"); + let bundle = SpendBundle::new(vec![spend], G2Element::default()); + + let additions = bundle.additions().expect("additions"); + + assert_eq!(additions.len(), 1); + assert_eq!( + additions[0].parent_coin_info.as_ref(), + &hex::decode(expect_parent).expect("hex::decode") + ); + assert_eq!( + additions[0].puzzle_hash.as_ref(), + &hex::decode(expect_ph).expect("hex::decode") + ); + assert_eq!(additions[0].amount, 1); + } + + fn test_impl(solution: &str, body: F) { + let solution = hex::decode(solution).expect("hex::decode"); + let test_coin = Coin::new( + (&hex::decode("4444444444444444444444444444444444444444444444444444444444444444") + .unwrap()) + .into(), + (&hex::decode("3333333333333333333333333333333333333333333333333333333333333333") + .unwrap()) + .into(), + 1, + ); + let spend = CoinSpend::new( + test_coin.clone(), + Program::new(vec![1_u8].into()), + Program::new(solution.into()), + ); + let bundle = SpendBundle::new(vec![spend], G2Element::default()); + body(test_coin, bundle); + } + + // TODO: Once we have condition types that implement ToClvm and an Encoder + // that serialize directly to bytes, these test solutions can be expressed + // in a much more readable way + #[test] + fn test_single_create_coin() { + // This is a solution to the identity puzzle: + // ((CREATE_COIN . (222222..22 . (1 . NIL))) . + // )) + let solution = "ff\ +ff33\ +ffa02222222222222222222222222222222222222222222222222222222222222222\ +ff01\ +80\ +80"; + test_impl(solution, |test_coin: Coin, bundle: SpendBundle| { + let additions = bundle.additions().expect("additions"); + + let new_coin = Coin::new( + test_coin.coin_id().into(), + (&hex::decode("2222222222222222222222222222222222222222222222222222222222222222") + .unwrap()) + .into(), + 1, + ); + assert_eq!(additions, [new_coin]); + }); + } + + #[test] + fn test_invalid_condition() { + // This is a solution to the identity puzzle: + // (((1 . CREATE_COIN) . (222222..22 . (1 . NIL))) . + // )) + let solution = "ff\ +ffff0133\ +ffa02222222222222222222222222222222222222222222222222222222222222222\ +ff01\ +80\ +80"; + + test_impl(solution, |_test_coin, bundle: SpendBundle| { + assert_eq!(bundle.additions().unwrap_err().1, "invalid condition"); + }); + } + + #[test] + fn test_invalid_spend() { + // This is a solution to the identity puzzle: + // ((CREATE_COIN . (222222..22 . ((1 . 1) . NIL))) . + // )) + let solution = "ff\ +ff33\ +ffa02222222222222222222222222222222222222222222222222222222222222222\ +ffff0101\ +80\ +80"; + + test_impl(solution, |_test_coin, bundle: SpendBundle| { + assert_eq!(bundle.additions().unwrap_err().1, "failed to parse spend"); + }); + } +} diff --git a/test-bundles/1000101.bundle b/test-bundles/1000101.bundle new file mode 100644 index 0000000000000000000000000000000000000000..3d91923544c84080f1dc14d8b894c931d7862dd8 GIT binary patch literal 16295 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 literal 0 HcmV?d00001 diff --git a/test-bundles/3000253.bundle b/test-bundles/3000253.bundle new file mode 100644 index 0000000000000000000000000000000000000000..0143c3e794a5ea9a623968abfbca50e5b0aa2ff6 GIT binary patch literal 3469 zcmZQzVBpxhPFjQeUD^i&memIZuS9N<;J)AHeU#zT&wsjnzA4kwwyu4kb>QiIo|0d$ zr|l|^UHJEV-0EbX>+{|gd}aUsPn(s2fq~`M$xoaAGyVV12q&2Tb3?hz|NnFTXZ`8$c|MhKB!)U=|BV8JGZ?1Eeu^gN*=5 zfHXt(0TnX-Hv-uKbOQT-Rv(=6Q6$1sIB1i-|6S!;B=*^J+BK(#ji7rgbGmv`pk zIoIz?gIP>nZN1no8^2+YoKz97y5abaxf_pw9MSMJ=XA*LpT<&sg&j&g3El@vq?2EY zcdmW%;p=g(WzSbmBqGY$`IF8b1%>TsloJ!>w*MF0b!N5Jn^fPvr!=BOsm#7@#aAA2 zp=VQ7MDAVFy&v)SC^%*q4o;hAC6&?0^eypq=*PCGgO(m!>#Wv#eyegnvhH!$Dk5Tr zb-|?z!J{!lY-S`jW}bS!)xE99RkxyUL*mUF>&^GY-{YIy)>G>A=B{wQ()=i5VkS0m z;=IwAp?1tf>@75~(7ZgEJ=2)CidB6R$L*DGg~YRc-__sLis0QLKt#;2+<7v`W;AA~ z9W#n=SGxC=KV?2&dahsd4)2GsL(4Qgo?NO{ShdZeG2p;tVq)e|C5 zg`K~C(Kmoges%btn^)f^`U None: + buf = open(f"test-bundles/{input_file}.bundle", "rb").read() + bundle = ty.from_bytes(buf) + + additions = bundle.additions() + removals = bundle.removals() + + add = f"{additions}" + assert add == expected_add + + rem = f"{removals}" + assert rem == expected_rem diff --git a/wheel/chia_rs.pyi b/wheel/chia_rs.pyi index 07589e428..9f3e3c972 100644 --- a/wheel/chia_rs.pyi +++ b/wheel/chia_rs.pyi @@ -2131,6 +2131,12 @@ class SubSlotProofs: class SpendBundle: coin_spends: List[CoinSpend] aggregated_signature: G2Element + @staticmethod + def aggregate(sbs: List[SpendBundle]) -> SpendBundle: ... + def name(self) -> bytes32: ... + def removals(self) -> List[Coin]: ... + def additions(self) -> List[Coin]: ... + def debug(self) -> None: ... def __init__( self, coin_spends: Sequence[CoinSpend], diff --git a/wheel/generate_type_stubs.py b/wheel/generate_type_stubs.py index 614136fe0..071d027cf 100644 --- a/wheel/generate_type_stubs.py +++ b/wheel/generate_type_stubs.py @@ -208,6 +208,13 @@ def parse_rust_source(filename: str) -> List[Tuple[str, List[str]]]: "def to_program(self) -> ChiaProgram: ...", "def uncurry(self) -> Tuple[ChiaProgram, ChiaProgram]: ...", ], + "SpendBundle": [ + "@staticmethod\n def aggregate(sbs: List[SpendBundle]) -> SpendBundle: ...", + "def name(self) -> bytes32: ...", + "def removals(self) -> List[Coin]: ...", + "def additions(self) -> List[Coin]: ...", + "def debug(self) -> None: ...", + ], } classes = []