1use chia_protocol::Bytes32;
2use chia_sdk_types::puzzles::{
3 Eip712PrefixAndDomainSeparator, P2_EIP712_MESSAGE_PUZZLE_HASH, P2Eip712MessageArgs,
4 P2Eip712MessageSolution,
5};
6use chia_secp::{K1PublicKey, K1Signature};
7use clvm_traits::FromClvm;
8use clvmr::{Allocator, NodePtr};
9use sha3::{Digest, Keccak256};
10
11use crate::{DriverError, Layer, Puzzle, Spend, SpendContext};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct P2Eip712MessageLayer {
23 pub prefix_and_domain_separator: Eip712PrefixAndDomainSeparator,
24 pub public_key: K1PublicKey,
25}
26
27impl P2Eip712MessageLayer {
28 pub fn new(
29 public_key: K1PublicKey,
30 prefix_and_domain_separator: Eip712PrefixAndDomainSeparator,
31 ) -> Self {
32 Self {
33 prefix_and_domain_separator,
34 public_key,
35 }
36 }
37
38 pub fn from_genesis_challenge(public_key: K1PublicKey, genesis_challenge: Bytes32) -> Self {
41 Self {
42 prefix_and_domain_separator: Self::prefix_and_domain_separator(genesis_challenge),
43 public_key,
44 }
45 }
46
47 pub fn domain_separator(genesis_challenge: Bytes32) -> Bytes32 {
52 let type_hash = Keccak256::digest(b"EIP712Domain(string name,string version,bytes32 salt)");
53
54 let mut to_hash = Vec::new();
55 to_hash.extend_from_slice(&type_hash);
56 to_hash.extend_from_slice(&Keccak256::digest(b"Chia Coin Spend"));
57 to_hash.extend_from_slice(&Keccak256::digest(b"1"));
58 to_hash.extend_from_slice(&genesis_challenge);
59
60 Bytes32::new(Keccak256::digest(&to_hash).into())
61 }
62
63 pub fn prefix_and_domain_separator(
66 genesis_challenge: Bytes32,
67 ) -> Eip712PrefixAndDomainSeparator {
68 let mut bytes = [0u8; 34];
69 bytes[0] = 0x19;
70 bytes[1] = 0x01;
71 bytes[2..].copy_from_slice(&Self::domain_separator(genesis_challenge));
72 bytes.into()
73 }
74
75 pub fn type_hash() -> Bytes32 {
77 Bytes32::new(
78 Keccak256::digest(b"ChiaCoinSpend(bytes32 coin_id,bytes32 delegated_puzzle_hash)")
79 .into(),
80 )
81 }
82
83 pub fn hash_to_sign(&self, coin_id: Bytes32, delegated_puzzle_hash: Bytes32) -> Bytes32 {
88 let mut to_hash = Vec::with_capacity(96);
89 to_hash.extend_from_slice(&Self::type_hash());
90 to_hash.extend_from_slice(&coin_id);
91 to_hash.extend_from_slice(&delegated_puzzle_hash);
92 let message_hash = Keccak256::digest(&to_hash);
93
94 let mut to_hash = Vec::with_capacity(34 + 32);
95 to_hash.extend_from_slice(&self.prefix_and_domain_separator);
96 to_hash.extend_from_slice(&message_hash);
97
98 Bytes32::new(Keccak256::digest(&to_hash).into())
99 }
100
101 pub fn spend(
104 &self,
105 ctx: &mut SpendContext,
106 my_id: Bytes32,
107 signature: K1Signature,
108 delegated_spend: Spend,
109 ) -> Result<Spend, DriverError> {
110 let signed_hash = self.hash_to_sign(my_id, ctx.tree_hash(delegated_spend.puzzle).into());
111 self.construct_spend(
112 ctx,
113 P2Eip712MessageSolution {
114 my_id,
115 signed_hash,
116 signature,
117 delegated_puzzle: delegated_spend.puzzle,
118 delegated_solution: delegated_spend.solution,
119 },
120 )
121 }
122}
123
124impl Layer for P2Eip712MessageLayer {
125 type Solution = P2Eip712MessageSolution<NodePtr, NodePtr>;
126
127 fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result<Option<Self>, DriverError> {
128 let Some(puzzle) = puzzle.as_curried() else {
129 return Ok(None);
130 };
131
132 if puzzle.mod_hash != P2_EIP712_MESSAGE_PUZZLE_HASH {
133 return Ok(None);
134 }
135
136 let args = P2Eip712MessageArgs::from_clvm(allocator, puzzle.args)?;
137
138 if args.type_hash != Self::type_hash() {
139 return Ok(None);
140 }
141
142 Ok(Some(Self {
143 prefix_and_domain_separator: args.prefix_and_domain_separator,
144 public_key: args.public_key,
145 }))
146 }
147
148 fn parse_solution(
149 allocator: &Allocator,
150 solution: NodePtr,
151 ) -> Result<Self::Solution, DriverError> {
152 Ok(P2Eip712MessageSolution::from_clvm(allocator, solution)?)
153 }
154
155 fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
156 ctx.curry(P2Eip712MessageArgs {
157 prefix_and_domain_separator: self.prefix_and_domain_separator,
158 type_hash: Self::type_hash(),
159 public_key: self.public_key,
160 })
161 }
162
163 fn construct_solution(
164 &self,
165 ctx: &mut SpendContext,
166 solution: Self::Solution,
167 ) -> Result<NodePtr, DriverError> {
168 ctx.alloc(&solution)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 use chia_consensus::consensus_constants::TEST_CONSTANTS;
177 use chia_protocol::Bytes;
178 use chia_sdk_test::Simulator;
179 use chia_sdk_types::Conditions;
180 use chia_secp::K1SecretKey;
181 use clvm_traits::{ToClvm, clvm_quote};
182 use clvm_utils::ToTreeHash;
183 use clvmr::chia_dialect::ENABLE_KECCAK_OPS_OUTSIDE_GUARD;
184 use clvmr::reduction::Reduction;
185 use clvmr::serde::node_from_bytes;
186 use hex_literal::hex;
187 use rand::{Rng, SeedableRng};
188 use rand_chacha::ChaCha8Rng;
189 use rstest::rstest;
190
191 fn k1_pair(seed: u64) -> (K1SecretKey, K1PublicKey) {
192 let mut rng = ChaCha8Rng::seed_from_u64(seed);
193 let sk = K1SecretKey::from_bytes(&rng.random()).unwrap();
194 let pk = sk.public_key();
195 (sk, pk)
196 }
197
198 #[test]
199 fn test_type_hash() {
200 assert_eq!(
201 P2Eip712MessageLayer::type_hash(),
202 Bytes32::new(hex!(
203 "72930978f119c79f9de7a13bd50c9b3261132d7b4819bdf0d3ca4d4c37ade070"
204 ))
205 );
206 }
207
208 #[test]
209 fn test_domain_separator_is_deterministic() {
210 let a = P2Eip712MessageLayer::domain_separator(TEST_CONSTANTS.genesis_challenge);
214 let b = P2Eip712MessageLayer::domain_separator(TEST_CONSTANTS.genesis_challenge);
215 assert_eq!(a, b);
216 }
217
218 #[test]
219 fn test_domain_separator_mainnet_fixture() {
220 let mainnet_genesis = Bytes32::new(hex!(
222 "ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb"
223 ));
224 assert_eq!(
229 P2Eip712MessageLayer::domain_separator(mainnet_genesis),
230 Bytes32::new(hex!(
231 "38d765d3bce341eed11f92fc1311d575f34cc9ee0fc0e1f03820e11aebf6b5b2"
232 ))
233 );
234 }
235
236 #[test]
239 fn test_softfork_cost() -> anyhow::Result<()> {
240 let mut ctx = SpendContext::new();
241 let puzzle_bytes =
247 hex!("ff02ffff03ffff09ffff3eff02ffff3eff05ff0bff178080ff2f80ff80ffff01ff088080ff0180");
248 let puzzle_ptr = node_from_bytes(&mut ctx, puzzle_bytes.as_slice())?;
249 let solution_ptr = vec![
250 Bytes::new(
252 hex!("1901098ccd7d09a29365582c3f7590712bc2c2eb8503586f8a4c628c61c73ffbe4aa")
253 .to_vec(),
254 ),
255 Bytes::new(
257 hex!("72930978f119c79f9de7a13bd50c9b3261132d7b4819bdf0d3ca4d4c37ade070").to_vec(),
258 ),
259 Bytes::new(
261 hex!("5c777c45fd52a17a54e420742cadc56172847d9a106ff0ff8af38ef757d84829").to_vec(),
262 ),
263 Bytes::new(
265 hex!("d842dfa1453a130a8be66bc32708a2d1884662d7daaa4aae530be3259fa6712f").to_vec(),
266 ),
267 Bytes::new(
269 hex!("9f61fdf6077c3eeb96eaa4dd450b11ba3ae17746a2c304388218137972c7ba4c").to_vec(),
270 ),
271 ]
272 .to_clvm(&mut *ctx)?;
273
274 let Reduction(cost, _) = clvmr::run_program(
275 &mut ctx,
276 &clvmr::ChiaDialect::new(ENABLE_KECCAK_OPS_OUTSIDE_GUARD),
277 puzzle_ptr,
278 solution_ptr,
279 11_000_000_000,
280 )?;
281
282 assert_eq!(cost, 2605);
283 Ok(())
284 }
285
286 #[rstest]
287 #[case::successful_spend(true)]
288 #[case::incorrect_signed_hash(false)]
289 fn test_p2_eip712_message(#[case] correct_signed_hash: bool) -> anyhow::Result<()> {
290 let (sk, pk) = k1_pair(0xC0DE_F00D);
291
292 let mut sim = Simulator::new();
293 let mut ctx = SpendContext::new();
294 let ctx = &mut ctx;
295
296 let layer =
297 P2Eip712MessageLayer::from_genesis_challenge(pk, TEST_CONSTANTS.genesis_challenge);
298 let coin_puzzle_reveal = layer.construct_puzzle(ctx)?;
299 let coin_puzzle_hash = ctx.tree_hash(coin_puzzle_reveal);
300
301 let coin = sim.new_coin(coin_puzzle_hash.into(), 1337);
302
303 let delegated_puzzle_ptr =
304 clvm_quote!(Conditions::new().reserve_fee(1337)).to_clvm(&mut **ctx)?;
305 let delegated_solution_ptr = ctx.nil();
306
307 let signed_hash: Bytes32 = if correct_signed_hash {
310 layer.hash_to_sign(coin.coin_id(), ctx.tree_hash(delegated_puzzle_ptr).into())
311 } else {
312 layer
313 .hash_to_sign(coin.coin_id(), ctx.tree_hash(delegated_puzzle_ptr).into())
314 .tree_hash()
315 .into()
316 };
317
318 let signed_hash_bytes: [u8; 32] = signed_hash.into();
319 let signature = sk.sign_prehashed(&signed_hash_bytes)?;
320
321 let coin_spend = layer.construct_coin_spend(
322 ctx,
323 coin,
324 P2Eip712MessageSolution {
325 my_id: coin.coin_id(),
326 signed_hash,
327 signature,
328 delegated_puzzle: delegated_puzzle_ptr,
329 delegated_solution: delegated_solution_ptr,
330 },
331 )?;
332
333 ctx.insert(coin_spend);
334
335 if correct_signed_hash {
336 sim.spend_coins(ctx.take(), &[])?;
337 } else {
338 let err = sim
339 .spend_coins(ctx.take(), &[])
340 .expect_err("spend should fail when signed_hash is wrong");
341 let msg = err.to_string();
342 assert!(
343 msg.contains("clvm raise") || msg.to_lowercase().contains("raise"),
344 "unexpected error: {msg}"
345 );
346 }
347
348 Ok(())
349 }
350}