-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmod.rs
More file actions
1944 lines (1747 loc) · 72.9 KB
/
mod.rs
File metadata and controls
1944 lines (1747 loc) · 72.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! BitGo-specific PSBT parsing that handles multiple network formats
//!
//! This module provides PSBT deserialization that works across different
//! bitcoin-like networks, including those with non-standard transaction formats.
pub mod p2tr_musig2_input;
#[cfg(test)]
mod p2tr_musig2_input_utxolib;
mod propkv;
pub mod psbt_wallet_input;
pub mod psbt_wallet_output;
mod sighash;
mod zcash_psbt;
use crate::Network;
use miniscript::bitcoin::{psbt::Psbt, secp256k1, CompressedPublicKey, Txid};
pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, BITGO};
pub use sighash::validate_sighash_type;
use zcash_psbt::ZcashPsbt;
#[derive(Debug)]
pub enum DeserializeError {
/// Standard bitcoin consensus decoding error
Consensus(miniscript::bitcoin::consensus::encode::Error),
/// PSBT-specific error
Psbt(miniscript::bitcoin::psbt::Error),
/// Network-specific error message
Network(String),
}
impl std::fmt::Display for DeserializeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeserializeError::Consensus(e) => write!(f, "{}", e),
DeserializeError::Psbt(e) => write!(f, "{}", e),
DeserializeError::Network(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for DeserializeError {}
impl From<miniscript::bitcoin::consensus::encode::Error> for DeserializeError {
fn from(e: miniscript::bitcoin::consensus::encode::Error) -> Self {
DeserializeError::Consensus(e)
}
}
impl From<miniscript::bitcoin::psbt::Error> for DeserializeError {
fn from(e: miniscript::bitcoin::psbt::Error) -> Self {
DeserializeError::Psbt(e)
}
}
#[derive(Debug)]
pub enum SerializeError {
/// Standard bitcoin consensus encoding error
Consensus(std::io::Error),
/// Network-specific error message
Network(String),
}
impl std::fmt::Display for SerializeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SerializeError::Consensus(e) => write!(f, "{}", e),
SerializeError::Network(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for SerializeError {}
impl From<std::io::Error> for SerializeError {
fn from(e: std::io::Error) -> Self {
SerializeError::Consensus(e)
}
}
impl From<DeserializeError> for SerializeError {
fn from(e: DeserializeError) -> Self {
match e {
DeserializeError::Consensus(ce) => {
// Convert consensus encode error to io error
SerializeError::Network(format!("Consensus error: {}", ce))
}
DeserializeError::Psbt(pe) => SerializeError::Network(format!("PSBT error: {}", pe)),
DeserializeError::Network(msg) => SerializeError::Network(msg),
}
}
}
#[derive(Debug, Clone)]
pub enum BitGoPsbt {
BitcoinLike(Psbt, Network),
Zcash(ZcashPsbt, Network),
}
// Re-export types from submodules for convenience
pub use psbt_wallet_input::{InputScriptType, ParsedInput, ScriptId};
pub use psbt_wallet_output::ParsedOutput;
/// Parsed transaction with wallet information
#[derive(Debug, Clone)]
pub struct ParsedTransaction {
pub inputs: Vec<ParsedInput>,
pub outputs: Vec<ParsedOutput>,
pub spend_amount: u64,
pub miner_fee: u64,
pub virtual_size: u32,
}
/// Error type for transaction parsing
#[derive(Debug)]
pub enum ParseTransactionError {
/// Failed to parse input
Input {
index: usize,
error: psbt_wallet_input::ParseInputError,
},
/// Input value overflow when adding to total
InputValueOverflow { index: usize },
/// Failed to parse output
Output {
index: usize,
error: psbt_wallet_output::ParseOutputError,
},
/// Output value overflow when adding to total
OutputValueOverflow { index: usize },
/// Spend amount overflow
SpendAmountOverflow { index: usize },
/// Fee calculation error (outputs exceed inputs)
FeeCalculation,
}
impl std::fmt::Display for ParseTransactionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseTransactionError::Input { index, error } => {
write!(f, "Input {}: {}", index, error)
}
ParseTransactionError::InputValueOverflow { index } => {
write!(f, "Input {}: value overflow", index)
}
ParseTransactionError::Output { index, error } => {
write!(f, "Output {}: {}", index, error)
}
ParseTransactionError::OutputValueOverflow { index } => {
write!(f, "Output {}: value overflow", index)
}
ParseTransactionError::SpendAmountOverflow { index } => {
write!(f, "Output {}: spend amount overflow", index)
}
ParseTransactionError::FeeCalculation => {
write!(f, "Fee calculation error: outputs exceed inputs")
}
}
}
}
impl std::error::Error for ParseTransactionError {}
impl BitGoPsbt {
/// Deserialize a PSBT from bytes, using network-specific logic
pub fn deserialize(psbt_bytes: &[u8], network: Network) -> Result<BitGoPsbt, DeserializeError> {
match network {
Network::Zcash | Network::ZcashTestnet => {
// Zcash uses overwintered transaction format which is not compatible
// with standard Bitcoin transaction deserialization
let zcash_psbt = ZcashPsbt::deserialize(psbt_bytes)?;
Ok(BitGoPsbt::Zcash(zcash_psbt, network))
}
// All other networks use standard Bitcoin transaction format
Network::Bitcoin
| Network::BitcoinTestnet3
| Network::BitcoinTestnet4
| Network::BitcoinPublicSignet
| Network::BitcoinBitGoSignet
| Network::BitcoinCash
| Network::BitcoinCashTestnet
| Network::Ecash
| Network::EcashTestnet
| Network::BitcoinGold
| Network::BitcoinGoldTestnet
| Network::BitcoinSV
| Network::BitcoinSVTestnet
| Network::Dash
| Network::DashTestnet
| Network::Dogecoin
| Network::DogecoinTestnet
| Network::Litecoin
| Network::LitecoinTestnet => Ok(BitGoPsbt::BitcoinLike(
Psbt::deserialize(psbt_bytes)?,
network,
)),
}
}
pub fn network(&self) -> Network {
match self {
BitGoPsbt::BitcoinLike(_, network) => *network,
BitGoPsbt::Zcash(_, network) => *network,
}
}
/// Combine/merge data from another PSBT into this one
///
/// This method copies MuSig2 nonces and signatures (proprietary key-value pairs) from the
/// source PSBT to this PSBT. This is useful for merging PSBTs during the nonce exchange
/// and signature collection phases.
///
/// # Arguments
/// * `source_psbt` - The source PSBT containing data to merge
///
/// # Returns
/// Ok(()) if data was successfully merged
///
/// # Errors
/// Returns error if networks don't match
pub fn combine_musig2_nonces(&mut self, source_psbt: &BitGoPsbt) -> Result<(), String> {
// Check network match
if self.network() != source_psbt.network() {
return Err(format!(
"Network mismatch: destination is {}, source is {}",
self.network(),
source_psbt.network()
));
}
let source = source_psbt.psbt();
let dest = self.psbt_mut();
// Check that both PSBTs have the same number of inputs
if source.inputs.len() != dest.inputs.len() {
return Err(format!(
"PSBT input count mismatch: source has {} inputs, destination has {}",
source.inputs.len(),
dest.inputs.len()
));
}
// Copy MuSig2 nonces and partial signatures (proprietary key-values with BITGO identifier)
for (source_input, dest_input) in source.inputs.iter().zip(dest.inputs.iter_mut()) {
// Only process if the input is a MuSig2 input
if !p2tr_musig2_input::Musig2Input::is_musig2_input(source_input) {
continue;
}
// Parse nonces from source input using native Musig2 functions
let nonces = p2tr_musig2_input::parse_musig2_nonces(source_input)
.map_err(|e| format!("Failed to parse MuSig2 nonces from source: {}", e))?;
// Copy each nonce to the destination input
for nonce in nonces {
let (key, value) = nonce.to_key_value().to_key_value();
dest_input.proprietary.insert(key, value);
}
// Also copy partial signatures if present
// Partial sigs are stored as tap_script_sigs in the PSBT input
for (control_block, leaf_script) in &source_input.tap_script_sigs {
dest_input
.tap_script_sigs
.insert(*control_block, *leaf_script);
}
}
Ok(())
}
/// Serialize the PSBT to bytes, using network-specific logic
pub fn serialize(&self) -> Result<Vec<u8>, SerializeError> {
match self {
BitGoPsbt::BitcoinLike(psbt, _network) => Ok(psbt.serialize()),
BitGoPsbt::Zcash(zcash_psbt, _network) => Ok(zcash_psbt.serialize()?),
}
}
pub fn into_psbt(self) -> Psbt {
match self {
BitGoPsbt::BitcoinLike(psbt, _network) => psbt,
BitGoPsbt::Zcash(zcash_psbt, _network) => zcash_psbt.into_bitcoin_psbt(),
}
}
/// Get a reference to the underlying PSBT
///
/// This works for both BitcoinLike and Zcash PSBTs, returning a reference
/// to the inner Bitcoin-compatible PSBT structure.
pub fn psbt(&self) -> &Psbt {
match self {
BitGoPsbt::BitcoinLike(ref psbt, _network) => psbt,
BitGoPsbt::Zcash(ref zcash_psbt, _network) => &zcash_psbt.psbt,
}
}
/// Get a mutable reference to the underlying PSBT
///
/// This works for both BitcoinLike and Zcash PSBTs, returning a reference
/// to the inner Bitcoin-compatible PSBT structure.
pub fn psbt_mut(&mut self) -> &mut Psbt {
match self {
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => psbt,
BitGoPsbt::Zcash(ref mut zcash_psbt, _network) => &mut zcash_psbt.psbt,
}
}
pub fn finalize_input<C: secp256k1::Verification>(
&mut self,
secp: &secp256k1::Secp256k1<C>,
input_index: usize,
) -> Result<(), String> {
use miniscript::psbt::PsbtExt;
match self {
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => {
// Use custom bitgo p2trMusig2 input finalization for MuSig2 inputs
if p2tr_musig2_input::Musig2Input::is_musig2_input(&psbt.inputs[input_index]) {
let mut ctx = p2tr_musig2_input::Musig2Context::new(psbt, input_index)
.map_err(|e| e.to_string())?;
ctx.finalize_input(secp).map_err(|e| e.to_string())?;
return Ok(());
}
// other inputs can be finalized using the standard miniscript::psbt::finalize_input
psbt.finalize_inp_mut(secp, input_index)
.map_err(|e| e.to_string())?;
Ok(())
}
BitGoPsbt::Zcash(_zcash_psbt, _network) => {
todo!("Zcash PSBT finalization not yet implemented");
}
}
}
/// Finalize all inputs in the PSBT, attempting each input even if some fail.
/// Similar to miniscript::psbt::PsbtExt::finalize_mut.
///
/// # Returns
/// - `Ok(())` if all inputs were successfully finalized
/// - `Err(Vec<String>)` containing error messages for each failed input
///
/// # Note
/// This method will attempt to finalize ALL inputs, collecting errors for any that fail.
/// It does not stop at the first error.
pub fn finalize_mut<C: secp256k1::Verification>(
&mut self,
secp: &secp256k1::Secp256k1<C>,
) -> Result<(), Vec<String>> {
let num_inputs = self.psbt().inputs.len();
let errors: Vec<String> = (0..num_inputs)
.filter_map(|index| {
self.finalize_input(secp, index)
.err()
.map(|e| format!("Input {}: {}", index, e))
})
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Finalize all inputs and consume the PSBT, returning the finalized PSBT.
/// Similar to miniscript::psbt::PsbtExt::finalize.
///
/// # Returns
/// - `Ok(Psbt)` if all inputs were successfully finalized
/// - `Err(String)` containing a formatted error message if any input failed
pub fn finalize<C: secp256k1::Verification>(
mut self,
secp: &secp256k1::Secp256k1<C>,
) -> Result<Psbt, String> {
match self.finalize_mut(secp) {
Ok(()) => Ok(self.into_psbt()),
Err(errors) => Err(format!(
"Failed to finalize {} input(s): {}",
errors.len(),
errors.join("; ")
)),
}
}
/// Get the unsigned transaction ID
pub fn unsigned_txid(&self) -> Txid {
self.psbt().unsigned_tx.compute_txid()
}
/// Helper function to create a MuSig2 context for an input
///
/// This validates that:
/// 1. The PSBT is BitcoinLike (not Zcash)
/// 2. The input index is valid
/// 3. The input is a MuSig2 input
///
/// Returns a Musig2Context for the specified input
fn musig2_context<'a>(
&'a mut self,
input_index: usize,
) -> Result<p2tr_musig2_input::Musig2Context<'a>, String> {
if self.network().mainnet() != Network::Bitcoin {
return Err("MuSig2 not supported for non-Bitcoin networks".to_string());
}
if matches!(self, BitGoPsbt::Zcash(_, _)) {
return Err("MuSig2 not supported for Zcash".to_string());
}
let psbt = self.psbt_mut();
if input_index >= psbt.inputs.len() {
return Err(format!("Input index {} out of bounds", input_index));
}
// Validate this is a MuSig2 input
if !p2tr_musig2_input::Musig2Input::is_musig2_input(&psbt.inputs[input_index]) {
return Err(format!("Input {} is not a MuSig2 input", input_index));
}
// Create and return the context
p2tr_musig2_input::Musig2Context::new(psbt, input_index).map_err(|e| e.to_string())
}
/// Set the counterparty's (BitGo's) nonce in the PSBT
///
/// # Arguments
/// * `input_index` - The index of the MuSig2 input
/// * `participant_pub_key` - The counterparty's public key
/// * `pub_nonce` - The counterparty's public nonce
pub fn set_counterparty_nonce(
&mut self,
input_index: usize,
participant_pub_key: CompressedPublicKey,
pub_nonce: musig2::PubNonce,
) -> Result<(), String> {
let mut ctx = self.musig2_context(input_index)?;
let tap_output_key = ctx.musig2_input().participants.tap_output_key;
// Set the nonce
ctx.set_nonce(participant_pub_key, tap_output_key, pub_nonce)
.map_err(|e| e.to_string())
}
/// Generate and set a user nonce for a MuSig2 input using State-Machine API
///
/// This method uses the State-Machine API from the musig2 crate, which encapsulates
/// the SecNonce internally to prevent accidental reuse. This is the recommended
/// production API.
///
/// # Arguments
/// * `input_index` - The index of the MuSig2 input
/// * `xpriv` - The user's extended private key (will be derived for the input)
/// * `session_id` - 32-byte session ID (use rand::thread_rng().gen() in production)
///
/// # Returns
/// A tuple of (FirstRound, PubNonce) - keep FirstRound secret for signing later,
/// send PubNonce to the counterparty
pub fn generate_nonce_first_round(
&mut self,
input_index: usize,
xpriv: &miniscript::bitcoin::bip32::Xpriv,
session_id: [u8; 32],
) -> Result<(musig2::FirstRound, musig2::PubNonce), String> {
let mut ctx = self.musig2_context(input_index)?;
ctx.generate_nonce_first_round(xpriv, session_id)
.map_err(|e| e.to_string())
}
/// Sign a MuSig2 input using State-Machine API
///
/// This method uses the State-Machine API from the musig2 crate. The FirstRound
/// from nonce generation encapsulates the secret nonce, preventing reuse.
///
/// # Arguments
/// * `input_index` - The index of the MuSig2 input
/// * `first_round` - The FirstRound from generate_nonce_first_round()
/// * `xpriv` - The user's extended private key
///
/// # Returns
/// Ok(()) if the signature was successfully created and added to the PSBT
pub fn sign_with_first_round(
&mut self,
input_index: usize,
first_round: musig2::FirstRound,
xpriv: &miniscript::bitcoin::bip32::Xpriv,
) -> Result<(), String> {
let mut ctx = self.musig2_context(input_index)?;
ctx.sign_with_first_round(first_round, xpriv)
.map_err(|e| e.to_string())
}
/// Sign a single input with a raw private key
///
/// This method signs a specific input using the provided private key. It automatically
/// detects the input type and uses the appropriate signing method:
/// - Replay protection inputs (P2SH-P2PK): Signs with legacy P2SH sighash
/// - Regular inputs: Uses standard PSBT signing
/// - MuSig2 inputs: Returns error (requires FirstRound state, use sign_with_first_round)
///
/// # Arguments
/// - `input_index`: The index of the input to sign
/// - `privkey`: The private key to sign with
///
/// # Returns
/// - `Ok(())` if signing was successful
/// - `Err(String)` if signing fails or input type is not supported
pub fn sign_with_privkey(
&mut self,
input_index: usize,
privkey: &secp256k1::SecretKey,
) -> Result<(), String> {
use miniscript::bitcoin::{
ecdsa::Signature as EcdsaSignature, hashes::Hash, sighash::SighashCache, PublicKey,
};
// Get network before mutable borrow
let network = self.network();
let is_testnet = network.is_testnet();
let psbt = self.psbt_mut();
// Check bounds
if input_index >= psbt.inputs.len() {
return Err(format!(
"Input index {} out of bounds (total inputs: {})",
input_index,
psbt.inputs.len()
));
}
// Check if this is a MuSig2 input
if p2tr_musig2_input::Musig2Input::is_musig2_input(&psbt.inputs[input_index]) {
return Err(
"MuSig2 inputs cannot be signed with raw privkey. Use sign_with_first_round instead."
.to_string(),
);
}
let secp = secp256k1::Secp256k1::new();
// Derive public key from private key
let public_key = PublicKey::from_slice(
&secp256k1::PublicKey::from_secret_key(&secp, privkey).serialize(),
)
.map_err(|e| format!("Failed to derive public key: {}", e))?;
// Check if this is a replay protection input (P2SH-P2PK)
if let Some(redeem_script) = &psbt.inputs[input_index].redeem_script.clone() {
// Try to extract pubkey from redeem script
if let Ok(redeem_pubkey) = Self::extract_pubkey_from_p2pk_redeem_script(redeem_script) {
// This is a replay protection input - verify the derived pubkey matches
if public_key != redeem_pubkey {
return Err(
"Public key mismatch: derived pubkey does not match redeem_script pubkey"
.to_string(),
);
}
// Sign the replay protection input with legacy P2SH sighash
let sighash_type = miniscript::bitcoin::sighash::EcdsaSighashType::All;
let cache = SighashCache::new(&psbt.unsigned_tx);
let sighash = cache
.legacy_signature_hash(input_index, redeem_script, sighash_type.to_u32())
.map_err(|e| format!("Failed to compute sighash: {}", e))?;
// Create ECDSA signature
let message = secp256k1::Message::from_digest(sighash.to_byte_array());
let signature = secp.sign_ecdsa(&message, privkey);
let ecdsa_sig = EcdsaSignature {
signature,
sighash_type,
};
// Add signature to partial_sigs
psbt.inputs[input_index]
.partial_sigs
.insert(public_key, ecdsa_sig);
return Ok(());
}
}
// For regular inputs (non-RP, non-MuSig2), use standard signing via miniscript
// This will handle legacy, SegWit, and Taproot script path inputs
match self {
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => {
// Create a key provider that returns our single key
// Convert SecretKey to PrivateKey for the GetKey trait
// Note: The network parameter is only used for WIF serialization, not for signing
let bitcoin_network = if is_testnet {
miniscript::bitcoin::Network::Testnet
} else {
miniscript::bitcoin::Network::Bitcoin
};
let private_key = miniscript::bitcoin::PrivateKey::new(*privkey, bitcoin_network);
let key_map = std::collections::BTreeMap::from_iter([(public_key, private_key)]);
// Sign the PSBT
let result = psbt.sign(&key_map, &secp);
// Check if our specific input was signed
match result {
Ok(signing_keys) => {
if signing_keys.contains_key(&input_index) {
Ok(())
} else {
Err(format!(
"Input {} was not signed (no key found or already signed)",
input_index
))
}
}
Err((partial_success, errors)) => {
// Check if there's an error for our specific input
if let Some(error) = errors.get(&input_index) {
Err(format!("Failed to sign input {}: {:?}", input_index, error))
} else if partial_success.contains_key(&input_index) {
// Input was signed successfully despite other errors
Ok(())
} else {
Err(format!("Input {} was not signed", input_index))
}
}
}
}
BitGoPsbt::Zcash(_zcash_psbt, _network) => {
Err("Zcash signing not yet implemented".to_string())
}
}
}
/// Sign the PSBT with the provided key.
/// Wraps the underlying PSBT's sign method from miniscript::psbt::PsbtExt.
///
/// # Type Parameters
/// - `C`: Signing context from secp256k1
/// - `K`: Key type that implements `psbt::GetKey` trait
///
/// # Returns
/// - `Ok(SigningKeysMap)` on success, mapping input index to keys used for signing
/// - `Err((SigningKeysMap, SigningErrors))` on failure, containing both partial success info and errors
pub fn sign<C, K>(
&mut self,
k: &K,
secp: &secp256k1::Secp256k1<C>,
) -> Result<
miniscript::bitcoin::psbt::SigningKeysMap,
(
miniscript::bitcoin::psbt::SigningKeysMap,
miniscript::bitcoin::psbt::SigningErrors,
),
>
where
C: secp256k1::Signing + secp256k1::Verification,
K: miniscript::bitcoin::psbt::GetKey,
{
match self {
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => psbt.sign(k, secp),
BitGoPsbt::Zcash(_zcash_psbt, _network) => {
// Return an error indicating Zcash signing is not implemented
Err((
Default::default(),
std::collections::BTreeMap::from_iter([(
0,
miniscript::bitcoin::psbt::SignError::KeyNotFound,
)]),
))
}
}
}
/// Parse inputs with wallet keys and replay protection
///
/// # Arguments
/// - `wallet_keys`: The wallet's root keys for deriving scripts
/// - `replay_protection`: Scripts that are allowed as inputs without wallet validation
///
/// # Returns
/// - `Ok(Vec<ParsedInput>)` with parsed inputs
/// - `Err(ParseTransactionError)` if input parsing fails
fn parse_inputs(
&self,
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
replay_protection: &crate::fixed_script_wallet::ReplayProtection,
) -> Result<Vec<ParsedInput>, ParseTransactionError> {
let psbt = self.psbt();
let network = self.network();
psbt.unsigned_tx
.input
.iter()
.zip(psbt.inputs.iter())
.enumerate()
.map(|(input_index, (tx_input, psbt_input))| {
ParsedInput::parse(
psbt_input,
tx_input,
wallet_keys,
replay_protection,
network,
)
.map_err(|error| ParseTransactionError::Input {
index: input_index,
error,
})
})
.collect()
}
/// Parse outputs with wallet keys to identify which outputs belong to the wallet
///
/// # Arguments
/// - `wallet_keys`: The wallet's root keys for deriving scripts
///
/// # Returns
/// - `Ok(Vec<ParsedOutput>)` with parsed outputs
/// - `Err(ParseTransactionError)` if output parsing fails
///
/// # Note
/// This method does NOT validate wallet inputs. It only parses outputs to identify
/// which ones belong to the provided wallet keys.
fn parse_outputs(
&self,
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
) -> Result<Vec<ParsedOutput>, ParseTransactionError> {
let psbt = self.psbt();
let network = self.network();
psbt.unsigned_tx
.output
.iter()
.zip(psbt.outputs.iter())
.enumerate()
.map(|(output_index, (tx_output, psbt_output))| {
ParsedOutput::parse(psbt_output, tx_output, wallet_keys, network).map_err(|error| {
ParseTransactionError::Output {
index: output_index,
error,
}
})
})
.collect()
}
/// Calculate total input value from parsed inputs
///
/// # Returns
/// - `Ok(u64)` with total input value
/// - `Err(ParseTransactionError)` if overflow occurs
fn sum_input_values(parsed_inputs: &[ParsedInput]) -> Result<u64, ParseTransactionError> {
parsed_inputs
.iter()
.enumerate()
.try_fold(0u64, |total, (index, input)| {
total
.checked_add(input.value)
.ok_or(ParseTransactionError::InputValueOverflow { index })
})
}
/// Calculate total output value and spend amount from transaction outputs and parsed outputs
///
/// # Returns
/// - `Ok((total_value, spend_amount))` with total output value and external spend amount
/// - `Err(ParseTransactionError)` if overflow occurs
fn sum_output_values(
tx_outputs: &[miniscript::bitcoin::TxOut],
parsed_outputs: &[ParsedOutput],
) -> Result<(u64, u64), ParseTransactionError> {
tx_outputs
.iter()
.zip(parsed_outputs.iter())
.enumerate()
.try_fold(
(0u64, 0u64),
|(total_value, spend), (index, (tx_output, parsed_output))| {
let new_total = total_value
.checked_add(tx_output.value.to_sat())
.ok_or(ParseTransactionError::OutputValueOverflow { index })?;
let new_spend = if parsed_output.is_external() {
spend
.checked_add(tx_output.value.to_sat())
.ok_or(ParseTransactionError::SpendAmountOverflow { index })?
} else {
spend
};
Ok((new_total, new_spend))
},
)
}
/// Helper function to extract public key from a P2PK redeem script
///
/// # Arguments
/// - `redeem_script`: The redeem script to parse (expected format: <pubkey> OP_CHECKSIG)
///
/// # Returns
/// - `Ok(PublicKey)` if parsing succeeds
/// - `Err(String)` if the script format is invalid
fn extract_pubkey_from_p2pk_redeem_script(
redeem_script: &miniscript::bitcoin::ScriptBuf,
) -> Result<miniscript::bitcoin::PublicKey, String> {
use miniscript::bitcoin::{opcodes::all::OP_CHECKSIG, script::Instruction, PublicKey};
// Extract public key from redeem script
// For P2SH(P2PK), redeem_script is: <pubkey> OP_CHECKSIG
let mut redeem_instructions = redeem_script.instructions();
let public_key_bytes = match redeem_instructions.next() {
Some(Ok(Instruction::PushBytes(bytes))) => bytes.as_bytes(),
_ => return Err("Invalid redeem script format: missing public key".to_string()),
};
// Verify the script ends with OP_CHECKSIG
match redeem_instructions.next() {
Some(Ok(Instruction::Op(op))) if op == OP_CHECKSIG => {}
_ => return Err("Redeem script does not end with OP_CHECKSIG".to_string()),
}
PublicKey::from_slice(public_key_bytes).map_err(|e| format!("Invalid public key: {}", e))
}
/// Helper function to parse an ECDSA signature from final_script_sig
///
/// # Returns
/// - `Ok(bitcoin::ecdsa::Signature)` if parsing succeeds
/// - `Err(String)` if parsing fails
fn parse_signature_from_script_sig(
final_script_sig: &miniscript::bitcoin::ScriptBuf,
) -> Result<miniscript::bitcoin::ecdsa::Signature, String> {
use miniscript::bitcoin::{ecdsa::Signature, script::Instruction};
// Extract signature from final_script_sig
// For P2SH(P2PK), the scriptSig is: <signature> <redeemScript>
let mut instructions = final_script_sig.instructions();
let signature_bytes = match instructions.next() {
Some(Ok(Instruction::PushBytes(bytes))) => bytes.as_bytes(),
_ => return Err("Invalid final_script_sig format".to_string()),
};
if signature_bytes.is_empty() {
return Err("Empty signature in final_script_sig".to_string());
}
Signature::from_slice(signature_bytes)
.map_err(|e| format!("Invalid signature in final_script_sig: {}", e))
}
/// Verify if a replay protection input has a valid signature
///
/// This method checks if a given input is a replay protection input and verifies the signature.
/// Replay protection inputs (like P2shP2pk) don't use standard derivation paths,
/// so this method verifies signatures without deriving from xpub.
///
/// For P2PK replay protection inputs:
/// - Extracts public key from `redeem_script`
/// - Checks for signature in `partial_sigs` (non-finalized) or `final_script_sig` (finalized)
/// - Computes the legacy P2SH sighash using the redeem script
/// - Verifies the ECDSA signature
///
/// # Arguments
/// - `secp`: Secp256k1 context for signature verification
/// - `input_index`: The index of the input to check
/// - `replay_protection`: Replay protection configuration
///
/// # Returns
/// - `Ok(true)` if the input is a replay protection input and has a valid signature
/// - `Ok(false)` if the input is a replay protection input but has no valid signature
/// - `Err(String)` if the input is not a replay protection input, index is out of bounds, or verification fails
pub fn verify_replay_protection_signature<C: secp256k1::Verification>(
&self,
secp: &secp256k1::Secp256k1<C>,
input_index: usize,
replay_protection: &crate::fixed_script_wallet::ReplayProtection,
) -> Result<bool, String> {
use miniscript::bitcoin::{hashes::Hash, sighash::SighashCache};
let psbt = self.psbt();
// Check input index bounds
if input_index >= psbt.inputs.len() {
return Err(format!("Input index {} out of bounds", input_index));
}
let input = &psbt.inputs[input_index];
let prevout = psbt.unsigned_tx.input[input_index].previous_output;
// Get output script from input
let (output_script, _value) =
psbt_wallet_input::get_output_script_and_value(input, prevout)
.map_err(|e| format!("Failed to get output script: {}", e))?;
// Verify this is a replay protection input
if !replay_protection.is_replay_protection_input(output_script) {
return Err(format!(
"Input {} is not a replay protection input",
input_index
));
}
// Get redeem script and extract public key
let redeem_script = input
.redeem_script
.as_ref()
.ok_or_else(|| "Missing redeem_script for replay protection input".to_string())?;
let public_key = Self::extract_pubkey_from_p2pk_redeem_script(redeem_script)?;
// Get signature from partial_sigs (non-finalized) or final_script_sig (finalized)
// The bitcoin crate's ecdsa::Signature type contains both .signature and .sighash_type
let ecdsa_sig = if let Some(&partial_sig) = input.partial_sigs.get(&public_key) {
partial_sig
} else if let Some(final_script_sig) = &input.final_script_sig {
Self::parse_signature_from_script_sig(final_script_sig)?
} else {
// No signature present (neither partial nor final)
return Ok(false);
};
// Compute legacy P2SH sighash
let cache = SighashCache::new(&psbt.unsigned_tx);
let sighash = cache
.legacy_signature_hash(input_index, redeem_script, ecdsa_sig.sighash_type.to_u32())
.map_err(|e| format!("Failed to compute sighash: {}", e))?;
// Verify the signature using the bitcoin crate's built-in verification
let message = secp256k1::Message::from_digest(sighash.to_byte_array());
match secp.verify_ecdsa(&message, &ecdsa_sig.signature, &public_key.inner) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
/// Helper method to verify signature with a compressed public key
///
/// This method checks if a signature exists for the given public key.
/// It handles both ECDSA and Taproot script path signatures.
///
/// # Arguments
/// - `secp`: Secp256k1 context for signature verification
/// - `input_index`: The index of the input to check
/// - `public_key`: The compressed public key to verify the signature for
///
/// # Returns
/// - `Ok(true)` if a valid signature exists for the public key
/// - `Ok(false)` if no signature exists for the public key
/// - `Err(String)` if verification fails
fn verify_signature_with_pubkey<C: secp256k1::Verification>(
&self,
secp: &secp256k1::Secp256k1<C>,
input_index: usize,
public_key: CompressedPublicKey,
) -> Result<bool, String> {
let psbt = self.psbt();
let input = &psbt.inputs[input_index];
// Check for Taproot script path signatures first
if !input.tap_script_sigs.is_empty() {
return psbt_wallet_input::verify_taproot_script_signature(
secp,
psbt,
input_index,
public_key,
);
}
// Fall back to ECDSA signature verification for legacy/SegWit inputs
psbt_wallet_input::verify_ecdsa_signature(secp, psbt, input_index, public_key)
}
/// Verify if a valid signature exists for a given extended public key at the specified input index
///
/// This method derives the public key from the xpub using the derivation path found in the
/// PSBT input, then verifies the signature. It supports:
/// - ECDSA signatures (for legacy/SegWit inputs)
/// - Schnorr signatures (for Taproot script path inputs)
/// - MuSig2 partial signatures (for Taproot keypath MuSig2 inputs)
///
/// # Arguments
/// - `secp`: Secp256k1 context for signature verification and key derivation
/// - `input_index`: The index of the input to check
/// - `xpub`: The extended public key to derive from and verify the signature for
///
/// # Returns
/// - `Ok(true)` if a valid signature exists for the derived public key
/// - `Ok(false)` if no signature exists for the derived public key
/// - `Err(String)` if the input index is out of bounds, derivation fails, or verification fails
pub fn verify_signature_with_xpub<C: secp256k1::Verification>(
&self,
secp: &secp256k1::Secp256k1<C>,
input_index: usize,
xpub: &miniscript::bitcoin::bip32::Xpub,
) -> Result<bool, String> {
let psbt = self.psbt();
// Check input index bounds
if input_index >= psbt.inputs.len() {