diff --git a/Cargo.lock b/Cargo.lock index bdb913ce..caab2a5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,8 +462,10 @@ dependencies = [ "clvmr", "hex", "hex-literal", + "k256", "num-bigint", "rstest", + "sha3", "thiserror", ] @@ -729,8 +731,7 @@ dependencies = [ [[package]] name = "clvmr" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd48d84ed6eac4638131341402f30476c9f6c6970ed3ed6984bdf125c5a09538" +source = "git+https://github.com/Chia-Network/clvm_rs?rev=2f413e72fcf1bcafa4a3117f2c2a0a3a0e7e1c6b#2f413e72fcf1bcafa4a3117f2c2a0a3a0e7e1c6b" dependencies = [ "chia-bls 0.10.0", "hex-literal", @@ -741,6 +742,7 @@ dependencies = [ "num-traits", "p256", "sha2", + "sha3", ] [[package]] @@ -1171,6 +1173,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1245,12 +1253,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.1", ] [[package]] @@ -1300,6 +1308,15 @@ dependencies = [ "signature", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1378,7 +1395,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -2181,6 +2198,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 75118d35..76863005 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,7 +136,12 @@ napi-derive = "2.12.2" napi = { version = "2.12.2", default-features = false } paste = "1.0.15" bigdecimal = "0.4.6" +k256 = "0.13.4" +sha3 = "0.10.8" [profile.release] lto = true strip = "symbols" + +[patch.crates-io] +clvmr = { git = "https://github.com/Chia-Network/clvm_rs", rev = "2f413e72fcf1bcafa4a3117f2c2a0a3a0e7e1c6b" } diff --git a/crates/chia-sdk-driver/Cargo.toml b/crates/chia-sdk-driver/Cargo.toml index 3ed272bc..bd233bb1 100644 --- a/crates/chia-sdk-driver/Cargo.toml +++ b/crates/chia-sdk-driver/Cargo.toml @@ -33,6 +33,8 @@ hex-literal = { workspace = true } num-bigint = { workspace = true} hex = { workspace = true } bigdecimal = { workspace = true } +k256 = { workspace = true } +sha3 = { workspace = true } [dev-dependencies] chia-sdk-test = { workspace = true } diff --git a/crates/chia-sdk-driver/src/layers.rs b/crates/chia-sdk-driver/src/layers.rs index 12612475..86e321f1 100644 --- a/crates/chia-sdk-driver/src/layers.rs +++ b/crates/chia-sdk-driver/src/layers.rs @@ -2,8 +2,10 @@ mod cat_layer; mod did_layer; mod nft_ownership_layer; mod nft_state_layer; +mod p2_controller_puzzle_layer; mod p2_delegated_conditions_layer; mod p2_delegated_singleton_layer; +mod p2_eip712_message_layer; mod p2_one_of_many; mod p2_singleton; mod royalty_transfer_layer; @@ -15,8 +17,10 @@ pub use cat_layer::*; pub use did_layer::*; pub use nft_ownership_layer::*; pub use nft_state_layer::*; +pub use p2_controller_puzzle_layer::*; pub use p2_delegated_conditions_layer::*; pub use p2_delegated_singleton_layer::*; +pub use p2_eip712_message_layer::*; pub use p2_one_of_many::*; pub use p2_singleton::*; pub use royalty_transfer_layer::*; diff --git a/crates/chia-sdk-driver/src/layers/p2_controller_puzzle_layer.rs b/crates/chia-sdk-driver/src/layers/p2_controller_puzzle_layer.rs new file mode 100644 index 00000000..14e9538d --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_controller_puzzle_layer.rs @@ -0,0 +1,175 @@ +use chia_protocol::Bytes32; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; + +// https://github.com/Yakuhito/hermes/blob/master/clsp/p2_controller_puzzle.clsp +pub const P2_CONTROLLER_PUZZLE_PUZZLE: [u8; 151] = hex!("ff02ffff01ff04ffff04ff04ffff04ffff0117ffff04ffff02ff06ffff04ff02ffff04ff0bff80808080ffff04ff05ff8080808080ffff02ff0bff178080ffff04ffff01ff43ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080"); +pub const P2_CONTROLLER_PUZZLE_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + " + d5415713619e318bfa7820e06e2b163beef32d82294a5a7fcf9c3c69b0949c88 + " +)); + +/// The p2 controller puzzle [`Layer`] allows a coin to be 'owned' by another puzzle, +/// which sends a message containing a delegated puzzle for the controlled coin to run. +/// +/// This is useful since it enables wallets to only ask for one signature instead of +/// one signature/coin when using standard-like puzzles. Specifically, this puzzle +/// was created to avoid having to sign multiple EIP-712 messages when using the +/// p2 EIP-712 message puzzle. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2ControllerPuzzleLayer { + pub controller_puzzle_hash: Bytes32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2ControllerPuzzleArgs { + pub controller_puzzle_hash: Bytes32, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct P2ControllerPuzzleSolution { + pub delegated_puzzle: P, + pub delegated_solution: S, +} + +impl P2ControllerPuzzleLayer { + pub fn new(controller_puzzle_hash: Bytes32) -> Self { + Self { + controller_puzzle_hash, + } + } + + pub fn spend( + &self, + ctx: &mut SpendContext, + delegated_spend: Spend, + ) -> Result { + self.construct_spend( + ctx, + P2ControllerPuzzleSolution { + delegated_puzzle: delegated_spend.puzzle, + delegated_solution: delegated_spend.solution, + }, + ) + } +} + +impl Layer for P2ControllerPuzzleLayer { + type Solution = P2ControllerPuzzleSolution; + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.p2_controller_puzzle_puzzle()?, + args: P2ControllerPuzzleArgs { + controller_puzzle_hash: self.controller_puzzle_hash, + }, + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_CONTROLLER_PUZZLE_PUZZLE_HASH { + return Ok(None); + } + + let args = P2ControllerPuzzleArgs::from_clvm(allocator, puzzle.args)?; + + Ok(Some(Self { + controller_puzzle_hash: args.controller_puzzle_hash, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(Self::Solution::from_clvm(allocator, solution)?) + } +} + +#[cfg(test)] +mod tests { + use crate::assert_puzzle_hash; + + use super::*; + use chia_protocol::{Bytes, CoinSpend}; + use chia_sdk_test::Simulator; + use chia_sdk_types::Conditions; + use clvm_traits::clvm_quote; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(P2_CONTROLLER_PUZZLE_PUZZLE => P2_CONTROLLER_PUZZLE_PUZZLE_HASH); + + Ok(()) + } + + #[test] + fn test_p2_controller_puzzle() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let controller_puzzle = ctx.allocator.one(); + let controller_puzzle_hash = ctx.tree_hash(controller_puzzle); + + let layer = P2ControllerPuzzleLayer::new(controller_puzzle_hash.into()); + let coin_puzzle = layer.construct_puzzle(ctx)?; + let coin_puzzle_hash = ctx.tree_hash(coin_puzzle); + + let controller_coin = sim.new_coin(controller_puzzle_hash.into(), 42); + let coin = sim.new_coin(coin_puzzle_hash.into(), 69); + + let delegated_puzzle = + clvm_quote!(Conditions::new().reserve_fee(42 + 69)).to_clvm(&mut ctx.allocator)?; + let delegated_solution = ctx.allocator.nil(); + + let delegated_puzzle_hash = ctx.tree_hash(delegated_puzzle); + + let coin_spend = layer.construct_coin_spend( + ctx, + coin, + P2ControllerPuzzleSolution { + delegated_puzzle, + delegated_solution, + }, + )?; + ctx.insert(coin_spend); + + let controller_solution = Conditions::new().send_message( + 23, + Bytes::from(delegated_puzzle_hash.to_vec()), + vec![coin.coin_id().to_clvm(&mut ctx.allocator)?], + ); + let controller_solution = controller_solution.to_clvm(&mut ctx.allocator)?; + + let controller_coin_spend = CoinSpend::new( + controller_coin, + ctx.serialize(&controller_puzzle)?, + ctx.serialize(&controller_solution)?, + ); + ctx.insert(controller_coin_spend); + + sim.spend_coins(ctx.take(), &[])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_eip712_message_layer.rs b/crates/chia-sdk-driver/src/layers/p2_eip712_message_layer.rs new file mode 100644 index 00000000..39261275 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_eip712_message_layer.rs @@ -0,0 +1,355 @@ +use chia_protocol::{Bytes32, BytesImpl}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; +use sha3::{Digest, Keccak256}; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; + +// https://github.com/Yakuhito/hermes/blob/master/clsp/p2_eip712_message.clsp +pub const P2_EIP712_MESSAGE_PUZZLE: [u8; 276] = hex!("ff02ffff01ff02ffff03ffff22ffff20ffff8413d61f00ff17ff5fff81bf8080ffff20ffff24ffff01820ab9ffff0101ffff01ff02ffff03ffff09ffff3eff02ffff3eff05ff0bff178080ff2f80ff80ffff01ff088080ff0180ffff04ff05ffff04ff0bffff04ff2fffff04ffff02ff06ffff04ff02ffff04ff82017fff80808080ffff04ff5fff808080808080808080ffff01ff04ffff04ff04ffff04ff2fff808080ffff02ff82017fff8202ff8080ffff01ff088080ff0180ffff04ffff01ff46ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080"); +pub const P2_EIP712_MESSAGE_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + " + aacce7b99db5b1e9eb16d676fa5f1a2e469ef589f29c4ab0010bac338a4df085 + " +)); + +pub type EthPubkeyBytes = BytesImpl<33>; +pub type EthSignatureBytes = BytesImpl<64>; +pub type Eip712PrefixAndDomainSeparator = BytesImpl<34>; + +/// The p2 EIP-712 [`Layer`] allows an Ethereum wallet to control coins by signing an +/// EIP-712 message containing a delegated puzzle (for example to output [`Conditions`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2Eip712MessageLayer { + pub prefix_and_domain_separator: Eip712PrefixAndDomainSeparator, + pub pubkey: EthPubkeyBytes, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2Eip712MessageArgs { + pub prefix_and_domain_separator: Eip712PrefixAndDomainSeparator, + pub type_hash: Bytes32, + pub pubkey: EthPubkeyBytes, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct P2Eip712MessageSolution { + pub my_id: Bytes32, + pub signed_hash: Bytes32, + pub signature: EthSignatureBytes, + pub delegated_puzzle: P, + pub delegated_solution: S, +} + +impl P2Eip712MessageLayer { + pub fn new( + pubkey: EthPubkeyBytes, + prefix_and_domain_separator: Eip712PrefixAndDomainSeparator, + ) -> Self { + Self { + prefix_and_domain_separator, + pubkey, + } + } + + pub fn from_genesis_challenge(pubkey: EthPubkeyBytes, genesis_challenge: Bytes32) -> Self { + Self { + prefix_and_domain_separator: P2Eip712MessageLayer::prefix_and_domain_separator( + genesis_challenge, + ), + pubkey, + } + } + + pub fn spend( + &self, + ctx: &mut SpendContext, + my_id: Bytes32, + signature: EthSignatureBytes, + delegated_spend: Spend, + ) -> Result { + self.construct_spend( + ctx, + P2Eip712MessageSolution { + my_id, + signed_hash: self.hash_to_sign(my_id, ctx.tree_hash(delegated_spend.puzzle).into()), + signature, + delegated_puzzle: delegated_spend.puzzle, + delegated_solution: delegated_spend.solution, + }, + ) + } + + pub fn domain_separator(genesis_challenge: Bytes32) -> Bytes32 { + let type_hash = Keccak256::digest(b"EIP712Domain(string name,string version,bytes32 salt)"); + + let mut to_hash = Vec::new(); + to_hash.extend_from_slice(&type_hash); + to_hash.extend_from_slice(&Keccak256::digest("Chia Coin Spend")); + to_hash.extend_from_slice(&Keccak256::digest("1")); + to_hash.extend_from_slice(&genesis_challenge); + + Bytes32::new(Keccak256::digest(&to_hash).into()) + } + + pub fn prefix_and_domain_separator( + genesis_challenge: Bytes32, + ) -> Eip712PrefixAndDomainSeparator { + let mut pads = [0u8; 34]; + pads[0] = 0x19; + pads[1] = 0x01; + pads[2..].copy_from_slice(&Self::domain_separator(genesis_challenge)); + pads.into() + } + + pub fn type_hash() -> Bytes32 { + Bytes32::new( + Keccak256::digest(b"ChiaCoinSpend(bytes32 coin_id,bytes32 delegated_puzzle_hash)") + .into(), + ) + } + + pub fn hash_to_sign(&self, coin_id: Bytes32, delegated_puzzle_hash: Bytes32) -> Bytes32 { + /* + bytes32 messageHash = keccak256(abi.encode( + typeHash, + coin_id, + delegated_puzzle_hash + )); + */ + let mut to_hash = Vec::new(); + to_hash.extend_from_slice(&P2Eip712MessageLayer::type_hash()); + to_hash.extend_from_slice(&coin_id); + to_hash.extend_from_slice(&delegated_puzzle_hash); + + let message_hash = Keccak256::digest(&to_hash); + + let mut to_hash = Vec::new(); + to_hash.extend_from_slice(&self.prefix_and_domain_separator); + to_hash.extend_from_slice(&message_hash); + + Bytes32::new(Keccak256::digest(&to_hash).into()) + } +} + +impl Layer for P2Eip712MessageLayer { + type Solution = P2Eip712MessageSolution; + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.p2_eip712_message_puzzle()?, + args: P2Eip712MessageArgs { + prefix_and_domain_separator: self.prefix_and_domain_separator, + type_hash: P2Eip712MessageLayer::type_hash(), + pubkey: self.pubkey, + }, + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_EIP712_MESSAGE_PUZZLE_HASH { + return Ok(None); + } + + let args = P2Eip712MessageArgs::from_clvm(allocator, puzzle.args)?; + + if args.type_hash != Self::type_hash() { + return Ok(None); + } + + Ok(Some(Self { + prefix_and_domain_separator: args.prefix_and_domain_separator, + pubkey: args.pubkey, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(P2Eip712MessageSolution::from_clvm(allocator, solution)?) + } +} + +#[cfg(test)] +mod tests { + use crate::assert_puzzle_hash; + + use super::*; + use chia_consensus::consensus_constants::TEST_CONSTANTS; + use chia_protocol::Bytes; + use chia_sdk_test::Simulator; + use chia_sdk_types::Conditions; + use clvm_traits::clvm_quote; + use clvm_utils::ToTreeHash; + use clvmr::chia_dialect::ENABLE_KECCAK_OPS_OUTSIDE_GUARD; + use clvmr::reduction::Reduction; + use clvmr::serde::node_from_bytes; + use k256::ecdsa::signature::hazmat::PrehashVerifier; + use k256::ecdsa::{Signature as K1Signature, SigningKey, VerifyingKey as K1VerifyingKey}; + use k256::elliptic_curve::rand_core::OsRng; + use rstest::rstest; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(P2_EIP712_MESSAGE_PUZZLE => P2_EIP712_MESSAGE_PUZZLE_HASH); + Ok(()) + } + + #[test] + fn test_type_hash() { + assert_eq!( + P2Eip712MessageLayer::type_hash(), + Bytes32::new(hex!( + "72930978f119c79f9de7a13bd50c9b3261132d7b4819bdf0d3ca4d4c37ade070" + )) + ); + } + + #[test] + fn test_domain_separator() { + assert_eq!( + P2Eip712MessageLayer::domain_separator(TEST_CONSTANTS.genesis_challenge), + Bytes32::new(hex!( + "acfd7ee1b3beb56b11d29d9e48debee9edf2f457d1dbdc19b63e58a6884501af" + )) + ); + } + + #[test] + fn test_softfork_cost() -> anyhow::Result<()> { + let ctx = &mut SpendContext::new(); + // code running inside softfork + // run -d '(mod (PREFIX_AND_DOMAIN_SEPARATOR TYPE_HASH my_id delegated_puzzle_hash signed_hash) (if (= (keccak256 PREFIX_AND_DOMAIN_SEPARATOR (keccak256 TYPE_HASH my_id delegated_puzzle_hash)) signed_hash) () (x)))' + let puzzle_bytes = + hex!("ff02ffff03ffff09ffff3eff02ffff3eff05ff0bff178080ff2f80ff80ffff01ff088080ff0180"); + + let puzzle_ptr = node_from_bytes(&mut ctx.allocator, puzzle_bytes.as_slice())?; + let solution_ptr = vec![ + Bytes::new( + hex!("1901098ccd7d09a29365582c3f7590712bc2c2eb8503586f8a4c628c61c73ffbe4aa") + .to_vec(), + ), // PREFIX_AND_DOMAIN_SEPARATOR (different than those used on testnet11/mainnet) + Bytes::new( + hex!("72930978f119c79f9de7a13bd50c9b3261132d7b4819bdf0d3ca4d4c37ade070").to_vec(), + ), // TYPE_HASH + Bytes::new( + hex!("5c777c45fd52a17a54e420742cadc56172847d9a106ff0ff8af38ef757d84829").to_vec(), + ), // my_id + Bytes::new( + hex!("d842dfa1453a130a8be66bc32708a2d1884662d7daaa4aae530be3259fa6712f").to_vec(), + ), // delegated_puzzle_hash + Bytes::new( + hex!("9f61fdf6077c3eeb96eaa4dd450b11ba3ae17746a2c304388218137972c7ba4c").to_vec(), + ), // signed_hash + ] + .to_clvm(&mut ctx.allocator)?; + + let Reduction(cost, _) = clvmr::run_program( + &mut ctx.allocator, + &clvmr::ChiaDialect::new(ENABLE_KECCAK_OPS_OUTSIDE_GUARD), + puzzle_ptr, + solution_ptr, + 11_000_000_000, + )?; + + assert_eq!(cost, 2605); + Ok(()) + } + + #[rstest] + #[case::successful_spend(true)] + #[case::incorrect_signed_hash(false)] + fn test_p2_eip712_message(#[case] correct_signed_hash: bool) -> anyhow::Result<()> { + let signing_key = SigningKey::random(&mut OsRng); + + // actual test + let ctx = &mut SpendContext::new(); + let mut sim = Simulator::new(); + + let pubkey = signing_key.verifying_key().to_sec1_bytes().to_vec(); + let layer = P2Eip712MessageLayer::from_genesis_challenge( + pubkey.try_into().unwrap(), + TEST_CONSTANTS.genesis_challenge, + ); + let coin_puzzle_reveal = layer.construct_puzzle(ctx)?; + let coin_puzzle_hash = ctx.tree_hash(coin_puzzle_reveal); + + let coin = sim.new_coin(coin_puzzle_hash.into(), 1337); + + let delegated_puzzle_ptr = + clvm_quote!(Conditions::new().reserve_fee(1337)).to_clvm(&mut ctx.allocator)?; + let delegated_solution_ptr = ctx.allocator.nil(); + + let hash_to_sign: Bytes32 = if correct_signed_hash { + layer.hash_to_sign(coin.coin_id(), ctx.tree_hash(delegated_puzzle_ptr).into()) + } else { + layer + .hash_to_sign(coin.coin_id(), ctx.tree_hash(delegated_puzzle_ptr).into()) + .tree_hash() + .into() + }; + + let signature_og: K1Signature = signing_key + .sign_prehash_recoverable(&hash_to_sign.to_vec())? + .0; + let signature: EthSignatureBytes = signature_og.to_vec().try_into().unwrap(); + + let coin_spend = layer.construct_coin_spend( + ctx, + coin, + P2Eip712MessageSolution { + my_id: coin.coin_id(), + signed_hash: hash_to_sign, + signature, + delegated_puzzle: delegated_puzzle_ptr, + delegated_solution: delegated_solution_ptr, + }, + )?; + + ctx.insert(coin_spend); + + let verifier = + K1VerifyingKey::from_sec1_bytes(&signing_key.verifying_key().to_sec1_bytes())?; + assert_eq!(verifier, *signing_key.verifying_key()); + let msg = hash_to_sign.to_vec(); + let sig = K1Signature::from_slice(&signature)?; + assert_eq!(sig, K1Signature::from_slice(&signature_og.to_vec())?); + let result = verifier.verify_prehash(msg.as_ref(), &sig); + assert!(result.is_ok()); + + if correct_signed_hash { + sim.spend_coins(ctx.take(), &[])?; + } else { + assert_eq!( + sim.spend_coins(ctx.take(), &[]) + .err() + .unwrap() + .to_string() + .split(": ") + .last() + .unwrap(), + "clvm raise" + ); + } + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/spend_context.rs b/crates/chia-sdk-driver/src/spend_context.rs index b0be1d14..7b392a43 100644 --- a/crates/chia-sdk-driver/src/spend_context.rs +++ b/crates/chia-sdk-driver/src/spend_context.rs @@ -27,9 +27,11 @@ use clvm_utils::{tree_hash, TreeHash}; use clvmr::{serde::node_from_bytes, Allocator, NodePtr}; use crate::{ - DriverError, Spend, P2_DELEGATED_CONDITIONS_PUZZLE, P2_DELEGATED_CONDITIONS_PUZZLE_HASH, - P2_DELEGATED_SINGLETON_PUZZLE, P2_DELEGATED_SINGLETON_PUZZLE_HASH, P2_ONE_OF_MANY_PUZZLE, - P2_ONE_OF_MANY_PUZZLE_HASH, P2_SINGLETON_PUZZLE, P2_SINGLETON_PUZZLE_HASH, + DriverError, Spend, P2_CONTROLLER_PUZZLE_PUZZLE, P2_CONTROLLER_PUZZLE_PUZZLE_HASH, + P2_DELEGATED_CONDITIONS_PUZZLE, P2_DELEGATED_CONDITIONS_PUZZLE_HASH, + P2_DELEGATED_SINGLETON_PUZZLE, P2_DELEGATED_SINGLETON_PUZZLE_HASH, P2_EIP712_MESSAGE_PUZZLE, + P2_EIP712_MESSAGE_PUZZLE_HASH, P2_ONE_OF_MANY_PUZZLE, P2_ONE_OF_MANY_PUZZLE_HASH, + P2_SINGLETON_PUZZLE, P2_SINGLETON_PUZZLE_HASH, }; /// A wrapper around [`Allocator`] that caches puzzles and keeps track of a list of [`CoinSpend`]. @@ -209,6 +211,19 @@ impl SpendContext { ) } + /// Allocate the p2 EIP-712 message puzzle and return its pointer. + pub fn p2_eip712_message_puzzle(&mut self) -> Result { + self.puzzle(P2_EIP712_MESSAGE_PUZZLE_HASH, &P2_EIP712_MESSAGE_PUZZLE) + } + + /// Allocate the p2 controllr puzzle puzzle and return its pointer. + pub fn p2_controller_puzzle_puzzle(&mut self) -> Result { + self.puzzle( + P2_CONTROLLER_PUZZLE_PUZZLE_HASH, + &P2_CONTROLLER_PUZZLE_PUZZLE, + ) + } + /// Preload a puzzle into the cache. pub fn preload(&mut self, puzzle_hash: TreeHash, ptr: NodePtr) { self.puzzles.insert(puzzle_hash, ptr); diff --git a/crates/chia-sdk-test/src/announcements.rs b/crates/chia-sdk-test/src/announcements.rs index 0bab5cc2..98805939 100644 --- a/crates/chia-sdk-test/src/announcements.rs +++ b/crates/chia-sdk-test/src/announcements.rs @@ -1,10 +1,10 @@ use chia_protocol::{Bytes, Bytes32, CoinSpend}; use chia_sdk_types::{ - announcement_id, AssertCoinAnnouncement, AssertPuzzleAnnouncement, CreateCoinAnnouncement, - CreatePuzzleAnnouncement, + announcement_id, run_puzzle, AssertCoinAnnouncement, AssertPuzzleAnnouncement, + CreateCoinAnnouncement, CreatePuzzleAnnouncement, }; use clvm_traits::{FromClvm, ToClvm}; -use clvmr::{reduction::Reduction, run_program, Allocator, ChiaDialect, NodePtr}; +use clvmr::{Allocator, NodePtr}; #[derive(Debug, Default, Clone)] pub struct Announcements { @@ -117,13 +117,7 @@ pub fn announcements_for_spend(coin_spend: &CoinSpend) -> anyhow::Result::from_clvm(allocator, output)?; diff --git a/crates/chia-sdk-test/src/simulator.rs b/crates/chia-sdk-test/src/simulator.rs index 96da6e1b..3b303f53 100644 --- a/crates/chia-sdk-test/src/simulator.rs +++ b/crates/chia-sdk-test/src/simulator.rs @@ -1,12 +1,24 @@ -use std::collections::HashSet; +use std::{ + collections::HashSet, + time::{Duration, Instant}, +}; -use chia_bls::{DerivableKey, PublicKey, SecretKey}; +use chia_bls::{aggregate_verify_gt, hash_to_g2, DerivableKey, PublicKey, SecretKey}; use chia_consensus::{ - gen::validation_error::ErrorCode, spendbundle_validation::validate_clvm_and_signature, + allocator::make_allocator, + consensus_constants::ConsensusConstants, + gen::{owned_conditions::OwnedSpendBundleConditions, validation_error::ErrorCode}, + spendbundle_conditions::run_spendbundle, + spendbundle_validation::ValidationPair, }; use chia_protocol::{Bytes32, Coin, CoinSpend, CoinState, Program, SpendBundle}; use chia_puzzles::standard::StandardArgs; use chia_sdk_types::TESTNET11_CONSTANTS; +use clvmr::{ + chia_dialect::{ENABLE_KECCAK, ENABLE_KECCAK_OPS_OUTSIDE_GUARD}, + sha2::Sha256, + LIMIT_HEAP, +}; use fastrand::Rng; use indexmap::{IndexMap, IndexSet}; @@ -286,3 +298,54 @@ impl Simulator { self.height += 1; } } + +// currently in mempool_manager.py +// called in threads from pre_validate_spend_bundle() +// pybinding returns (error, cached_results, new_cache_entries, duration) +fn validate_clvm_and_signature( + spend_bundle: &SpendBundle, + max_cost: u64, + constants: &ConsensusConstants, + height: u32, +) -> Result<(OwnedSpendBundleConditions, Vec, Duration), ErrorCode> { + let start_time = Instant::now(); + let mut a = make_allocator(LIMIT_HEAP); + let (sbc, pkm_pairs) = run_spendbundle( + &mut a, + spend_bundle, + max_cost, + height, + ENABLE_KECCAK | ENABLE_KECCAK_OPS_OUTSIDE_GUARD, + constants, + ) + .map_err(|e| e.1)?; + let conditions = OwnedSpendBundleConditions::from(&a, sbc); + + // Collect all pairs in a single vector to avoid multiple iterations + let mut pairs = Vec::new(); + + let mut aug_msg = Vec::::new(); + + for (pk, msg) in pkm_pairs { + aug_msg.clear(); + aug_msg.extend_from_slice(&pk.to_bytes()); + aug_msg.extend(&*msg); + let aug_hash = hash_to_g2(&aug_msg); + let pairing = aug_hash.pair(&pk); + + let mut key = Sha256::new(); + key.update(&aug_msg); + pairs.push((key.finalize(), pairing)); + } + // Verify aggregated signature + let result = aggregate_verify_gt( + &spend_bundle.aggregated_signature, + pairs.iter().map(|tuple| &tuple.1), + ); + if !result { + return Err(ErrorCode::BadAggregateSignature); + } + + // Collect results + Ok((conditions, pairs, start_time.elapsed())) +} diff --git a/crates/chia-sdk-types/src/run_puzzle.rs b/crates/chia-sdk-types/src/run_puzzle.rs index 1c779223..38000822 100644 --- a/crates/chia-sdk-types/src/run_puzzle.rs +++ b/crates/chia-sdk-types/src/run_puzzle.rs @@ -1,4 +1,5 @@ use clvmr::{ + chia_dialect::{ENABLE_KECCAK, ENABLE_KECCAK_OPS_OUTSIDE_GUARD}, reduction::{EvalErr, Reduction}, Allocator, NodePtr, }; @@ -10,7 +11,7 @@ pub fn run_puzzle( ) -> Result { let Reduction(_cost, output) = clvmr::run_program( allocator, - &clvmr::ChiaDialect::new(0), + &clvmr::ChiaDialect::new(ENABLE_KECCAK | ENABLE_KECCAK_OPS_OUTSIDE_GUARD), puzzle, solution, 11_000_000_000, diff --git a/napi/src/clvm.rs b/napi/src/clvm.rs index 28ec77f5..e7b79ee9 100644 --- a/napi/src/clvm.rs +++ b/napi/src/clvm.rs @@ -7,6 +7,7 @@ use chia::{ }; use chia_wallet_sdk::{self as sdk, HashedPtr, SpendContext}; use clvmr::{ + chia_dialect::{ENABLE_KECCAK, ENABLE_KECCAK_OPS_OUTSIDE_GUARD}, run_program, serde::{node_from_bytes, node_from_bytes_backrefs}, ChiaDialect, NodePtr, MEMPOOL_MODE, @@ -70,7 +71,7 @@ impl ClvmAllocator { max_cost: BigInt, mempool_mode: bool, ) -> Result { - let mut flags = 0; + let mut flags = ENABLE_KECCAK | ENABLE_KECCAK_OPS_OUTSIDE_GUARD; if mempool_mode { flags |= MEMPOOL_MODE;