diff --git a/cfd_protocol/Cargo.toml b/cfd_protocol/Cargo.toml index a8c604d..ca8d394 100644 --- a/cfd_protocol/Cargo.toml +++ b/cfd_protocol/Cargo.toml @@ -9,6 +9,7 @@ anyhow = "1" bdk = { git = "https://github.com/coblox/bdk", rev = "acf157a99a305226203d2b55a567291a93c64720" } rand = "0.6" rand_chacha = "0.1" +secp256k1-zkp = { version = "0.4", features = ["hashes", "global-context"] } [dev-dependencies] diff --git a/cfd_protocol/src/lib.rs b/cfd_protocol/src/lib.rs index 0747bcd..1a6bdf9 100644 --- a/cfd_protocol/src/lib.rs +++ b/cfd_protocol/src/lib.rs @@ -1,20 +1,28 @@ #[cfg(test)] mod tests { use anyhow::Result; + use bdk::bitcoin::Txid; + use bdk::SignOptions; use bdk::{ bitcoin::{ hashes::hex::ToHex, - secp256k1::{All, Secp256k1, SecretKey}, + util::bip143::SigHashCache, util::{bip32::ExtendedPrivKey, psbt::PartiallySignedTransaction}, - Address, Amount, Network, OutPoint, PrivateKey, PublicKey, Script, Transaction, TxIn, - TxOut, + Address, Amount, Network, OutPoint, PrivateKey, PublicKey, Script, SigHash, + SigHashType, Transaction, TxIn, TxOut, }, descriptor::Descriptor, miniscript::{descriptor::Wsh, DescriptorTrait}, wallet::AddressIndex, }; - use rand::{CryptoRng, RngCore, SeedableRng}; + use bitcoin::util::psbt::Global; + use rand::RngCore; + use rand::{CryptoRng, SeedableRng}; use rand_chacha::ChaChaRng; + use secp256k1_zkp::{self, schnorrsig}; + use secp256k1_zkp::{bitcoin_hashes::sha256t_hash_newtype, SECP256K1}; + use secp256k1_zkp::{bitcoin_hashes::*, EcdsaAdaptorSignature}; + use secp256k1_zkp::{Secp256k1, SecretKey}; /// Refund transaction fee. It is paid evenly by the maker and the /// taker. @@ -27,6 +35,9 @@ mod tests { /// transaction consists of 1 input and 2 outputs. const REFUND_TX_FEE: u64 = 10_000; + /// In satoshi per vbyte. + const MIN_RELAY_FEE: u64 = 1; + #[test] fn run_cfd_protocol() { let mut rng = ChaChaRng::seed_from_u64(0); @@ -35,9 +46,11 @@ mod tests { 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 oracle = Oracle::new(&mut rng); + let (_event, announcement) = announce(&mut rng); + + let (maker_sk, maker_pk) = make_keypair(&mut rng); + let (taker_sk, taker_pk) = make_keypair(&mut rng); let payouts = vec![ Payout { @@ -56,8 +69,8 @@ mod tests { // 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_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 maker_address = maker_wallet.get_address(AddressIndex::New).unwrap(); let taker_address = taker_wallet.get_address(AddressIndex::New).unwrap(); @@ -79,7 +92,7 @@ mod tests { builder.finish().unwrap() }; - let (lock_psbt, dlc_outpoint) = create_lock_psbt( + let lock_tx = LockTransaction::new( maker_psbt, taker_psbt, maker_pk, @@ -88,8 +101,8 @@ mod tests { ) .unwrap(); - let refund_tx = create_refund_tx( - dlc_outpoint, + let refund_tx = RefundTransaction::new( + &lock_tx, refund_timelock, &maker_address, &taker_address, @@ -99,156 +112,339 @@ mod tests { let cets = payouts .iter() - .map(|payout| create_cet(dlc_outpoint, payout, &maker_address, &taker_address)) + .map(|payout| { + ContractExecutionTransaction::new(&lock_tx, payout, &maker_address, &taker_address) + }) + .collect::>(); + + let maker_refund_sig = secp.sign( + &secp256k1_zkp::Message::from_slice(&refund_tx.sighash()).unwrap(), + &maker_sk, + ); + let taker_refund_sig = secp.sign( + &secp256k1_zkp::Message::from_slice(&refund_tx.sighash()).unwrap(), + &taker_sk, + ); + + let maker_cet_encsigs = cets + .iter() + .map(|cet| { + ( + cet.txid(), + cet.encsign(maker_sk, &oracle.public_key(), &announcement.nonce_pk()), + ) + }) + .collect::>(); + let taker_cet_encsigs = cets + .iter() + .map(|cet| { + ( + cet.txid(), + cet.encsign(taker_sk, &oracle.public_key(), &announcement.nonce_pk()), + ) + }) .collect::>(); - dbg!(lock_psbt); - dbg!(refund_tx); - dbg!(cets); + let mut signed_lock_tx = lock_tx.to_psbt(); + maker_wallet + .sign( + &mut signed_lock_tx, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + ) + .unwrap(); + + maker_wallet + .sign( + &mut signed_lock_tx, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + ) + .unwrap(); - // 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 + // TODO: Verify signatures as we go + // TODO: Verify validity of all transactions using bitcoinconsensus via bdk + // TODO: Upstream -> Activate rust-bitcoin/bitcoinconsensus feature on bdk + } + + const BIP340_MIDSTATE: [u8; 32] = [ + 0x9c, 0xec, 0xba, 0x11, 0x23, 0x92, 0x53, 0x81, 0x11, 0x67, 0x91, 0x12, 0xd1, 0x62, 0x7e, + 0x0f, 0x97, 0xc8, 0x75, 0x50, 0x00, 0x3c, 0xc7, 0x65, 0x90, 0xf6, 0x11, 0x64, 0x33, 0xe9, + 0xb6, 0x6a, + ]; + + sha256t_hash_newtype!( + BIP340Hash, + BIP340HashTag, + BIP340_MIDSTATE, + 64, + doc = "bip340 hash", + true + ); + + /// Compute a signature point for the given oracle public key, announcement nonce public key and message. + fn compute_signature_point( + oracle_pk: &schnorrsig::PublicKey, + nonce_pk: &schnorrsig::PublicKey, + message: Message, + ) -> Result { + fn schnorr_pubkey_to_pubkey( + pk: &schnorrsig::PublicKey, + ) -> Result { + let mut buf = Vec::::with_capacity(33); + buf.push(0x02); + buf.extend(&pk.serialize()); + Ok(secp256k1_zkp::PublicKey::from_slice(&buf)?) + } + + let hash = { + let mut buf = Vec::::new(); + buf.extend(&nonce_pk.serialize()); + buf.extend(&oracle_pk.serialize()); + buf.extend( + secp256k1_zkp::Message::from(message) + .as_ref() + .iter() + .cloned() + .collect::>(), + ); + BIP340Hash::hash(&buf).into_inner().to_vec() + }; + let mut oracle_pk = schnorr_pubkey_to_pubkey(oracle_pk)?; + oracle_pk.mul_assign(SECP256K1, &hash)?; + let nonce_pk = schnorr_pubkey_to_pubkey(nonce_pk)?; + Ok(nonce_pk.combine(&oracle_pk)?) } #[derive(Debug, Clone)] struct ContractExecutionTransaction { inner: Transaction, message: Message, + sighash: SigHash, } - 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() - }; + impl ContractExecutionTransaction { + fn new( + lock_tx: &LockTransaction, + payout: &Payout, + maker_address: &Address, + taker_address: &Address, + ) -> Self { + let dlc_input = TxIn { + previous_output: lock_tx.dlc_outpoint(), + ..Default::default() + }; + + let fee = Self::calculate_vbytes() * MIN_RELAY_FEE; + + let tx = Transaction { + version: 2, + lock_time: 0, + input: vec![dlc_input], + output: payout.to_txouts(maker_address, taker_address, fee), + }; + + let sighash = SigHashCache::new(&tx).signature_hash( + 0, + &lock_tx.dlc_descriptor.script_code(), + lock_tx.dlc_amount.as_sat(), + SigHashType::All, + ); - let inner = Transaction { - version: 2, - lock_time: 0, - input: vec![dlc_input], - output: payout.to_txouts(maker_address, taker_address), - }; + Self { + inner: tx, + message: payout.message, + sighash, + } + } - ContractExecutionTransaction { - inner, - message: payout.message, + fn encsign( + &self, + sk: SecretKey, + oracle_pk: &schnorrsig::PublicKey, + nonce_pk: &schnorrsig::PublicKey, + ) -> Result { + let signature_point = compute_signature_point(oracle_pk, nonce_pk, self.message)?; + + Ok(EcdsaAdaptorSignature::encrypt( + SECP256K1, + &secp256k1_zkp::Message::from_slice(&self.sighash)?, + &sk, + &signature_point, + )) } - } - 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() - }; + fn txid(&self) -> Txid { + self.inner.txid() + } - let per_party_fee = REFUND_TX_FEE / 2; + fn calculate_vbytes() -> u64 { + // TODO: Do it properly + 100 + } + } - let maker_output = TxOut { - value: maker_amount.as_sat() - per_party_fee, - script_pubkey: maker_address.script_pubkey(), - }; + #[derive(Debug, Clone)] + struct RefundTransaction { + inner: Transaction, + sighash: SigHash, + } - let taker_output = TxOut { - value: taker_amount.as_sat() - per_party_fee, - script_pubkey: taker_address.script_pubkey(), - }; + impl RefundTransaction { + fn new( + lock_tx: &LockTransaction, + lock_time: u32, + maker_address: &Address, + taker_address: &Address, + maker_amount: Amount, + taker_amount: Amount, + ) -> Self { + let dlc_input = TxIn { + previous_output: lock_tx.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(), + }; + + let tx = Transaction { + version: 2, + lock_time, + input: vec![dlc_input], + output: vec![maker_output, taker_output], + }; + + let sighash = SigHashCache::new(&tx).signature_hash( + 0, + &lock_tx.dlc_descriptor.script_code(), + lock_tx.dlc_amount.as_sat(), + SigHashType::All, + ); - Transaction { - version: 2, - lock_time, - input: vec![dlc_input], - output: vec![maker_output, taker_output], + Self { inner: tx, sighash } } - } - - 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 sighash(&self) -> SigHash { + self.sighash } } - fn create_lock_psbt( - maker_psbt: PartiallySignedTransaction, - taker_psbt: PartiallySignedTransaction, - maker_pk: PublicKey, - taker_pk: PublicKey, + #[derive(Debug, Clone)] + struct LockTransaction { + inner: PartiallySignedTransaction, + dlc_descriptor: Descriptor, 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(), - }; + impl LockTransaction { + fn new( + maker_psbt: PartiallySignedTransaction, + taker_psbt: PartiallySignedTransaction, + maker_pk: PublicKey, + taker_pk: PublicKey, + dlc_amount: Amount, + ) -> Result { + 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 inner = PartiallySignedTransaction { + global: Global::from_unsigned_tx(lock_tx)?, + inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(), + outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(), + }; + + Ok(Self { + inner, + dlc_descriptor, + dlc_amount, + }) + } - let dlc_outpoint = dlc_outpoint(&lock_tx, &dlc_descriptor); - let lock_psbt = PartiallySignedTransaction::from_unsigned_tx(lock_tx)?; + fn dlc_outpoint(&self) -> OutPoint { + let txid = self.inner.global.unsigned_tx.txid(); + let vout = self + .inner + .global + .unsigned_tx + .output + .iter() + .position(|out| out.script_pubkey == self.dlc_descriptor.script_pubkey()) + .expect("to find dlc output in lock tx"); + + OutPoint { + txid, + vout: vout as u32, + } + } - Ok((lock_psbt, dlc_outpoint)) + fn to_psbt(&self) -> PartiallySignedTransaction { + self.inner.clone() + } } - fn build_wallet( + fn build_wallet( + rng: &mut R, utxo_amount: Amount, num_utxos: u8, - ) -> Result> { + ) -> Result> + where + R: RngCore + CryptoRng, + { 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 mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + let key = ExtendedPrivKey::new_master(Network::Regtest, &seed)?; let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", key))); let mut database = bdk::database::MemoryDatabase::new(); @@ -278,19 +474,34 @@ mod tests { } impl Payout { - fn to_txouts(self, maker_address: &Address, taker_address: &Address) -> Vec { - [ + fn to_txouts( + self, + maker_address: &Address, + taker_address: &Address, + fee: u64, + ) -> Vec { + let mut txouts = [ (self.maker_amount, maker_address), (self.taker_amount, taker_address), ] .iter() .filter_map(|(amount, address)| { - (amount == &Amount::ZERO).then(|| TxOut { + (amount != &Amount::ZERO).then(|| TxOut { value: amount.as_sat(), script_pubkey: address.script_pubkey(), }) }) - .collect() + .collect::>(); + + // TODO: Rewrite this + if txouts.len() == 1 { + txouts[0].value -= fee; + } else if txouts.len() == 2 { + txouts[0].value -= fee / 2; + txouts[1].value -= fee / 2; + } + + txouts } } @@ -300,7 +511,105 @@ mod tests { Lose, } - pub fn build_dlc_descriptor(maker_pk: PublicKey, taker_pk: PublicKey) -> Descriptor { + impl From for secp256k1_zkp::Message { + fn from(msg: Message) -> Self { + // TODO: Tag hash with prefix and other public data + secp256k1_zkp::Message::from_hashed_data::( + msg.to_string().as_bytes(), + ) + } + } + + impl ToString for Message { + fn to_string(&self) -> String { + match self { + Message::Win => "win".to_string(), + Message::Lose => "lose".to_string(), + } + } + } + + struct Oracle { + key_pair: schnorrsig::KeyPair, + } + + impl Oracle { + fn new(rng: &mut R) -> Self + where + R: RngCore + CryptoRng, + { + let key_pair = schnorrsig::KeyPair::new(SECP256K1, rng); + + Self { key_pair } + } + + fn public_key(&self) -> schnorrsig::PublicKey { + schnorrsig::PublicKey::from_keypair(SECP256K1, &self.key_pair) + } + + fn attest(&self, event: Event, msg: Message) -> schnorrsig::Signature { + secp_utils::schnorr_sign_with_nonce(&msg.into(), &self.key_pair, &event.nonce) + } + } + + fn announce(rng: &mut R) -> (Event, Announcement) + where + R: RngCore + CryptoRng, + { + let event = Event::new(rng); + let announcement = event.announcement(); + + (event, announcement) + } + + /// Represents the oracle's commitment to a nonce that will be used to + /// sign a specific event in the future. + struct Event { + /// Nonce. + /// + /// Must remain secret. + nonce: SecretKey, + nonce_pk: schnorrsig::PublicKey, + } + + impl Event { + fn new(rng: &mut R) -> Self + where + R: RngCore + CryptoRng, + { + let nonce = SecretKey::new(rng); + + let key_pair = schnorrsig::KeyPair::from_secret_key(SECP256K1, nonce); + let nonce_pk = schnorrsig::PublicKey::from_keypair(SECP256K1, &key_pair); + + Self { nonce, nonce_pk } + } + + fn announcement(&self) -> Announcement { + Announcement { + nonce_pk: self.nonce_pk, + } + } + } + + /// Public message which can be used by anyone to perform a DLC + /// protocol based on a specific event. + /// + /// These would normally include more information to identify the + /// specific event, but we omit this for simplicity. See: + /// https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#oracle-events + #[derive(Clone, Copy)] + struct Announcement { + nonce_pk: schnorrsig::PublicKey, + } + + impl Announcement { + fn nonce_pk(&self) -> schnorrsig::PublicKey { + self.nonce_pk + } + } + + 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 @@ -316,13 +625,13 @@ mod tests { Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor")) } - fn make_keypair(rng: &mut R, secp: &Secp256k1) -> (SecretKey, PublicKey) + fn make_keypair(rng: &mut R) -> (SecretKey, PublicKey) where R: RngCore + CryptoRng, { let sk = SecretKey::new(rng); let pk = PublicKey::from_private_key( - secp, + SECP256K1, &PrivateKey { compressed: true, network: Network::Regtest, @@ -332,4 +641,52 @@ mod tests { (sk, pk) } + + mod secp_utils { + use super::*; + + use secp256k1_zkp::secp256k1_zkp_sys::types::c_void; + use secp256k1_zkp::secp256k1_zkp_sys::CPtr; + use std::os::raw::c_int; + use std::os::raw::c_uchar; + use std::ptr; + + /// Create a Schnorr signature using the provided nonce instead of generating one. + pub fn schnorr_sign_with_nonce( + msg: &secp256k1_zkp::Message, + keypair: &schnorrsig::KeyPair, + nonce: &SecretKey, + ) -> schnorrsig::Signature { + unsafe { + let mut sig = [0u8; secp256k1_zkp::constants::SCHNORRSIG_SIGNATURE_SIZE]; + assert_eq!( + 1, + secp256k1_zkp::ffi::secp256k1_schnorrsig_sign( + *SECP256K1.ctx(), + sig.as_mut_c_ptr(), + msg.as_c_ptr(), + keypair.as_ptr(), + Some(constant_nonce_fn), + nonce.as_c_ptr() as *const c_void + ) + ); + + schnorrsig::Signature::from_slice(&sig).unwrap() + } + } + + extern "C" fn constant_nonce_fn( + nonce32: *mut c_uchar, + _msg32: *const c_uchar, + _key32: *const c_uchar, + _xonly_pk32: *const c_uchar, + _algo16: *const c_uchar, + data: *mut c_void, + ) -> c_int { + unsafe { + ptr::copy_nonoverlapping(data as *const c_uchar, nonce32, 32); + } + 1 + } + } }