From 8e4d3254f2c7e279872f3006899f586fa39bf6f6 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Tue, 21 Sep 2021 15:46:24 +1000 Subject: [PATCH] Add close_transaction API to cfd_protocol lib --- cfd_protocol/src/lib.rs | 7 ++- cfd_protocol/src/protocol.rs | 2 +- cfd_protocol/src/protocol/transactions.rs | 55 ++++++++++++++++++ cfd_protocol/tests/cfds.rs | 70 ++++++++++++++++++++++- 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/cfd_protocol/src/lib.rs b/cfd_protocol/src/lib.rs index 35ddd74..ed74d3e 100644 --- a/cfd_protocol/src/lib.rs +++ b/cfd_protocol/src/lib.rs @@ -5,8 +5,9 @@ pub mod interval; pub use oracle::{attest, nonce}; pub use protocol::{ - commit_descriptor, compute_adaptor_point, create_cfd_transactions, finalize_spend_transaction, - lock_descriptor, punish_transaction, renew_cfd_transactions, spending_tx_sighash, - CfdTransactions, PartyParams, Payout, PunishParams, TransactionExt, WalletExt, + close_transaction, commit_descriptor, compute_adaptor_point, create_cfd_transactions, + finalize_spend_transaction, lock_descriptor, punish_transaction, renew_cfd_transactions, + spending_tx_sighash, CfdTransactions, PartyParams, Payout, PunishParams, TransactionExt, + WalletExt, }; pub use secp256k1_zkp; diff --git a/cfd_protocol/src/protocol.rs b/cfd_protocol/src/protocol.rs index c1f8b2b..3584221 100644 --- a/cfd_protocol/src/protocol.rs +++ b/cfd_protocol/src/protocol.rs @@ -1,5 +1,5 @@ pub use transaction_ext::TransactionExt; -pub use transactions::punish_transaction; +pub use transactions::{close_transaction, punish_transaction}; use crate::interval; use crate::protocol::sighash_ext::SigHashExt; diff --git a/cfd_protocol/src/protocol/transactions.rs b/cfd_protocol/src/protocol/transactions.rs index b0f1c87..9109946 100644 --- a/cfd_protocol/src/protocol/transactions.rs +++ b/cfd_protocol/src/protocol/transactions.rs @@ -345,6 +345,61 @@ impl RefundTransaction { } } +/// Build a transaction which closes the CFD contract. +/// +/// This transaction spends directly from the lock transaction. Both +/// parties must agree on the split of coins between `maker_amount` +/// and `taker_amount`. +pub fn close_transaction( + lock_descriptor: &Descriptor, + lock_outpoint: OutPoint, + lock_amount: Amount, + (maker_address, maker_amount): (&Address, Amount), + (taker_address, taker_amount): (&Address, Amount), +) -> Result<(Transaction, secp256k1_zkp::Message)> { + /// Expected size of signed transaction in virtual bytes, plus a + /// buffer to account for different signature lengths. + const SIGNED_VBYTES: f64 = 167.5 + (3.0 * 2.0) / 4.0; + + let lock_input = TxIn { + previous_output: lock_outpoint, + ..Default::default() + }; + + // TODO: The fee could take into account the network state in this + // case, since this transaction is to be broadcast immediately + // after building and signing it + let fee = SIGNED_VBYTES * SATS_PER_VBYTE as f64; + let fee_per_party = (fee / 2.0) as u64; + + let maker_output = TxOut { + value: maker_amount.as_sat() - fee_per_party, + script_pubkey: maker_address.script_pubkey(), + }; + let taker_output = TxOut { + value: taker_amount.as_sat() - fee_per_party, + script_pubkey: taker_address.script_pubkey(), + }; + + let tx = Transaction { + version: 2, + lock_time: 0, + input: vec![lock_input], + output: vec![maker_output, taker_output], + }; + + let sighash = SigHashCache::new(&tx) + .signature_hash( + 0, + &lock_descriptor.script_code(), + lock_amount.as_sat(), + SigHashType::All, + ) + .to_message(); + + Ok((tx, sighash)) +} + pub fn punish_transaction( commit_descriptor: &Descriptor, address: &Address, diff --git a/cfd_protocol/tests/cfds.rs b/cfd_protocol/tests/cfds.rs index 1ec6749..6cfdf9a 100644 --- a/cfd_protocol/tests/cfds.rs +++ b/cfd_protocol/tests/cfds.rs @@ -8,7 +8,7 @@ use bdk::SignOptions; use bit_vec::BitVec; use bitcoin::util::psbt::PartiallySignedTransaction; use cfd_protocol::{ - attest, commit_descriptor, compute_adaptor_point, create_cfd_transactions, + attest, close_transaction, commit_descriptor, compute_adaptor_point, create_cfd_transactions, finalize_spend_transaction, interval, lock_descriptor, nonce, punish_transaction, renew_cfd_transactions, spending_tx_sighash, CfdTransactions, Payout, PunishParams, TransactionExt, WalletExt, @@ -262,6 +262,74 @@ fn renew_cfd() { ) } +#[test] +fn collaboratively_close_cfd() { + let mut rng = thread_rng(); + + let maker_lock_amount = Amount::ONE_BTC; + let taker_lock_amount = Amount::ONE_BTC; + + let maker_wallet = build_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap(); + let taker_wallet = build_wallet(&mut rng, Amount::from_btc(0.4).unwrap(), 5).unwrap(); + + let oracle = Oracle::new(&mut rng); + let (_, announcement) = announce(&mut rng); + + let payouts = vec![Payout::new( + 0..=10_000, + Amount::from_btc(1.5).unwrap(), + Amount::from_btc(0.5).unwrap(), + ) + .unwrap()] + .concat(); + + let refund_timelock = 0; + + let (maker_cfd_txs, _, maker, taker, maker_addr, taker_addr) = create_cfd_txs( + &mut rng, + (&maker_wallet, maker_lock_amount), + (&taker_wallet, taker_lock_amount), + (oracle.public_key(), &announcement.nonce_pks()), + payouts, + refund_timelock, + ); + + let lock_tx = maker_cfd_txs.lock.extract_tx(); + let lock_desc = lock_descriptor(maker.pk, taker.pk); + let (lock_outpoint, lock_amount) = { + let outpoint = lock_tx + .outpoint(&lock_desc.script_pubkey()) + .expect("lock script to be in lock tx"); + let amount = Amount::from_sat(lock_tx.output[outpoint.vout as usize].value); + + (outpoint, amount) + }; + + let maker_amount = Amount::ONE_BTC; + let taker_amount = Amount::ONE_BTC; + + let (close_tx, close_sighash) = close_transaction( + &lock_desc, + lock_outpoint, + lock_amount, + (&maker_addr, maker_amount), + (&taker_addr, taker_amount), + ) + .expect("to build close tx"); + + let sig_maker = SECP256K1.sign(&close_sighash, &maker.sk); + let sig_taker = SECP256K1.sign(&close_sighash, &taker.sk); + let signed_close_tx = finalize_spend_transaction( + close_tx, + &lock_desc, + (maker.pk, sig_maker), + (taker.pk, sig_taker), + ) + .expect("to sign close tx"); + + check_tx(&lock_tx, &signed_close_tx, &lock_desc).expect("valid close tx"); +} + fn create_cfd_txs( rng: &mut (impl RngCore + CryptoRng), (maker_wallet, maker_lock_amount): (&bdk::Wallet<(), bdk::database::MemoryDatabase>, Amount),