diff --git a/swap/src/xmr_first_protocol/transactions/emergency_refund.rs b/swap/src/xmr_first_protocol/transactions/emergency_refund.rs new file mode 100644 index 00000000..8f59f4f1 --- /dev/null +++ b/swap/src/xmr_first_protocol/transactions/emergency_refund.rs @@ -0,0 +1,155 @@ +use crate::bitcoin::wallet::Watchable; +use crate::bitcoin::{ + verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs, + NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, +}; +use crate::xmr_first_protocol::transactions::btc_lock::BtcLock; +use ::bitcoin::util::bip143::SigHashCache; +use ::bitcoin::{SigHash, SigHashType, Txid}; +use anyhow::{bail, Context, Result}; +use bitcoin::Script; +use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; +use ecdsa_fun::fun::Scalar; +use ecdsa_fun::nonce::Deterministic; +use ecdsa_fun::Signature; +use miniscript::{Descriptor, DescriptorTrait}; +use sha2::Sha256; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct EmergencyRefund { + inner: Transaction, + digest: SigHash, + lock_output_descriptor: Descriptor<::bitcoin::PublicKey>, + watch_script: Script, +} + +impl EmergencyRefund { + pub fn new(tx_lock: &BtcLock, redeem_address: &Address) -> Self { + // lock_input is the shared output that is now being used as an input for the + // redeem transaction + let tx_redeem = tx_lock.build_spend_transaction(redeem_address, None); + + let digest = SigHashCache::new(&tx_redeem).signature_hash( + 0, // Only one input: lock_input (lock transaction) + &tx_lock.output_descriptor.script_code(), + tx_lock.lock_amount().as_sat(), + SigHashType::All, + ); + + Self { + inner: tx_redeem, + digest, + lock_output_descriptor: tx_lock.output_descriptor.clone(), + watch_script: redeem_address.script_pubkey(), + } + } + + pub fn txid(&self) -> Txid { + self.inner.txid() + } + + pub fn digest(&self) -> SigHash { + self.digest + } + + pub fn encsig( + &self, + b: SecretKey, + S_a_bitcoin: PublicKey, + ) -> crate::bitcoin::EncryptedSignature { + b.encsign(S_a_bitcoin, self.digest()) + } + + pub fn complete( + mut self, + a: SecretKey, + s_a: Scalar, + B: PublicKey, + encrypted_signature: EncryptedSignature, + ) -> Result { + verify_encsig( + B, + PublicKey::from(s_a.clone()), + &self.digest(), + &encrypted_signature, + ) + .context("Invalid encrypted signature received")?; + + let sig_a = a.sign(self.digest()); + let adaptor = Adaptor::, Deterministic>::default(); + let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature); + + let satisfier = { + let mut satisfier = HashMap::with_capacity(2); + + let A = ::bitcoin::PublicKey { + compressed: true, + key: a.public.into(), + }; + let B = ::bitcoin::PublicKey { + compressed: true, + key: B.0.into(), + }; + + // The order in which these are inserted doesn't matter + satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All)); + satisfier.insert(B, (sig_b.into(), ::bitcoin::SigHashType::All)); + + satisfier + }; + + self.lock_output_descriptor + .satisfy(&mut self.inner.input[0], satisfier) + .context("Failed to sign Bitcoin redeem transaction")?; + + Ok(self.inner) + } + + pub fn extract_signature_by_key( + &self, + candidate_transaction: Transaction, + B: PublicKey, + ) -> Result { + let input = match candidate_transaction.input.as_slice() { + [input] => input, + [] => bail!("no inputs"), + [inputs @ ..] => bail!("too many inputs"), + }; + + let sigs = match input + .witness + .iter() + .map(|vec| vec.as_slice()) + .collect::>() + .as_slice() + { + [sig_1, sig_2, _script] => [sig_1, sig_2] + .iter() + .map(|sig| { + bitcoin::secp256k1::Signature::from_der(&sig[..sig.len() - 1]) + .map(Signature::from) + }) + .collect::, _>>(), + [] => bail!("empty witness stack"), + [witnesses @ ..] => bail!("not three witnesses"), + }?; + + let sig = sigs + .into_iter() + .find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok()) + .context("Neither signature on witness stack verifies against B")?; + + Ok(sig) + } +} + +impl Watchable for EmergencyRefund { + fn id(&self) -> Txid { + self.txid() + } + + fn script(&self) -> Script { + self.watch_script.clone() + } +}