From 91134e3b2a24f5fc75d74e66e2c28b4914751a37 Mon Sep 17 00:00:00 2001 From: Lucas Soriano del Pino Date: Mon, 30 Aug 2021 20:16:43 +1000 Subject: [PATCH] Generate all DLC transactions in test --- Cargo.toml | 1 + cfd_protocol/Cargo.toml | 13 +- cfd_protocol/src/lib.rs | 332 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 342 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f426dd..292b3e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] members = [ "cfd_protocol" ] +resolver = "2" diff --git a/cfd_protocol/Cargo.toml b/cfd_protocol/Cargo.toml index 1e74c01..a8c604d 100644 --- a/cfd_protocol/Cargo.toml +++ b/cfd_protocol/Cargo.toml @@ -3,8 +3,15 @@ name = "cfd_protocol" version = "0.1.0" edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +anyhow = "1" +# bdk = { version = "0.10" } +bdk = { git = "https://github.com/coblox/bdk", rev = "acf157a99a305226203d2b55a567291a93c64720" } +rand = "0.6" +rand_chacha = "0.1" + +[dev-dependencies] -dlc = { git = "https://github.com/p2pderivatives/rust-dlc" } \ No newline at end of file +# bdk = { version = "0.10", features = ["verify"] } +bdk = { git = "https://github.com/coblox/bdk", rev = "acf157a99a305226203d2b55a567291a93c64720", features = ["verify"] } +bitcoin = { version = "0.27", features = ["rand"] } diff --git a/cfd_protocol/src/lib.rs b/cfd_protocol/src/lib.rs index 75eacee..0747bcd 100644 --- a/cfd_protocol/src/lib.rs +++ b/cfd_protocol/src/lib.rs @@ -1,5 +1,335 @@ #[cfg(test)] mod tests { + use anyhow::Result; + use bdk::{ + bitcoin::{ + hashes::hex::ToHex, + secp256k1::{All, Secp256k1, SecretKey}, + util::{bip32::ExtendedPrivKey, psbt::PartiallySignedTransaction}, + Address, Amount, Network, OutPoint, PrivateKey, PublicKey, Script, Transaction, TxIn, + TxOut, + }, + descriptor::Descriptor, + miniscript::{descriptor::Wsh, DescriptorTrait}, + wallet::AddressIndex, + }; + use rand::{CryptoRng, RngCore, SeedableRng}; + use rand_chacha::ChaChaRng; + + /// Refund transaction fee. It is paid evenly by the maker and the + /// taker. + /// + /// Ideally we don't commit to a transaction fee ahead of time and + /// instead resort to fee-bumping. But that would be unfair for the + /// party that executes the fee-bumping. + /// + /// TODO: Calculate reasonable fee given the fact that the + /// transaction consists of 1 input and 2 outputs. + const REFUND_TX_FEE: u64 = 10_000; + #[test] - fn it_works() {} + fn run_cfd_protocol() { + let mut rng = ChaChaRng::seed_from_u64(0); + let secp = Secp256k1::new(); + + let maker_dlc_amount = Amount::ONE_BTC; + let taker_dlc_amount = Amount::ONE_BTC; + + let (_oracle_sk, oracle_pk) = make_keypair(&mut rng, &secp); + let (maker_sk, maker_pk) = make_keypair(&mut rng, &secp); + let (taker_sk, taker_pk) = make_keypair(&mut rng, &secp); + + let payouts = vec![ + Payout { + message: Message::Win, + maker_amount: Amount::from_btc(2.0).unwrap(), + taker_amount: Amount::ZERO, + }, + Payout { + message: Message::Lose, + maker_amount: Amount::ZERO, + taker_amount: Amount::from_btc(2.0).unwrap(), + }, + ]; + + // FIXME: Choose a value based on the network time or current + // block height + let refund_timelock = 0; + + let maker_wallet = build_wallet(Amount::from_btc(0.4).unwrap(), 5).unwrap(); + let taker_wallet = build_wallet(Amount::from_btc(0.4).unwrap(), 5).unwrap(); + + let maker_address = maker_wallet.get_address(AddressIndex::New).unwrap(); + let taker_address = taker_wallet.get_address(AddressIndex::New).unwrap(); + + // NOTE: We are probably paying too many transaction fees + let (maker_psbt, _) = { + let mut builder = maker_wallet.build_tx(); + builder + .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic) + .add_recipient(Script::new(), maker_dlc_amount.as_sat()); + builder.finish().unwrap() + }; + + let (taker_psbt, _) = { + let mut builder = taker_wallet.build_tx(); + builder + .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic) + .add_recipient(Script::new(), taker_dlc_amount.as_sat()); + builder.finish().unwrap() + }; + + let (lock_psbt, dlc_outpoint) = create_lock_psbt( + maker_psbt, + taker_psbt, + maker_pk, + taker_pk, + maker_dlc_amount + taker_dlc_amount, + ) + .unwrap(); + + let refund_tx = create_refund_tx( + dlc_outpoint, + refund_timelock, + &maker_address, + &taker_address, + maker_dlc_amount, + taker_dlc_amount, + ); + + let cets = payouts + .iter() + .map(|payout| create_cet(dlc_outpoint, payout, &maker_address, &taker_address)) + .collect::>(); + + dbg!(lock_psbt); + dbg!(refund_tx); + dbg!(cets); + + // TODO: Exchange signatures on refund transaction + // TODO: Exchange encsignatures on all CETs + // TODO: Exchange signatures on lock transaction + // TODO: Verify validity of all tranasactions using bitcoinconsensus via bdk + } + + #[derive(Debug, Clone)] + struct ContractExecutionTransaction { + inner: Transaction, + message: Message, + } + + fn create_cet( + dlc_outpoint: OutPoint, + payout: &Payout, + maker_address: &Address, + taker_address: &Address, + ) -> ContractExecutionTransaction { + let dlc_input = TxIn { + previous_output: dlc_outpoint, + ..Default::default() + }; + + let inner = Transaction { + version: 2, + lock_time: 0, + input: vec![dlc_input], + output: payout.to_txouts(maker_address, taker_address), + }; + + ContractExecutionTransaction { + inner, + message: payout.message, + } + } + + fn create_refund_tx( + dlc_outpoint: OutPoint, + lock_time: u32, + maker_address: &Address, + taker_address: &Address, + maker_amount: Amount, + taker_amount: Amount, + ) -> Transaction { + let dlc_input = TxIn { + previous_output: dlc_outpoint, + ..Default::default() + }; + + let per_party_fee = REFUND_TX_FEE / 2; + + let maker_output = TxOut { + value: maker_amount.as_sat() - per_party_fee, + script_pubkey: maker_address.script_pubkey(), + }; + + let taker_output = TxOut { + value: taker_amount.as_sat() - per_party_fee, + script_pubkey: taker_address.script_pubkey(), + }; + + Transaction { + version: 2, + lock_time, + input: vec![dlc_input], + output: vec![maker_output, taker_output], + } + } + + fn dlc_outpoint(lock_tx: &Transaction, dlc_descriptor: &Descriptor) -> OutPoint { + let txid = lock_tx.txid(); + let vout = lock_tx + .output + .iter() + .position(|out| out.script_pubkey == dlc_descriptor.script_pubkey()) + .expect("to find dlc output in lock tx"); + + OutPoint { + txid, + vout: vout as u32, + } + } + + fn create_lock_psbt( + maker_psbt: PartiallySignedTransaction, + taker_psbt: PartiallySignedTransaction, + maker_pk: PublicKey, + taker_pk: PublicKey, + dlc_amount: Amount, + ) -> Result<(PartiallySignedTransaction, OutPoint)> { + let dlc_descriptor = build_dlc_descriptor(maker_pk, taker_pk); + + let maker_change = maker_psbt + .global + .unsigned_tx + .output + .into_iter() + .filter(|out| !out.script_pubkey.is_empty()) + .collect::>(); + + let taker_change = taker_psbt + .global + .unsigned_tx + .output + .into_iter() + .filter(|out| !out.script_pubkey.is_empty()) + .collect(); + + let dlc_output = TxOut { + value: dlc_amount.as_sat(), + script_pubkey: dlc_descriptor + .address(Network::Regtest) + .expect("can derive address from descriptor") + .script_pubkey(), + }; + + let lock_tx = Transaction { + version: 2, + lock_time: 0, + input: vec![ + maker_psbt.global.unsigned_tx.input, + taker_psbt.global.unsigned_tx.input, + ] + .concat(), + output: vec![vec![dlc_output], maker_change, taker_change].concat(), + }; + + let dlc_outpoint = dlc_outpoint(&lock_tx, &dlc_descriptor); + let lock_psbt = PartiallySignedTransaction::from_unsigned_tx(lock_tx)?; + + Ok((lock_psbt, dlc_outpoint)) + } + + fn build_wallet( + utxo_amount: Amount, + num_utxos: u8, + ) -> Result> { + use bdk::populate_test_db; + use bdk::testutils; + + // FIXME: Using the same key for every instance of the wallet will lead to bugs in the tests + let key = "tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m".parse::().unwrap(); + + let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", key))); + + let mut database = bdk::database::MemoryDatabase::new(); + + for index in 0..num_utxos { + populate_test_db!( + &mut database, + testutils! { + @tx ( (@external descriptors, index as u32) => utxo_amount.as_sat() ) (@confirmations 1) + }, + Some(100) + ); + } + + let wallet = bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database)?; + + Ok(wallet) + } + + // NOTE: This is a simplification. Our use-case will not work with + // a simple enumeration of possible messages + #[derive(Debug, Clone, Copy)] + struct Payout { + message: Message, + maker_amount: Amount, + taker_amount: Amount, + } + + impl Payout { + fn to_txouts(self, maker_address: &Address, taker_address: &Address) -> Vec { + [ + (self.maker_amount, maker_address), + (self.taker_amount, taker_address), + ] + .iter() + .filter_map(|(amount, address)| { + (amount == &Amount::ZERO).then(|| TxOut { + value: amount.as_sat(), + script_pubkey: address.script_pubkey(), + }) + }) + .collect() + } + } + + #[derive(Debug, Clone, Copy)] + enum Message { + Win, + Lose, + } + + pub fn build_dlc_descriptor(maker_pk: PublicKey, taker_pk: PublicKey) -> Descriptor { + const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; + + // NOTE: This shouldn't be a source of error, but maybe it is + let maker_pk = ToHex::to_hex(&maker_pk.key); + let taker_pk = ToHex::to_hex(&taker_pk.key); + + let miniscript = MINISCRIPT_TEMPLATE + .replace("A", &maker_pk) + .replace("B", &taker_pk); + + let miniscript = miniscript.parse().expect("a valid miniscript"); + + Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor")) + } + + fn make_keypair(rng: &mut R, secp: &Secp256k1) -> (SecretKey, PublicKey) + where + R: RngCore + CryptoRng, + { + let sk = SecretKey::new(rng); + let pk = PublicKey::from_private_key( + secp, + &PrivateKey { + compressed: true, + network: Network::Regtest, + key: sk, + }, + ); + + (sk, pk) + } }