|
7 | 7 | use crate::fixed_script_wallet::wallet_scripts::parse_multisig_script_2_of_3; |
8 | 8 | use miniscript::bitcoin::blockdata::opcodes::all::OP_PUSHBYTES_0; |
9 | 9 | use miniscript::bitcoin::blockdata::script::Builder; |
| 10 | +use miniscript::bitcoin::ecdsa::Signature as EcdsaSig; |
10 | 11 | use miniscript::bitcoin::psbt::Psbt; |
11 | 12 | use miniscript::bitcoin::script::PushBytesBuf; |
12 | | -use miniscript::bitcoin::{Transaction, Witness}; |
| 13 | +use miniscript::bitcoin::{CompressedPublicKey, ScriptBuf, Transaction, TxIn, Witness}; |
13 | 14 |
|
14 | 15 | /// Build a half-signed transaction in legacy format from a PSBT. |
15 | 16 | /// |
@@ -147,3 +148,106 @@ pub fn build_half_signed_legacy_tx(psbt: &Psbt) -> Result<Transaction, String> { |
147 | 148 |
|
148 | 149 | Ok(tx) |
149 | 150 | } |
| 151 | + |
| 152 | +/// A partial signature extracted from a legacy half-signed input. |
| 153 | +pub struct LegacyPartialSig { |
| 154 | + pub pubkey: CompressedPublicKey, |
| 155 | + pub sig: EcdsaSig, |
| 156 | +} |
| 157 | + |
| 158 | +/// Determines whether a legacy input uses segwit (witness data) and whether it |
| 159 | +/// has a p2sh wrapper (scriptSig pushing a redeem script). |
| 160 | +/// |
| 161 | +/// Returns `(is_p2sh, is_segwit, multisig_script)`. |
| 162 | +fn classify_legacy_input(tx_in: &TxIn) -> Result<(bool, bool, ScriptBuf), String> { |
| 163 | + let has_witness = !tx_in.witness.is_empty(); |
| 164 | + let has_script_sig = !tx_in.script_sig.is_empty(); |
| 165 | + |
| 166 | + if has_witness { |
| 167 | + // Segwit: witness contains [empty, sig0?, sig1?, sig2?, witnessScript] |
| 168 | + let witness_items: Vec<&[u8]> = tx_in.witness.iter().collect(); |
| 169 | + if witness_items.len() < 5 { |
| 170 | + return Err(format!( |
| 171 | + "Expected at least 5 witness items, got {}", |
| 172 | + witness_items.len() |
| 173 | + )); |
| 174 | + } |
| 175 | + let multisig_script = ScriptBuf::from(witness_items.last().unwrap().to_vec()); |
| 176 | + let is_p2sh = has_script_sig; // p2shP2wsh has scriptSig, p2wsh does not |
| 177 | + Ok((is_p2sh, true, multisig_script)) |
| 178 | + } else if has_script_sig { |
| 179 | + // p2sh only: scriptSig = [OP_0, sig0?, sig1?, sig2?, redeemScript] |
| 180 | + // Parse the scriptSig instructions to extract the redeemScript (last push) |
| 181 | + let instructions: Vec<_> = tx_in |
| 182 | + .script_sig |
| 183 | + .instructions() |
| 184 | + .collect::<Result<Vec<_>, _>>() |
| 185 | + .map_err(|e| format!("Failed to parse scriptSig: {}", e))?; |
| 186 | + if instructions.len() < 5 { |
| 187 | + return Err(format!( |
| 188 | + "Expected at least 5 scriptSig items, got {}", |
| 189 | + instructions.len() |
| 190 | + )); |
| 191 | + } |
| 192 | + let last = instructions.last().unwrap(); |
| 193 | + let multisig_bytes = match last { |
| 194 | + miniscript::bitcoin::script::Instruction::PushBytes(bytes) => bytes.as_bytes(), |
| 195 | + _ => return Err("Last scriptSig item is not a push".to_string()), |
| 196 | + }; |
| 197 | + Ok((true, false, ScriptBuf::from(multisig_bytes.to_vec()))) |
| 198 | + } else { |
| 199 | + Err("Input has neither witness nor scriptSig".to_string()) |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +/// Extract a partial signature from a legacy half-signed input. |
| 204 | +/// |
| 205 | +/// This is the inverse of the signature placement in `build_half_signed_legacy_tx`. |
| 206 | +/// It parses the scriptSig/witness to find the single signature and its position |
| 207 | +/// in the 2-of-3 multisig, then returns the corresponding pubkey and signature. |
| 208 | +pub fn unsign_legacy_input(tx_in: &TxIn) -> Result<LegacyPartialSig, String> { |
| 209 | + let (_, is_segwit, multisig_script) = classify_legacy_input(tx_in)?; |
| 210 | + |
| 211 | + let pubkeys = parse_multisig_script_2_of_3(&multisig_script)?; |
| 212 | + |
| 213 | + // Extract the 3 signature slots (index 1..=3, skipping the leading OP_0/empty) |
| 214 | + let sig_slots: Vec<Vec<u8>> = if is_segwit { |
| 215 | + let items: Vec<&[u8]> = tx_in.witness.iter().collect(); |
| 216 | + // witness = [empty, sig0?, sig1?, sig2?, witnessScript] |
| 217 | + items[1..=3].iter().map(|s| s.to_vec()).collect() |
| 218 | + } else { |
| 219 | + // scriptSig = [OP_0, sig0?, sig1?, sig2?, redeemScript] |
| 220 | + let instructions: Vec<_> = tx_in |
| 221 | + .script_sig |
| 222 | + .instructions() |
| 223 | + .collect::<Result<Vec<_>, _>>() |
| 224 | + .map_err(|e| format!("Failed to parse scriptSig: {}", e))?; |
| 225 | + // instructions[0] = OP_0, [1..=3] = sigs, [4] = redeemScript |
| 226 | + instructions[1..=3] |
| 227 | + .iter() |
| 228 | + .map(|inst| match inst { |
| 229 | + miniscript::bitcoin::script::Instruction::PushBytes(bytes) => { |
| 230 | + bytes.as_bytes().to_vec() |
| 231 | + } |
| 232 | + miniscript::bitcoin::script::Instruction::Op(_) => vec![], |
| 233 | + }) |
| 234 | + .collect() |
| 235 | + }; |
| 236 | + |
| 237 | + // Find the non-empty signature slot |
| 238 | + let mut found_sig = None; |
| 239 | + for (i, slot) in sig_slots.iter().enumerate() { |
| 240 | + if !slot.is_empty() { |
| 241 | + if found_sig.is_some() { |
| 242 | + return Err("Expected exactly 1 signature, found multiple".to_string()); |
| 243 | + } |
| 244 | + let sig = EcdsaSig::from_slice(slot) |
| 245 | + .map_err(|e| format!("Failed to parse signature at position {}: {}", i, e))?; |
| 246 | + let pubkey = CompressedPublicKey::from_slice(&pubkeys[i].to_bytes()) |
| 247 | + .map_err(|e| format!("Failed to convert pubkey: {}", e))?; |
| 248 | + found_sig = Some(LegacyPartialSig { pubkey, sig }); |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + found_sig.ok_or_else(|| "No signature found in input".to_string()) |
| 253 | +} |
0 commit comments