1use bitcoin::psbt::Psbt;
10use bitcoin::secp256k1::PublicKey;
11
12use silent_payments_core::keys::{ScanPublicKey, SpendPublicKey};
13
14use crate::dleq::DleqProof;
15use crate::error::{DleqError, PsbtError};
16use crate::fields;
17
18pub struct SpPsbtConstructor {
29 inner: Psbt,
30}
31
32impl SpPsbtConstructor {
33 pub fn new(psbt: Psbt) -> Result<Self, PsbtError> {
38 Ok(Self { inner: psbt })
39 }
40
41 pub fn add_sp_output(
51 mut self,
52 output_index: usize,
53 scan_pubkey: &ScanPublicKey,
54 spend_pubkey: &SpendPublicKey,
55 ) -> Result<Self, PsbtError> {
56 if output_index >= self.inner.outputs.len() {
57 return Err(PsbtError::OutputIndexOutOfBounds {
58 index: output_index,
59 count: self.inner.outputs.len(),
60 });
61 }
62
63 fields::set_sp_output_info(
64 &mut self.inner.outputs[output_index],
65 scan_pubkey.as_inner(),
66 spend_pubkey.as_inner(),
67 );
68
69 Ok(self)
70 }
71
72 pub fn add_sp_label(mut self, output_index: usize, label: u32) -> Result<Self, PsbtError> {
81 if output_index >= self.inner.outputs.len() {
82 return Err(PsbtError::OutputIndexOutOfBounds {
83 index: output_index,
84 count: self.inner.outputs.len(),
85 });
86 }
87
88 fields::set_sp_output_label(&mut self.inner.outputs[output_index], label);
89
90 Ok(self)
91 }
92
93 pub fn into_updater(self) -> Result<SpPsbtUpdater, PsbtError> {
101 let has_sp_output = self
102 .inner
103 .outputs
104 .iter()
105 .any(|output| fields::get_sp_output_info(output).is_some());
106
107 if !has_sp_output {
108 return Err(PsbtError::NoSpOutputs);
109 }
110
111 Ok(SpPsbtUpdater { inner: self.inner })
112 }
113}
114
115pub struct SpPsbtUpdater {
125 inner: Psbt,
126}
127
128impl SpPsbtUpdater {
129 pub fn inner(&self) -> &Psbt {
131 &self.inner
132 }
133
134 pub fn inner_mut(&mut self) -> &mut Psbt {
139 &mut self.inner
140 }
141
142 pub fn into_signer(self) -> SpPsbtSigner {
146 SpPsbtSigner { inner: self.inner }
147 }
148}
149
150pub struct SpPsbtSigner {
161 inner: Psbt,
162}
163
164impl SpPsbtSigner {
165 pub fn add_ecdh_share(
179 mut self,
180 input_index: usize,
181 scan_pubkey: &ScanPublicKey,
182 ecdh_share: &PublicKey,
183 dleq_proof: &DleqProof,
184 signer_pubkey: &PublicKey,
185 secp: &bitcoin::secp256k1::Secp256k1<impl bitcoin::secp256k1::Verification>,
186 ) -> Result<Self, PsbtError> {
187 if input_index >= self.inner.inputs.len() {
189 return Err(PsbtError::OutputIndexOutOfBounds {
190 index: input_index,
191 count: self.inner.inputs.len(),
192 });
193 }
194
195 if fields::get_global_ecdh_share(&self.inner, scan_pubkey.as_inner()).is_some() {
197 return Err(PsbtError::DuplicateShareType {
198 scan_key: hex_encode_pubkey(scan_pubkey.as_inner()),
199 });
200 }
201
202 let valid = dleq_proof.verify(signer_pubkey, scan_pubkey.as_inner(), ecdh_share, secp)?;
204 if !valid {
205 return Err(PsbtError::InvalidProof(DleqError::VerificationFailed));
206 }
207
208 fields::set_input_ecdh_share(
210 &mut self.inner.inputs[input_index],
211 scan_pubkey.as_inner(),
212 ecdh_share,
213 );
214 fields::set_input_dleq_proof(
215 &mut self.inner.inputs[input_index],
216 scan_pubkey.as_inner(),
217 dleq_proof.as_bytes(),
218 );
219
220 Ok(self)
221 }
222
223 pub fn add_global_ecdh_share(
231 mut self,
232 scan_pubkey: &ScanPublicKey,
233 ecdh_share: &PublicKey,
234 dleq_proof: &DleqProof,
235 signer_pubkey: &PublicKey,
236 secp: &bitcoin::secp256k1::Secp256k1<impl bitcoin::secp256k1::Verification>,
237 ) -> Result<Self, PsbtError> {
238 let has_per_input = self
240 .inner
241 .inputs
242 .iter()
243 .any(|input| fields::get_input_ecdh_share(input, scan_pubkey.as_inner()).is_some());
244
245 if has_per_input {
246 return Err(PsbtError::DuplicateShareType {
247 scan_key: hex_encode_pubkey(scan_pubkey.as_inner()),
248 });
249 }
250
251 let valid = dleq_proof.verify(signer_pubkey, scan_pubkey.as_inner(), ecdh_share, secp)?;
253 if !valid {
254 return Err(PsbtError::InvalidProof(DleqError::VerificationFailed));
255 }
256
257 fields::set_global_ecdh_share(&mut self.inner, scan_pubkey.as_inner(), ecdh_share);
259 fields::set_global_dleq_proof(
260 &mut self.inner,
261 scan_pubkey.as_inner(),
262 dleq_proof.as_bytes(),
263 );
264
265 Ok(self)
266 }
267
268 pub fn into_extractor(self) -> Result<SpPsbtExtractor, PsbtError> {
277 let mut scan_keys: Vec<PublicKey> = Vec::new();
279 for output in &self.inner.outputs {
280 if let Some((scan_key, _spend_key)) = fields::get_sp_output_info(output) {
281 if !scan_keys.contains(&scan_key) {
282 scan_keys.push(scan_key);
283 }
284 }
285 }
286
287 for scan_key in &scan_keys {
289 let has_global = fields::get_global_ecdh_share(&self.inner, scan_key).is_some();
290
291 if has_global {
292 continue;
293 }
294
295 let all_inputs_have_shares = self
297 .inner
298 .inputs
299 .iter()
300 .all(|input| fields::get_input_ecdh_share(input, scan_key).is_some());
301
302 if !all_inputs_have_shares {
303 return Err(PsbtError::IncompleteSigning);
304 }
305 }
306
307 Ok(SpPsbtExtractor { inner: self.inner })
308 }
309}
310
311fn hex_encode_pubkey(key: &PublicKey) -> String {
313 key.serialize().iter().map(|b| format!("{b:02x}")).collect()
314}
315
316pub struct SpPsbtExtractor {
322 inner: Psbt,
323}
324
325impl SpPsbtExtractor {
326 pub fn extract(
336 mut self,
337 _secp: &bitcoin::secp256k1::Secp256k1<impl bitcoin::secp256k1::Verification>,
338 ) -> Result<(Psbt, Vec<crate::output::ExtractedSpOutput>), PsbtError> {
339 let sp_outputs: Vec<(usize, PublicKey, PublicKey)> = self
341 .inner
342 .outputs
343 .iter()
344 .enumerate()
345 .filter_map(|(idx, output)| {
346 fields::get_sp_output_info(output).map(|(scan, spend)| (idx, scan, spend))
347 })
348 .collect();
349
350 if sp_outputs.is_empty() {
351 return Err(PsbtError::NoSpOutputs);
352 }
353
354 let mut groups: Vec<(PublicKey, Vec<(usize, PublicKey)>)> = Vec::new();
356 for (idx, scan_key, spend_key) in &sp_outputs {
357 if let Some(group) = groups.iter_mut().find(|(sk, _)| sk == scan_key) {
358 group.1.push((*idx, *spend_key));
359 } else {
360 groups.push((*scan_key, vec![(*idx, *spend_key)]));
361 }
362 }
363
364 let mut extracted: Vec<crate::output::ExtractedSpOutput> = Vec::new();
365
366 for (scan_key, outputs_for_key) in &groups {
367 let shared_secret = self.aggregate_ecdh_share(scan_key)?;
369
370 for (k, (output_index, spend_key)) in outputs_for_key.iter().enumerate() {
372 let output_pubkey = silent_payments_core::crypto::compute_output_pubkey(
373 &SpendPublicKey::from(*spend_key),
374 &shared_secret,
375 k as u32,
376 )?;
377
378 let (x_only, _parity) = output_pubkey.x_only_public_key();
380 let tweaked = bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(x_only);
381 let script = bitcoin::ScriptBuf::new_p2tr_tweaked(tweaked);
382
383 self.inner.unsigned_tx.output[*output_index].script_pubkey = script.clone();
385
386 extracted.push(crate::output::ExtractedSpOutput::new(
387 script,
388 x_only,
389 *output_index,
390 *scan_key,
391 ));
392 }
393 }
394
395 Ok((self.inner, extracted))
396 }
397
398 fn aggregate_ecdh_share(&self, scan_key: &PublicKey) -> Result<PublicKey, PsbtError> {
403 if let Some(global_share) = fields::get_global_ecdh_share(&self.inner, scan_key) {
405 return Ok(global_share);
406 }
407
408 let shares: Vec<PublicKey> = self
410 .inner
411 .inputs
412 .iter()
413 .filter_map(|input| fields::get_input_ecdh_share(input, scan_key))
414 .collect();
415
416 if shares.is_empty() {
417 return Err(PsbtError::MissingEcdhShare {
418 scan_key: hex_encode_pubkey(scan_key),
419 });
420 }
421
422 let refs: Vec<&PublicKey> = shares.iter().collect();
424 PublicKey::combine_keys(&refs).map_err(|e| {
425 PsbtError::Crypto(silent_payments_core::CryptoError::Secp256k1(e.to_string()))
426 })
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use bitcoin::absolute::LockTime;
434 use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
435 use bitcoin::transaction::{Transaction, Version};
436 use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
437
438 fn test_pubkey(byte: u8) -> PublicKey {
440 let secp = Secp256k1::new();
441 let mut key_bytes = [0u8; 32];
442 key_bytes[31] = byte;
443 let sk = SecretKey::from_slice(&key_bytes).unwrap();
444 sk.public_key(&secp)
445 }
446
447 #[cfg(feature = "experimental-dleq")]
449 fn test_secret_key(byte: u8) -> SecretKey {
450 let mut key_bytes = [0u8; 32];
451 key_bytes[31] = byte;
452 SecretKey::from_slice(&key_bytes).unwrap()
453 }
454
455 fn test_psbt(n_inputs: usize, n_outputs: usize) -> Psbt {
457 let tx = Transaction {
458 version: Version::TWO,
459 lock_time: LockTime::ZERO,
460 input: (0..n_inputs).map(|_| TxIn::default()).collect(),
461 output: (0..n_outputs)
462 .map(|_| TxOut {
463 value: Amount::from_sat(50_000),
464 script_pubkey: ScriptBuf::new(),
465 })
466 .collect(),
467 };
468 Psbt::from_unsigned_tx(tx).unwrap()
469 }
470
471 fn test_psbt_with_outputs(n_outputs: usize) -> Psbt {
473 test_psbt(1, n_outputs)
474 }
475
476 fn build_signer(
478 n_inputs: usize,
479 n_outputs: usize,
480 scan_byte: u8,
481 spend_byte: u8,
482 ) -> SpPsbtSigner {
483 let psbt = test_psbt(n_inputs, n_outputs);
484 let scan = ScanPublicKey::from(test_pubkey(scan_byte));
485 let spend = SpendPublicKey::from(test_pubkey(spend_byte));
486
487 let constructor = SpPsbtConstructor::new(psbt).unwrap();
488 let constructor = constructor.add_sp_output(0, &scan, &spend).unwrap();
489 let updater = constructor.into_updater().unwrap();
490 updater.into_signer()
491 }
492
493 #[test]
496 fn test_constructor_add_sp_output() {
497 let psbt = test_psbt_with_outputs(1);
498 let scan = ScanPublicKey::from(test_pubkey(1));
499 let spend = SpendPublicKey::from(test_pubkey(2));
500
501 let constructor = SpPsbtConstructor::new(psbt).unwrap();
502 let constructor = constructor.add_sp_output(0, &scan, &spend).unwrap();
503
504 let updater = constructor.into_updater().unwrap();
506 let (got_scan, got_spend) =
507 fields::get_sp_output_info(&updater.inner().outputs[0]).expect("should read back");
508 assert_eq!(got_scan, *scan.as_inner());
509 assert_eq!(got_spend, *spend.as_inner());
510 }
511
512 #[test]
513 fn test_constructor_output_index_out_of_bounds() {
514 let psbt = test_psbt_with_outputs(1);
515 let scan = ScanPublicKey::from(test_pubkey(1));
516 let spend = SpendPublicKey::from(test_pubkey(2));
517
518 let constructor = SpPsbtConstructor::new(psbt).unwrap();
519 let result = constructor.add_sp_output(5, &scan, &spend);
520
521 assert!(
522 matches!(
523 result,
524 Err(PsbtError::OutputIndexOutOfBounds { index: 5, count: 1 })
525 ),
526 "should reject out-of-bounds index, got: {:?}",
527 result.err()
528 );
529 }
530
531 #[test]
532 fn test_constructor_into_updater_requires_sp_output() {
533 let psbt = test_psbt_with_outputs(1);
534 let constructor = SpPsbtConstructor::new(psbt).unwrap();
535
536 let result = constructor.into_updater();
537 assert!(
538 matches!(result, Err(PsbtError::NoSpOutputs)),
539 "should require at least one SP output, got: {:?}",
540 result.err()
541 );
542 }
543
544 #[test]
545 fn test_constructor_into_updater_succeeds() {
546 let psbt = test_psbt_with_outputs(1);
547 let scan = ScanPublicKey::from(test_pubkey(1));
548 let spend = SpendPublicKey::from(test_pubkey(2));
549
550 let constructor = SpPsbtConstructor::new(psbt).unwrap();
551 let constructor = constructor.add_sp_output(0, &scan, &spend).unwrap();
552
553 let updater = constructor.into_updater();
554 assert!(updater.is_ok(), "should transition after adding SP output");
555 }
556
557 #[test]
558 fn test_constructor_add_sp_label() {
559 let psbt = test_psbt_with_outputs(1);
560 let scan = ScanPublicKey::from(test_pubkey(1));
561 let spend = SpendPublicKey::from(test_pubkey(2));
562
563 let constructor = SpPsbtConstructor::new(psbt).unwrap();
564 let constructor = constructor.add_sp_output(0, &scan, &spend).unwrap();
565 let constructor = constructor.add_sp_label(0, 42).unwrap();
566
567 let updater = constructor.into_updater().unwrap();
568 let label = fields::get_sp_output_label(&updater.inner().outputs[0]);
569 assert_eq!(label, Some(42));
570 }
571
572 #[test]
575 fn test_updater_inner_mut_access() {
576 let psbt = test_psbt_with_outputs(2);
577 let scan = ScanPublicKey::from(test_pubkey(1));
578 let spend = SpendPublicKey::from(test_pubkey(2));
579
580 let constructor = SpPsbtConstructor::new(psbt).unwrap();
581 let constructor = constructor.add_sp_output(0, &scan, &spend).unwrap();
582 let mut updater = constructor.into_updater().unwrap();
583
584 let psbt_mut = updater.inner_mut();
586 assert_eq!(psbt_mut.outputs.len(), 2, "should have 2 outputs");
587
588 let original_len = psbt_mut.inputs.len();
590 assert!(original_len > 0, "should have at least one input");
591 }
592
593 #[test]
594 fn test_updater_into_signer() {
595 let psbt = test_psbt_with_outputs(1);
596 let scan = ScanPublicKey::from(test_pubkey(1));
597 let spend = SpendPublicKey::from(test_pubkey(2));
598
599 let constructor = SpPsbtConstructor::new(psbt).unwrap();
600 let constructor = constructor.add_sp_output(0, &scan, &spend).unwrap();
601 let updater = constructor.into_updater().unwrap();
602
603 let _signer = updater.into_signer();
605 }
606
607 #[cfg(feature = "experimental-dleq")]
610 #[test]
611 fn test_signer_add_ecdh_share_validates_dleq() {
612 let secp = Secp256k1::new();
613 let scan_sk = test_secret_key(10);
614 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
615 let signer_sk = test_secret_key(5);
617 let signer_pk = signer_sk.public_key(&secp);
618
619 let aux_rand = [0x42u8; 32];
621 let (ecdh_share, proof) =
622 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
623
624 let signer = build_signer(1, 1, 10, 20);
625 let signer = signer
626 .add_ecdh_share(0, &scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
627 .expect("valid DLEQ proof should be accepted");
628
629 let result = signer.into_extractor();
631 assert!(result.is_ok(), "should transition with all shares present");
632 }
633
634 #[test]
635 fn test_signer_rejects_invalid_dleq() {
636 let secp = Secp256k1::new();
637 let scan_pk = ScanPublicKey::from(test_pubkey(10));
638 let signer_pk = test_pubkey(5);
639 let ecdh_share = test_pubkey(30); let proof = DleqProof::from_bytes([0xAB; 64]);
643
644 let signer = build_signer(1, 1, 10, 20);
645 let result = signer.add_ecdh_share(0, &scan_pk, &ecdh_share, &proof, &signer_pk, &secp);
646
647 assert!(
648 matches!(result, Err(PsbtError::InvalidProof(_))),
649 "should reject invalid DLEQ proof, got: {:?}",
650 result.err()
651 );
652 }
653
654 #[cfg(feature = "experimental-dleq")]
655 #[test]
656 fn test_signer_rejects_duplicate_global_and_per_input() {
657 let secp = Secp256k1::new();
658 let scan_sk = test_secret_key(10);
659 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
660
661 let signer_sk = test_secret_key(5);
662 let signer_pk = signer_sk.public_key(&secp);
663
664 let aux_rand = [0x42u8; 32];
665 let (ecdh_share, proof) =
666 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
667
668 let aux_rand2 = [0x43u8; 32];
670 let (ecdh_share2, proof2) =
671 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand2, &secp).unwrap();
672
673 let signer = build_signer(1, 1, 10, 20);
674
675 let signer = signer
677 .add_global_ecdh_share(&scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
678 .unwrap();
679
680 let result = signer.add_ecdh_share(0, &scan_pk, &ecdh_share2, &proof2, &signer_pk, &secp);
682 assert!(
683 matches!(result, Err(PsbtError::DuplicateShareType { .. })),
684 "should reject per-input share when global exists, got: {:?}",
685 result.err()
686 );
687 }
688
689 #[cfg(feature = "experimental-dleq")]
690 #[test]
691 fn test_signer_rejects_global_when_per_input_exists() {
692 let secp = Secp256k1::new();
693 let scan_sk = test_secret_key(10);
694 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
695
696 let signer_sk = test_secret_key(5);
697 let signer_pk = signer_sk.public_key(&secp);
698
699 let aux_rand = [0x42u8; 32];
700 let (ecdh_share, proof) =
701 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
702
703 let aux_rand2 = [0x43u8; 32];
704 let (ecdh_share2, proof2) =
705 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand2, &secp).unwrap();
706
707 let signer = build_signer(1, 1, 10, 20);
708
709 let signer = signer
711 .add_ecdh_share(0, &scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
712 .unwrap();
713
714 let result =
716 signer.add_global_ecdh_share(&scan_pk, &ecdh_share2, &proof2, &signer_pk, &secp);
717 assert!(
718 matches!(result, Err(PsbtError::DuplicateShareType { .. })),
719 "should reject global share when per-input exists, got: {:?}",
720 result.err()
721 );
722 }
723
724 #[test]
725 fn test_signer_into_extractor_requires_all_shares() {
726 let signer = build_signer(1, 1, 10, 20);
728
729 let result = signer.into_extractor();
730 assert!(
731 matches!(result, Err(PsbtError::IncompleteSigning)),
732 "should require all ECDH shares, got: {:?}",
733 result.err()
734 );
735 }
736
737 #[cfg(feature = "experimental-dleq")]
738 #[test]
739 fn test_signer_into_extractor_succeeds_with_global_share() {
740 let secp = Secp256k1::new();
741 let scan_sk = test_secret_key(10);
742 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
743
744 let signer_sk = test_secret_key(5);
745 let signer_pk = signer_sk.public_key(&secp);
746
747 let aux_rand = [0x42u8; 32];
748 let (ecdh_share, proof) =
749 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
750
751 let signer = build_signer(1, 1, 10, 20);
752 let signer = signer
753 .add_global_ecdh_share(&scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
754 .unwrap();
755
756 let result = signer.into_extractor();
757 assert!(
758 result.is_ok(),
759 "should transition with global share, got: {:?}",
760 result.err()
761 );
762 }
763
764 #[cfg(feature = "experimental-dleq")]
767 #[test]
768 fn test_extractor_single_signer_flow() {
769 let secp = Secp256k1::new();
770 let scan_sk = test_secret_key(10);
771 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
772 let spend_pk = SpendPublicKey::from(test_pubkey(20));
773
774 let signer_sk = test_secret_key(5);
775 let signer_pk = signer_sk.public_key(&secp);
776
777 let psbt = test_psbt(1, 1);
779 let constructor = SpPsbtConstructor::new(psbt).unwrap();
780 let constructor = constructor.add_sp_output(0, &scan_pk, &spend_pk).unwrap();
781 let updater = constructor.into_updater().unwrap();
782 let signer = updater.into_signer();
783
784 let aux_rand = [0x42u8; 32];
786 let (ecdh_share, proof) =
787 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
788
789 let signer = signer
790 .add_global_ecdh_share(&scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
791 .unwrap();
792
793 let extractor = signer.into_extractor().unwrap();
794 let (psbt, outputs) = extractor.extract(&secp).unwrap();
795
796 assert_eq!(outputs.len(), 1, "should produce one extracted output");
798 assert_eq!(outputs[0].output_index(), 0);
799 assert_eq!(outputs[0].scan_pubkey(), scan_pk.as_inner());
800
801 let script = outputs[0].script_pubkey();
803 assert_eq!(script.len(), 34, "P2TR script should be 34 bytes");
804 assert_eq!(script.as_bytes()[0], 0x51, "should start with OP_1");
805 assert_eq!(script.as_bytes()[1], 0x20, "should have PUSH32");
806
807 assert_eq!(
809 psbt.unsigned_tx.output[0].script_pubkey, *script,
810 "PSBT output scriptPubKey should be updated"
811 );
812 }
813
814 #[cfg(feature = "experimental-dleq")]
815 #[test]
816 fn test_extractor_updates_psbt_outputs() {
817 let secp = Secp256k1::new();
818 let scan_sk = test_secret_key(10);
819 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
820 let spend_pk = SpendPublicKey::from(test_pubkey(20));
821
822 let signer_sk = test_secret_key(5);
823 let signer_pk = signer_sk.public_key(&secp);
824
825 let psbt = test_psbt(1, 2);
826 let constructor = SpPsbtConstructor::new(psbt).unwrap();
827 let constructor = constructor.add_sp_output(0, &scan_pk, &spend_pk).unwrap();
828 let updater = constructor.into_updater().unwrap();
829 let signer = updater.into_signer();
830
831 let aux_rand = [0x42u8; 32];
832 let (ecdh_share, proof) =
833 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
834
835 let signer = signer
836 .add_global_ecdh_share(&scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
837 .unwrap();
838
839 let extractor = signer.into_extractor().unwrap();
840 let (psbt, _outputs) = extractor.extract(&secp).unwrap();
841
842 let script0 = &psbt.unsigned_tx.output[0].script_pubkey;
844 assert_eq!(script0.len(), 34, "SP output should have P2TR script");
845
846 let script1 = &psbt.unsigned_tx.output[1].script_pubkey;
848 assert!(script1.is_empty(), "non-SP output should be unchanged");
849 }
850
851 #[cfg(feature = "experimental-dleq")]
852 #[test]
853 fn test_extractor_returns_metadata() {
854 let secp = Secp256k1::new();
855 let scan_sk = test_secret_key(10);
856 let scan_pk = ScanPublicKey::from(scan_sk.public_key(&secp));
857 let spend_pk = SpendPublicKey::from(test_pubkey(20));
858
859 let signer_sk = test_secret_key(5);
860 let signer_pk = signer_sk.public_key(&secp);
861
862 let psbt = test_psbt(1, 1);
863 let constructor = SpPsbtConstructor::new(psbt).unwrap();
864 let constructor = constructor.add_sp_output(0, &scan_pk, &spend_pk).unwrap();
865 let updater = constructor.into_updater().unwrap();
866 let signer = updater.into_signer();
867
868 let aux_rand = [0x42u8; 32];
869 let (ecdh_share, proof) =
870 DleqProof::generate(&signer_sk, scan_pk.as_inner(), &aux_rand, &secp).unwrap();
871
872 let signer = signer
873 .add_global_ecdh_share(&scan_pk, &ecdh_share, &proof, &signer_pk, &secp)
874 .unwrap();
875
876 let extractor = signer.into_extractor().unwrap();
877 let (_psbt, outputs) = extractor.extract(&secp).unwrap();
878
879 assert_eq!(outputs.len(), 1);
881 let output = &outputs[0];
882 assert_eq!(output.output_index(), 0, "should reference output index 0");
883 assert_eq!(
884 output.scan_pubkey(),
885 scan_pk.as_inner(),
886 "should have correct scan pubkey"
887 );
888
889 assert_eq!(output.x_only_pubkey().serialize().len(), 32);
891
892 let script = output.script_pubkey();
894 let xonly_bytes = output.x_only_pubkey().serialize();
895 assert_eq!(
896 &script.as_bytes()[2..34],
897 &xonly_bytes,
898 "P2TR script should embed the x-only pubkey"
899 );
900 }
901}