Skip to main content

chains_sdk/bitcoin/psbt/
v0.rs

1//! **PSBT v0** — Core PSBT structure (BIP-174) with BIP-371 Taproot extensions.
2//!
3//! Implements serialization, deserialization, input/output maps, and
4//! key-value encoding for Partially Signed Bitcoin Transactions.
5
6use crate::crypto;
7use crate::encoding;
8use crate::error::SignerError;
9use std::collections::BTreeMap;
10
11/// PSBT magic bytes: `0x70736274` ("psbt" in ASCII).
12const PSBT_MAGIC: [u8; 4] = [0x70, 0x73, 0x62, 0x74];
13
14/// PSBT separator byte.
15const PSBT_SEPARATOR: u8 = 0xff;
16
17// ─── Key Types ──────────────────────────────────────────────────────
18
19/// PSBT global key types.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21#[repr(u8)]
22pub enum GlobalKey {
23    /// The unsigned transaction.
24    UnsignedTx = 0x00,
25    /// Extended public key (xpub) for BIP-32 derivation.
26    Xpub = 0x01,
27    /// PSBT version number.
28    Version = 0xFB,
29}
30
31/// PSBT per-input key types.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[repr(u8)]
34pub enum InputKey {
35    /// Non-witness UTXO (full previous transaction).
36    NonWitnessUtxo = 0x00,
37    /// Witness UTXO (previous output value + scriptPubKey).
38    WitnessUtxo = 0x01,
39    /// Partial signature.
40    PartialSig = 0x02,
41    /// Sighash type.
42    SighashType = 0x03,
43    /// Input redeem script.
44    RedeemScript = 0x04,
45    /// Input witness script.
46    WitnessScript = 0x05,
47    /// BIP-32 derivation path for a pubkey.
48    Bip32Derivation = 0x06,
49    /// Finalized scriptSig.
50    FinalScriptSig = 0x07,
51    /// Finalized scriptWitness.
52    FinalScriptWitness = 0x08,
53    /// BIP-371: Taproot key-path signature.
54    TapKeySig = 0x13,
55    /// BIP-371: Taproot script-path signature.
56    TapScriptSig = 0x14,
57    /// BIP-371: Taproot leaf script.
58    TapLeafScript = 0x15,
59    /// BIP-371: Taproot BIP-32 derivation.
60    TapBip32Derivation = 0x16,
61    /// BIP-371: Taproot internal key.
62    TapInternalKey = 0x17,
63    /// BIP-371: Taproot merkle root.
64    TapMerkleRoot = 0x18,
65}
66
67/// PSBT per-output key types.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69#[repr(u8)]
70pub enum OutputKey {
71    /// Output redeem script.
72    RedeemScript = 0x00,
73    /// Output witness script.
74    WitnessScript = 0x01,
75    /// BIP-32 derivation path.
76    Bip32Derivation = 0x02,
77    /// BIP-371: Taproot internal key.
78    TapInternalKey = 0x05,
79    /// BIP-371: Taproot tree.
80    TapTree = 0x06,
81    /// BIP-371: Taproot BIP-32 derivation.
82    TapBip32Derivation = 0x07,
83}
84
85// ─── Key-Value Pair ─────────────────────────────────────────────────
86
87/// A PSBT key-value pair.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct KeyValuePair {
90    /// The key bytes (key_type || key_data).
91    pub key: Vec<u8>,
92    /// The value bytes.
93    pub value: Vec<u8>,
94}
95
96// ─── PSBT Structure ─────────────────────────────────────────────────
97
98/// A Partially Signed Bitcoin Transaction.
99#[derive(Clone, Debug)]
100pub struct Psbt {
101    /// Global key-value pairs (keyed by full key bytes).
102    pub global: BTreeMap<Vec<u8>, Vec<u8>>,
103    /// Per-input key-value pairs.
104    pub inputs: Vec<BTreeMap<Vec<u8>, Vec<u8>>>,
105    /// Per-output key-value pairs.
106    pub outputs: Vec<BTreeMap<Vec<u8>, Vec<u8>>>,
107}
108
109impl Psbt {
110    /// Create a new empty PSBT.
111    pub fn new() -> Self {
112        Self {
113            global: BTreeMap::new(),
114            inputs: Vec::new(),
115            outputs: Vec::new(),
116        }
117    }
118
119    /// Set the unsigned transaction.
120    pub fn set_unsigned_tx(&mut self, raw_tx: &[u8]) {
121        self.global
122            .insert(vec![GlobalKey::UnsignedTx as u8], raw_tx.to_vec());
123    }
124
125    /// Get the unsigned transaction bytes.
126    pub fn unsigned_tx(&self) -> Option<&Vec<u8>> {
127        self.global.get(&vec![GlobalKey::UnsignedTx as u8])
128    }
129
130    /// Add an input with an empty key-value map.
131    pub fn add_input(&mut self) -> usize {
132        let idx = self.inputs.len();
133        self.inputs.push(BTreeMap::new());
134        idx
135    }
136
137    /// Add an output with an empty key-value map.
138    pub fn add_output(&mut self) -> usize {
139        let idx = self.outputs.len();
140        self.outputs.push(BTreeMap::new());
141        idx
142    }
143
144    /// Set a key-value pair for a specific input.
145    pub fn set_input_kv(&mut self, input_idx: usize, key: Vec<u8>, value: Vec<u8>) {
146        if let Some(map) = self.inputs.get_mut(input_idx) {
147            map.insert(key, value);
148        }
149    }
150
151    /// Set a key-value pair for a specific output.
152    pub fn set_output_kv(&mut self, output_idx: usize, key: Vec<u8>, value: Vec<u8>) {
153        if let Some(map) = self.outputs.get_mut(output_idx) {
154            map.insert(key, value);
155        }
156    }
157
158    /// Set witness UTXO for an input.
159    pub fn set_witness_utxo(&mut self, input_idx: usize, amount: u64, script_pubkey: &[u8]) {
160        let mut value = Vec::new();
161        value.extend_from_slice(&amount.to_le_bytes());
162        encoding::encode_compact_size(&mut value, script_pubkey.len() as u64);
163        value.extend_from_slice(script_pubkey);
164        self.set_input_kv(input_idx, vec![InputKey::WitnessUtxo as u8], value);
165    }
166
167    /// Set the Taproot internal key for an input (BIP-371).
168    pub fn set_tap_internal_key(&mut self, input_idx: usize, x_only_key: &[u8; 32]) {
169        self.set_input_kv(
170            input_idx,
171            vec![InputKey::TapInternalKey as u8],
172            x_only_key.to_vec(),
173        );
174    }
175
176    /// Set the Taproot merkle root for an input (BIP-371).
177    pub fn set_tap_merkle_root(&mut self, input_idx: usize, merkle_root: &[u8; 32]) {
178        self.set_input_kv(
179            input_idx,
180            vec![InputKey::TapMerkleRoot as u8],
181            merkle_root.to_vec(),
182        );
183    }
184
185    /// Set the Taproot key-path signature for an input (BIP-371).
186    pub fn set_tap_key_sig(&mut self, input_idx: usize, signature: &[u8]) {
187        self.set_input_kv(
188            input_idx,
189            vec![InputKey::TapKeySig as u8],
190            signature.to_vec(),
191        );
192    }
193
194    /// Sign a SegWit v0 (P2WPKH) input using the provided signer.
195    ///
196    /// Reads the witness UTXO from the input map, computes the BIP-143 sighash,
197    /// signs with ECDSA, and stores the partial signature in the PSBT.
198    ///
199    /// # Arguments
200    /// - `input_idx` — The input index to sign
201    /// - `signer` — A `BitcoinSigner` whose public key matches the input
202    /// - `sighash_type` — Sighash flag (typically `All`)
203    pub fn sign_segwit_input(
204        &mut self,
205        input_idx: usize,
206        signer: &crate::bitcoin::BitcoinSigner,
207        sighash_type: crate::bitcoin::tapscript::SighashType,
208    ) -> Result<(), SignerError> {
209        use crate::bitcoin::sighash;
210        use crate::bitcoin::transaction::*;
211        use crate::traits::Signer;
212
213        // Extract witness UTXO from input map
214        let witness_utxo_key = vec![InputKey::WitnessUtxo as u8];
215        let utxo_data = self
216            .inputs
217            .get(input_idx)
218            .and_then(|m| m.get(&witness_utxo_key))
219            .ok_or_else(|| SignerError::SigningFailed("missing witness UTXO for input".into()))?
220            .clone();
221
222        let (amount, script_pk) = parse_witness_utxo_value(&utxo_data, "witness UTXO")?;
223
224        // Extract pubkey hash from P2WPKH scriptPubKey: OP_0 OP_PUSH20 <hash>
225        if script_pk.len() != 22 || script_pk[0] != 0x00 || script_pk[1] != 0x14 {
226            return Err(SignerError::SigningFailed(
227                "input is not P2WPKH (expected OP_0 OP_PUSH20)".into(),
228            ));
229        }
230        let mut pubkey_hash = [0u8; 20];
231        pubkey_hash.copy_from_slice(&script_pk[2..22]);
232
233        // Verify the signer's pubkey matches this input
234        let expected_hash = crate::crypto::hash160(&signer.public_key_bytes());
235        if pubkey_hash != expected_hash {
236            return Err(SignerError::SigningFailed(
237                "signer public key does not match the P2WPKH input".into(),
238            ));
239        }
240
241        // Get the unsigned transaction
242        let tx_bytes = self
243            .unsigned_tx()
244            .ok_or_else(|| SignerError::SigningFailed("missing unsigned tx".into()))?
245            .clone();
246
247        // Minimal tx parsing for sighash: we need to build a Transaction struct
248        let tx = parse_unsigned_tx(&tx_bytes)?;
249
250        // Compute BIP-143 sighash
251        let script_code = sighash::p2wpkh_script_code(&pubkey_hash);
252        let prev_out = sighash::PrevOut {
253            script_code,
254            value: amount,
255        };
256        let sighash_value = sighash::segwit_v0_sighash(&tx, input_idx, &prev_out, sighash_type)?;
257
258        // Sign
259        let sig = signer.sign_prehashed(&sighash_value)?;
260        let mut sig_bytes = sig.to_bytes();
261        sig_bytes.push(sighash_type.to_byte());
262
263        // Store as partial signature: key = 0x02 || compressed_pubkey
264        let pubkey = signer.public_key_bytes();
265        let mut key = vec![InputKey::PartialSig as u8];
266        key.extend_from_slice(&pubkey);
267        self.set_input_kv(input_idx, key, sig_bytes);
268
269        Ok(())
270    }
271
272    /// Sign a Taproot (P2TR) input using the provided Schnorr signer.
273    ///
274    /// Reads the witness UTXO from the input map, computes the BIP-341 sighash,
275    /// signs with Schnorr, and stores the key-path signature in the PSBT.
276    pub fn sign_taproot_input(
277        &mut self,
278        input_idx: usize,
279        signer: &crate::bitcoin::schnorr::SchnorrSigner,
280        sighash_type: crate::bitcoin::tapscript::SighashType,
281    ) -> Result<(), SignerError> {
282        use crate::bitcoin::sighash;
283        use crate::bitcoin::transaction::*;
284        use crate::traits::Signer;
285
286        // Extract all witness UTXOs for taproot sighash (needs all prevouts)
287        let mut prevouts = Vec::new();
288        let witness_utxo_key = vec![InputKey::WitnessUtxo as u8];
289        for (i, input_map) in self.inputs.iter().enumerate() {
290            let utxo_data = input_map.get(&witness_utxo_key).ok_or_else(|| {
291                SignerError::SigningFailed(format!("missing witness UTXO for input {i}"))
292            })?;
293            let context = format!("witness UTXO {i}");
294            let (amount, script_pk) = parse_witness_utxo_value(utxo_data, &context)?;
295            prevouts.push(TxOut {
296                value: amount,
297                script_pubkey: script_pk.to_vec(),
298            });
299        }
300
301        // Get the unsigned transaction
302        let tx_bytes = self
303            .unsigned_tx()
304            .ok_or_else(|| SignerError::SigningFailed("missing unsigned tx".into()))?
305            .clone();
306        let tx = parse_unsigned_tx(&tx_bytes)?;
307
308        // Compute BIP-341 sighash
309        let sighash_value =
310            sighash::taproot_key_path_sighash(&tx, input_idx, &prevouts, sighash_type)?;
311
312        // Sign with Schnorr
313        let sig = signer.sign(&sighash_value)?;
314        let mut sig_bytes = sig.bytes.to_vec();
315        // Append sighash byte only if not Default (0x00)
316        if sighash_type.to_byte() != 0x00 {
317            sig_bytes.push(sighash_type.to_byte());
318        }
319
320        // Store as BIP-371 tap key sig
321        self.set_tap_key_sig(input_idx, &sig_bytes);
322        Ok(())
323    }
324
325    /// Serialize the PSBT to binary format.
326    ///
327    /// Format: `magic || 0xFF || global_map || 0x00 || input_maps... || output_maps...`
328    pub fn serialize(&self) -> Vec<u8> {
329        let mut data = Vec::new();
330
331        // Magic
332        data.extend_from_slice(&PSBT_MAGIC);
333        data.push(PSBT_SEPARATOR);
334
335        // Global map
336        for (key, value) in &self.global {
337            encoding::encode_compact_size(&mut data, key.len() as u64);
338            data.extend_from_slice(key);
339            encoding::encode_compact_size(&mut data, value.len() as u64);
340            data.extend_from_slice(value);
341        }
342        data.push(0x00); // end of global map
343
344        // Input maps
345        for input in &self.inputs {
346            for (key, value) in input {
347                encoding::encode_compact_size(&mut data, key.len() as u64);
348                data.extend_from_slice(key);
349                encoding::encode_compact_size(&mut data, value.len() as u64);
350                data.extend_from_slice(value);
351            }
352            data.push(0x00); // end of input map
353        }
354
355        // Output maps
356        for output in &self.outputs {
357            for (key, value) in output {
358                encoding::encode_compact_size(&mut data, key.len() as u64);
359                data.extend_from_slice(key);
360                encoding::encode_compact_size(&mut data, value.len() as u64);
361                data.extend_from_slice(value);
362            }
363            data.push(0x00); // end of output map
364        }
365
366        data
367    }
368
369    /// Deserialize a PSBT from binary format.
370    ///
371    /// Parses the unsigned transaction from the global map (key `0x00`) to
372    /// determine the exact number of input and output maps, then reads
373    /// that many maps in order.
374    ///
375    /// Per BIP-174, this parser requires a valid unsigned transaction in the
376    /// global map and rejects malformed/truncated structures.
377    pub fn deserialize(data: &[u8]) -> Result<Self, SignerError> {
378        if data.len() < 5 {
379            return Err(SignerError::ParseError("PSBT too short".into()));
380        }
381        if data[..4] != PSBT_MAGIC {
382            return Err(SignerError::ParseError("invalid PSBT magic".into()));
383        }
384        if data[4] != PSBT_SEPARATOR {
385            return Err(SignerError::ParseError("missing PSBT separator".into()));
386        }
387
388        let mut offset = 5;
389        let mut psbt = Psbt::new();
390
391        // Parse global map
392        psbt.global = parse_kv_map(data, &mut offset)?;
393
394        // Try to extract input/output counts from the unsigned transaction (key 0x00)
395        let counts = psbt
396            .global
397            .get(&vec![0x00])
398            .and_then(|raw_tx| extract_tx_io_counts(raw_tx));
399
400        let (num_inputs, num_outputs) = counts.ok_or_else(|| {
401            SignerError::ParseError(
402                "PSBT: missing or malformed unsigned transaction (key 0x00)".into(),
403            )
404        })?;
405
406        // Parse exactly num_inputs input maps, then num_outputs output maps
407        for i in 0..num_inputs {
408            if offset >= data.len() {
409                return Err(SignerError::ParseError(format!(
410                    "PSBT truncated: expected {} inputs, got {}",
411                    num_inputs, i
412                )));
413            }
414            psbt.inputs.push(parse_kv_map(data, &mut offset)?);
415        }
416        for i in 0..num_outputs {
417            if offset >= data.len() {
418                return Err(SignerError::ParseError(format!(
419                    "PSBT truncated: expected {} outputs, got {}",
420                    num_outputs, i
421                )));
422            }
423            psbt.outputs.push(parse_kv_map(data, &mut offset)?);
424        }
425
426        // Reject trailing bytes
427        if offset != data.len() {
428            return Err(SignerError::ParseError(format!(
429                "PSBT has {} trailing bytes",
430                data.len() - offset
431            )));
432        }
433
434        Ok(psbt)
435    }
436
437    /// Compute the PSBT ID (SHA256 of the serialized PSBT).
438    pub fn psbt_id(&self) -> [u8; 32] {
439        let serialized = self.serialize();
440        crypto::sha256(&serialized)
441    }
442}
443
444impl Default for Psbt {
445    fn default() -> Self {
446        Self::new()
447    }
448}
449
450// ─── Parsing Helpers ────────────────────────────────────────────────
451
452/// Parse a key-value map from PSBT binary data.
453fn parse_kv_map(
454    data: &[u8],
455    offset: &mut usize,
456) -> Result<BTreeMap<Vec<u8>, Vec<u8>>, SignerError> {
457    let mut map = BTreeMap::new();
458
459    loop {
460        if *offset >= data.len() {
461            return Err(SignerError::ParseError(
462                "PSBT map missing terminator".into(),
463            ));
464        }
465
466        // Read key length
467        let key_len = encoding::read_compact_size(data, offset)?;
468        if key_len == 0 {
469            // End of map
470            return Ok(map);
471        }
472
473        // Read key
474        let key_len_usize = usize::try_from(key_len).map_err(|_| {
475            SignerError::ParseError("PSBT key length exceeds platform usize".into())
476        })?;
477        let end = offset
478            .checked_add(key_len_usize)
479            .ok_or_else(|| SignerError::ParseError("PSBT key length overflow".into()))?;
480        if end > data.len() {
481            return Err(SignerError::ParseError("PSBT key truncated".into()));
482        }
483        let key = data[*offset..end].to_vec();
484        *offset = end;
485
486        // Read value length
487        let val_len = encoding::read_compact_size(data, offset)?;
488
489        // Read value
490        let val_len_usize = usize::try_from(val_len).map_err(|_| {
491            SignerError::ParseError("PSBT value length exceeds platform usize".into())
492        })?;
493        let end = offset
494            .checked_add(val_len_usize)
495            .ok_or_else(|| SignerError::ParseError("PSBT value length overflow".into()))?;
496        if end > data.len() {
497            return Err(SignerError::ParseError("PSBT value truncated".into()));
498        }
499        let value = data[*offset..end].to_vec();
500        *offset = end;
501
502        // Reject duplicate keys (BIP-174 requirement)
503        if map.contains_key(&key) {
504            return Err(SignerError::ParseError("PSBT: duplicate key in map".into()));
505        }
506        map.insert(key, value);
507    }
508}
509
510/// Extract input and output counts from a raw unsigned transaction.
511///
512/// Parses just enough of the transaction to read the varint counts.
513/// Returns `None` if the data is too short or malformed.
514fn extract_tx_io_counts(raw_tx: &[u8]) -> Option<(usize, usize)> {
515    if raw_tx.len() < 10 {
516        return None; // Too short for any valid tx
517    }
518    // Skip version (4 bytes)
519    let mut offset = 4;
520    // Read input count
521    let num_inputs =
522        usize::try_from(encoding::read_compact_size(raw_tx, &mut offset).ok()?).ok()?;
523    // Skip all inputs: each has outpoint(36) + varint(script_len) + script + sequence(4)
524    for _ in 0..num_inputs {
525        // outpoint (32 txid + 4 vout)
526        offset = offset.checked_add(36)?;
527        if offset > raw_tx.len() {
528            return None;
529        }
530        // scriptSig length + data
531        let script_len =
532            usize::try_from(encoding::read_compact_size(raw_tx, &mut offset).ok()?).ok()?;
533        let end = offset.checked_add(script_len)?.checked_add(4)?;
534        if end > raw_tx.len() {
535            return None;
536        }
537        offset = end;
538    }
539    // Read output count
540    let num_outputs =
541        usize::try_from(encoding::read_compact_size(raw_tx, &mut offset).ok()?).ok()?;
542    // Sanity check
543    if num_inputs > 10_000 || num_outputs > 10_000 {
544        return None;
545    }
546    Some((num_inputs, num_outputs))
547}
548
549fn parse_witness_utxo_value<'a>(
550    utxo_data: &'a [u8],
551    context: &str,
552) -> Result<(u64, &'a [u8]), SignerError> {
553    if utxo_data.len() < 9 {
554        return Err(SignerError::SigningFailed(format!("{context} too short")));
555    }
556    let mut amount_bytes = [0u8; 8];
557    amount_bytes.copy_from_slice(&utxo_data[..8]);
558    let amount = u64::from_le_bytes(amount_bytes);
559
560    let mut utxo_off = 8usize;
561    let script_len_u64 = encoding::read_compact_size(utxo_data, &mut utxo_off)?;
562    let script_len = usize::try_from(script_len_u64).map_err(|_| {
563        SignerError::SigningFailed(format!("{context} script length exceeds platform usize"))
564    })?;
565    let script_end = utxo_off
566        .checked_add(script_len)
567        .ok_or_else(|| SignerError::SigningFailed(format!("{context} script length overflow")))?;
568    if script_end > utxo_data.len() {
569        return Err(SignerError::SigningFailed(format!(
570            "{context} script truncated"
571        )));
572    }
573    if script_end != utxo_data.len() {
574        return Err(SignerError::SigningFailed(format!(
575            "{context} has trailing bytes"
576        )));
577    }
578    Ok((amount, &utxo_data[utxo_off..script_end]))
579}
580
581// ─── Tests ──────────────────────────────────────────────────────────
582
583#[cfg(test)]
584#[allow(clippy::unwrap_used, clippy::expect_used)]
585mod tests {
586    use super::*;
587
588    #[test]
589    fn test_psbt_new() {
590        let psbt = Psbt::new();
591        assert!(psbt.global.is_empty());
592        assert!(psbt.inputs.is_empty());
593        assert!(psbt.outputs.is_empty());
594    }
595
596    #[test]
597    fn test_psbt_set_unsigned_tx() {
598        let mut psbt = Psbt::new();
599        let fake_tx = vec![0x01, 0x02, 0x03, 0x04];
600        psbt.set_unsigned_tx(&fake_tx);
601        assert_eq!(psbt.unsigned_tx(), Some(&fake_tx));
602    }
603
604    #[test]
605    fn test_psbt_add_input_output() {
606        let mut psbt = Psbt::new();
607        let idx_in = psbt.add_input();
608        assert_eq!(idx_in, 0);
609        let idx_out = psbt.add_output();
610        assert_eq!(idx_out, 0);
611        assert_eq!(psbt.inputs.len(), 1);
612        assert_eq!(psbt.outputs.len(), 1);
613    }
614
615    #[test]
616    fn test_psbt_serialize_magic() {
617        let psbt = Psbt::new();
618        let data = psbt.serialize();
619        assert_eq!(&data[..4], &PSBT_MAGIC);
620        assert_eq!(data[4], PSBT_SEPARATOR);
621    }
622
623    #[test]
624    fn test_psbt_serialize_deserialize_roundtrip() {
625        let mut psbt = Psbt::new();
626        // Build a minimal valid unsigned tx: version(4) + 0 inputs + 0 outputs + locktime(4)
627        let mut raw_tx = Vec::new();
628        raw_tx.extend_from_slice(&1i32.to_le_bytes()); // version
629        raw_tx.push(0x01); // 1 input
630        raw_tx.extend_from_slice(&[0xAA; 32]); // txid
631        raw_tx.extend_from_slice(&0u32.to_le_bytes()); // vout
632        raw_tx.push(0x00); // empty scriptSig
633        raw_tx.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes()); // sequence
634        raw_tx.push(0x01); // 1 output
635        raw_tx.extend_from_slice(&50000u64.to_le_bytes()); // value
636        raw_tx.push(0x00); // empty scriptPubKey
637        raw_tx.extend_from_slice(&0u32.to_le_bytes()); // locktime
638        psbt.set_unsigned_tx(&raw_tx);
639        let idx = psbt.add_input();
640        psbt.add_output();
641        let script_pk = [
642            0x00u8, 0x14, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
643            0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
644        ];
645        psbt.set_witness_utxo(idx, 50000, &script_pk);
646
647        let serialized = psbt.serialize();
648        let parsed = Psbt::deserialize(&serialized).expect("valid PSBT");
649
650        // Global should match
651        assert_eq!(parsed.global.len(), psbt.global.len());
652        assert_eq!(parsed.unsigned_tx(), psbt.unsigned_tx());
653    }
654
655    #[test]
656    fn test_psbt_deserialize_invalid() {
657        assert!(Psbt::deserialize(&[]).is_err());
658        assert!(Psbt::deserialize(&[0x00, 0x01, 0x02, 0x03, 0xFF]).is_err());
659        assert!(Psbt::deserialize(&[0x70, 0x73, 0x62, 0x74, 0x00]).is_err()); // wrong separator
660    }
661
662    #[test]
663    fn test_psbt_set_taproot_fields() {
664        let mut psbt = Psbt::new();
665        let idx = psbt.add_input();
666        let key = [0xAA; 32];
667        let root = [0xBB; 32];
668        let sig = [0xCC; 64];
669
670        psbt.set_tap_internal_key(idx, &key);
671        psbt.set_tap_merkle_root(idx, &root);
672        psbt.set_tap_key_sig(idx, &sig);
673
674        let input = &psbt.inputs[0];
675        assert_eq!(
676            input.get(&vec![InputKey::TapInternalKey as u8]),
677            Some(&key.to_vec())
678        );
679        assert_eq!(
680            input.get(&vec![InputKey::TapMerkleRoot as u8]),
681            Some(&root.to_vec())
682        );
683        assert_eq!(
684            input.get(&vec![InputKey::TapKeySig as u8]),
685            Some(&sig.to_vec())
686        );
687    }
688
689    #[test]
690    fn test_psbt_psbt_id_deterministic() {
691        let mut psbt = Psbt::new();
692        psbt.set_unsigned_tx(&[0x01, 0x00]);
693        let id1 = psbt.psbt_id();
694        let id2 = psbt.psbt_id();
695        assert_eq!(id1, id2);
696    }
697
698    #[test]
699    fn test_psbt_empty_roundtrip() {
700        let psbt = Psbt::new();
701        let data = psbt.serialize();
702        // Empty PSBT without unsigned tx should now be rejected
703        assert!(Psbt::deserialize(&data).is_err());
704    }
705
706    #[test]
707    fn test_psbt_multiple_inputs() {
708        let mut psbt = Psbt::new();
709        psbt.add_input();
710        psbt.add_input();
711        psbt.add_input();
712        assert_eq!(psbt.inputs.len(), 3);
713    }
714
715    #[test]
716    fn test_compact_size_roundtrip() {
717        for val in [0u64, 1, 252, 253, 0xFFFF, 0x10000, 0xFFFFFFFF, 0x100000000] {
718            let mut buf = Vec::new();
719            encoding::encode_compact_size(&mut buf, val);
720            let mut offset = 0;
721            let parsed = encoding::read_compact_size(&buf, &mut offset).expect("valid");
722            assert_eq!(parsed, val, "failed for value {val}");
723        }
724    }
725
726    #[test]
727    fn test_parse_kv_map_rejects_huge_key_length() {
728        let mut data = vec![0xFF];
729        data.extend_from_slice(&u64::MAX.to_le_bytes());
730        let mut offset = 0;
731        assert!(parse_kv_map(&data, &mut offset).is_err());
732    }
733
734    #[test]
735    fn test_parse_kv_map_rejects_missing_terminator() {
736        // key_len=1, key=0x01, val_len=1, val=0x02 (no terminating 0x00 key_len)
737        let data = vec![0x01, 0x01, 0x01, 0x02];
738        let mut offset = 0;
739        assert!(parse_kv_map(&data, &mut offset).is_err());
740    }
741
742    #[test]
743    fn test_extract_tx_io_counts_rejects_oversized_script_len() {
744        let mut raw_tx = Vec::new();
745        raw_tx.extend_from_slice(&1u32.to_le_bytes()); // version
746        raw_tx.push(0x01); // 1 input
747        raw_tx.extend_from_slice(&[0u8; 32]); // prev txid
748        raw_tx.extend_from_slice(&0u32.to_le_bytes()); // vout
749        raw_tx.push(0xFF); // script_len prefix (u64)
750        raw_tx.extend_from_slice(&u64::MAX.to_le_bytes());
751        assert_eq!(extract_tx_io_counts(&raw_tx), None);
752    }
753
754    #[test]
755    fn test_parse_witness_utxo_rejects_trailing_bytes() {
756        let mut utxo = Vec::new();
757        utxo.extend_from_slice(&50_000u64.to_le_bytes());
758        encoding::encode_compact_size(&mut utxo, 22);
759        utxo.extend_from_slice(&[0x00, 0x14]);
760        utxo.extend_from_slice(&[0xAA; 20]);
761        utxo.push(0x99); // trailing garbage
762
763        assert!(parse_witness_utxo_value(&utxo, "witness UTXO").is_err());
764    }
765
766    #[test]
767    fn test_psbt_witness_utxo() {
768        let mut psbt = Psbt::new();
769        let idx = psbt.add_input();
770        let script_pk = vec![0x00, 0x14, 0xAA, 0xBB, 0xCC]; // simplified
771        psbt.set_witness_utxo(idx, 100000, &script_pk);
772
773        let input = &psbt.inputs[0];
774        let value = input
775            .get(&vec![InputKey::WitnessUtxo as u8])
776            .expect("exists");
777        // First 8 bytes should be amount in LE
778        assert_eq!(&value[..8], &100000u64.to_le_bytes());
779    }
780}