1use crate::crypto;
31use crate::error::SignerError;
32use crate::ethereum::abi::{self, AbiValue};
33use crate::security;
34use zeroize::{Zeroize, ZeroizeOnDrop};
35
36const OP_IF: u8 = 0x63;
41const OP_ELSE: u8 = 0x67;
42const OP_ENDIF: u8 = 0x68;
43const OP_DROP: u8 = 0x75;
44const OP_EQUALVERIFY: u8 = 0x88;
45const OP_SHA256: u8 = 0xa8;
46const OP_CHECKSIG: u8 = 0xac;
47const OP_CLTV: u8 = 0xb1; const OP_CSV: u8 = 0xb2; #[derive(Clone, Zeroize, ZeroizeOnDrop)]
58pub struct SwapSecret {
59 pub preimage: [u8; 32],
61 #[zeroize(skip)]
63 pub hash: [u8; 32],
64}
65
66impl SwapSecret {
67 pub fn generate() -> Result<Self, SignerError> {
72 let mut preimage = [0u8; 32];
73 security::secure_random(&mut preimage)?;
74 let hash = crypto::sha256(&preimage);
75 Ok(Self { preimage, hash })
76 }
77
78 #[must_use]
80 pub fn from_preimage(preimage: [u8; 32]) -> Self {
81 let hash = crypto::sha256(&preimage);
82 Self { preimage, hash }
83 }
84
85 #[must_use]
87 pub fn verify(preimage: &[u8; 32], expected_hash: &[u8; 32]) -> bool {
88 let computed = crypto::sha256(preimage);
89 ct_eq(&computed, expected_hash)
91 }
92}
93
94fn ct_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
96 let mut diff = 0u8;
97 for i in 0..32 {
98 diff |= a[i] ^ b[i];
99 }
100 diff == 0
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct HtlcParams {
110 pub hash_lock: [u8; 32],
112 pub time_lock: u64,
114 pub sender: [u8; 20],
116 pub receiver: [u8; 20],
118}
119
120#[must_use]
124pub fn is_expired(time_lock: u64, current_time: u64) -> bool {
125 current_time >= time_lock
126}
127
128#[must_use]
147pub fn build_bitcoin_htlc_script(
148 hash_lock: &[u8; 32],
149 locktime: u32,
150 receiver_pubkey: &[u8; 33],
151 sender_pubkey: &[u8; 33],
152) -> Vec<u8> {
153 let mut script = Vec::with_capacity(128);
154
155 script.push(OP_IF);
157 script.push(OP_SHA256);
158 script.push(32); script.extend_from_slice(hash_lock);
160 script.push(OP_EQUALVERIFY);
161 script.push(33); script.extend_from_slice(receiver_pubkey);
163 script.push(OP_CHECKSIG);
164
165 script.push(OP_ELSE);
167 push_script_number(&mut script, locktime as i64);
168 script.push(OP_CLTV);
169 script.push(OP_DROP);
170 script.push(33);
171 script.extend_from_slice(sender_pubkey);
172 script.push(OP_CHECKSIG);
173
174 script.push(OP_ENDIF);
175
176 script
177}
178
179#[must_use]
184pub fn build_bitcoin_htlc_csv_script(
185 hash_lock: &[u8; 32],
186 sequence: u32,
187 receiver_pubkey: &[u8; 33],
188 sender_pubkey: &[u8; 33],
189) -> Vec<u8> {
190 let mut script = Vec::with_capacity(128);
191
192 script.push(OP_IF);
193 script.push(OP_SHA256);
194 script.push(32);
195 script.extend_from_slice(hash_lock);
196 script.push(OP_EQUALVERIFY);
197 script.push(33);
198 script.extend_from_slice(receiver_pubkey);
199 script.push(OP_CHECKSIG);
200
201 script.push(OP_ELSE);
202 push_script_number(&mut script, sequence as i64);
203 script.push(OP_CSV);
204 script.push(OP_DROP);
205 script.push(33);
206 script.extend_from_slice(sender_pubkey);
207 script.push(OP_CHECKSIG);
208
209 script.push(OP_ENDIF);
210
211 script
212}
213
214#[must_use]
218pub fn build_btc_claim_witness(
219 signature: &[u8],
220 preimage: &[u8; 32],
221 redeem_script: &[u8],
222) -> Vec<Vec<u8>> {
223 vec![
224 signature.to_vec(),
225 preimage.to_vec(),
226 vec![0x01], redeem_script.to_vec(),
228 ]
229}
230
231#[must_use]
235pub fn build_btc_refund_witness(signature: &[u8], redeem_script: &[u8]) -> Vec<Vec<u8>> {
236 vec![
237 signature.to_vec(),
238 vec![], redeem_script.to_vec(),
240 ]
241}
242
243#[must_use]
247pub fn htlc_script_pubkey(redeem_script: &[u8]) -> Vec<u8> {
248 let hash = crypto::sha256(redeem_script);
249 let mut spk = Vec::with_capacity(34);
250 spk.push(0x00); spk.push(32); spk.extend_from_slice(&hash);
253 spk
254}
255
256#[must_use]
262pub fn encode_eth_htlc_lock(params: &HtlcParams) -> Vec<u8> {
263 let lock_fn = abi::Function::new("lock(bytes32,uint256,address)");
264 lock_fn.encode(&[
265 AbiValue::Uint256(params.hash_lock),
266 AbiValue::from_u64(params.time_lock),
267 AbiValue::Address(params.receiver),
268 ])
269}
270
271#[must_use]
273pub fn encode_eth_htlc_claim(preimage: &[u8; 32]) -> Vec<u8> {
274 let claim_fn = abi::Function::new("claim(bytes32)");
275 claim_fn.encode(&[AbiValue::Uint256(*preimage)])
276}
277
278#[must_use]
280pub fn encode_eth_htlc_refund(hash_lock: &[u8; 32]) -> Vec<u8> {
281 let refund_fn = abi::Function::new("refund(bytes32)");
282 refund_fn.encode(&[AbiValue::Uint256(*hash_lock)])
283}
284
285#[must_use]
287pub fn encode_eth_htlc_lock_tokens(token: &[u8; 20], params: &HtlcParams, amount: u64) -> Vec<u8> {
288 let lock_fn = abi::Function::new("lockTokens(address,bytes32,uint256,address,uint256)");
289 lock_fn.encode(&[
290 AbiValue::Address(*token),
291 AbiValue::Uint256(params.hash_lock),
292 AbiValue::from_u64(params.time_lock),
293 AbiValue::Address(params.receiver),
294 AbiValue::from_u64(amount),
295 ])
296}
297
298fn push_script_number(script: &mut Vec<u8>, n: i64) {
304 if n == 0 {
305 script.push(0x00);
306 return;
307 }
308 if (1..=16).contains(&n) {
309 script.push(0x50 + n as u8);
310 return;
311 }
312
313 let negative = n < 0;
314 let mut abs_n = if negative { (-n) as u64 } else { n as u64 };
315 let mut bytes = Vec::new();
316
317 while abs_n > 0 {
318 bytes.push((abs_n & 0xFF) as u8);
319 abs_n >>= 8;
320 }
321
322 if bytes.last().is_some_and(|b| b & 0x80 != 0) {
323 bytes.push(if negative { 0x80 } else { 0x00 });
324 } else if negative {
325 let last = bytes.len() - 1;
326 bytes[last] |= 0x80;
327 }
328
329 script.push(bytes.len() as u8);
330 script.extend_from_slice(&bytes);
331}
332
333#[cfg(test)]
338#[allow(clippy::unwrap_used, clippy::expect_used)]
339mod tests {
340 use super::*;
341
342 const RECEIVER_PK: [u8; 33] = [0x02; 33];
343 const SENDER_PK: [u8; 33] = [0x03; 33];
344 const RECEIVER_ADDR: [u8; 20] = [0xBB; 20];
345 const SENDER_ADDR: [u8; 20] = [0xAA; 20];
346
347 fn sample_params() -> HtlcParams {
348 let secret = SwapSecret::from_preimage([0xDD; 32]);
349 HtlcParams {
350 hash_lock: secret.hash,
351 time_lock: 1_700_000_000,
352 sender: SENDER_ADDR,
353 receiver: RECEIVER_ADDR,
354 }
355 }
356
357 #[test]
360 fn test_generate_secret_unique() {
361 let s1 = SwapSecret::generate().unwrap();
362 let s2 = SwapSecret::generate().unwrap();
363 assert_ne!(s1.preimage, s2.preimage);
364 assert_ne!(s1.hash, s2.hash);
365 }
366
367 #[test]
368 fn test_secret_hash_matches() {
369 let secret = SwapSecret::generate().unwrap();
370 assert_eq!(crypto::sha256(&secret.preimage), secret.hash);
371 }
372
373 #[test]
374 fn test_from_preimage() {
375 let preimage = [0xAB; 32];
376 let secret = SwapSecret::from_preimage(preimage);
377 assert_eq!(secret.preimage, preimage);
378 assert_eq!(secret.hash, crypto::sha256(&preimage));
379 }
380
381 #[test]
382 fn test_verify_preimage_correct() {
383 let secret = SwapSecret::generate().unwrap();
384 assert!(SwapSecret::verify(&secret.preimage, &secret.hash));
385 }
386
387 #[test]
388 fn test_verify_preimage_incorrect() {
389 let secret = SwapSecret::generate().unwrap();
390 let wrong = [0xFF; 32];
391 assert!(!SwapSecret::verify(&wrong, &secret.hash));
392 }
393
394 #[test]
395 fn test_verify_constant_time() {
396 let a = [0xAA; 32];
398 let b = [0xAA; 32];
399 let c = [0xBB; 32];
400 assert!(ct_eq(&a, &b));
401 assert!(!ct_eq(&a, &c));
402 }
403
404 #[test]
407 fn test_not_expired() {
408 assert!(!is_expired(1_700_000_000, 1_699_999_999));
409 }
410
411 #[test]
412 fn test_exactly_expired() {
413 assert!(is_expired(1_700_000_000, 1_700_000_000));
414 }
415
416 #[test]
417 fn test_past_expired() {
418 assert!(is_expired(1_700_000_000, 1_800_000_000));
419 }
420
421 #[test]
424 fn test_btc_htlc_script_contains_sha256() {
425 let hash = [0xAA; 32];
426 let script = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
427 assert!(script.contains(&OP_SHA256));
428 }
429
430 #[test]
431 fn test_btc_htlc_script_contains_cltv() {
432 let hash = [0xAA; 32];
433 let script = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
434 assert!(script.contains(&OP_CLTV));
435 }
436
437 #[test]
438 fn test_btc_htlc_script_structure() {
439 let hash = [0xAA; 32];
440 let script = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
441 assert_eq!(script[0], OP_IF);
442 assert_eq!(*script.last().unwrap(), OP_ENDIF);
443 assert!(script.contains(&OP_ELSE));
444 }
445
446 #[test]
447 fn test_btc_htlc_script_contains_hash_lock() {
448 let hash = [0xAA; 32];
449 let script = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
450 let has_hash = script.windows(32).any(|w| w == hash);
451 assert!(has_hash, "script must contain hash lock");
452 }
453
454 #[test]
455 fn test_btc_htlc_script_contains_pubkeys() {
456 let hash = [0xAA; 32];
457 let script = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
458 let has_receiver = script.windows(33).any(|w| w == RECEIVER_PK);
459 let has_sender = script.windows(33).any(|w| w == SENDER_PK);
460 assert!(has_receiver);
461 assert!(has_sender);
462 }
463
464 #[test]
465 fn test_btc_htlc_csv_contains_csv() {
466 let hash = [0xAA; 32];
467 let script = build_bitcoin_htlc_csv_script(&hash, 144, &RECEIVER_PK, &SENDER_PK);
468 assert!(script.contains(&OP_CSV));
469 assert!(!script.contains(&OP_CLTV)); }
471
472 #[test]
473 fn test_btc_htlc_deterministic() {
474 let hash = [0xAA; 32];
475 let s1 = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
476 let s2 = build_bitcoin_htlc_script(&hash, 500_000, &RECEIVER_PK, &SENDER_PK);
477 assert_eq!(s1, s2);
478 }
479
480 #[test]
483 fn test_btc_claim_witness_structure() {
484 let preimage = [0xBB; 32];
485 let sig = vec![0xCC; 64];
486 let script = vec![0xDD; 100];
487 let witness = build_btc_claim_witness(&sig, &preimage, &script);
488 assert_eq!(witness.len(), 4);
489 assert_eq!(witness[0], sig);
490 assert_eq!(witness[1].as_slice(), &preimage);
491 assert_eq!(witness[2], vec![0x01]); assert_eq!(witness[3], script);
493 }
494
495 #[test]
496 fn test_btc_refund_witness_structure() {
497 let sig = vec![0xCC; 64];
498 let script = vec![0xDD; 100];
499 let witness = build_btc_refund_witness(&sig, &script);
500 assert_eq!(witness.len(), 3);
501 assert_eq!(witness[0], sig);
502 assert!(witness[1].is_empty()); assert_eq!(witness[2], script);
504 }
505
506 #[test]
509 fn test_htlc_script_pubkey_length() {
510 let script = vec![0xAA; 50];
511 let spk = htlc_script_pubkey(&script);
512 assert_eq!(spk.len(), 34); }
514
515 #[test]
516 fn test_htlc_script_pubkey_witness_v0() {
517 let script = vec![0xAA; 50];
518 let spk = htlc_script_pubkey(&script);
519 assert_eq!(spk[0], 0x00); assert_eq!(spk[1], 32); }
522
523 #[test]
526 fn test_eth_htlc_lock_selector() {
527 let params = sample_params();
528 let data = encode_eth_htlc_lock(¶ms);
529 let expected = abi::function_selector("lock(bytes32,uint256,address)");
530 assert_eq!(&data[..4], &expected);
531 }
532
533 #[test]
534 fn test_eth_htlc_lock_length() {
535 let params = sample_params();
536 let data = encode_eth_htlc_lock(¶ms);
537 assert_eq!(data.len(), 100);
539 }
540
541 #[test]
542 fn test_eth_htlc_claim_selector() {
543 let data = encode_eth_htlc_claim(&[0xAA; 32]);
544 let expected = abi::function_selector("claim(bytes32)");
545 assert_eq!(&data[..4], &expected);
546 }
547
548 #[test]
549 fn test_eth_htlc_claim_length() {
550 let data = encode_eth_htlc_claim(&[0xAA; 32]);
551 assert_eq!(data.len(), 36); }
553
554 #[test]
555 fn test_eth_htlc_refund_selector() {
556 let data = encode_eth_htlc_refund(&[0xAA; 32]);
557 let expected = abi::function_selector("refund(bytes32)");
558 assert_eq!(&data[..4], &expected);
559 }
560
561 #[test]
562 fn test_eth_htlc_lock_tokens_selector() {
563 let params = sample_params();
564 let data = encode_eth_htlc_lock_tokens(&[0xFF; 20], ¶ms, 1000);
565 let expected =
566 abi::function_selector("lockTokens(address,bytes32,uint256,address,uint256)");
567 assert_eq!(&data[..4], &expected);
568 }
569
570 #[test]
573 fn test_e2e_swap_flow() {
574 let secret = SwapSecret::generate().unwrap();
576
577 assert!(SwapSecret::verify(&secret.preimage, &secret.hash));
579
580 let btc_script = build_bitcoin_htlc_script(&secret.hash, 500_000, &RECEIVER_PK, &SENDER_PK);
582 assert!(!btc_script.is_empty());
583
584 let params = HtlcParams {
586 hash_lock: secret.hash,
587 time_lock: 1_700_000_000,
588 sender: SENDER_ADDR,
589 receiver: RECEIVER_ADDR,
590 };
591 let eth_lock = encode_eth_htlc_lock(¶ms);
592 assert!(!eth_lock.is_empty());
593
594 let eth_claim = encode_eth_htlc_claim(&secret.preimage);
596 assert!(!eth_claim.is_empty());
597
598 let spk = htlc_script_pubkey(&btc_script);
600 assert_eq!(spk.len(), 34);
601 }
602
603 #[test]
604 fn test_e2e_expired_refund() {
605 let secret = SwapSecret::generate().unwrap();
606 let time_lock = 1_700_000_000u64;
607
608 assert!(!is_expired(time_lock, 1_699_000_000));
610
611 assert!(is_expired(time_lock, 1_700_000_001));
613
614 let eth_refund = encode_eth_htlc_refund(&secret.hash);
616 assert!(!eth_refund.is_empty());
617 }
618}