From 4e2e3914cb26d3256c9a11bb7648b1f48c33f764 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 20 Sep 2021 15:37:45 +1000 Subject: [PATCH] Reorganize modules --- cfd_protocol/src/protocol.rs | 476 +------------------ cfd_protocol/src/protocol/sighash_ext.rs | 15 + cfd_protocol/src/protocol/transaction_ext.rs | 26 + cfd_protocol/src/protocol/transactions.rs | 441 +++++++++++++++++ 4 files changed, 499 insertions(+), 459 deletions(-) create mode 100644 cfd_protocol/src/protocol/sighash_ext.rs create mode 100644 cfd_protocol/src/protocol/transaction_ext.rs create mode 100644 cfd_protocol/src/protocol/transactions.rs diff --git a/cfd_protocol/src/protocol.rs b/cfd_protocol/src/protocol.rs index 1d86122..058cd3d 100644 --- a/cfd_protocol/src/protocol.rs +++ b/cfd_protocol/src/protocol.rs @@ -1,13 +1,14 @@ +use crate::protocol::sighash_ext::SigHashExt; +use crate::protocol::transactions::{ + lock_transaction, CommitTransaction, ContractExecutionTransaction, RefundTransaction, +}; use crate::{oracle, Interval}; use anyhow::{bail, Context, Result}; use bdk::bitcoin::hashes::hex::ToHex; -use bdk::bitcoin::hashes::Hash; use bdk::bitcoin::util::bip143::SigHashCache; -use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; -use bdk::bitcoin::{ - Address, Amount, OutPoint, PublicKey, Script, SigHash, SigHashType, Transaction, TxIn, TxOut, -}; +use bdk::bitcoin::util::psbt::PartiallySignedTransaction; +use bdk::bitcoin::{Address, Amount, PublicKey, SigHashType, Transaction, TxOut}; use bdk::database::BatchDatabase; use bdk::descriptor::Descriptor; use bdk::miniscript::descriptor::Wsh; @@ -18,12 +19,15 @@ use itertools::Itertools; use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, Signature, SECP256K1}; use std::collections::HashMap; use std::iter::FromIterator; +mod sighash_ext; +mod transaction_ext; +mod transactions; -/// In satoshi per vbyte. -const SATS_PER_VBYTE: f64 = 1.0; +pub use transaction_ext::TransactionExt; +pub use transactions::punish_transaction; /// Static script to be used to create lock tx -const DUMMY_2OF2_MULITISIG: &str = +const DUMMY_2OF2_MULTISIG: &str = "0020b5aa99ed7e0fa92483eb045ab8b7a59146d4d9f6653f21ba729b4331895a5b46"; pub trait WalletExt { @@ -40,9 +44,7 @@ where .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic) .fee_rate(FeeRate::from_sat_per_vb(1.0)) .add_recipient( - DUMMY_2OF2_MULITISIG - .parse() - .expect("Should be valid script"), + DUMMY_2OF2_MULTISIG.parse().expect("Should be valid script"), amount.as_sat(), ); let (lock_psbt, _) = builder.finish()?; @@ -196,7 +198,7 @@ fn build_cfds( let sighash = tx.sighash().to_message(); let sig = SECP256K1.sign(&sighash, &identity_sk); - (tx.inner, sig) + (tx.into_inner(), sig) }; let cets = payouts @@ -212,14 +214,14 @@ fn build_cfds( let encsig = cet.encsign(identity_sk, &oracle_pk)?; - Ok((cet.inner, encsig, payout.msg_nonce_pairs)) + Ok((cet.into_inner(), encsig, payout.msg_nonce_pairs)) }) .collect::>>() .context("cannot build and sign all cets")?; Ok(CfdTransactions { lock: lock_tx, - commit: (commit_tx.inner, commit_encsig), + commit: (commit_tx.into_inner(), commit_encsig), cets, refund, }) @@ -306,109 +308,6 @@ pub fn finalize_spend_transaction( Ok(tx) } -pub fn punish_transaction( - commit_descriptor: &Descriptor, - address: &Address, - encsig: EcdsaAdaptorSignature, - sk: SecretKey, - revocation_them_sk: SecretKey, - pub_them_pk: PublicKey, - revoked_commit_tx: &Transaction, -) -> Result { - /// Expected size of signed transaction in virtual bytes, plus a - /// buffer to account for different signature lengths. - const SIGNED_VBYTES: f64 = 219.5 + (3.0 * 3.0) / 4.0; - - let input = revoked_commit_tx - .input - .clone() - .into_iter() - .exactly_one() - .context("commit transaction inputs != 1")?; - - let publish_them_sk = input - .witness - .iter() - .filter_map(|elem| { - let elem = elem.as_slice(); - Signature::from_der(&elem[..elem.len() - 1]).ok() - }) - .find_map(|sig| encsig.recover(SECP256K1, &sig, &pub_them_pk.key).ok()) - .context("could not recover publish sk from commit tx")?; - - let commit_outpoint = revoked_commit_tx - .outpoint(&commit_descriptor.script_pubkey()) - .expect("to find commit output in commit tx"); - let commit_amount = revoked_commit_tx.output[commit_outpoint.vout as usize].value; - - let mut punish_tx = { - let output = TxOut { - value: commit_amount, - script_pubkey: address.script_pubkey(), - }; - let mut tx = Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: commit_outpoint, - ..Default::default() - }], - output: vec![output], - }; - - let fee = SIGNED_VBYTES * SATS_PER_VBYTE; - tx.output[0].value = commit_amount - fee as u64; - - tx - }; - - let sighash = SigHashCache::new(&punish_tx).signature_hash( - 0, - &commit_descriptor.script_code(), - commit_amount, - SigHashType::All, - ); - - let satisfier = { - let pk = { - let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &sk); - PublicKey { - compressed: true, - key, - } - }; - let pk_hash = pk.pubkey_hash().as_hash(); - let sig_sk = SECP256K1.sign(&sighash.to_message(), &sk); - - let pub_them_pk_hash = pub_them_pk.pubkey_hash().as_hash(); - let sig_pub_them = SECP256K1.sign(&sighash.to_message(), &publish_them_sk); - - let rev_them_pk = { - let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &revocation_them_sk); - PublicKey { - compressed: true, - key, - } - }; - let rev_them_pk_hash = rev_them_pk.pubkey_hash().as_hash(); - let sig_rev_them = SECP256K1.sign(&sighash.to_message(), &revocation_them_sk); - - let sighash_all = SigHashType::All; - HashMap::from_iter(vec![ - (pk_hash, (pk, (sig_sk, sighash_all))), - (pub_them_pk_hash, (pub_them_pk, (sig_pub_them, sighash_all))), - (rev_them_pk_hash, (rev_them_pk, (sig_rev_them, sighash_all))), - ]) - }; - - commit_descriptor.satisfy(&mut punish_tx.input[0], satisfier)?; - - Ok(punish_tx) -} - -// NOTE: We have decided to not order any verification utility because -// the APIs would be incredibly thin - #[derive(Clone)] pub struct PartyParams { pub lock_psbt: PartiallySignedTransaction, @@ -532,7 +431,7 @@ impl Payout { /// Compute a signature point for the given oracle public key, announcement nonce public key and /// message. -pub fn compute_signature_point( +fn compute_signature_point( oracle_pk: &schnorrsig::PublicKey, nonce_pk: &schnorrsig::PublicKey, msg: &[u8], @@ -551,81 +450,6 @@ pub fn compute_signature_point( Ok(nonce_pk.combine(&oracle_pk)?) } -#[derive(Debug, Clone)] -struct ContractExecutionTransaction { - inner: Transaction, - msg_nonce_pairs: Vec<(Vec, schnorrsig::PublicKey)>, - sighash: SigHash, - commit_descriptor: Descriptor, -} - -impl ContractExecutionTransaction { - /// Expected size of signed transaction in virtual bytes, plus a - /// buffer to account for different signature lengths. - const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; - - fn new( - commit_tx: &CommitTransaction, - payout: Payout, - maker_address: &Address, - taker_address: &Address, - relative_timelock_in_blocks: u32, - ) -> Result { - let msg_nonce_pairs = payout.msg_nonce_pairs.clone(); - let commit_input = TxIn { - previous_output: commit_tx.outpoint(), - sequence: relative_timelock_in_blocks, - ..Default::default() - }; - - let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; - fee += commit_tx.fee() as f64; - let output = payout - .with_updated_fee( - Amount::from_sat(fee as u64), - maker_address.script_pubkey().dust_value(), - taker_address.script_pubkey().dust_value(), - )? - .into_txouts(maker_address, taker_address); - - let tx = Transaction { - version: 2, - lock_time: 0, - input: vec![commit_input], - output, - }; - - let sighash = SigHashCache::new(&tx).signature_hash( - 0, - &commit_tx.descriptor.script_code(), - commit_tx.amount.as_sat(), - SigHashType::All, - ); - - Ok(Self { - inner: tx, - msg_nonce_pairs, - sighash, - commit_descriptor: commit_tx.descriptor(), - }) - } - - fn encsign( - &self, - sk: SecretKey, - oracle_pk: &schnorrsig::PublicKey, - ) -> Result { - let adaptor_point = compute_adaptor_point(oracle_pk, &self.msg_nonce_pairs)?; - - Ok(EcdsaAdaptorSignature::encrypt( - SECP256K1, - &self.sighash.to_message(), - &sk, - &adaptor_point, - )) - } -} - pub fn compute_adaptor_point( oracle_pk: &schnorrsig::PublicKey, msg_nonce_pairs: &[(Vec, schnorrsig::PublicKey)], @@ -640,272 +464,6 @@ pub fn compute_adaptor_point( Ok(adaptor_point) } -#[derive(Debug, Clone)] -struct RefundTransaction { - inner: Transaction, - sighash: SigHash, - commit_output_descriptor: Descriptor, -} - -impl RefundTransaction { - /// Expected size of signed transaction in virtual bytes, plus a - /// buffer to account for different signature lengths. - const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; - - fn new( - commit_tx: &CommitTransaction, - relative_locktime_in_blocks: u32, - maker_address: &Address, - taker_address: &Address, - maker_amount: Amount, - taker_amount: Amount, - ) -> Self { - let commit_input = TxIn { - previous_output: commit_tx.outpoint(), - sequence: relative_locktime_in_blocks, - ..Default::default() - }; - - let maker_output = TxOut { - value: maker_amount.as_sat(), - script_pubkey: maker_address.script_pubkey(), - }; - - let taker_output = TxOut { - value: taker_amount.as_sat(), - script_pubkey: taker_address.script_pubkey(), - }; - - let mut tx = Transaction { - version: 2, - lock_time: 0, - input: vec![commit_input], - output: vec![maker_output, taker_output], - }; - - let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; - fee += commit_tx.fee() as f64; - tx.output[0].value -= (fee / 2.0) as u64; - tx.output[1].value -= (fee / 2.0) as u64; - - let commit_output_descriptor = commit_tx.descriptor(); - - let sighash = SigHashCache::new(&tx).signature_hash( - 0, - &commit_tx.descriptor().script_code(), - commit_tx.amount().as_sat(), - SigHashType::All, - ); - - Self { - inner: tx, - sighash, - commit_output_descriptor, - } - } - - fn sighash(&self) -> SigHash { - self.sighash - } -} - -#[derive(Debug, Clone)] -struct CommitTransaction { - inner: Transaction, - descriptor: Descriptor, - amount: Amount, - sighash: SigHash, - lock_descriptor: Descriptor, - fee: u64, -} - -impl CommitTransaction { - /// Expected size of signed transaction in virtual bytes, plus a - /// buffer to account for different signature lengths. - const SIGNED_VBYTES: f64 = 148.5 + (3.0 * 2.0) / 4.0; - - fn new( - lock_tx: &Transaction, - (maker_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), - (taker_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), - ) -> Result { - let lock_descriptor = lock_descriptor(maker_pk, taker_pk); - let (lock_outpoint, lock_amount) = { - let outpoint = lock_tx - .outpoint(&lock_descriptor.script_pubkey()) - .context("lock script not found in lock tx")?; - let amount = lock_tx.output[outpoint.vout as usize].value; - - (outpoint, amount) - }; - - let lock_input = TxIn { - previous_output: lock_outpoint, - ..Default::default() - }; - - let descriptor = commit_descriptor( - (maker_pk, maker_rev_pk, maker_publish_pk), - (taker_pk, taker_rev_pk, taker_publish_pk), - ); - - let output = TxOut { - value: lock_amount, - script_pubkey: descriptor.script_pubkey(), - }; - - let mut inner = Transaction { - version: 2, - lock_time: 0, - input: vec![lock_input], - output: vec![output], - }; - let fee = (Self::SIGNED_VBYTES * SATS_PER_VBYTE as f64) as u64; - - let commit_tx_amount = lock_amount - fee as u64; - inner.output[0].value = commit_tx_amount; - - let sighash = SigHashCache::new(&inner).signature_hash( - 0, - &lock_descriptor.script_code(), - lock_amount, - SigHashType::All, - ); - - Ok(Self { - inner, - descriptor, - lock_descriptor, - amount: Amount::from_sat(commit_tx_amount), - sighash, - fee, - }) - } - - fn encsign(&self, sk: SecretKey, publish_them_pk: &PublicKey) -> EcdsaAdaptorSignature { - EcdsaAdaptorSignature::encrypt( - SECP256K1, - &self.sighash.to_message(), - &sk, - &publish_them_pk.key, - ) - } - - fn outpoint(&self) -> OutPoint { - self.inner - .outpoint(&self.descriptor.script_pubkey()) - .expect("to find commit output in commit tx") - } - - fn amount(&self) -> Amount { - self.amount - } - - fn descriptor(&self) -> Descriptor { - self.descriptor.clone() - } - - fn fee(&self) -> u64 { - self.fee - } -} - -fn lock_transaction( - maker_psbt: PartiallySignedTransaction, - taker_psbt: PartiallySignedTransaction, - maker_pk: PublicKey, - taker_pk: PublicKey, - amount: Amount, -) -> PartiallySignedTransaction { - let lock_descriptor = lock_descriptor(maker_pk, taker_pk); - - let maker_change = maker_psbt - .global - .unsigned_tx - .output - .into_iter() - .filter(|out| { - out.script_pubkey != DUMMY_2OF2_MULITISIG.parse().expect("To be a valid script") - }) - .collect::>(); - - let taker_change = taker_psbt - .global - .unsigned_tx - .output - .into_iter() - .filter(|out| { - out.script_pubkey != DUMMY_2OF2_MULITISIG.parse().expect("To be a valid script") - }) - .collect::>(); - - let lock_output = TxOut { - value: amount.as_sat(), - script_pubkey: lock_descriptor.script_pubkey(), - }; - - let input = vec![ - maker_psbt.global.unsigned_tx.input, - taker_psbt.global.unsigned_tx.input, - ] - .concat(); - - let output = std::iter::once(lock_output) - .chain(maker_change) - .chain(taker_change) - .collect(); - - let lock_tx = Transaction { - version: 2, - lock_time: 0, - input, - output, - }; - - PartiallySignedTransaction { - global: Global::from_unsigned_tx(lock_tx).expect("to be unsigned"), - inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(), - outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(), - } -} - -pub trait TransactionExt { - fn get_virtual_size(&self) -> f64; - fn outpoint(&self, script_pubkey: &Script) -> Result; -} - -impl TransactionExt for Transaction { - fn get_virtual_size(&self) -> f64 { - self.get_weight() as f64 / 4.0 - } - - fn outpoint(&self, script_pubkey: &Script) -> Result { - let vout = self - .output - .iter() - .position(|out| &out.script_pubkey == script_pubkey) - .context("script pubkey not found in tx")?; - - Ok(OutPoint { - txid: self.txid(), - vout: vout as u32, - }) - } -} - -trait SigHashExt { - fn to_message(self) -> secp256k1_zkp::Message; -} - -impl SigHashExt for SigHash { - fn to_message(self) -> secp256k1_zkp::Message { - use secp256k1_zkp::bitcoin_hashes::Hash; - let hash = secp256k1_zkp::bitcoin_hashes::sha256d::Hash::from_inner(*self.as_inner()); - - hash.into() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/cfd_protocol/src/protocol/sighash_ext.rs b/cfd_protocol/src/protocol/sighash_ext.rs new file mode 100644 index 0000000..84b38b6 --- /dev/null +++ b/cfd_protocol/src/protocol/sighash_ext.rs @@ -0,0 +1,15 @@ +use bdk::bitcoin::hashes::Hash; +use bdk::bitcoin::SigHash; + +pub(super) trait SigHashExt { + fn to_message(self) -> secp256k1_zkp::Message; +} + +impl SigHashExt for SigHash { + fn to_message(self) -> secp256k1_zkp::Message { + use secp256k1_zkp::bitcoin_hashes::Hash; + let hash = secp256k1_zkp::bitcoin_hashes::sha256d::Hash::from_inner(*self.as_inner()); + + hash.into() + } +} diff --git a/cfd_protocol/src/protocol/transaction_ext.rs b/cfd_protocol/src/protocol/transaction_ext.rs new file mode 100644 index 0000000..effab95 --- /dev/null +++ b/cfd_protocol/src/protocol/transaction_ext.rs @@ -0,0 +1,26 @@ +use anyhow::{Context, Result}; +use bdk::bitcoin::{OutPoint, Script, Transaction}; + +pub trait TransactionExt { + fn get_virtual_size(&self) -> f64; + fn outpoint(&self, script_pubkey: &Script) -> Result; +} + +impl TransactionExt for Transaction { + fn get_virtual_size(&self) -> f64 { + self.get_weight() as f64 / 4.0 + } + + fn outpoint(&self, script_pubkey: &Script) -> Result { + let vout = self + .output + .iter() + .position(|out| &out.script_pubkey == script_pubkey) + .context("script pubkey not found in tx")?; + + Ok(OutPoint { + txid: self.txid(), + vout: vout as u32, + }) + } +} diff --git a/cfd_protocol/src/protocol/transactions.rs b/cfd_protocol/src/protocol/transactions.rs new file mode 100644 index 0000000..89cecc3 --- /dev/null +++ b/cfd_protocol/src/protocol/transactions.rs @@ -0,0 +1,441 @@ +use crate::protocol::sighash_ext::SigHashExt; +use crate::protocol::transaction_ext::TransactionExt; +use crate::protocol::{ + commit_descriptor, compute_adaptor_point, lock_descriptor, Payout, DUMMY_2OF2_MULTISIG, +}; + +use anyhow::{Context, Result}; +use bdk::bitcoin::util::bip143::SigHashCache; +use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; +use bdk::bitcoin::{ + Address, Amount, OutPoint, PublicKey, SigHash, SigHashType, Transaction, TxIn, TxOut, +}; +use bdk::descriptor::Descriptor; +use bdk::miniscript::DescriptorTrait; +use itertools::Itertools; +use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, Signature, SECP256K1}; +use std::collections::HashMap; +use std::iter::FromIterator; + +/// In satoshi per vbyte. +const SATS_PER_VBYTE: f64 = 1.0; + +pub(super) fn lock_transaction( + maker_psbt: PartiallySignedTransaction, + taker_psbt: PartiallySignedTransaction, + maker_pk: PublicKey, + taker_pk: PublicKey, + amount: Amount, +) -> PartiallySignedTransaction { + let lock_descriptor = lock_descriptor(maker_pk, taker_pk); + + let maker_change = maker_psbt + .global + .unsigned_tx + .output + .into_iter() + .filter(|out| { + out.script_pubkey != DUMMY_2OF2_MULTISIG.parse().expect("To be a valid script") + }) + .collect::>(); + + let taker_change = taker_psbt + .global + .unsigned_tx + .output + .into_iter() + .filter(|out| { + out.script_pubkey != DUMMY_2OF2_MULTISIG.parse().expect("To be a valid script") + }) + .collect::>(); + + let lock_output = TxOut { + value: amount.as_sat(), + script_pubkey: lock_descriptor.script_pubkey(), + }; + + let input = vec![ + maker_psbt.global.unsigned_tx.input, + taker_psbt.global.unsigned_tx.input, + ] + .concat(); + + let output = std::iter::once(lock_output) + .chain(maker_change) + .chain(taker_change) + .collect(); + + let lock_tx = Transaction { + version: 2, + lock_time: 0, + input, + output, + }; + + PartiallySignedTransaction { + global: Global::from_unsigned_tx(lock_tx).expect("to be unsigned"), + inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(), + outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(), + } +} + +#[derive(Debug, Clone)] +pub(super) struct CommitTransaction { + inner: Transaction, + descriptor: Descriptor, + amount: Amount, + sighash: SigHash, + lock_descriptor: Descriptor, + fee: u64, +} + +impl CommitTransaction { + /// Expected size of signed transaction in virtual bytes, plus a + /// buffer to account for different signature lengths. + const SIGNED_VBYTES: f64 = 148.5 + (3.0 * 2.0) / 4.0; + + pub(super) fn new( + lock_tx: &Transaction, + (maker_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), + (taker_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), + ) -> Result { + let lock_descriptor = lock_descriptor(maker_pk, taker_pk); + let (lock_outpoint, lock_amount) = { + let outpoint = lock_tx + .outpoint(&lock_descriptor.script_pubkey()) + .context("lock script not found in lock tx")?; + let amount = lock_tx.output[outpoint.vout as usize].value; + + (outpoint, amount) + }; + + let lock_input = TxIn { + previous_output: lock_outpoint, + ..Default::default() + }; + + let descriptor = commit_descriptor( + (maker_pk, maker_rev_pk, maker_publish_pk), + (taker_pk, taker_rev_pk, taker_publish_pk), + ); + + let output = TxOut { + value: lock_amount, + script_pubkey: descriptor.script_pubkey(), + }; + + let mut inner = Transaction { + version: 2, + lock_time: 0, + input: vec![lock_input], + output: vec![output], + }; + let fee = (Self::SIGNED_VBYTES * SATS_PER_VBYTE as f64) as u64; + + let commit_tx_amount = lock_amount - fee as u64; + inner.output[0].value = commit_tx_amount; + + let sighash = SigHashCache::new(&inner).signature_hash( + 0, + &lock_descriptor.script_code(), + lock_amount, + SigHashType::All, + ); + + Ok(Self { + inner, + descriptor, + lock_descriptor, + amount: Amount::from_sat(commit_tx_amount), + sighash, + fee, + }) + } + + pub(super) fn encsign( + &self, + sk: SecretKey, + publish_them_pk: &PublicKey, + ) -> EcdsaAdaptorSignature { + EcdsaAdaptorSignature::encrypt( + SECP256K1, + &self.sighash.to_message(), + &sk, + &publish_them_pk.key, + ) + } + + pub(super) fn into_inner(self) -> Transaction { + self.inner + } + + fn outpoint(&self) -> OutPoint { + self.inner + .outpoint(&self.descriptor.script_pubkey()) + .expect("to find commit output in commit tx") + } + + fn amount(&self) -> Amount { + self.amount + } + + fn descriptor(&self) -> Descriptor { + self.descriptor.clone() + } + + fn fee(&self) -> u64 { + self.fee + } +} + +#[derive(Debug, Clone)] +pub(super) struct ContractExecutionTransaction { + inner: Transaction, + msg_nonce_pairs: Vec<(Vec, schnorrsig::PublicKey)>, + sighash: SigHash, + commit_descriptor: Descriptor, +} + +impl ContractExecutionTransaction { + /// Expected size of signed transaction in virtual bytes, plus a + /// buffer to account for different signature lengths. + const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; + + pub(super) fn new( + commit_tx: &CommitTransaction, + payout: Payout, + maker_address: &Address, + taker_address: &Address, + relative_timelock_in_blocks: u32, + ) -> Result { + let msg_nonce_pairs = payout.msg_nonce_pairs.clone(); + let commit_input = TxIn { + previous_output: commit_tx.outpoint(), + sequence: relative_timelock_in_blocks, + ..Default::default() + }; + + let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; + fee += commit_tx.fee() as f64; + let output = payout + .with_updated_fee( + Amount::from_sat(fee as u64), + maker_address.script_pubkey().dust_value(), + taker_address.script_pubkey().dust_value(), + )? + .into_txouts(maker_address, taker_address); + + let tx = Transaction { + version: 2, + lock_time: 0, + input: vec![commit_input], + output, + }; + + let sighash = SigHashCache::new(&tx).signature_hash( + 0, + &commit_tx.descriptor.script_code(), + commit_tx.amount.as_sat(), + SigHashType::All, + ); + + Ok(Self { + inner: tx, + msg_nonce_pairs, + sighash, + commit_descriptor: commit_tx.descriptor(), + }) + } + + pub(super) fn encsign( + &self, + sk: SecretKey, + oracle_pk: &schnorrsig::PublicKey, + ) -> Result { + let adaptor_point = compute_adaptor_point(oracle_pk, &self.msg_nonce_pairs)?; + + Ok(EcdsaAdaptorSignature::encrypt( + SECP256K1, + &self.sighash.to_message(), + &sk, + &adaptor_point, + )) + } + + pub(super) fn into_inner(self) -> Transaction { + self.inner + } +} + +#[derive(Debug, Clone)] +pub(super) struct RefundTransaction { + inner: Transaction, + sighash: SigHash, + commit_output_descriptor: Descriptor, +} + +impl RefundTransaction { + /// Expected size of signed transaction in virtual bytes, plus a + /// buffer to account for different signature lengths. + const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; + + pub(super) fn new( + commit_tx: &CommitTransaction, + relative_locktime_in_blocks: u32, + maker_address: &Address, + taker_address: &Address, + maker_amount: Amount, + taker_amount: Amount, + ) -> Self { + let commit_input = TxIn { + previous_output: commit_tx.outpoint(), + sequence: relative_locktime_in_blocks, + ..Default::default() + }; + + let maker_output = TxOut { + value: maker_amount.as_sat(), + script_pubkey: maker_address.script_pubkey(), + }; + + let taker_output = TxOut { + value: taker_amount.as_sat(), + script_pubkey: taker_address.script_pubkey(), + }; + + let mut tx = Transaction { + version: 2, + lock_time: 0, + input: vec![commit_input], + output: vec![maker_output, taker_output], + }; + + let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; + fee += commit_tx.fee() as f64; + tx.output[0].value -= (fee / 2.0) as u64; + tx.output[1].value -= (fee / 2.0) as u64; + + let commit_output_descriptor = commit_tx.descriptor(); + + let sighash = SigHashCache::new(&tx).signature_hash( + 0, + &commit_tx.descriptor().script_code(), + commit_tx.amount().as_sat(), + SigHashType::All, + ); + + Self { + inner: tx, + sighash, + commit_output_descriptor, + } + } + + pub(super) fn sighash(&self) -> SigHash { + self.sighash + } + + pub(super) fn into_inner(self) -> Transaction { + self.inner + } +} + +pub fn punish_transaction( + commit_descriptor: &Descriptor, + address: &Address, + encsig: EcdsaAdaptorSignature, + sk: SecretKey, + revocation_them_sk: SecretKey, + pub_them_pk: PublicKey, + revoked_commit_tx: &Transaction, +) -> Result { + /// Expected size of signed transaction in virtual bytes, plus a + /// buffer to account for different signature lengths. + const SIGNED_VBYTES: f64 = 219.5 + (3.0 * 3.0) / 4.0; + + let input = revoked_commit_tx + .input + .clone() + .into_iter() + .exactly_one() + .context("commit transaction inputs != 1")?; + + let publish_them_sk = input + .witness + .iter() + .filter_map(|elem| { + let elem = elem.as_slice(); + Signature::from_der(&elem[..elem.len() - 1]).ok() + }) + .find_map(|sig| encsig.recover(SECP256K1, &sig, &pub_them_pk.key).ok()) + .context("could not recover publish sk from commit tx")?; + + let commit_outpoint = revoked_commit_tx + .outpoint(&commit_descriptor.script_pubkey()) + .expect("to find commit output in commit tx"); + let commit_amount = revoked_commit_tx.output[commit_outpoint.vout as usize].value; + + let mut punish_tx = { + let output = TxOut { + value: commit_amount, + script_pubkey: address.script_pubkey(), + }; + let mut tx = Transaction { + version: 2, + lock_time: 0, + input: vec![TxIn { + previous_output: commit_outpoint, + ..Default::default() + }], + output: vec![output], + }; + + let fee = SIGNED_VBYTES * SATS_PER_VBYTE; + tx.output[0].value = commit_amount - fee as u64; + + tx + }; + + let sighash = SigHashCache::new(&punish_tx).signature_hash( + 0, + &commit_descriptor.script_code(), + commit_amount, + SigHashType::All, + ); + + let satisfier = { + let pk = { + let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &sk); + PublicKey { + compressed: true, + key, + } + }; + let pk_hash = pk.pubkey_hash().as_hash(); + let sig_sk = SECP256K1.sign(&sighash.to_message(), &sk); + + let pub_them_pk_hash = pub_them_pk.pubkey_hash().as_hash(); + let sig_pub_them = SECP256K1.sign(&sighash.to_message(), &publish_them_sk); + + let rev_them_pk = { + let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &revocation_them_sk); + PublicKey { + compressed: true, + key, + } + }; + let rev_them_pk_hash = rev_them_pk.pubkey_hash().as_hash(); + let sig_rev_them = SECP256K1.sign(&sighash.to_message(), &revocation_them_sk); + + let sighash_all = SigHashType::All; + HashMap::from_iter(vec![ + (pk_hash, (pk, (sig_sk, sighash_all))), + (pub_them_pk_hash, (pub_them_pk, (sig_pub_them, sighash_all))), + (rev_them_pk_hash, (rev_them_pk, (sig_rev_them, sighash_all))), + ]) + }; + + commit_descriptor.satisfy(&mut punish_tx.input[0], satisfier)?; + + Ok(punish_tx) +}