diff --git a/src/bin/teleport.rs b/src/bin/teleport.rs index db778a7f..639f6c23 100644 --- a/src/bin/teleport.rs +++ b/src/bin/teleport.rs @@ -9,7 +9,7 @@ use structopt::StructOpt; use teleport::{ error::TeleportError, - maker::server::MakerBehavior, + maker::MakerBehavior, scripts::{ maker::run_maker, market::download_and_display_offers, @@ -180,7 +180,6 @@ fn main() -> Result<(), TeleportError> { port.unwrap_or(6102), Some(WalletMode::Testing), maker_special_behavior, - None, )?; } Subcommand::GetFidelityBondAddress { year_and_month } => { diff --git a/src/error.rs b/src/error.rs index 1a106854..5dd5bed6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@ use std::{error, io}; -use crate::{market::directory::DirectoryServerError, wallet::WalletError}; +use crate::{ + maker::error::MakerError, market::directory::DirectoryServerError, + protocol::error::ContractError, wallet::WalletError, +}; // error enum for the whole project // try to make functions return this @@ -13,6 +16,9 @@ pub enum TeleportError { Socks(tokio_socks::Error), Wallet(WalletError), Market(DirectoryServerError), + Json(serde_json::Error), + Maker(MakerError), + Contract(ContractError), } impl From> for TeleportError { @@ -50,3 +56,21 @@ impl From for TeleportError { Self::Market(value) } } + +impl From for TeleportError { + fn from(value: serde_json::Error) -> Self { + Self::Json(value) + } +} + +impl From for TeleportError { + fn from(value: MakerError) -> Self { + Self::Maker(value) + } +} + +impl From for TeleportError { + fn from(value: ContractError) -> Self { + Self::Contract(value) + } +} diff --git a/src/maker/config.rs b/src/maker/config.rs new file mode 100644 index 00000000..32298dfd --- /dev/null +++ b/src/maker/config.rs @@ -0,0 +1,38 @@ +#[derive(Debug, Clone)] +pub struct MakerConfig { + pub port: u16, + pub heart_beat_interval_secs: u64, + pub rpc_ping_interval_secs: u64, + pub watchtower_ping_interval_secs: u64, + pub directory_servers_refresh_interval_secs: u64, + pub idle_connection_timeout: u64, + pub onion_addrs: String, + pub absolute_fee_sats: u64, + pub amount_relative_fee_ppb: u64, + pub time_relative_fee_ppb: u64, + pub required_confirms: u64, + // Minimum reaction time between contract transaction of two hops + pub min_contract_reaction_time: u16, + pub min_size: u64, +} + +impl MakerConfig { + /// Init a default configuration with given port and address + pub fn init(port: u16, onion_addrs: String) -> Self { + Self { + port, + heart_beat_interval_secs: 3, + rpc_ping_interval_secs: 60, + watchtower_ping_interval_secs: 300, + directory_servers_refresh_interval_secs: 60 * 60 * 12, //12 Hours + idle_connection_timeout: 300, + onion_addrs, + absolute_fee_sats: 1000, + amount_relative_fee_ppb: 10_000_000, + time_relative_fee_ppb: 100_000, + required_confirms: 1, + min_contract_reaction_time: 48, + min_size: 10_000, + } + } +} diff --git a/src/maker/error.rs b/src/maker/error.rs new file mode 100644 index 00000000..ec580af8 --- /dev/null +++ b/src/maker/error.rs @@ -0,0 +1,62 @@ +use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard}; + +use bitcoin::secp256k1; + +use crate::{ + protocol::error::ContractError, + wallet::{Wallet, WalletError}, +}; + +#[derive(Debug)] +pub enum MakerError { + IO(std::io::Error), + Json(serde_json::Error), + UnexpectedMessage { expected: String, got: String }, + General(&'static str), + MutexWallet, + Secp(secp256k1::Error), + ContractError(ContractError), + Wallet(WalletError), +} + +impl From for MakerError { + fn from(value: std::io::Error) -> Self { + Self::IO(value) + } +} + +impl From for MakerError { + fn from(value: serde_json::Error) -> Self { + Self::Json(value) + } +} + +impl<'a> From>> for MakerError { + fn from(_: PoisonError>) -> Self { + Self::MutexWallet + } +} + +impl<'a> From>> for MakerError { + fn from(_: PoisonError>) -> Self { + Self::MutexWallet + } +} + +impl From for MakerError { + fn from(value: secp256k1::Error) -> Self { + Self::Secp(value) + } +} + +impl From for MakerError { + fn from(value: ContractError) -> Self { + Self::ContractError(value) + } +} + +impl From for MakerError { + fn from(value: WalletError) -> Self { + Self::Wallet(value) + } +} diff --git a/src/maker/handlers.rs b/src/maker/handlers.rs new file mode 100644 index 00000000..ac861305 --- /dev/null +++ b/src/maker/handlers.rs @@ -0,0 +1,623 @@ +use bitcoin::{ + hashes::Hash, + secp256k1::{self, Secp256k1, Signature}, + Amount, OutPoint, PublicKey, Transaction, Txid, +}; +use bitcoincore_rpc::RpcApi; + +use crate::protocol::{ + messages::{MultisigPrivkey, PrivKeyHandover}, + Hash160, +}; + +use crate::{ + maker::maker::ExpectedMessage, + protocol::{ + contract::{ + calculate_coinswap_fee, create_receivers_contract_tx, find_funding_output_index, + read_contract_locktime, read_hashvalue_from_contract, + read_pubkeys_from_multisig_redeemscript, FUNDING_TX_VBYTE_SIZE, + }, + messages::{ + ContractSigsAsRecvrAndSender, ContractSigsForRecvr, ContractSigsForRecvrAndSender, + ContractSigsForSender, HashPreimage, MakerToTakerMessage, Offer, ProofOfFunding, + ReqContractSigsForRecvr, ReqContractSigsForSender, SenderContractTxInfo, + TakerToMakerMessage, + }, + }, + wallet::{IncomingSwapCoin, SwapCoin, WalletSwapCoin}, + watchtower::{ + client::register_coinswap_with_watchtowers, + routines::{ContractTransaction, ContractsInfo}, + }, +}; + +use super::{ + error::MakerError, + maker::{Maker, MakerBehavior}, +}; + +impl Maker { + pub async fn handle_message( + &mut self, + message: &TakerToMakerMessage, + ) -> Result, MakerError> { + let connection_state = self + .connection_states + .last_mut() + .expect("A connection state should exist for each handle message call"); + log::debug!("{:#?}", message); + let outgoing_message = match connection_state.allowed_message { + ExpectedMessage::TakerHello => { + if let TakerToMakerMessage::TakerHello(_) = message { + connection_state.allowed_message = ExpectedMessage::NewlyConnectedTaker; + None + } else { + return Err(MakerError::UnexpectedMessage { + expected: "TakerHello".to_string(), + got: format!("{}", message), + }); + } + } + ExpectedMessage::NewlyConnectedTaker => match message { + TakerToMakerMessage::ReqGiveOffer(_) => { + let (tweakable_point, max_size) = { + let wallet_reader = self.wallet.read().await; + let max_size = wallet_reader.store.offer_maxsize; + let tweakable_point = wallet_reader.get_tweakable_keypair().1; + (tweakable_point, max_size) + }; + connection_state.allowed_message = ExpectedMessage::ReqContractSigsForSender; + Some(MakerToTakerMessage::RespOffer(Offer { + absolute_fee_sat: self.config.absolute_fee_sats, + amount_relative_fee_ppb: self.config.amount_relative_fee_ppb, + time_relative_fee_ppb: self.config.time_relative_fee_ppb, + required_confirms: self.config.required_confirms, + minimum_locktime: self.config.min_contract_reaction_time, + max_size, + min_size: self.config.min_size, + tweakable_point, + })) + } + TakerToMakerMessage::ReqContractSigsForSender(message) => { + connection_state.allowed_message = ExpectedMessage::ProofOfFunding; + Some(self.handle_req_contract_sigs_for_sender(&message).await?) + } + TakerToMakerMessage::RespProofOfFunding(proof) => { + connection_state.allowed_message = + ExpectedMessage::ProofOfFundingORContractSigsForRecvrAndSender; + Some(self.handle_proof_of_funding(&proof).await?) + } + TakerToMakerMessage::ReqContractSigsForRecvr(message) => { + connection_state.allowed_message = ExpectedMessage::HashPreimage; + Some(self.handle_sign_receivers_contract_tx(&message).await?) + } + TakerToMakerMessage::RespHashPreimage(message) => { + connection_state.allowed_message = ExpectedMessage::PrivateKeyHandover; + Some(self.handle_hash_preimage(&message).await?) + } + _ => { + return Err(MakerError::General( + "Unexpected Newly Connected Taker message", + )); + } + }, + ExpectedMessage::ReqContractSigsForSender => { + if let TakerToMakerMessage::ReqContractSigsForSender(message) = message { + connection_state.allowed_message = ExpectedMessage::ProofOfFunding; + Some(self.handle_req_contract_sigs_for_sender(&message).await?) + } else { + return Err(MakerError::UnexpectedMessage { + expected: "ReqContractSigsForSender".to_string(), + got: format!("{}", message), + }); + } + } + ExpectedMessage::ProofOfFunding => { + if let TakerToMakerMessage::RespProofOfFunding(proof) = message { + connection_state.allowed_message = + ExpectedMessage::ProofOfFundingORContractSigsForRecvrAndSender; + Some(self.handle_proof_of_funding(&proof).await?) + } else { + return Err(MakerError::UnexpectedMessage { + expected: "Proof OF Funding".to_string(), + got: format!("{}", message), + }); + } + } + ExpectedMessage::ProofOfFundingORContractSigsForRecvrAndSender => { + match message { + TakerToMakerMessage::RespProofOfFunding(proof) => { + connection_state.allowed_message = + ExpectedMessage::ProofOfFundingORContractSigsForRecvrAndSender; + Some(self.handle_proof_of_funding(&proof).await?) + } + TakerToMakerMessage::RespContractSigsForRecvrAndSender(message) => { + // Nothing to send. Maker now creates and broadcasts his funding Txs + connection_state.allowed_message = ExpectedMessage::ReqContractSigsForRecvr; + self.handle_senders_and_receivers_contract_sigs(&message) + .await?; + None + } + _ => { + return Err(MakerError::General( + "Expected proof of funding or sender's and reciever's contract signatures", + )); + } + } + } + ExpectedMessage::ReqContractSigsForRecvr => { + if let TakerToMakerMessage::ReqContractSigsForRecvr(message) = message { + connection_state.allowed_message = ExpectedMessage::HashPreimage; + Some(self.handle_sign_receivers_contract_tx(&message).await?) + } else { + return Err(MakerError::General( + "Expected reciever's contract transaction", + )); + } + } + ExpectedMessage::HashPreimage => { + if let TakerToMakerMessage::RespHashPreimage(message) = message { + connection_state.allowed_message = ExpectedMessage::PrivateKeyHandover; + Some(self.handle_hash_preimage(&message).await?) + } else { + return Err(MakerError::General("Expected hash preimgae")); + } + } + ExpectedMessage::PrivateKeyHandover => { + if let TakerToMakerMessage::RespPrivKeyHandover(message) = message { + // Nothing to send. Succesfully completed swap + self.handle_private_key_handover(&message).await?; + None + } else { + return Err(MakerError::General("expected privatekey handover")); + } + } + }; + + match outgoing_message { + Some(reply_message) => Ok(Some(reply_message)), + None => Ok(None), + } + } + + /// This is the first message handler for the Maker. It receives a [ReqContractSigsForSender] message, + /// checks the validity of contract transactions, and provide's the signature for the sender side. + /// This will fail if the maker doesn't have enough utxos to fund the next coinswap hop, or the contract + /// transaction isn't valid. + pub async fn handle_req_contract_sigs_for_sender( + &self, + message: &ReqContractSigsForSender, + ) -> Result { + if let MakerBehavior::CloseOnSignSendersContractTx = self.behavior { + return Err(MakerError::General( + "closing connection early due to special maker behavior", + )); + } + + // Verify and sign the contract transaction, check function definition for all the checks. + let sigs = self.verify_and_sign_contract_tx(&message).await?; + + let funding_txids = message + .txs_info + .iter() + .map(|txinfo| txinfo.senders_contract_tx.input[0].previous_output.txid) + .collect::>(); + + let total_funding_amount = message + .txs_info + .iter() + .fold(0u64, |acc, txinfo| acc + txinfo.funding_input_value); + + if total_funding_amount >= self.config.min_size + && total_funding_amount < self.wallet.read().await.store.offer_maxsize + { + log::info!( + "messageed contracts amount={}, for funding txids = {:?}", + Amount::from_sat(total_funding_amount), + funding_txids + ); + Ok(MakerToTakerMessage::RespContractSigsForSender( + ContractSigsForSender { sigs }, + )) + } else { + log::info!( + "rejecting contracts for amount={} because not enough funds", + Amount::from_sat(total_funding_amount) + ); + Err(MakerError::General("not enough funds")) + } + } + + /// Validates the [ProofOfFunding] message, initiate the next hop, + /// and create the [ReqContractSigsAsRecvrAndSender] message. + pub async fn handle_proof_of_funding( + &mut self, + message: &ProofOfFunding, + ) -> Result { + // Basic verification of ProofOfFunding Message. + // Check function definition for all the checks performed. + let hashvalue = self.verify_proof_of_funding(&message).await?; + log::debug!("proof of funding valid, creating own funding txes"); + + let connection_state = self + .connection_states + .last_mut() + .expect("Connection state should be the last state pushed"); + + let wallet_read = self.wallet.read().await; + // Import transactions and addresses into Bitcoin core's wallet. + // Add IncomingSwapcoin to Maker's Wallet + for funding_info in &message.confirmed_funding_txes { + let (pubkey1, pubkey2) = + read_pubkeys_from_multisig_redeemscript(&funding_info.multisig_redeemscript)?; + + let funding_output_index = find_funding_output_index(&funding_info)?; + let funding_output = funding_info + .funding_tx + .output + .iter() + .nth(funding_output_index as usize) + .expect("funding output expected at this index"); + + wallet_read.import_wallet_multisig_redeemscript(&pubkey1, &pubkey2)?; + wallet_read.import_tx_with_merkleproof( + &funding_info.funding_tx, + &funding_info.funding_tx_merkleproof, + )?; + wallet_read.import_wallet_contract_redeemscript(&funding_info.contract_redeemscript)?; + + let receiver_contract_tx = create_receivers_contract_tx( + OutPoint { + txid: funding_info.funding_tx.txid(), + vout: funding_output_index as u32, + }, + funding_output.value, + &funding_info.contract_redeemscript, + ); + + //let (coin_privkey, coin_other_pubkey, hashlock_privkey) = incoming_swapcoin_keys; + let (tweakable_privkey, _) = wallet_read.get_tweakable_keypair(); + let mut multisig_privkey = tweakable_privkey; + multisig_privkey.add_assign(funding_info.multisig_nonce.as_ref())?; + + let multisig_pubkey = PublicKey { + compressed: true, + key: secp256k1::PublicKey::from_secret_key(&Secp256k1::new(), &multisig_privkey), + }; + + let other_pubkey = if multisig_pubkey == pubkey1 { + pubkey2 + } else { + pubkey1 + }; + + let mut hashlock_privkey = tweakable_privkey; + hashlock_privkey.add_assign(funding_info.hashlock_nonce.as_ref())?; + + log::debug!( + "Adding incoming_swapcoin contract_tx = {:?} fo = {:?}", + receiver_contract_tx.clone(), + funding_output + ); + + connection_state + .incoming_swapcoins + .push(IncomingSwapCoin::new( + multisig_privkey, + other_pubkey, + receiver_contract_tx.clone(), + funding_info.contract_redeemscript.clone(), + hashlock_privkey, + funding_output.value, + )); + } + + // Calculate output amounts for the next hop + let incoming_amount = message.confirmed_funding_txes.iter().fold(0u64, |acc, fi| { + let index = find_funding_output_index(fi).unwrap(); + let txout = fi + .funding_tx + .output + .iter() + .nth(index as usize) + .expect("output at index expected"); + acc + txout.value + }); + + let coinswap_fees = calculate_coinswap_fee( + self.config.absolute_fee_sats, + self.config.amount_relative_fee_ppb, + self.config.time_relative_fee_ppb, + incoming_amount, + self.config.required_confirms, //time_in_blocks just 1 for now + ); + let miner_fees_paid_by_taker = FUNDING_TX_VBYTE_SIZE + * message.next_fee_rate + * (message.next_coinswap_info.len() as u64) + / 1000; + + let outgoing_amount = incoming_amount - coinswap_fees - miner_fees_paid_by_taker; + + // Create outgoing coinswap of the next hop + let (my_funding_txes, outgoing_swapcoins, total_miner_fee) = { + let mut wallet_write = self.wallet.write().await; + wallet_write.initalize_coinswap( + outgoing_amount, + &message + .next_coinswap_info + .iter() + .map(|next_hop| next_hop.next_multisig_pubkey) + .collect::>(), + &message + .next_coinswap_info + .iter() + .map(|next_hop| next_hop.next_hashlock_pubkey) + .collect::>(), + hashvalue, + message.next_locktime, + message.next_fee_rate, + )? + }; + + log::info!( + "Proof of funding valid. Incoming funding txes, txids = {:?}", + message + .confirmed_funding_txes + .iter() + .map(|cft| cft.funding_tx.txid()) + .collect::>() + ); + log::info!( + "incoming_amount={}, incoming_locktime={}, hashvalue={}", + Amount::from_sat(incoming_amount), + read_contract_locktime(&message.confirmed_funding_txes[0].contract_redeemscript) + .unwrap(), + //unwrap() as format of contract_redeemscript already checked in verify_proof_of_funding + hashvalue + ); + log::info!( + concat!( + "outgoing_amount={}, outgoing_locktime={}, miner fees paid by taker={}, ", + "actual miner fee={}, coinswap_fees={}, POTENTIALLY EARNED={}" + ), + Amount::from_sat(outgoing_amount), + message.next_locktime, + Amount::from_sat(miner_fees_paid_by_taker), + Amount::from_sat(total_miner_fee), + Amount::from_sat(coinswap_fees), + Amount::from_sat(incoming_amount - outgoing_amount - total_miner_fee) + ); + + connection_state.pending_funding_txes = my_funding_txes; + connection_state.outgoing_swapcoins = outgoing_swapcoins; + log::debug!( + "Incoming_swapcoins = {:#?}\nOutgoing_swapcoins = {:#?}", + connection_state.incoming_swapcoins, + connection_state.outgoing_swapcoins, + ); + + // Craft ReqContractSigsAsRecvrAndSender message to send to the Taker. + let receivers_contract_txs = connection_state + .incoming_swapcoins + .iter() + .map(|isc| isc.contract_tx.clone()) + .collect::>(); + + let senders_contract_txs_info = connection_state + .outgoing_swapcoins + .iter() + .map(|outgoing_swapcoin| SenderContractTxInfo { + contract_tx: outgoing_swapcoin.contract_tx.clone(), + timelock_pubkey: outgoing_swapcoin.get_timelock_pubkey(), + multisig_redeemscript: outgoing_swapcoin.get_multisig_redeemscript(), + funding_amount: outgoing_swapcoin.funding_amount, + }) + .collect::>(); + + Ok(MakerToTakerMessage::ReqContractSigsAsRecvrAndSender( + ContractSigsAsRecvrAndSender { + receivers_contract_txs, + senders_contract_txs_info, + }, + )) + } + + pub async fn handle_senders_and_receivers_contract_sigs( + &mut self, + message: &ContractSigsForRecvrAndSender, + ) -> Result<(), MakerError> { + let connection_state = self + .connection_states + .last_mut() + .expect("Connection state should be the last state pushed"); + if message.receivers_sigs.len() != connection_state.incoming_swapcoins.len() { + return Err(MakerError::General( + "invalid number of reciever's signatures", + )); + } + for (receivers_sig, incoming_swapcoin) in message + .receivers_sigs + .iter() + .zip(connection_state.incoming_swapcoins.iter_mut()) + { + if !incoming_swapcoin.verify_contract_tx_sig(receivers_sig) { + return Err(MakerError::General("invalid reciever's signature")); + } + incoming_swapcoin.others_contract_sig = Some(receivers_sig.clone()); + } + + if message.senders_sigs.len() != connection_state.outgoing_swapcoins.len() { + return Err(MakerError::General("invalid number of sender's signatures")); + } + + for (senders_sig, outgoing_swapcoin) in message + .senders_sigs + .iter() + .zip(connection_state.outgoing_swapcoins.iter_mut()) + { + if !outgoing_swapcoin.verify_contract_tx_sig(senders_sig) { + return Err(MakerError::General("invalid sender's signature")); + } + + outgoing_swapcoin.others_contract_sig = Some(senders_sig.clone()); + } + + let (wallet_label, internal_addresses) = { + let wallet_reader = self.wallet.read().await; + let wallet_label = wallet_reader.get_core_wallet_label(); + let internal_addresses = wallet_reader + .get_next_internal_addresses(connection_state.incoming_swapcoins.len() as u32)?; + + let mut my_funding_txids = Vec::::new(); + for my_funding_tx in &connection_state.pending_funding_txes { + log::debug!("Broadcasting My Funding Tx : {:#?}", my_funding_tx); + let txid = wallet_reader + .rpc + .send_raw_transaction(my_funding_tx) + .map_err(|e| MakerError::Wallet(e.into()))?; + assert_eq!(txid, my_funding_tx.txid()); + my_funding_txids.push(txid); + } + log::info!("Broadcasted My Funding Txes: {:?}", my_funding_txids); + + (wallet_label, internal_addresses) + }; + + register_coinswap_with_watchtowers(ContractsInfo { + contract_txes: connection_state + .incoming_swapcoins + .iter() + .zip(internal_addresses.iter()) + .map(|(isc, addr)| ContractTransaction { + tx: isc.get_fully_signed_contract_tx(), + redeemscript: isc.contract_redeemscript.clone(), + hashlock_spend_without_preimage: Some( + isc.create_hashlock_spend_without_preimage(addr), + ), + timelock_spend: None, + timelock_spend_broadcasted: false, + }) + .chain( + connection_state + .outgoing_swapcoins + .iter() + .zip(internal_addresses.iter()) + .map(|(osc, addr)| ContractTransaction { + tx: osc.get_fully_signed_contract_tx(), + redeemscript: osc.contract_redeemscript.clone(), + hashlock_spend_without_preimage: None, + timelock_spend: Some(osc.create_timelock_spend(addr)), + timelock_spend_broadcasted: false, + }), + ) + .collect::>(), + wallet_label, + }) + .await + .map_err(|_| MakerError::General("Watchtower registration failed"))?; + + { + let mut wallet_writer = self.wallet.write().await; + for (incoming_sc, outgoing_sc) in connection_state + .incoming_swapcoins + .iter() + .zip(connection_state.outgoing_swapcoins.iter()) + { + wallet_writer.add_incoming_swapcoin(incoming_sc); + wallet_writer.add_outgoing_swapcoin(outgoing_sc); + } + wallet_writer.save_to_disk()?; + } + + // Set the swapcoins info to empty as they have been saved to the wallet by this point. + connection_state.incoming_swapcoins = Vec::new(); + connection_state.outgoing_swapcoins = Vec::new(); + connection_state.pending_funding_txes = Vec::new(); + + Ok(()) + } + + pub async fn handle_sign_receivers_contract_tx( + &self, + message: &ReqContractSigsForRecvr, + ) -> Result { + let mut sigs = Vec::::new(); + for receivers_contract_tx_info in &message.txs { + sigs.push( + //the fact that the peer knows the correct multisig_redeemscript is what ensures + //security here, a random peer out there who isnt involved in a coinswap wont know + //what the multisig_redeemscript is + self.wallet + .read() + .await + .find_outgoing_swapcoin(&receivers_contract_tx_info.multisig_redeemscript) + .expect("Outgoing swapcoin not found") + .sign_contract_tx_with_my_privkey(&receivers_contract_tx_info.contract_tx)?, + ); + } + Ok(MakerToTakerMessage::RespContractSigsForRecvr( + ContractSigsForRecvr { sigs }, + )) + } + + pub async fn handle_hash_preimage( + &self, + message: &HashPreimage, + ) -> Result { + let hashvalue = Hash160::hash(&message.preimage); + + let mut wallet_write = self.wallet.write().await; + + for multisig_redeemscript in &message.senders_multisig_redeemscripts { + let incoming_swapcoin = wallet_write + .find_incoming_swapcoin_mut(&multisig_redeemscript) + .expect("Incoming swampcoin expected"); + if read_hashvalue_from_contract(&incoming_swapcoin.contract_redeemscript)? != hashvalue + { + return Err(MakerError::General("not correct hash preimage")); + } + incoming_swapcoin.hash_preimage = Some(message.preimage); + } + //TODO tell preimage to watchtowers + + log::info!("received preimage for hashvalue={}", hashvalue); + let mut swapcoin_private_keys = Vec::::new(); + + for multisig_redeemscript in &message.receivers_multisig_redeemscripts { + let outgoing_swapcoin = wallet_write + .find_outgoing_swapcoin(&multisig_redeemscript) + .expect("outgoing swapcoin expected"); + if read_hashvalue_from_contract(&outgoing_swapcoin.contract_redeemscript)? != hashvalue + { + return Err(MakerError::General("not correct hash preimage")); + } + + swapcoin_private_keys.push(MultisigPrivkey { + multisig_redeemscript: multisig_redeemscript.clone(), + key: outgoing_swapcoin.my_privkey, + }); + } + + wallet_write.save_to_disk()?; + Ok(MakerToTakerMessage::RespPrivKeyHandover(PrivKeyHandover { + multisig_privkeys: swapcoin_private_keys, + })) + } + + pub async fn handle_private_key_handover( + &self, + message: &PrivKeyHandover, + ) -> Result<(), MakerError> { + let mut wallet_write = self.wallet.write().await; + for swapcoin_private_key in &message.multisig_privkeys { + wallet_write + .find_incoming_swapcoin_mut(&swapcoin_private_key.multisig_redeemscript) + .expect("incoming swapcoin not found") + .apply_privkey(swapcoin_private_key.key)? + } + wallet_write.save_to_disk()?; + log::info!("Successfully Completed Coinswap"); + Ok(()) + } +} diff --git a/src/maker/maker.rs b/src/maker/maker.rs new file mode 100644 index 00000000..4995ff7b --- /dev/null +++ b/src/maker/maker.rs @@ -0,0 +1,248 @@ +use std::path::PathBuf; + +use bitcoin::{ + secp256k1::{self, Secp256k1, Signature}, + OutPoint, PublicKey, Transaction, +}; +use bitcoincore_rpc::RpcApi; +use tokio::sync::RwLock; + +use crate::{ + protocol::{contract::check_hashvalues_are_equal, messages::ReqContractSigsForSender, Hash160}, + wallet::{RPCConfig, WalletMode}, +}; + +use crate::{ + protocol::{ + contract::{ + check_hashlock_has_pubkey, check_multisig_has_pubkey, check_reedemscript_is_multisig, + find_funding_output_index, read_contract_locktime, redeemscript_to_scriptpubkey, + }, + messages::ProofOfFunding, + }, + wallet::{IncomingSwapCoin, OutgoingSwapCoin, Wallet, WalletError}, +}; + +use super::{config::MakerConfig, error::MakerError}; + +//used to configure the maker do weird things for testing +#[derive(Debug, Clone, Copy)] +pub enum MakerBehavior { + Normal, + CloseOnSignSendersContractTx, +} +// A structure denoting expectation of type of taker message. +// Used in the [ConnectionState] structure. +// +// If the recieved message doesn't match expected method, +// protocol error will be returned. +#[derive(Debug, Default)] +pub(super) enum ExpectedMessage { + #[default] + TakerHello, + NewlyConnectedTaker, + ReqContractSigsForSender, + ProofOfFunding, + ProofOfFundingORContractSigsForRecvrAndSender, + ReqContractSigsForRecvr, + HashPreimage, + PrivateKeyHandover, +} + +#[derive(Debug, Default)] +pub(super) struct ConnectionState { + pub(super) allowed_message: ExpectedMessage, + pub(super) incoming_swapcoins: Vec, + pub(super) outgoing_swapcoins: Vec, + pub(super) pending_funding_txes: Vec, +} + +pub struct Maker { + pub(super) behavior: MakerBehavior, + pub(super) config: MakerConfig, + pub(super) connection_states: Vec, + pub(super) wallet: RwLock, + pub(super) shutdown: RwLock, +} + +impl Maker { + pub fn init( + wallet_file_name: &PathBuf, + port: u16, + onion_addrs: String, + wallet_mode: Option, + behavior: MakerBehavior, + ) -> Result { + let rpc_config = RPCConfig::default(); + + let mut wallet = Wallet::load(&rpc_config, wallet_file_name, wallet_mode)?; + + wallet.sync()?; + + Ok(Self { + behavior, + config: MakerConfig::init(port, onion_addrs), + connection_states: Vec::new(), + wallet: RwLock::new(wallet), + shutdown: RwLock::new(false), + }) + } + + pub async fn shutdown(&mut self) { + *self.shutdown.write().await = true; + } + + /// Checks consistency of the [ProofOfFunding] message and return the Hashvalue + /// used in hashlock transaction. + pub async fn verify_proof_of_funding( + &self, + message: &ProofOfFunding, + ) -> Result { + if message.confirmed_funding_txes.len() == 0 { + return Err(MakerError::General("No funding txs provided by Taker")); + } + + let wallet_reader = self.wallet.read().await; + + for funding_info in &message.confirmed_funding_txes { + //check that the funding transaction pays to correct multisig + log::debug!( + "Proof of Funding: \ntx = {:#?}\nMultisig_Reedimscript = {:x}", + funding_info.funding_tx, + funding_info.multisig_redeemscript + ); + //let (funding_output_index, funding_output) = find_funding_output(funding_info)?; + + // check that the new locktime is sufficently short enough compared to the + // locktime in the provided funding tx + let locktime = read_contract_locktime(&funding_info.contract_redeemscript)?; + if locktime - message.next_locktime < self.config.min_contract_reaction_time { + return Err(MakerError::General( + "Next hop locktime too close to current hop locktime", + )); + } + + let funding_output_index = find_funding_output_index(funding_info)?; + + //check the funding_tx is confirmed confirmed to required depth + if let Some(txout) = wallet_reader + .rpc + .get_tx_out(&funding_info.funding_tx.txid(), funding_output_index, None) + .map_err(WalletError::Rpc)? + { + if txout.confirmations < self.config.required_confirms as u32 { + return Err(MakerError::General( + "funding tx not confirmed to required depth", + )); + } + } else { + return Err(MakerError::General("funding tx output doesnt exist")); + } + + check_reedemscript_is_multisig(&funding_info.multisig_redeemscript)?; + + let (_, tweabale_pubkey) = wallet_reader.get_tweakable_keypair(); + + check_multisig_has_pubkey( + &funding_info.multisig_redeemscript, + &tweabale_pubkey, + &funding_info.multisig_nonce, + )?; + + check_hashlock_has_pubkey( + &funding_info.contract_redeemscript, + &tweabale_pubkey, + &funding_info.hashlock_nonce, + )?; + + //check that the provided contract matches the scriptpubkey from the + //cache which was populated when the ReqContractSigsForSender message arrived + let contract_spk = redeemscript_to_scriptpubkey(&funding_info.contract_redeemscript); + + if !wallet_reader.does_prevout_match_cached_contract( + &OutPoint { + txid: funding_info.funding_tx.txid(), + vout: funding_output_index as u32, + }, + &contract_spk, + )? { + return Err(MakerError::General( + "provided contract does not match sender contract tx, rejecting", + )); + } + } + + Ok(check_hashvalues_are_equal(&message)?) + } + + /// Verify the contract transaction for Sender and return the signatures. + pub async fn verify_and_sign_contract_tx( + &self, + message: &ReqContractSigsForSender, + ) -> Result, MakerError> { + let mut wallet_writer = self.wallet.write().await; + let mut sigs = Vec::::new(); + for txinfo in &message.txs_info { + if txinfo.senders_contract_tx.input.len() != 1 + || txinfo.senders_contract_tx.output.len() != 1 + { + return Err(MakerError::General( + "invalid number of inputs or outputs in contract transaction", + )); + } + + if !wallet_writer.does_prevout_match_cached_contract( + &txinfo.senders_contract_tx.input[0].previous_output, + &txinfo.senders_contract_tx.output[0].script_pubkey, + )? { + return Err(MakerError::General( + "taker attempting multiple contract attack, rejecting", + )); + } + + let (tweakable_privkey, tweakable_pubkey) = wallet_writer.get_tweakable_keypair(); + + check_multisig_has_pubkey( + &txinfo.multisig_redeemscript, + &tweakable_pubkey, + &txinfo.multisig_nonce, + )?; + + let secp = Secp256k1::new(); + + let mut hashlock_privkey = tweakable_privkey; + hashlock_privkey.add_assign(txinfo.hashlock_nonce.as_ref())?; + + let hashlock_pubkey = PublicKey { + compressed: true, + key: secp256k1::PublicKey::from_secret_key(&secp, &hashlock_privkey), + }; + + crate::protocol::contract::is_contract_out_valid( + &txinfo.senders_contract_tx.output[0], + &hashlock_pubkey, + &txinfo.timelock_pubkey, + &message.hashvalue, + &message.locktime, + &self.config.min_contract_reaction_time, + )?; + + wallet_writer.cache_prevout_to_contract( + txinfo.senders_contract_tx.input[0].previous_output, + txinfo.senders_contract_tx.output[0].script_pubkey.clone(), + )?; + + let mut multisig_privkey = tweakable_privkey; + multisig_privkey.add_assign(txinfo.multisig_nonce.as_ref())?; + + let sig = crate::protocol::contract::sign_contract_tx( + &txinfo.senders_contract_tx, + &txinfo.multisig_redeemscript, + txinfo.funding_input_value, + &multisig_privkey, + )?; + sigs.push(sig); + } + Ok(sigs) + } +} diff --git a/src/maker/mod.rs b/src/maker/mod.rs index 74f47ad3..51a7cd1f 100644 --- a/src/maker/mod.rs +++ b/src/maker/mod.rs @@ -1 +1,245 @@ -pub mod server; +pub mod config; +pub mod error; +mod handlers; +pub mod maker; + +use std::{ + net::Ipv4Addr, + sync::Arc, + time::{Duration, Instant}, +}; + +use bitcoin::Network; +use bitcoincore_rpc::RpcApi; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + net::TcpListener, + select, + sync::{mpsc, RwLock}, + time::sleep, +}; + +pub use maker::{Maker, MakerBehavior}; + +use crate::{ + maker::maker::ConnectionState, + market::directory::post_maker_address_to_directory_servers, + protocol::messages::{MakerHello, MakerToTakerMessage, TakerToMakerMessage}, + utill::send_message, + wallet::WalletError, + watchtower::client::ping_watchtowers, +}; + +use self::error::MakerError; + +/// Start the Maker server loop +pub async fn start_maker_server(maker: Arc>) -> Result<(), MakerError> { + // Arc it so we can pass the reference across threads. + let maker_writer = maker.write().await; + + log::debug!( + "Running maker with special behavior = {:?}", + maker_writer.behavior + ); + let mut wallet_write = maker_writer.wallet.write().await; + wallet_write.refresh_offer_maxsize_cache()?; + + log::info!("Pinging watchtowers. . ."); + ping_watchtowers() + .await + .map_err(|_| MakerError::General("Watchtower connection failed"))?; + + if wallet_write.store.network != Network::Regtest { + if maker_writer.config.onion_addrs == "myhiddenserviceaddress.onion:6102" { + panic!("You must set config variable MAKER_ONION_ADDR in file src/maker_protocol.rs"); + } + log::info!( + "Adding my address ({}) to the directory servers. . .", + maker_writer.config.onion_addrs + ); + post_maker_address_to_directory_servers( + wallet_write.store.network, + &maker_writer.config.onion_addrs, + ) + .await + .expect("unable to add my address to the directory servers, is tor reachable?"); + } + + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, maker_writer.config.port)).await?; + log::info!("Listening On Port {}", maker_writer.config.port); + + let (server_loop_comms_tx, mut server_loop_comms_rx) = mpsc::channel::(100); + let mut accepting_clients = true; + let mut last_rpc_ping = Instant::now(); + let mut last_watchtowers_ping = Instant::now(); + let mut last_directory_servers_refresh = Instant::now(); + + //let my_kill_flag = self.kill_flag.clone(); + + //let config = self.config.clone(); + + // Loop to keep checking for new connections + loop { + let (mut socket, addr) = select! { + new_client = listener.accept() => new_client?, + client_err = server_loop_comms_rx.recv() => { + //unwrap the option here because we'll never close the mscp so it will always work + match client_err.as_ref().unwrap() { + MakerError::Wallet(WalletError::Rpc(e)) => { + //doublecheck the rpc connection here because sometimes the rpc error + //will be unrelated to the connection itself e.g. "insufficent funds" + let rpc_connection_success = wallet_write.rpc.get_best_block_hash().is_ok(); + if !rpc_connection_success { + log::warn!("lost connection with bitcoin node, temporarily shutting \ + down server until connection reestablished, error={:?}", e); + accepting_clients = false; + } + continue; + }, + _ => log::error!("ending server"), + } + break Err(client_err.unwrap()); + }, + _ = sleep(Duration::from_secs(maker_writer.config.heart_beat_interval_secs)) => { + let mut rpc_ping_success = true; + let mut watchtowers_ping_success = true; + + let rpc_ping_interval = Duration::from_secs(maker_writer.config.rpc_ping_interval_secs); + if Instant::now().saturating_duration_since(last_rpc_ping) > rpc_ping_interval { + last_rpc_ping = Instant::now(); + rpc_ping_success = wallet_write + .refresh_offer_maxsize_cache() + .is_ok(); + log::debug!("rpc_ping_success = {}", rpc_ping_success); + } + let watchtowers_ping_interval + = Duration::from_secs(maker_writer.config.watchtower_ping_interval_secs); + if Instant::now().saturating_duration_since(last_watchtowers_ping) + > watchtowers_ping_interval { + last_watchtowers_ping = Instant::now(); + watchtowers_ping_success = ping_watchtowers().await.is_ok(); + log::debug!("watchtowers_ping_success = {}", watchtowers_ping_success); + } + accepting_clients = rpc_ping_success && watchtowers_ping_success; + if !accepting_clients { + log::warn!("not accepting clients, rpc_ping_success={} \ + watchtowers_ping_success={}", rpc_ping_success, watchtowers_ping_success); + } + + if *maker_writer.shutdown.read().await { + break Err(MakerError::General("kill flag is true")); + } + + let directory_servers_refresh_interval = Duration::from_secs( + maker_writer.config.directory_servers_refresh_interval_secs + ); + let wallet_network = wallet_write.store.network; + if wallet_network != Network::Regtest + && Instant::now().saturating_duration_since(last_directory_servers_refresh) + > directory_servers_refresh_interval { + last_directory_servers_refresh = Instant::now(); + let result_expiry_time = post_maker_address_to_directory_servers( + wallet_network, + &maker_writer.config.onion_addrs + ).await; + log::info!("Refreshing my address at the directory servers = {:?}", + result_expiry_time); + } + continue; + }, + }; + + if !accepting_clients { + log::warn!("Rejecting Connection From {:?}", addr); + continue; + } + + log::info!( + "[{}] ===> Accepted Connection on port={}", + addr.port(), + addr.port() + ); + //let client_rpc = Arc::clone(&rpc); + //let client_wallet = Arc::clone(&self.wallet); + let server_loop_comms_tx = server_loop_comms_tx.clone(); + //let maker_behavior = maker_writer.behavior; + let idle_connection_timeout = maker_writer.config.idle_connection_timeout; + + // Clone the Arc to pass in the thread below + let arc_maker = maker.clone(); + + // Spawn a thread to handle one taker connection. + tokio::spawn(async move { + let (socket_reader, mut socket_writer) = socket.split(); + let mut reader = BufReader::new(socket_reader); + + let mut maker_writer = arc_maker.write().await; + + // Push a default connection state for this new connection. + maker_writer + .connection_states + .push(ConnectionState::default()); + + if let Err(e) = send_message( + &mut socket_writer, + &MakerToTakerMessage::MakerHello(MakerHello { + protocol_version_min: 0, + protocol_version_max: 0, + }), + ) + .await + { + log::error!("io error sending first message: {:?}", e); + return; + } + log::info!("[{}] <=== MakerHello", addr.port()); + + loop { + let mut line = String::new(); + select! { + readline_ret = reader.read_line(&mut line) => { + match readline_ret { + Ok(n) if n == 0 => { + log::info!("[{}] Connection closed by peer", addr.port()); + break; + } + Ok(_n) => (), + Err(e) => { + log::error!("error reading from socket: {:?}", e); + break; + } + } + }, + _ = sleep(Duration::from_secs(idle_connection_timeout)) => { + log::info!("[{}] Idle connection closed", addr.port()); + break; + }, + }; + + line = line.trim_end().to_string(); + let message: TakerToMakerMessage = serde_json::from_str(&line).unwrap(); + log::info!("[{}] ===> {} ", addr.port(), message); + + let message_result = maker_writer.handle_message(&message).await; + match message_result { + Ok(reply) => { + if let Some(message) = reply { + log::info!("[{}] <=== {} ", addr.port(), message); + log::debug!("{:#?}", message); + if let Err(e) = send_message(&mut socket_writer, &message).await { + log::error!("closing due to io error sending message: {:?}", e); + break; + } + } + //if reply is None then dont send anything to client + } + Err(err) => { + log::error!("error handling client request: {:?}", err); + server_loop_comms_tx.send(err).await.unwrap(); + break; + } + }; + } + }); + } +} diff --git a/src/maker/server.rs b/src/maker/server.rs index 826de76a..a343ca30 100644 --- a/src/maker/server.rs +++ b/src/maker/server.rs @@ -35,10 +35,10 @@ use crate::{ market::directory::post_maker_address_to_directory_servers, protocol::{ contract::{ - calculate_coinswap_fee, create_receivers_contract_tx, find_funding_output, - read_hashvalue_from_contract, read_locktime_from_contract, + calculate_coinswap_fee, create_receivers_contract_tx, find_funding_output_index, + read_contract_locktime, read_hashvalue_from_contract, read_pubkeys_from_multisig_redeemscript, validate_and_sign_senders_contract_tx, - verify_proof_of_funding, MAKER_FUNDING_TX_VBYTE_SIZE, + verify_proof_of_funding, FUNDING_TX_VBYTE_SIZE, }, messages::{ ContractSigsAsRecvrAndSender, ContractSigsForRecvr, ContractSigsForRecvrAndSender, @@ -303,24 +303,6 @@ async fn run( } Err(err) => { log::error!("error handling client request: {:?}", err); - // match err { - // // TeleportError::Network(_e) => (), - // // TeleportError::Protocol(_e) => (), - // // TeleportError::Disk(e) => server_loop_comms_tx - // // .send(TeleportError::Disk(e)) - // // .await - // // .unwrap(), - // // TeleportError::Rpc(e) => server_loop_comms_tx - // // .send(TeleportError::Rpc(e)) - // // .await - // // .unwrap(), - // // TeleportError::Socks(e) => server_loop_comms_tx - // // .send(TeleportError::Socks(e)) - // // .await - // // .unwrap(), - // // TeleportError::Wallet(e) => server_loop_comms_tx.send(TeleportError::Wallet(e)).await.unwrap() - - // }; server_loop_comms_tx.send(err).await.unwrap(); break; } @@ -332,7 +314,7 @@ async fn run( async fn send_message( socket_writer: &mut WriteHalf<'_>, - first_message: &MakerToTakerMessage, + first_message: &impl serde::Serialize, ) -> Result<(), TeleportError> { let mut message_bytes = serde_json::to_vec(first_message).map_err(|e| std::io::Error::from(e))?; @@ -531,8 +513,8 @@ fn handle_sign_senders_contract_tx( let mut total_amount = 0; for txinfo in message.txs_info { let sig = validate_and_sign_senders_contract_tx( - &txinfo.multisig_key_nonce, - &txinfo.hashlock_key_nonce, + &txinfo.multisig_nonce, + &txinfo.hashlock_nonce, &txinfo.timelock_pubkey, &txinfo.senders_contract_tx, &txinfo.multisig_redeemscript, @@ -584,7 +566,7 @@ fn handle_proof_of_funding( funding_info.funding_tx, funding_info.multisig_redeemscript ); - let (funding_output_index, funding_output) = match find_funding_output( + let (funding_output_index, funding_output) = match find_funding_output_index( &funding_info.funding_tx, &funding_info.multisig_redeemscript, ) { @@ -688,7 +670,7 @@ fn handle_proof_of_funding( 1, //time_in_blocks just 1 for now ); let miner_fees_paid_by_taker = - MAKER_FUNDING_TX_VBYTE_SIZE * proof.next_fee_rate * (proof.next_coinswap_info.len() as u64) + FUNDING_TX_VBYTE_SIZE * proof.next_fee_rate * (proof.next_coinswap_info.len() as u64) / 1000; let outgoing_amount = incoming_amount - coinswap_fees - miner_fees_paid_by_taker; @@ -721,8 +703,7 @@ fn handle_proof_of_funding( log::info!( "incoming_amount={}, incoming_locktime={}, hashvalue={}", Amount::from_sat(incoming_amount), - read_locktime_from_contract(&proof.confirmed_funding_txes[0].contract_redeemscript) - .unwrap(), + read_contract_locktime(&proof.confirmed_funding_txes[0].contract_redeemscript).unwrap(), //unwrap() as format of contract_redeemscript already checked in verify_proof_of_funding hashvalue ); diff --git a/src/protocol/contract.rs b/src/protocol/contract.rs index d2dd81d4..78db203d 100644 --- a/src/protocol/contract.rs +++ b/src/protocol/contract.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use std::convert::TryInto; use bitcoin::{ @@ -8,7 +6,6 @@ use bitcoin::{ script::{Builder, Instruction, Script}, }, hashes::Hash, - secp256k1, secp256k1::{ rand::{rngs::OsRng, RngCore}, Message, Secp256k1, SecretKey, Signature, @@ -19,9 +16,9 @@ use bitcoin::{ pub use bitcoin::hashes::hash160::Hash as Hash160; -use bitcoincore_rpc::{Client, RpcApi}; +use crate::{error::TeleportError, protocol::messages::FundingTxInfo}; -use crate::{error::TeleportError, protocol::messages::FundingTxInfo, wallet::Wallet}; +use super::{error::ContractError, messages::ProofOfFunding}; //relatively simple handling of miner fees for now, each funding transaction is considered // to have the same size, and taker will pay all the maker's miner fees based on that @@ -36,33 +33,7 @@ use crate::{error::TeleportError, protocol::messages::FundingTxInfo, wallet::Wal // we could avoid this guessing by adding one more round trip to the protocol where the maker // calculates exactly how big the transactions will be and then taker knows exactly the miner fee // to pay for -pub const MAKER_FUNDING_TX_VBYTE_SIZE: u64 = 372; - -//like the Incoming/OutgoingSwapCoin structs but no privkey or signature information -//used by the taker to monitor coinswaps between two makers -#[derive(Debug, Clone)] -pub struct WatchOnlySwapCoin { - pub sender_pubkey: PublicKey, - pub receiver_pubkey: PublicKey, - pub contract_tx: Transaction, - pub contract_redeemscript: Script, - pub funding_amount: u64, -} - -// pub trait SwapCoin { -// fn get_multisig_redeemscript(&self) -> Script; -// fn get_contract_tx(&self) -> Transaction; -// fn get_contract_redeemscript(&self) -> Script; -// fn get_timelock_pubkey(&self) -> PublicKey; -// fn get_timelock(&self) -> u16; -// fn get_hashlock_pubkey(&self) -> PublicKey; -// fn get_hashvalue(&self) -> Hash160; -// fn get_funding_amount(&self) -> u64; -// fn verify_contract_tx_receiver_sig(&self, sig: &Signature) -> bool; -// fn verify_contract_tx_sender_sig(&self, sig: &Signature) -> bool; -// fn apply_privkey(&mut self, privkey: SecretKey) -> Result<(), TeleportError>; -// fn is_hash_preimage_known(&self) -> bool; -// } +pub const FUNDING_TX_VBYTE_SIZE: u64 = 372; pub fn calculate_coinswap_fee( absolute_fee_sat: u64, @@ -111,41 +82,97 @@ pub fn create_multisig_redeemscript(key1: &PublicKey, key2: &PublicKey) -> Scrip } pub fn derive_maker_pubkey_and_nonce( - tweakable_point: PublicKey, -) -> Result<(PublicKey, SecretKey), secp256k1::Error> { + tweakable_point: &PublicKey, +) -> Result<(PublicKey, SecretKey), ContractError> { let mut nonce_bytes = [0u8; 32]; let mut rng = OsRng::new().unwrap(); rng.fill_bytes(&mut nonce_bytes); let nonce = SecretKey::from_slice(&nonce_bytes)?; - let maker_pubkey = calculate_maker_pubkey_from_nonce(tweakable_point, nonce)?; - + let maker_pubkey = calculate_pubkey_from_nonce(tweakable_point, &nonce)?; Ok((maker_pubkey, nonce)) } -pub fn calculate_maker_pubkey_from_nonce( - tweakable_point: PublicKey, - nonce: SecretKey, -) -> Result { +pub fn calculate_pubkey_from_nonce( + tweakable_point: &PublicKey, + nonce: &SecretKey, +) -> Result { let secp = Secp256k1::new(); - let nonce_point = bitcoin::secp256k1::PublicKey::from_secret_key(&secp, &nonce); + let nonce_point = bitcoin::secp256k1::PublicKey::from_secret_key(&secp, nonce); Ok(PublicKey { compressed: true, key: tweakable_point.key.combine(&nonce_point)?, }) } -pub fn find_funding_output<'a>( - funding_tx: &'a Transaction, - multisig_redeemscript: &Script, -) -> Option<(u32, &'a TxOut)> { - let multisig_spk = redeemscript_to_scriptpubkey(&multisig_redeemscript); - funding_tx +// TODO: Just return the index, TxOut can be found from there. +pub fn find_funding_output_index(funding_tx_info: &FundingTxInfo) -> Result { + let multisig_spk = redeemscript_to_scriptpubkey(&funding_tx_info.multisig_redeemscript); + funding_tx_info + .funding_tx .output .iter() .enumerate() - .map(|(i, o)| (i as u32, o)) .find(|(_i, o)| o.script_pubkey == multisig_spk) + .map(|(index, _)| index as u32) + .ok_or(ContractError::Protocol( + "Funding output doesn't match with multisig reedimscript", + )) +} + +pub fn check_reedemscript_is_multisig(redeemscript: &Script) -> Result<(), ContractError> { + //pattern match to check redeemscript is really a 2of2 multisig + let mut ms_rs_bytes = redeemscript.to_bytes(); + const PUB_PLACEHOLDER: [u8; 33] = [0x02; 33]; + let pubkey_placeholder = PublicKey::from_slice(&PUB_PLACEHOLDER).unwrap(); + let template_ms_rs = + create_multisig_redeemscript(&pubkey_placeholder, &pubkey_placeholder).into_bytes(); + if ms_rs_bytes.len() != template_ms_rs.len() { + return Err(ContractError::Protocol( + "wrong multisig_redeemscript length", + )); + } + ms_rs_bytes.splice(2..35, PUB_PLACEHOLDER.iter().cloned()); + ms_rs_bytes.splice(36..69, PUB_PLACEHOLDER.iter().cloned()); + if ms_rs_bytes != template_ms_rs { + return Err(ContractError::Protocol( + "redeemscript not matching multisig template", + )); + } else { + Ok(()) + } +} + +pub fn check_multisig_has_pubkey( + redeemscript: &Script, + tweakable_point: &PublicKey, + nonce: &SecretKey, +) -> Result<(), ContractError> { + let (pubkey1, pubkey2) = read_pubkeys_from_multisig_redeemscript(redeemscript)?; + let my_pubkey = calculate_pubkey_from_nonce(tweakable_point, nonce)?; + if pubkey1 != my_pubkey && pubkey2 != my_pubkey { + return Err(ContractError::Protocol( + "wrong pubkeys in multisig_redeemscript", + )); + } else { + Ok(()) + } +} + +pub fn check_hashlock_has_pubkey( + contract_redeemscript: &Script, + tweakable_point: &PublicKey, + nonce: &SecretKey, +) -> Result<(), ContractError> { + let contract_hashlock_pubkey = read_hashlock_pubkey_from_contract(contract_redeemscript)?; + let derived_hashlock_pubkey = calculate_pubkey_from_nonce(tweakable_point, nonce)?; + if contract_hashlock_pubkey != derived_hashlock_pubkey { + return Err(ContractError::Protocol( + "contract hashlock pubkey doesnt match with key derived from nonce", + )); + } else { + Ok(()) + } } /// Convert a redeemscript into p2wsh scriptpubkey. @@ -161,8 +188,8 @@ pub fn redeemscript_to_scriptpubkey(redeemscript: &Script) -> Script { pub fn create_contract_redeemscript( pub_hashlock: &PublicKey, pub_timelock: &PublicKey, - hashvalue: Hash160, - locktime: u16, + hashvalue: &Hash160, + locktime: &u16, ) -> Script { //avoid the malleability from OP_IF attack, see: //https://lists.linuxfoundation.org/pipermail/lightning-dev/2016-September/000605.html @@ -224,7 +251,7 @@ pub fn create_contract_redeemscript( .push_opcode(opcodes::all::OP_ELSE) .push_key(&pub_timelock) .push_int(0) - .push_int(locktime as i64) + .push_int(*locktime as i64) .push_opcode(opcodes::all::OP_ENDIF) .push_opcode(opcodes::all::OP_CSV) .push_opcode(opcodes::all::OP_DROP) @@ -236,32 +263,63 @@ pub fn create_contract_redeemscript( //TODO put all these magic numbers in a const or something //a better way is to use redeemscript.instructions() like read_locktime_from_contract() -pub fn read_hashvalue_from_contract(redeemscript: &Script) -> Result { +pub fn read_hashvalue_from_contract(redeemscript: &Script) -> Result { if redeemscript.to_bytes().len() < 25 { - return Err("script too short"); + return Err(ContractError::Protocol("contract reedemscript too short")); } Ok(Hash160::from_inner( redeemscript.to_bytes()[4..24] .try_into() - .map_err(|_| "tryinto error")?, + .map_err(|_| ContractError::Protocol("hash value is not 20 bytes slice"))?, )) } -pub fn read_locktime_from_contract(redeemscript: &Script) -> Option { - match redeemscript.instructions().nth(12)?.ok()? { +//check that all the contract redeemscripts involve the same hashvalue +pub fn check_hashvalues_are_equal(message: &ProofOfFunding) -> Result { + let hashvalues = message + .confirmed_funding_txes + .iter() + .map(|funding_info| { + Ok(read_hashvalue_from_contract( + &funding_info.contract_redeemscript, + )?) + }) + .collect::, ContractError>>()?; + + if !hashvalues.iter().all(|value| value == &hashvalues[0]) { + return Err(ContractError::Protocol( + "contract reedemscript doesn't have equal hashvalues", + )); + } + + Ok(hashvalues[0]) +} + +pub fn read_contract_locktime(redeemscript: &Script) -> Result { + match redeemscript + .instructions() + .nth(12) + .expect("Insctructions expected")? + { Instruction::PushBytes(locktime_bytes) => match locktime_bytes.len() { - 1 => Some(locktime_bytes[0] as u16), + 1 => Ok(locktime_bytes[0] as u16), 2 | 3 => { let (int_bytes, _rest) = locktime_bytes.split_at(std::mem::size_of::()); - Some(u16::from_le_bytes(int_bytes.try_into().unwrap())) + Ok(u16::from_le_bytes(int_bytes.try_into().unwrap())) } - _ => None, + _ => Err(ContractError::Protocol( + "Can't read locktime value from contract reedemscript", + )), }, Instruction::Op(opcode) => { if let opcodes::Class::PushNum(n) = opcode.classify() { - Some(n.try_into().ok()?) + Ok(n.try_into().map_err(|_| { + ContractError::Protocol("Can't read locktime value from contract reedemscript") + })?) } else { - None + Err(ContractError::Protocol( + "Can't read locktime value from contract reedemscript", + )) } } } @@ -269,33 +327,30 @@ pub fn read_locktime_from_contract(redeemscript: &Script) -> Option { pub fn read_hashlock_pubkey_from_contract( redeemscript: &Script, -) -> Result { +) -> Result { if redeemscript.to_bytes().len() < 61 { - return Err("script too short"); + return Err(ContractError::Protocol("contract reedemscript too short")); } - PublicKey::from_slice(&redeemscript.to_bytes()[27..60]).map_err(|_| "pubkey error") + Ok(PublicKey::from_slice(&redeemscript.to_bytes()[27..60])?) } pub fn read_timelock_pubkey_from_contract( redeemscript: &Script, -) -> Result { +) -> Result { if redeemscript.to_bytes().len() < 99 { - return Err("script too short"); + return Err(ContractError::Protocol("contract reedemscript too short")); } - PublicKey::from_slice(&redeemscript.to_bytes()[65..98]).map_err(|_| "pubkey error") + Ok(PublicKey::from_slice(&redeemscript.to_bytes()[65..98])?) } pub fn read_pubkeys_from_multisig_redeemscript( redeemscript: &Script, -) -> Option<(PublicKey, PublicKey)> { +) -> Result<(PublicKey, PublicKey), ContractError> { let ms_rs_bytes = redeemscript.to_bytes(); //TODO put these magic numbers in consts, PUBKEY1_OFFSET maybe - let pubkey1 = PublicKey::from_slice(&ms_rs_bytes[2..35]); - let pubkey2 = PublicKey::from_slice(&ms_rs_bytes[36..69]); - if pubkey1.is_err() || pubkey2.is_err() { - return None; - } - Some((pubkey1.unwrap(), pubkey2.unwrap())) + let pubkey1 = PublicKey::from_slice(&ms_rs_bytes[2..35])?; + let pubkey2 = PublicKey::from_slice(&ms_rs_bytes[36..69])?; + Ok((pubkey1, pubkey2)) } /// Create a Contract Transaction for the "Sender" side of Coinswap. @@ -333,211 +388,206 @@ pub fn create_receivers_contract_tx( create_senders_contract_tx(input, input_value, contract_redeemscript) } -fn is_contract_out_valid( +pub fn is_contract_out_valid( contract_output: &TxOut, hashlock_pubkey: &PublicKey, timelock_pubkey: &PublicKey, - hashvalue: Hash160, - locktime: u16, - minimum_locktime: u16, -) -> Result<(), TeleportError> { + hashvalue: &Hash160, + locktime: &u16, + minimum_locktime: &u16, +) -> Result<(), ContractError> { if minimum_locktime > locktime { - return Err(TeleportError::Protocol("locktime too short")); + return Err(ContractError::Protocol("locktime too short")); } let redeemscript_from_request = create_contract_redeemscript(hashlock_pubkey, timelock_pubkey, hashvalue, locktime); let contract_spk_from_request = redeemscript_to_scriptpubkey(&redeemscript_from_request); if contract_output.script_pubkey != contract_spk_from_request { - return Err(TeleportError::Protocol( + return Err(ContractError::Protocol( "given transaction does not pay to requested contract", )); } Ok(()) } -//TODO perhaps rename this to include "_with_nonces" -//to match how "validate_and_sign_contract_tx" does it only with keys -pub fn validate_and_sign_senders_contract_tx( - multisig_key_nonce: &SecretKey, - hashlock_key_nonce: &SecretKey, - timelock_pubkey: &PublicKey, - senders_contract_tx: &Transaction, - multisig_redeemscript: &Script, - funding_input_value: u64, - hashvalue: Hash160, - locktime: u16, - minimum_locktime: u16, - tweakable_privkey: &SecretKey, - wallet: &mut Wallet, -) -> Result { - if senders_contract_tx.input.len() != 1 || senders_contract_tx.output.len() != 1 { - return Err(TeleportError::Protocol( - "invalid number of inputs or outputs", - )); - } - if !wallet.does_prevout_match_cached_contract( - &senders_contract_tx.input[0].previous_output, - &senders_contract_tx.output[0].script_pubkey, - )? { - return Err(TeleportError::Protocol( - "taker attempting multiple contract attack, rejecting", - )); - } - - let secp = Secp256k1::new(); - let mut hashlock_privkey_from_nonce = *tweakable_privkey; - hashlock_privkey_from_nonce - .add_assign(hashlock_key_nonce.as_ref()) - .map_err(|_| { - TeleportError::Protocol("error with hashlock tweakable privkey + hashlock nonce") - })?; - let hashlock_pubkey_from_nonce = PublicKey { - compressed: true, - key: secp256k1::PublicKey::from_secret_key(&secp, &hashlock_privkey_from_nonce), - }; - - is_contract_out_valid( - &senders_contract_tx.output[0], - &hashlock_pubkey_from_nonce, - &timelock_pubkey, - hashvalue, - locktime, - minimum_locktime, - )?; //note question mark here propagating the error upwards - - wallet.cache_prevout_to_contract( - senders_contract_tx.input[0].previous_output, - senders_contract_tx.output[0].script_pubkey.clone(), - )?; - - let mut multisig_privkey_from_nonce = *tweakable_privkey; - multisig_privkey_from_nonce - .add_assign(multisig_key_nonce.as_ref()) - .map_err(|_| { - TeleportError::Protocol("error with multisig tweakable privkey + multisig nonce") - })?; - - Ok(sign_contract_tx( - &senders_contract_tx, - &multisig_redeemscript, - funding_input_value, - &multisig_privkey_from_nonce, - ) - .map_err(|_| TeleportError::Protocol("error with signing contract tx"))?) -} - -//returns the keys of the multisig, ready for importing -//or None if the proof is invalid for some reason -//or an error if the RPC connection fails -pub fn verify_proof_of_funding( - rpc: Arc, - wallet: &mut Wallet, - funding_info: &FundingTxInfo, - funding_output_index: u32, - next_locktime: u16, - min_contract_react_time: u16, - //returns my_multisig_privkey, other_multisig_pubkey, my_hashlock_privkey -) -> Result<(SecretKey, PublicKey, SecretKey), TeleportError> { - //check the funding_tx exists and was really confirmed - if let Some(txout) = - rpc.get_tx_out(&funding_info.funding_tx.txid(), funding_output_index, None)? - { - if txout.confirmations < 1 { - return Err(TeleportError::Protocol("funding tx not confirmed")); - } - } else { - return Err(TeleportError::Protocol("funding tx output doesnt exist")); - } - - //pattern match to check redeemscript is really a 2of2 multisig - let mut ms_rs_bytes = funding_info.multisig_redeemscript.to_bytes(); - const PUB_PLACEHOLDER: [u8; 33] = [0x02; 33]; - let pubkey_placeholder = PublicKey::from_slice(&PUB_PLACEHOLDER).unwrap(); - let template_ms_rs = - create_multisig_redeemscript(&pubkey_placeholder, &pubkey_placeholder).into_bytes(); - if ms_rs_bytes.len() != template_ms_rs.len() { - return Err(TeleportError::Protocol( - "wrong multisig_redeemscript length", - )); - } - ms_rs_bytes.splice(2..35, PUB_PLACEHOLDER.iter().cloned()); - ms_rs_bytes.splice(36..69, PUB_PLACEHOLDER.iter().cloned()); - if ms_rs_bytes != template_ms_rs { - return Err(TeleportError::Protocol( - "multisig_redeemscript not matching template", - )); - } - - //check my pubkey is one of the pubkeys in the redeemscript - let (pubkey1, pubkey2) = - read_pubkeys_from_multisig_redeemscript(&funding_info.multisig_redeemscript) - .ok_or(TeleportError::Protocol("invalid multisig_redeemscript"))?; - let (tweakable_privkey, tweakable_point) = wallet.get_tweakable_keypair(); - let my_pubkey = calculate_maker_pubkey_from_nonce(tweakable_point, funding_info.multisig_nonce) - .map_err(|_| TeleportError::Protocol("unable to calculate maker pubkey from nonce"))?; - if pubkey1 != my_pubkey && pubkey2 != my_pubkey { - return Err(TeleportError::Protocol( - "wrong pubkeys in multisig_redeemscript", - )); - } - - //check that the new locktime is sufficently short enough compared to the - //locktime in the provided funding tx - let locktime = read_locktime_from_contract(&funding_info.contract_redeemscript).ok_or( - TeleportError::Protocol("unable to read locktime from contract"), - )?; - //this is the time the maker or his watchtowers have to be online, read - // the hash preimage from the blockchain and broadcast their own tx - if locktime - next_locktime < min_contract_react_time { - return Err(TeleportError::Protocol("locktime too short")); - } - - //check that provided hashlock_key_nonce really corresponds to the hashlock_pubkey in contract - let contract_hashlock_pubkey = - read_hashlock_pubkey_from_contract(&funding_info.contract_redeemscript) - .map_err(|_| TeleportError::Protocol("unable to read hashlock pubkey from contract"))?; - let derived_hashlock_pubkey = - calculate_maker_pubkey_from_nonce(tweakable_point, funding_info.hashlock_nonce) - .map_err(|_| TeleportError::Protocol("unable to calculate maker pubkey from nonce"))?; - if contract_hashlock_pubkey != derived_hashlock_pubkey { - return Err(TeleportError::Protocol( - "contract hashlock pubkey doesnt match key derived from nonce", - )); - } - - //check that the provided contract matches the scriptpubkey from the - //cache which was populated when the signsendercontracttx message arrived - let contract_spk = redeemscript_to_scriptpubkey(&funding_info.contract_redeemscript); - - if !wallet.does_prevout_match_cached_contract( - &OutPoint { - txid: funding_info.funding_tx.txid(), - vout: funding_output_index, - }, - &contract_spk, - )? { - return Err(TeleportError::Protocol( - "provided contract does not match sender contract tx, rejecting", - )); - } +// //TODO perhaps rename this to include "_with_nonces" +// //to match how "validate_and_sign_contract_tx" does it only with keys +// pub fn validate_and_sign_senders_contract_tx( +// // multisig_key_nonce: &SecretKey, +// // hashlock_key_nonce: &SecretKey, +// // timelock_pubkey: &PublicKey, +// // senders_contract_tx: &Transaction, +// // multisig_redeemscript: &Script, +// // funding_input_value: u64, +// txinfo: ContractTxInfoForSender, +// hashvalue: Hash160, +// locktime: u16, +// minimum_locktime: u16, +// wallet: &mut Wallet, +// ) -> Result { +// if txinfo.senders_contract_tx.input.len() != 1 || txinfo.senders_contract_tx.output.len() != 1 { +// return Err(ContractError::General( +// "invalid number of inputs or outputs".to_string(), +// )); +// } +// if !wallet.does_prevout_match_cached_contract( +// &txinfo.senders_contract_tx.input[0].previous_output, +// &txinfo.senders_contract_tx.output[0].script_pubkey, +// )? { +// return Err(ContractError::General( +// "taker attempting multiple contract attack, rejecting".to_string(), +// )); +// } + +// let tweakable_privkey = wallet.get_tweakable_keypair().0; + +// let secp = Secp256k1::new(); +// let mut hashlock_privkey = tweakable_privkey; +// hashlock_privkey.add_assign(txinfo.hashlock_nonce.as_ref())?; + +// let hashlock_pubkey = PublicKey { +// compressed: true, +// key: secp256k1::PublicKey::from_secret_key(&secp, &hashlock_privkey), +// }; + +// is_contract_out_valid( +// &txinfo.senders_contract_tx.output[0], +// &hashlock_pubkey, +// &txinfo.timelock_pubkey, +// hashvalue, +// locktime, +// minimum_locktime, +// )?; + +// wallet.cache_prevout_to_contract( +// txinfo.senders_contract_tx.input[0].previous_output, +// txinfo.senders_contract_tx.output[0].script_pubkey.clone(), +// )?; + +// let mut multisig_privkey = tweakable_privkey; +// multisig_privkey.add_assign(txinfo.multisig_nonce.as_ref())?; + +// Ok(sign_contract_tx( +// &txinfo.senders_contract_tx, +// &txinfo.multisig_redeemscript, +// txinfo.funding_input_value, +// &multisig_privkey, +// ) +// .map_err(|_| TeleportError::Protocol("error with signing contract tx"))?) +// } - let mut my_privkey = tweakable_privkey; - my_privkey - .add_assign(funding_info.multisig_nonce.as_ref()) - .map_err(|_| TeleportError::Protocol("error with wallet tweakable privkey + nonce"))?; - let mut hashlock_privkey = tweakable_privkey; - hashlock_privkey - .add_assign(funding_info.hashlock_nonce.as_ref()) - .map_err(|_| TeleportError::Protocol("error with wallet tweakable privkey + nonce"))?; - - let other_pubkey = if pubkey1 == my_pubkey { - pubkey2 - } else { - pubkey1 - }; - Ok((my_privkey, other_pubkey, hashlock_privkey)) -} +// //returns the keys of the multisig, ready for importing +// //or None if the proof is invalid for some reason +// //or an error if the RPC connection fails +// pub fn verify_proof_of_funding( +// rpc: Arc, +// wallet: &mut Wallet, +// funding_info: &FundingTxInfo, +// funding_output_index: u32, +// next_locktime: u16, +// min_contract_react_time: u16, +// //returns my_multisig_privkey, other_multisig_pubkey, my_hashlock_privkey +// ) -> Result<(SecretKey, PublicKey, SecretKey), TeleportError> { +// //check the funding_tx exists and was really confirmed +// if let Some(txout) = +// rpc.get_tx_out(&funding_info.funding_tx.txid(), funding_output_index, None)? +// { +// if txout.confirmations < 1 { +// return Err(TeleportError::Protocol("funding tx not confirmed")); +// } +// } else { +// return Err(TeleportError::Protocol("funding tx output doesnt exist")); +// } + +// //pattern match to check redeemscript is really a 2of2 multisig +// let mut ms_rs_bytes = funding_info.multisig_redeemscript.to_bytes(); +// const PUB_PLACEHOLDER: [u8; 33] = [0x02; 33]; +// let pubkey_placeholder = PublicKey::from_slice(&PUB_PLACEHOLDER).unwrap(); +// let template_ms_rs = +// create_multisig_redeemscript(&pubkey_placeholder, &pubkey_placeholder).into_bytes(); +// if ms_rs_bytes.len() != template_ms_rs.len() { +// return Err(TeleportError::Protocol( +// "wrong multisig_redeemscript length", +// )); +// } +// ms_rs_bytes.splice(2..35, PUB_PLACEHOLDER.iter().cloned()); +// ms_rs_bytes.splice(36..69, PUB_PLACEHOLDER.iter().cloned()); +// if ms_rs_bytes != template_ms_rs { +// return Err(TeleportError::Protocol( +// "multisig_redeemscript not matching template", +// )); +// } + +// //check my pubkey is one of the pubkeys in the redeemscript +// let (pubkey1, pubkey2) = +// read_pubkeys_from_multisig_redeemscript(&funding_info.multisig_redeemscript) +// .ok_or(TeleportError::Protocol("invalid multisig_redeemscript"))?; +// let (tweakable_privkey, tweakable_point) = wallet.get_tweakable_keypair(); +// let my_pubkey = calculate_pubkey_from_nonce(tweakable_point, funding_info.multisig_nonce) +// .map_err(|_| TeleportError::Protocol("unable to calculate maker pubkey from nonce"))?; +// if pubkey1 != my_pubkey && pubkey2 != my_pubkey { +// return Err(TeleportError::Protocol( +// "wrong pubkeys in multisig_redeemscript", +// )); +// } + +// //check that the new locktime is sufficently short enough compared to the +// //locktime in the provided funding tx +// let locktime = read_contract_locktime(&funding_info.contract_redeemscript).ok_or( +// TeleportError::Protocol("unable to read locktime from contract"), +// )?; +// //this is the time the maker or his watchtowers have to be online, read +// // the hash preimage from the blockchain and broadcast their own tx +// if locktime - next_locktime < min_contract_react_time { +// return Err(TeleportError::Protocol("locktime too short")); +// } + +// //check that provided hashlock_key_nonce really corresponds to the hashlock_pubkey in contract +// let contract_hashlock_pubkey = +// read_hashlock_pubkey_from_contract(&funding_info.contract_redeemscript) +// .map_err(|_| TeleportError::Protocol("unable to read hashlock pubkey from contract"))?; +// let derived_hashlock_pubkey = +// calculate_pubkey_from_nonce(tweakable_point, funding_info.hashlock_nonce) +// .map_err(|_| TeleportError::Protocol("unable to calculate maker pubkey from nonce"))?; +// if contract_hashlock_pubkey != derived_hashlock_pubkey { +// return Err(TeleportError::Protocol( +// "contract hashlock pubkey doesnt match key derived from nonce", +// )); +// } + +// //check that the provided contract matches the scriptpubkey from the +// //cache which was populated when the signsendercontracttx message arrived +// let contract_spk = redeemscript_to_scriptpubkey(&funding_info.contract_redeemscript); + +// if !wallet.does_prevout_match_cached_contract( +// &OutPoint { +// txid: funding_info.funding_tx.txid(), +// vout: funding_output_index, +// }, +// &contract_spk, +// )? { +// return Err(TeleportError::Protocol( +// "provided contract does not match sender contract tx, rejecting", +// )); +// } + +// let mut my_privkey = tweakable_privkey; +// my_privkey +// .add_assign(funding_info.multisig_nonce.as_ref()) +// .map_err(|_| TeleportError::Protocol("error with wallet tweakable privkey + nonce"))?; +// let mut hashlock_privkey = tweakable_privkey; +// hashlock_privkey +// .add_assign(funding_info.hashlock_nonce.as_ref()) +// .map_err(|_| TeleportError::Protocol("error with wallet tweakable privkey + nonce"))?; + +// let other_pubkey = if pubkey1 == my_pubkey { +// pubkey2 +// } else { +// pubkey1 +// }; +// Ok((my_privkey, other_pubkey, hashlock_privkey)) +// } pub fn validate_contract_tx( receivers_contract_tx: &Transaction, @@ -567,7 +617,7 @@ pub fn sign_contract_tx( multisig_redeemscript: &Script, funding_amount: u64, privkey: &SecretKey, -) -> Result { +) -> Result { let input_index = 0; let sighash = Message::from_slice( &SigHashCache::new(contract_tx).signature_hash( @@ -610,7 +660,10 @@ mod test { use bitcoin::{ consensus::encode::deserialize, hashes::hex::{FromHex, ToHex}, - secp256k1::rand::{random, thread_rng, Rng}, + secp256k1::{ + self, + rand::{random, thread_rng, Rng}, + }, PrivateKey, }; use std::{str::FromStr, string::String}; @@ -635,7 +688,7 @@ mod test { PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap(); let pubkey = sk.public_key(&secp); let nonce = SecretKey::from_slice(&[2; 32]).unwrap(); - let maker_key_computed = calculate_maker_pubkey_from_nonce(pubkey, nonce).unwrap(); + let maker_key_computed = calculate_pubkey_from_nonce(&pubkey, &nonce).unwrap(); let expected_pubkey = PublicKey::from_str( "03bf98c86c3d536136378cf43ac42861ece609de87f5a44e19b730e8e9bd791938", ) @@ -649,7 +702,7 @@ mod test { let privkey_org = PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap(); let pubkey_org = privkey_org.public_key(&secp); - let (pubkey_derived, nonce) = derive_maker_pubkey_and_nonce(pubkey_org.clone()).unwrap(); + let (pubkey_derived, nonce) = derive_maker_pubkey_and_nonce(&pubkey_org).unwrap(); let nonce_point = secp256k1::PublicKey::from_secret_key(&secp, &nonce); let expected_derivation = PublicKey { compressed: true, @@ -678,7 +731,7 @@ mod test { println!("randomly chosen locktime = {}", locktime); let contract_script = - create_contract_redeemscript(&pub_hashlock, &pub_timelock, hashvalue, locktime); + create_contract_redeemscript(&pub_hashlock, &pub_timelock, &hashvalue, &locktime); // Get the byte encoded locktime for script let locktime_bytecode = Builder::new().push_int(locktime as i64).into_script(); @@ -701,10 +754,7 @@ mod test { read_hashvalue_from_contract(&contract_script).unwrap(), hashvalue ); - assert_eq!( - read_locktime_from_contract(&contract_script).unwrap(), - locktime - ); + assert_eq!(read_contract_locktime(&contract_script).unwrap(), locktime); } #[test] @@ -736,10 +786,10 @@ mod test { #[test] fn test_find_funding_output() { // Create a 20f2 multi + another random spk - let multisig_reedemscript = Script::from(Vec::from_hex("5221032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af21039b6347398505f5ec93826dc61c19f47c66c0283ee9be980e29ce325a0f4679ef52ae").unwrap()); + let multisig_redeemscript = Script::from(Vec::from_hex("5221032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af21039b6347398505f5ec93826dc61c19f47c66c0283ee9be980e29ce325a0f4679ef52ae").unwrap()); let another_script = Script::from(Vec::from_hex("020000000156944c5d3f98413ef45cf54545538103cc9f298e0575820ad3591376e2e0f65d2a0000000000000000014871000000000000220020dad1b452caf4a0f26aecf1cc43aaae9b903a043c34f75ad9a36c86317b22236800000000").unwrap()); - let multi_script_pubkey = redeemscript_to_scriptpubkey(&multisig_reedemscript); + let multi_script_pubkey = redeemscript_to_scriptpubkey(&multisig_redeemscript); let another_script_pubkey = redeemscript_to_scriptpubkey(&another_script); // Create the funding transaction @@ -767,11 +817,17 @@ mod test { version: 2, }; + let funding_info = FundingTxInfo { + funding_tx, + multisig_redeemscript, + funding_tx_merkleproof: String::new(), + multisig_nonce: SecretKey::new(&mut OsRng::new().unwrap()), + contract_redeemscript: Script::new(), + hashlock_nonce: SecretKey::new(&mut OsRng::new().unwrap()), + }; + // Check the correct 2of2 multisig output is extracted from funding tx - assert_eq!( - (1u32, &funding_tx.output[1]), - find_funding_output(&funding_tx, &multisig_reedemscript).unwrap() - ); + assert_eq!(1u32, find_funding_output_index(&funding_info).unwrap()); } #[test] @@ -802,7 +858,7 @@ mod test { // Extract contract script data let hashvalue = read_hashvalue_from_contract(&contract_script).unwrap(); - let locktime = read_locktime_from_contract(&contract_script).unwrap(); + let locktime = read_contract_locktime(&contract_script).unwrap(); let (pub1, pub2) = read_pubkeys_from_contract_reedimscript(&contract_script).unwrap(); // Validates if contract outpoint is correct @@ -810,9 +866,9 @@ mod test { &contract_tx.output[0], &pub1, &pub2, - hashvalue, - locktime, - 2 + &hashvalue, + &locktime, + &2 ) .is_ok()); diff --git a/src/protocol/error.rs b/src/protocol/error.rs new file mode 100644 index 00000000..14bff5e6 --- /dev/null +++ b/src/protocol/error.rs @@ -0,0 +1,27 @@ +use bitcoin::secp256k1; + +#[derive(Debug)] +pub enum ContractError { + Keys(bitcoin::util::key::Error), + Secp(secp256k1::Error), + Protocol(&'static str), + Script(bitcoin::blockdata::script::Error), +} + +impl From for ContractError { + fn from(value: secp256k1::Error) -> Self { + Self::Secp(value) + } +} + +impl From for ContractError { + fn from(value: bitcoin::util::key::Error) -> Self { + Self::Keys(value) + } +} + +impl From for ContractError { + fn from(value: bitcoin::blockdata::script::Error) -> Self { + Self::Script(value) + } +} diff --git a/src/protocol/messages.rs b/src/protocol/messages.rs index 23a8e56a..8217651f 100644 --- a/src/protocol/messages.rs +++ b/src/protocol/messages.rs @@ -59,6 +59,8 @@ //! Taker -> Maker2: [`TakerToMakerMessage::RespHashPreimage`] (for Maker2-Taker HTLC). //! Maker2 -> Taker: [`MakerToTakerMessage::RespPrivKeyHandover`] (For Maker2-Taker funding multisig). +use std::fmt::Display; + use bitcoin::{ secp256k1::{SecretKey, Signature}, OutPoint, PublicKey, Script, Transaction, @@ -83,8 +85,8 @@ pub struct GiveOffer; /// Contract Sigs requesting information for the Sender side of the hop. #[derive(Debug, Serialize, Deserialize)] pub struct ContractTxInfoForSender { - pub multisig_key_nonce: SecretKey, - pub hashlock_key_nonce: SecretKey, + pub multisig_nonce: SecretKey, + pub hashlock_nonce: SecretKey, pub timelock_pubkey: PublicKey, pub senders_contract_tx: Transaction, pub multisig_redeemscript: Script, @@ -199,6 +201,23 @@ pub enum TakerToMakerMessage { RespPrivKeyHandover(PrivKeyHandover), } +impl Display for TakerToMakerMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::TakerHello(_) => write!(f, "TakerHello"), + Self::ReqGiveOffer(_) => write!(f, "ReqGiveOffer"), + Self::ReqContractSigsForSender(_) => write!(f, "ReqContractSigsForSender"), + Self::RespProofOfFunding(_) => write!(f, "RespProofOfFunding"), + Self::RespContractSigsForRecvrAndSender(_) => { + write!(f, "RespContractSigsForRecvrAndSender") + } + Self::ReqContractSigsForRecvr(_) => write!(f, "ReqContractSigsForRecvr"), + Self::RespHashPreimage(_) => write!(f, "RespHashPreimage"), + Self::RespPrivKeyHandover(_) => write!(f, "RespPrivKeyHandover"), + } + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct MakerHello { pub protocol_version_min: u32, @@ -221,7 +240,7 @@ pub struct Offer { pub absolute_fee_sat: u64, pub amount_relative_fee_ppb: u64, pub time_relative_fee_ppb: u64, - pub required_confirms: i32, + pub required_confirms: u64, pub minimum_locktime: u16, pub max_size: u64, pub min_size: u64, @@ -275,3 +294,20 @@ pub enum MakerToTakerMessage { /// Send the multisig private keys of the swap, declaring completion of the contract. RespPrivKeyHandover(PrivKeyHandover), } + +impl Display for MakerToTakerMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MakerHello(_) => write!(f, "MakerHello"), + Self::RespOffer(_) => write!(f, "RespOffer"), + Self::RespContractSigsForSender(_) => write!(f, "RespContractSigsForSender"), + Self::ReqContractSigsAsRecvrAndSender(_) => { + write!(f, "ReqContractSigsAsRecvrAndSender") + } + Self::RespContractSigsForRecvr(_) => { + write!(f, "RespContractSigsForRecvr") + } + Self::RespPrivKeyHandover(_) => write!(f, "RespPrivKeyHandover"), + } + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 27b97933..824fb9b8 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,4 +1,5 @@ pub mod contract; +pub mod error; pub mod messages; pub use contract::Hash160; diff --git a/src/scripts/maker.rs b/src/scripts/maker.rs index 53f96d5e..912d1092 100644 --- a/src/scripts/maker.rs +++ b/src/scripts/maker.rs @@ -1,49 +1,30 @@ -use std::{ - convert::TryFrom, - path::PathBuf, - sync::{Arc, RwLock}, -}; +use crate::maker::{start_maker_server, Maker, MakerBehavior}; +use std::{path::PathBuf, sync::Arc}; -use bitcoincore_rpc::Client; +use tokio::sync::RwLock; -use crate::maker::server::{start_maker, MakerBehavior, MakerConfig}; +use crate::{error::TeleportError, wallet::WalletMode}; -use crate::{ - error::TeleportError, - wallet::{RPCConfig, Wallet, WalletMode}, -}; - -pub fn run_maker( +#[tokio::main] +pub async fn run_maker( wallet_file_name: &PathBuf, port: u16, wallet_mode: Option, maker_behavior: MakerBehavior, - kill_flag: Option>>, -) -> Result<(), TeleportError> { - let rpc_config = RPCConfig::default(); - - let rpc = Client::try_from(&rpc_config)?; - - let mut wallet = Wallet::load(&rpc_config, wallet_file_name, wallet_mode)?; - - wallet.sync()?; - - let rpc_ptr = Arc::new(rpc); - let wallet_ptr = Arc::new(RwLock::new(wallet)); - let config = MakerConfig { +) -> Result>, TeleportError> { + // Hardcoded for now, tor doesn't work yet. + let onion_addrs = "myhiddenserviceaddress.onion:6102".to_string(); + let maker = Maker::init( + wallet_file_name, port, - rpc_ping_interval_secs: 60, - watchtower_ping_interval_secs: 300, - directory_servers_refresh_interval_secs: 60 * 60 * 12, //12 hours + onion_addrs, + wallet_mode, maker_behavior, - kill_flag: if kill_flag.is_none() { - Arc::new(RwLock::new(false)) - } else { - kill_flag.unwrap().clone() - }, - idle_connection_timeout: 300, - }; - start_maker(rpc_ptr, wallet_ptr, config); + )?; + + let arc_maker = Arc::new(RwLock::new(maker)); + + start_maker_server(arc_maker.clone()).await?; - Ok(()) + Ok(arc_maker) } diff --git a/src/scripts/wallet.rs b/src/scripts/wallet.rs index c25b495f..cdd72419 100644 --- a/src/scripts/wallet.rs +++ b/src/scripts/wallet.rs @@ -13,7 +13,7 @@ use crate::wallet::{ WalletError, WalletMode, WalletSwapCoin, }; -use crate::protocol::contract::read_locktime_from_contract; +use crate::protocol::contract::read_contract_locktime; use std::iter::repeat; @@ -204,7 +204,7 @@ pub fn display_wallet_balance( utxo.vout, contract_type, if swapcoin.is_hash_preimage_known() { "known" } else { "unknown" }, - read_locktime_from_contract(&swapcoin.get_contract_redeemscript()) + read_contract_locktime(&swapcoin.get_contract_redeemscript()) .expect("unable to read locktime from contract"), utxo.confirmations, utxo.amount @@ -237,7 +237,7 @@ pub fn display_wallet_balance( for (outgoing_swapcoin, utxo) in outgoing_contract_utxos { let txid = utxo.txid.to_hex(); let timelock = - read_locktime_from_contract(&outgoing_swapcoin.contract_redeemscript).unwrap(); + read_contract_locktime(&outgoing_swapcoin.contract_redeemscript).unwrap(); let hashvalue = outgoing_swapcoin.get_hashvalue().to_hex(); #[rustfmt::skip] println!("{}{}{}:{} {}{} {:<8} {:<7} {:<8} {}", @@ -269,7 +269,7 @@ pub fn display_wallet_balance( for (incoming_swapcoin, utxo) in incoming_contract_utxos { let txid = utxo.txid.to_hex(); let timelock = - read_locktime_from_contract(&incoming_swapcoin.contract_redeemscript).unwrap(); + read_contract_locktime(&incoming_swapcoin.contract_redeemscript).unwrap(); let hashvalue = incoming_swapcoin.get_hashvalue().to_hex(); #[rustfmt::skip] println!("{}{}{}:{} {}{} {:<8} {:<7} {:8} {}", diff --git a/src/taker/offers.rs b/src/taker/offers.rs index 6a824839..894a9297 100644 --- a/src/taker/offers.rs +++ b/src/taker/offers.rs @@ -124,14 +124,16 @@ pub async fn sync_offerbook_with_addresses( for addr in maker_addresses { let offers_writer = offers_writer_m.clone(); let taker_config: TakerConfig = config.clone(); - tokio::spawn(async move { - if let Err(_e) = offers_writer - .send(download_maker_offer(addr, taker_config).await) - .await - { - panic!("mpsc failed"); - } - }); + // tokio::spawn(async move { + // if let Err(_e) = offers_writer + // .send(download_maker_offer(addr, taker_config).await) + // .await + // { + // panic!("mpsc failed"); + // } + // }); + let offer = download_maker_offer(addr, taker_config).await; + offers_writer.send(offer).await.unwrap(); } let mut result = Vec::::new(); for _ in 0..maker_addresses_len { diff --git a/src/taker/routines.rs b/src/taker/routines.rs index 90070e37..aa97c6f5 100644 --- a/src/taker/routines.rs +++ b/src/taker/routines.rs @@ -16,8 +16,8 @@ use crate::{ error::TeleportError, protocol::{ contract::{ - calculate_coinswap_fee, create_contract_redeemscript, find_funding_output, - validate_contract_tx, MAKER_FUNDING_TX_VBYTE_SIZE, + calculate_coinswap_fee, create_contract_redeemscript, find_funding_output_index, + validate_contract_tx, FUNDING_TX_VBYTE_SIZE, }, messages::{ ContractSigsAsRecvrAndSender, ContractSigsForRecvr, ContractSigsForSender, @@ -53,7 +53,7 @@ pub async fn handshake_maker<'a>( let mut socket_reader = BufReader::new(reader); send_message( &mut socket_writer, - TakerToMakerMessage::TakerHello(TakerHello { + &TakerToMakerMessage::TakerHello(TakerHello { protocol_version_min: 0, protocol_version_max: 0, }), @@ -89,8 +89,8 @@ pub(crate) async fn req_sigs_for_sender_once( .map( |((&multisig_key_nonce, &hashlock_key_nonce), outgoing_swapcoin)| { ContractTxInfoForSender { - multisig_key_nonce, - hashlock_key_nonce, + multisig_nonce: multisig_key_nonce, + hashlock_nonce: hashlock_key_nonce, timelock_pubkey: outgoing_swapcoin.get_timelock_pubkey(), senders_contract_tx: outgoing_swapcoin.get_contract_tx(), multisig_redeemscript: outgoing_swapcoin.get_multisig_redeemscript(), @@ -101,7 +101,7 @@ pub(crate) async fn req_sigs_for_sender_once( .collect::>(); send_message( &mut socket_writer, - TakerToMakerMessage::ReqContractSigsForSender(ReqContractSigsForSender { + &TakerToMakerMessage::ReqContractSigsForSender(ReqContractSigsForSender { txs_info, hashvalue: outgoing_swapcoins[0].get_hashvalue(), locktime, @@ -146,7 +146,7 @@ pub(crate) async fn req_sigs_for_recvr_once( handshake_maker(&mut socket, maker_address).await?; send_message( &mut socket_writer, - TakerToMakerMessage::ReqContractSigsForRecvr(ReqContractSigsForRecvr { + &TakerToMakerMessage::ReqContractSigsForRecvr(ReqContractSigsForRecvr { txs: incoming_swapcoins .iter() .zip(receivers_contract_txes.iter()) @@ -200,7 +200,7 @@ pub(crate) async fn send_proof_of_funding_and_init_next_hop( ) -> Result<(ContractSigsAsRecvrAndSender, Vec