From 4fa3b5fd495e13be4acb08b20aacada34472329f Mon Sep 17 00:00:00 2001 From: fbock Date: Thu, 15 Aug 2024 15:41:04 +0200 Subject: [PATCH] use map_err for coordinator mod error handling, add endpoints for partitial sig --- .../coordinator/src/communication/api.rs | 7 + .../coordinator/src/communication/mod.rs | 26 +++ .../coordinator/src/coordinator/mod.rs | 197 +++++++++--------- .../coordinator/src/database/mod.rs | 83 +++++++- .../coordinator/src/wallet/payout_tx.rs | 4 + taptrade-cli-demo/trader/src/trading/mod.rs | 23 +- taptrade-cli-demo/trader/src/wallet/mod.rs | 11 +- todos.md | 3 +- 8 files changed, 239 insertions(+), 115 deletions(-) diff --git a/taptrade-cli-demo/coordinator/src/communication/api.rs b/taptrade-cli-demo/coordinator/src/communication/api.rs index 715e046..3af2c00 100644 --- a/taptrade-cli-demo/coordinator/src/communication/api.rs +++ b/taptrade-cli-demo/coordinator/src/communication/api.rs @@ -103,3 +103,10 @@ pub struct TradeObligationsUnsatisfied { pub robohash_hex: String, pub offer_id_hex: String, } + +#[derive(Debug, Deserialize)] +pub struct PayoutSignatureRequest { + pub partitial_sig_hex: String, + pub offer_id_hex: String, + pub robohash_hex: String, +} diff --git a/taptrade-cli-demo/coordinator/src/communication/mod.rs b/taptrade-cli-demo/coordinator/src/communication/mod.rs index a4f18cc..ded9b70 100755 --- a/taptrade-cli-demo/coordinator/src/communication/mod.rs +++ b/taptrade-cli-demo/coordinator/src/communication/mod.rs @@ -260,6 +260,31 @@ async fn poll_final_payout( } } +async fn submit_payout_signature( + Extension(coordinator): Extension>, + Json(payload): Json, +) -> Result { + match handle_payout_signature(&payload, coordinator).await { + Ok(_) => Ok(StatusCode::OK.into_response()), + // Err(RequestError::NotConfirmed) => { + // info!("Offer tx for final payout not confirmed"); + // Ok(StatusCode::NOT_ACCEPTABLE.into_response()) + // } + // Err(RequestError::NotFound) => { + // info!("Offer for final payout not found"); + // Ok(StatusCode::NOT_FOUND.into_response()) + // } + // Err(RequestError::Database(e)) => { + // error!("Database error fetching final payout: {e}"); + // Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + // } + _ => { + error!("Unknown error handling submit_payout_signature()"); + Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + } + } +} + async fn test_api() -> &'static str { "Hello, World!" } @@ -280,6 +305,7 @@ pub async fn api_server(coordinator: Arc) -> Result<()> { ) .route("/request-escrow", post(request_escrow)) .route("/poll-final-payout", post(poll_final_payout)) + .route("/submit-payout-signature", post(submit_payout_signature)) .layer(Extension(coordinator)); // add other routes here diff --git a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs index 916c60a..e6a6b05 100755 --- a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs @@ -37,51 +37,28 @@ pub async fn handle_maker_bond( let wallet = &coordinator.coordinator_wallet; let database = &coordinator.coordinator_db; - let bond_requirements = if let Ok(requirements) = database + let bond_requirements = database .fetch_bond_requirements(&payload.robohash_hex) .await - { - requirements - } else { - return Err(BondError::BondNotFound); - }; + .map_err(|_| BondError::BondNotFound)?; - match wallet + wallet .validate_bond_tx_hex(&payload.signed_bond_hex, &bond_requirements) .await - { - Ok(()) => (), - Err(e) => { - return Err(BondError::InvalidBond(e.to_string())); - } - } + .map_err(|e| BondError::InvalidBond(e.to_string()))?; debug!("\nBond validation successful"); let offer_id_hex: String = generate_random_order_id(16); // 16 bytes random offer id, maybe a different system makes more sense later on? (uuid or increasing counter...) // create address for taker bond - let new_taker_bond_address = match wallet.get_new_address().await { - Ok(address) => address, - Err(e) => { - let error = format!( - "Error generating taker bond address for offer id: {}. Error: {e}", - offer_id_hex - ); - return Err(BondError::CoordinatorError(error.to_string())); - } - }; - // insert bond into sql database and move offer to different table - let bond_locked_until_timestamp = match database + let new_taker_bond_address = wallet + .get_new_address() + .await + .map_err(|e| BondError::CoordinatorError(e.to_string()))?; + + let bond_locked_until_timestamp = database .move_offer_to_active(payload, &offer_id_hex, new_taker_bond_address) .await - { - Ok(timestamp) => timestamp, - Err(e) => { - debug!( - "Error in validate_bond_tx_hex in move_offer_to_active: {}", - e - ); - return Err(BondError::CoordinatorError(e.to_string())); - } - }; + .map_err(|e| BondError::CoordinatorError(e.to_string()))?; + Ok(OfferActivatedResponse { bond_locked_until_timestamp, offer_id_hex, @@ -94,12 +71,11 @@ pub async fn get_public_offers( ) -> Result { let database = &coordinator.coordinator_db; - let offers = match database.fetch_suitable_offers(request).await { - Ok(offers) => offers, - Err(e) => { - return Err(FetchOffersError::Database(e.to_string())); - } - }; + let offers = database + .fetch_suitable_offers(request) + .await + .map_err(|e| FetchOffersError::Database(e.to_string()))?; + if offers.is_none() { return Err(FetchOffersError::NoOffersAvailable); } @@ -115,41 +91,30 @@ pub async fn handle_taker_bond( let bond_requirements = database .fetch_taker_bond_requirements(&payload.offer.offer_id_hex) - .await; + .await + .map_err(|_| BondError::BondNotFound)?; + + wallet + .validate_bond_tx_hex(&payload.trade_data.signed_bond_hex, &bond_requirements) + .await + .map_err(|e| BondError::InvalidBond(e.to_string()))?; - match bond_requirements { - Ok(bond_requirements) => { - match wallet - .validate_bond_tx_hex(&payload.trade_data.signed_bond_hex, &bond_requirements) - .await - { - Ok(()) => (), - Err(e) => { - return Err(BondError::InvalidBond(e.to_string())); - } - } - } - Err(_) => return Err(BondError::BondNotFound), - } debug!("\nTaker bond validation successful"); - let escrow_output_data = match wallet.create_escrow_psbt(database, &payload).await { - Ok(escrow_output_data) => escrow_output_data, - Err(e) => { - return Err(BondError::CoordinatorError(e.to_string())); - } - }; + let escrow_output_data = wallet + .create_escrow_psbt(database, &payload) + .await + .map_err(|e| BondError::CoordinatorError(e.to_string()))?; debug!( "\nEscrow PSBT creation successful: {:?}", escrow_output_data ); - if let Err(e) = database + database .add_taker_info_and_move_table(payload, &escrow_output_data) .await - { - return Err(BondError::CoordinatorError(e.to_string())); - } + .map_err(|e| BondError::CoordinatorError(e.to_string()))?; + trace!("Taker information added to database and moved table successfully"); Ok(OfferTakenResponse { escrow_psbt_hex: escrow_output_data.escrow_psbt_hex, @@ -209,13 +174,10 @@ pub async fn fetch_escrow_confirmation_status( Err(e) => return Err(FetchEscrowConfirmationError::Database(e.to_string())), } - match database + database .fetch_escrow_tx_confirmation_status(&payload.offer_id_hex) .await - { - Ok(status) => Ok(status), - Err(e) => return Err(FetchEscrowConfirmationError::Database(e.to_string())), - } + .map_err(|e| FetchEscrowConfirmationError::Database(e.to_string())) } pub async fn handle_signed_escrow_psbt( @@ -234,13 +196,11 @@ pub async fn handle_signed_escrow_psbt( Err(e) => return Err(RequestError::Database(e.to_string())), }; - match wallet + wallet .validate_escrow_init_psbt(&payload.signed_psbt_hex) .await - { - Ok(()) => (), - Err(e) => return Err(RequestError::PsbtInvalid(e.to_string())), - }; + .map_err(|e| RequestError::PsbtInvalid(e.to_string()))?; + match database.insert_signed_escrow_psbt(payload).await { Ok(false) => return Err(RequestError::PsbtAlreadySubmitted), Ok(true) => (), @@ -257,12 +217,10 @@ pub async fn handle_signed_escrow_psbt( Err(e) => return Err(RequestError::Database(e.to_string())), }; - if let Err(e) = wallet + wallet .combine_and_broadcast_escrow_psbt(&maker_psbt, &taker_psbt) .await - { - return Err(RequestError::PsbtInvalid(e.to_string())); - } + .map_err(|e| RequestError::PsbtInvalid(e.to_string()))?; Ok(()) } @@ -274,12 +232,10 @@ pub async fn handle_obligation_confirmation( let database = &coordinator.coordinator_db; check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?; - if let Err(e) = database + database .set_trader_happy_field(&payload.offer_id_hex, &payload.robohash_hex, true) .await - { - return Err(RequestError::Database(e.to_string())); - } + .map_err(|e| RequestError::Database(e.to_string()))?; Ok(()) } @@ -290,13 +246,10 @@ pub async fn initiate_escrow( let database = &coordinator.coordinator_db; check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?; - - if let Err(e) = database + database .set_trader_happy_field(&payload.offer_id_hex, &payload.robohash_hex, false) .await - { - return Err(RequestError::Database(e.to_string())); - } + .map_err(|e| RequestError::Database(e.to_string()))?; Ok(()) } @@ -307,29 +260,30 @@ pub async fn handle_final_payout( ) -> Result { let database = &coordinator.coordinator_db; - check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?; - - let trader_happiness = match database.fetch_trader_happiness(&payload.offer_id_hex).await { - Ok(happiness) => happiness, - Err(e) => return Err(RequestError::Database(e.to_string())), - }; + let trader_happiness = database + .fetch_trader_happiness(&payload.offer_id_hex) + .await + .map_err(|e| RequestError::Database(e.to_string()))?; if trader_happiness.maker_happy.is_some_and(|x| x) && trader_happiness.taker_happy.is_some_and(|x| x) { - let escrow_payout_data = match database.fetch_payout_data(&payload.offer_id_hex).await { - Ok(payout_data) => payout_data, - Err(e) => return Err(RequestError::Database(e.to_string())), - }; + let escrow_payout_data = database + .fetch_payout_data(&payload.offer_id_hex) + .await + .map_err(|e| RequestError::Database(e.to_string()))?; - let payout_keyspend_psbt_hex = match coordinator + let payout_keyspend_psbt_hex = coordinator .coordinator_wallet .assemble_keyspend_payout_psbt(&escrow_payout_data) .await - { - Ok(psbt_hex) => psbt_hex, - Err(e) => return Err(RequestError::CoordinatorError(e.to_string())), - }; + .map_err(|e| RequestError::CoordinatorError(e.to_string()))?; + + database + .insert_keyspend_payout_psbt(&payload.offer_id_hex, &payout_keyspend_psbt_hex) + .await + .map_err(|e| RequestError::Database(e.to_string()))?; + return Ok(PayoutProcessingResult::ReadyPSBT(PayoutResponse { payout_psbt_hex: payout_keyspend_psbt_hex, agg_musig_nonce_hex: escrow_payout_data.agg_musig_nonce.to_string(), @@ -368,3 +322,38 @@ pub async fn handle_final_payout( Ok(PayoutProcessingResult::DecidingEscrow) } } + +pub async fn handle_payout_signature( + payload: &PayoutSignatureRequest, + coordinator: Arc, +) -> Result<(), RequestError> { + let database = &coordinator.coordinator_db; + let wallet = &coordinator.coordinator_wallet; + + check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?; + + let (maker_partitial_sig_hex, taker_partitial_sig_hex, payout_psbt_hex) = match database + .insert_partitial_sig_and_fetch_if_both( + &payload.partitial_sig_hex, + &payload.offer_id_hex, + &payload.robohash_hex, + ) + .await + { + Ok(Some((maker_partitial_sig, taker_partitial_sig, payout_transaction_psbt_hex))) => ( + maker_partitial_sig, + taker_partitial_sig, + bdk::bitcoin::psbt::PartiallySignedTransaction::deserialize( + &hex::decode(payout_transaction_psbt_hex) + .map_err(|e| RequestError::CoordinatorError(e.to_string()))?, + ), + ), + Ok(None) => return Ok(()), + Err(e) => return Err(RequestError::Database(e.to_string())), + }; + + let aggregated_signature = wallet + .aggregate_partitial_signatures(&maker_partitial_sig_hex, &taker_partitial_sig_hex)?; + + Ok(()) +} diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index 908e556..c1926c5 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -137,6 +137,8 @@ impl CoordinatorDB { musig_pubkey_compressed_hex_maker TEXT NOT NULL, musig_pub_nonce_hex_taker TEXT NOT NULL, musig_pubkey_compressed_hex_taker TEXT NOT NULL, + musig_partitial_sig_hex_maker TEXT, + musig_partitial_sig_hex_taker TEXT, escrow_psbt_hex TEXT NOT NULL, escrow_psbt_txid TEXT NOT NULL, signed_escrow_psbt_hex_maker TEXT, @@ -150,7 +152,8 @@ impl CoordinatorDB { escrow_amount_maker_sat INTEGER, escrow_amount_taker_sat INTEGER, escrow_fee_per_participant INTEGER, - escrow_output_descriptor TEXT + escrow_output_descriptor TEXT, + payout_transaction_psbt_hex TEXT )", // escrow_psbt_is_confirmed will be set 1 once the escrow psbt is confirmed onchain ) .execute(&db_pool) @@ -890,4 +893,82 @@ impl CoordinatorDB { musig_pubkey_hex_taker, ) } + + pub async fn insert_keyspend_payout_psbt( + &self, + offer_id_hex: &str, + payout_psbt_hex: &str, + ) -> Result<()> { + sqlx::query("UPDATE taken_offers SET payout_transaction_psbt_hex = ? WHERE offer_id = ?") + .bind(payout_psbt_hex) + .bind(offer_id_hex) + .execute(&*self.db_pool) + .await?; + Ok(()) + } + + pub async fn insert_partitial_sig_and_fetch_if_both( + &self, + partitial_sig_hex: &str, + offer_id_hex: &str, + robohash_hex: &str, + ) -> Result> { + // first check if the escrow psbt has already been submitted + let is_maker = self + .is_maker_in_taken_offers(offer_id_hex, robohash_hex) + .await?; + + let is_already_there = match is_maker { + true => { + let status = sqlx::query( + "SELECT musig_partitial_sig_maker FROM taken_offers WHERE offer_id = ?", + ) + .bind(offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + status + .get::, _>("musig_partitial_sig_maker") + .is_some() + } + false => { + let status = sqlx::query( + "SELECT musig_partitial_sig_taker FROM taken_offers WHERE offer_id = ?", + ) + .bind(offer_id_hex) + .fetch_one(&*self.db_pool) + .await?; + status + .get::, _>("musig_partitial_sig_taker") + .is_some() + } + }; + + if is_already_there { + return Err(anyhow!("Partial sig already submitted")); + } else { + let query = if is_maker { + "UPDATE taken_offers SET musig_partitial_sig_maker = ? WHERE offer_id = ?" + } else { + "UPDATE taken_offers SET musig_partitial_sig_taker = ? WHERE offer_id = ?" + }; + sqlx::query(query) + .bind(partitial_sig_hex) + .bind(offer_id_hex) + .execute(&*self.db_pool) + .await?; + } + + let row = sqlx::query( + "SELECT musig_partitial_sig_maker, musig_partitial_sig_taker, payout_transaction_psbt_hex FROM taken_offers WHERE offer_id = ?", + ).bind(offer_id_hex).fetch_one(&*self.db_pool).await?; + + let maker_sig: Option = row.try_get("musig_partitial_sig_maker")?; + let taker_sig: Option = row.try_get("musig_partitial_sig_taker")?; + let payout_tx_hex: String = row.try_get("payout_transaction_psbt_hex")?; + if let (Some(maker), Some(taker)) = (maker_sig, taker_sig) { + Ok(Some((maker, taker, payout_tx_hex))) + } else { + Ok(None) + } + } } diff --git a/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs b/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs index fd46681..bc5190d 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs @@ -14,6 +14,10 @@ fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> Ok((tx_fee_abs, tx_fee_abs / 2)) } +pub fn aggregate_partitial_signatures() -> anyhow::Result { + Ok(()) +} + impl CoordinatorWallet { fn get_escrow_utxo( &self, diff --git a/taptrade-cli-demo/trader/src/trading/mod.rs b/taptrade-cli-demo/trader/src/trading/mod.rs index 523b788..151c12d 100644 --- a/taptrade-cli-demo/trader/src/trading/mod.rs +++ b/taptrade-cli-demo/trader/src/trading/mod.rs @@ -22,6 +22,7 @@ use bdk::{ database::MemoryDatabase, wallet::AddressInfo, }; +use reqwest::header::ACCEPT_LANGUAGE; use std::{str::FromStr, thread, time::Duration}; pub fn run_maker(maker_config: &TraderSettings) -> Result<()> { @@ -52,10 +53,15 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> { info!("Waiting for other party to confirm the trade."); let (payout_keyspend_psbt, agg_pub_nonce, agg_pubk_ctx) = IsOfferReadyRequest::poll_payout(maker_config, &offer)?; - + debug!("Payout PSBT received: {}", &payout_keyspend_psbt); let signed_payout_psbt = wallet .validate_payout_psbt(&payout_keyspend_psbt)? - .sign_keyspend_payout_psbt(payout_keyspend_psbt, agg_pubk_ctx, agg_pub_nonce, local_musig_state: &offer.used_musig_config)?; + .sign_keyspend_payout_psbt( + payout_keyspend_psbt, + agg_pubk_ctx, + agg_pub_nonce, + offer.used_musig_config, + )?; // submit signed payout psbt back to coordinator panic!("Payout to be implemented!"); } else { @@ -86,7 +92,18 @@ pub fn run_taker(taker_config: &TraderSettings) -> Result<()> { TradeObligationsSatisfied::submit(&accepted_offer.offer_id_hex, taker_config)?; debug!("Waiting for other party to confirm the trade."); // pull for other parties confirmation, then receive the transaction to create MuSig signature for (keyspend) to payout address - let payout_keyspend_psbt = IsOfferReadyRequest::poll_payout(taker_config, &accepted_offer)?; + let (payout_keyspend_psbt, agg_pub_nonce, agg_pubk_ctx) = + IsOfferReadyRequest::poll_payout(taker_config, &accepted_offer)?; + + debug!("Received payout psbt: {}", &payout_keyspend_psbt); + let signed_payout_psbt = wallet + .validate_payout_psbt(&payout_keyspend_psbt)? + .sign_keyspend_payout_psbt( + payout_keyspend_psbt, + agg_pubk_ctx, + agg_pub_nonce, + accepted_offer.used_musig_config, + )?; // here we need to handle if the other party is not cooperating } else { error!("Trade failed."); diff --git a/taptrade-cli-demo/trader/src/wallet/mod.rs b/taptrade-cli-demo/trader/src/wallet/mod.rs index 3a41c02..5ab38db 100644 --- a/taptrade-cli-demo/trader/src/wallet/mod.rs +++ b/taptrade-cli-demo/trader/src/wallet/mod.rs @@ -218,10 +218,9 @@ impl TradingWallet { validated_payout_psbt: PartiallySignedTransaction, key_agg_context: KeyAggContext, agg_pub_nonce: AggNonce, - local_musig_state: &MuSigData, + local_musig_state: MuSigData, ) -> Result { - let payout_tx = validated_payout_psbt.extract_tx(); - let mut sig_hash_cache = SighashCache::new(payout_tx); + let mut sig_hash_cache = SighashCache::new(validated_payout_psbt.unsigned_tx.clone()); let utxo = validated_payout_psbt .iter_funding_utxos() @@ -230,10 +229,10 @@ impl TradingWallet { .clone(); // get the msg (sighash) to sign with the musig key - let keyspend_sig_hash_msg = sig_hash_cache + let binding = sig_hash_cache .taproot_key_spend_signature_hash(0, &Prevouts::All(&[utxo]), TapSighashType::All) - .context("Failed to create keyspend sighash")? - .as_byte_array(); + .context("Failed to create keyspend sighash")?; + let keyspend_sig_hash_msg = binding.as_byte_array(); let secret_nonce = local_musig_state.nonce.get_sec_for_signing()?; let seckey = local_musig_state.secret_key; diff --git a/todos.md b/todos.md index a6e1afd..324cc21 100644 --- a/todos.md +++ b/todos.md @@ -1,8 +1,9 @@ Thinks to improve when implementing the production ready library/coordinator: +* secure user authentification scheme for calls / unique trade ids * make api more generic (smaller) / maybe use websockets * review escrow output descriptor, maybe make it smaller(less specific cases, more generic)? * maybe hard code descriptor instead of compiling it from pieces? -* review for security flaws (error handling, logic bugs) +* review for security flaws (error handling, logic bugs, crypto bugs) * maybe switch wallet completely to core rpc instead of bdk wallet + core rpc * api rate limiting (e.g. backoff) ? * build trader toolkit to get funds out of escrow if coordinator dissapears