From b4b136e183cc008ef109d40d7d0a9b73e4d6481c Mon Sep 17 00:00:00 2001 From: fbock Date: Wed, 14 Aug 2024 14:11:52 +0200 Subject: [PATCH] finish keyspend psbt creation on coordinator, begin signing on client --- .../coordinator/src/communication/api.rs | 6 ++ .../src/communication/handler_errors.rs | 1 + .../coordinator/src/communication/mod.rs | 4 +- .../src/coordinator/coordinator_utils.rs | 67 ++++++++++++++++++- .../coordinator/src/coordinator/mod.rs | 23 +++++-- .../coordinator/src/database/mod.rs | 31 +++++++++ taptrade-cli-demo/coordinator/src/main.rs | 1 + .../coordinator/src/wallet/escrow_psbt.rs | 1 - .../coordinator/src/wallet/payout_tx.rs | 52 +++++++++++--- .../trader/src/communication/api.rs | 6 +- .../trader/src/communication/mod.rs | 12 ++-- taptrade-cli-demo/trader/src/trading/mod.rs | 8 ++- taptrade-cli-demo/trader/src/wallet/mod.rs | 17 +++++ 13 files changed, 201 insertions(+), 28 deletions(-) diff --git a/taptrade-cli-demo/coordinator/src/communication/api.rs b/taptrade-cli-demo/coordinator/src/communication/api.rs index f1b4ec6..2df76ad 100644 --- a/taptrade-cli-demo/coordinator/src/communication/api.rs +++ b/taptrade-cli-demo/coordinator/src/communication/api.rs @@ -91,6 +91,12 @@ pub struct PsbtSubmissionRequest { pub robohash_hex: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutResponse { + pub payout_psbt_hex: String, + pub agg_musig_nonce_hex: String, +} + #[derive(Debug, Serialize, Deserialize)] pub struct TradeObligationsUnsatisfied { pub robohash_hex: String, diff --git a/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs b/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs index 066303b..344236d 100644 --- a/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs +++ b/taptrade-cli-demo/coordinator/src/communication/handler_errors.rs @@ -21,6 +21,7 @@ pub enum FetchEscrowConfirmationError { pub enum RequestError { Database(String), NotConfirmed, + CoordinatorError(String), NotFound, PsbtAlreadySubmitted, PsbtInvalid(String), diff --git a/taptrade-cli-demo/coordinator/src/communication/mod.rs b/taptrade-cli-demo/coordinator/src/communication/mod.rs index b2c7493..a4f18cc 100755 --- a/taptrade-cli-demo/coordinator/src/communication/mod.rs +++ b/taptrade-cli-demo/coordinator/src/communication/mod.rs @@ -237,7 +237,9 @@ async fn poll_final_payout( match handle_final_payout(&payload, coordinator).await { Ok(PayoutProcessingResult::NotReady) => Ok(StatusCode::ACCEPTED.into_response()), Ok(PayoutProcessingResult::LostEscrow) => Ok(StatusCode::GONE.into_response()), - Ok(PayoutProcessingResult::ReadyPSBT(psbt)) => Ok(psbt.into_response()), + Ok(PayoutProcessingResult::ReadyPSBT(psbt_and_nonce)) => { + Ok(Json(psbt_and_nonce).into_response()) + } Ok(PayoutProcessingResult::DecidingEscrow) => Ok(StatusCode::PROCESSING.into_response()), Err(RequestError::NotConfirmed) => { info!("Offer tx for final payout not confirmed"); diff --git a/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs b/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs index 6f2b163..b178e19 100644 --- a/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/coordinator_utils.rs @@ -1,13 +1,78 @@ +use std::str::FromStr; + +use anyhow::Context; +use bdk::{ + bitcoin::{key::XOnlyPublicKey, Address}, + miniscript::Descriptor, +}; + use super::*; #[derive(Debug)] pub enum PayoutProcessingResult { - ReadyPSBT(String), + ReadyPSBT(PayoutResponse), NotReady, LostEscrow, DecidingEscrow, } +#[derive(Debug)] +pub struct PayoutData { + pub escrow_output_descriptor: Descriptor, + pub payout_address_maker: Address, + pub payout_address_taker: Address, + pub payout_amount_maker: u64, + pub payout_amount_taker: u64, + pub agg_musig_nonce: MusigAggNonce, +} + +impl PayoutData { + pub fn new_from_strings( + escrow_output_descriptor: &str, + payout_address_maker: &str, + payout_address_taker: &str, + payout_amount_maker: u64, + payout_amount_taker: u64, + musig_pub_nonce_hex_maker: &str, + musig_pub_nonce_hex_taker: &str, + ) -> Result { + let musig_pub_nonce_maker = match MusigPubNonce::from_hex(musig_pub_nonce_hex_maker) { + Ok(musig_pub_nonce_maker) => musig_pub_nonce_maker, + Err(e) => { + return Err(anyhow!( + "Error decoding maker musig pub nonce: {}", + e.to_string() + )) + } + }; + let musig_pub_nonce_taker = match MusigPubNonce::from_hex(musig_pub_nonce_hex_taker) { + Ok(musig_pub_nonce_taker) => musig_pub_nonce_taker, + Err(e) => { + return Err(anyhow!( + "Error decoding taker musig pub nonce: {}", + e.to_string() + )) + } + }; + + let agg_musig_nonce: MusigAggNonce = + musig2::AggNonce::sum([musig_pub_nonce_maker, musig_pub_nonce_taker]); + + Ok(Self { + escrow_output_descriptor: Descriptor::from_str(escrow_output_descriptor)?, + payout_address_maker: Address::from_str(payout_address_maker)? + .require_network(bdk::bitcoin::Network::Regtest) + .context("Maker payout address wrong network")?, + payout_address_taker: Address::from_str(payout_address_taker)? + .require_network(bdk::bitcoin::Network::Regtest) + .context("Taker payout address wrong Network")?, + payout_amount_maker, + payout_amount_taker, + agg_musig_nonce, + }) + } +} + pub fn generate_random_order_id(len: usize) -> String { // Generate `len` random bytes let bytes: Vec = rand::thread_rng() diff --git a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs index 6930096..852034e 100755 --- a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs @@ -317,12 +317,23 @@ pub async fn handle_final_payout( if trader_happiness.maker_happy.is_some_and(|x| x) && trader_happiness.taker_happy.is_some_and(|x| x) { - panic!("Implement wallet.assemble_keyspend_payout_psbt()"); - // let payout_keyspend_psbt_hex = wallet - // .assemble_keyspend_payout_psbt(&payload.offer_id_hex, &payload.robohash_hex) - // .await - // .context("Error assembling payout PSBT")?; - // return Ok(PayoutProcessingResult::ReadyPSBT(payout_keyspend_psbt_hex)); + 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 payout_keyspend_psbt_hex = match 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())), + }; + return Ok(PayoutProcessingResult::ReadyPSBT(PayoutResponse { + payout_psbt_hex: payout_keyspend_psbt_hex, + agg_musig_nonce_hex: escrow_payout_data.agg_musig_nonce.to_string(), + })); } else if (trader_happiness.maker_happy.is_none() || trader_happiness.taker_happy.is_none()) && !trader_happiness.escrow_ongoing { diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index cc53950..24157c8 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -853,4 +853,35 @@ impl CoordinatorDB { musig_pubkey_compressed_hex: row.get("musig_pubkey_hex"), }) } + + pub async fn fetch_payout_data(&self, trade_id: &str) -> Result { + let row = sqlx::query( + "SELECT escrow_output_descriptor, payout_address_maker, + payout_address_taker, musig_pub_nonce_hex_maker, musig_pub_nonce_hex_taker, + escrow_amount_maker_sat, escrow_amount_taker_sat + FROM taken_offers WHERE offer_id = ?", + ) + .bind(trade_id) + .fetch_one(&*self.db_pool) + .await + .context("SQL query to fetch escrow_ouput_descriptor failed.")?; + + let escrow_output_descriptor = row.try_get("escrow_output_descriptor")?; + let payout_address_maker = row.try_get("payout_address_maker")?; + let payout_address_taker = row.try_get("payout_address_taker")?; + let musig_pub_nonce_hex_maker: &str = row.try_get("musig_pub_nonce_hex_maker")?; + let musig_pub_nonce_hex_taker: &str = row.try_get("musig_pub_nonce_hex_taker")?; + let payout_amount_maker: u64 = row.try_get::("escrow_amount_maker_sat")? as u64; + let payout_amount_taker: u64 = row.try_get::("escrow_amount_taker_sat")? as u64; + + PayoutData::new_from_strings( + escrow_output_descriptor, + payout_address_maker, + payout_address_taker, + payout_amount_maker, + payout_amount_taker, + musig_pub_nonce_hex_maker, + musig_pub_nonce_hex_taker, + ) + } } diff --git a/taptrade-cli-demo/coordinator/src/main.rs b/taptrade-cli-demo/coordinator/src/main.rs index 90176cd..5286c97 100755 --- a/taptrade-cli-demo/coordinator/src/main.rs +++ b/taptrade-cli-demo/coordinator/src/main.rs @@ -13,6 +13,7 @@ use coordinator::{ use database::CoordinatorDB; use dotenvy::dotenv; use log::{debug, error, info, trace, warn}; +use musig2::{AggNonce as MusigAggNonce, PubNonce as MusigPubNonce}; use rand::Rng; use std::{ env, diff --git a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs index 987571a..eb8d281 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs @@ -1,5 +1,4 @@ use super::*; -use axum::routing::trace; use bdk::{ bitcoin::{psbt::PartiallySignedTransaction, PublicKey}, descriptor::{policy, Descriptor}, diff --git a/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs b/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs index dbb2d5c..fd46681 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/payout_tx.rs @@ -1,9 +1,9 @@ -use std::ops::Add; - /// construction of the transaction spending the escrow output after a successfull trade as keyspend transaction use super::*; +use bdk::bitcoin::psbt::Input; use bdk::bitcoin::psbt::PartiallySignedTransaction; use bdk::bitcoin::OutPoint; +use bdk::miniscript::Descriptor; fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> { let feerate = blockchain_backend.estimate_fee(6)?; @@ -15,24 +15,56 @@ fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> } impl CoordinatorWallet { + fn get_escrow_utxo( + &self, + descriptor: &Descriptor, + ) -> anyhow::Result<(Input, OutPoint)> { + let temp_wallet = Wallet::new( + &descriptor.to_string(), + None, + bdk::bitcoin::Network::Regtest, + MemoryDatabase::new(), + )?; + temp_wallet.sync(&self.backend, SyncOptions::default())?; + let available_utxos = temp_wallet.list_unspent()?; + if available_utxos.len() != 1 { + return Err(anyhow!("Expected exactly one utxo for escrow output")); + }; + + let input = temp_wallet.get_psbt_input(available_utxos[0].clone(), None, false)?; + let outpoint = available_utxos[0].outpoint; + Ok((input, outpoint)) + } + pub async fn assemble_keyspend_payout_psbt( &self, - escrow_output: OutPoint, - payout_addresses: HashMap, - ) -> Result { + payout_information: &PayoutData, + ) -> anyhow::Result { + let (escrow_utxo_psbt_input, escrow_utxo_outpoint) = + self.get_escrow_utxo(&payout_information.escrow_output_descriptor)?; + let (payout_psbt, _) = { let wallet = self.wallet.lock().await; let mut builder = wallet.build_tx(); let (tx_fee_abs, tx_fee_abs_sat_per_user) = get_tx_fees_abs_sat(&self.backend)?; - builder.add_utxo(escrow_output)?; - for (address, amount) in payout_addresses { - builder.add_recipient(address.script_pubkey(), amount - tx_fee_abs_sat_per_user); - } + // why 264 wu?: see escrow_psbt.tx + builder.add_foreign_utxo(escrow_utxo_outpoint, escrow_utxo_psbt_input, 264)?; + + builder.add_recipient( + payout_information.payout_address_maker.script_pubkey(), + payout_information.payout_amount_maker - tx_fee_abs_sat_per_user, + ); + + builder.add_recipient( + payout_information.payout_address_taker.script_pubkey(), + payout_information.payout_amount_taker - tx_fee_abs_sat_per_user, + ); + builder.fee_absolute(tx_fee_abs); builder.finish()? }; - Ok(payout_psbt) + Ok(payout_psbt.serialize_hex()) } } diff --git a/taptrade-cli-demo/trader/src/communication/api.rs b/taptrade-cli-demo/trader/src/communication/api.rs index 95b3415..ab02afc 100644 --- a/taptrade-cli-demo/trader/src/communication/api.rs +++ b/taptrade-cli-demo/trader/src/communication/api.rs @@ -110,11 +110,11 @@ pub struct TradeObligationsSatisfied { pub offer_id_hex: String, } -#[derive(Debug, Deserialize)] -pub struct PayoutPsbtResponse { +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutResponse { pub payout_psbt_hex: String, + pub agg_musig_nonce_hex: String, } - #[derive(Debug, Serialize)] pub struct TradeObligationsUnsatisfied { pub robohash_hex: String, diff --git a/taptrade-cli-demo/trader/src/communication/mod.rs b/taptrade-cli-demo/trader/src/communication/mod.rs index cc3f841..9669283 100644 --- a/taptrade-cli-demo/trader/src/communication/mod.rs +++ b/taptrade-cli-demo/trader/src/communication/mod.rs @@ -14,6 +14,7 @@ use bdk::{ bitcoin::{consensus::Encodable, psbt::PartiallySignedTransaction}, wallet::AddressInfo, }; +use musig2::AggNonce; use serde::{Deserialize, Serialize}; use std::{f32::consts::E, str::FromStr, thread::sleep, time::Duration}; @@ -247,7 +248,7 @@ impl IsOfferReadyRequest { pub fn poll_payout( trader_config: &TraderSettings, offer: &ActiveOffer, - ) -> Result> { + ) -> Result<(PartiallySignedTransaction, AggNonce)> { let request = IsOfferReadyRequest { robohash_hex: trader_config.robosats_robohash_hex.clone(), offer_id_hex: offer.offer_id_hex.clone(), @@ -290,10 +291,11 @@ impl IsOfferReadyRequest { )); } } - let final_psbt = PartiallySignedTransaction::from_str( - &res.json::()?.payout_psbt_hex, - )?; - Ok(Some(final_psbt)) + let payout_response: PayoutResponse = res.json()?; + let final_psbt = PartiallySignedTransaction::from_str(&payout_response.payout_psbt_hex)?; + let agg_nonce = AggNonce::from_str(&payout_response.agg_musig_nonce_hex) + .map_err(|e| anyhow!("Error parsing agg nonce: {}", e))?; + Ok((final_psbt, agg_nonce)) } } diff --git a/taptrade-cli-demo/trader/src/trading/mod.rs b/taptrade-cli-demo/trader/src/trading/mod.rs index 276626b..44634aa 100644 --- a/taptrade-cli-demo/trader/src/trading/mod.rs +++ b/taptrade-cli-demo/trader/src/trading/mod.rs @@ -50,7 +50,13 @@ pub fn run_maker(maker_config: &TraderSettings) -> Result<()> { // this represents the "confirm payment" / "confirm fiat recieved" button TradeObligationsSatisfied::submit(&offer.offer_id_hex, maker_config)?; info!("Waiting for other party to confirm the trade."); - let payout_keyspend_psbt = IsOfferReadyRequest::poll_payout(maker_config, &offer)?; + let (payout_keyspend_psbt, agg_pub_nonce) = + IsOfferReadyRequest::poll_payout(maker_config, &offer)?; + let signed_payout_psbt = wallet + .validate_payout_psbt(&payout_keyspend_psbt)? + .sign_payout_psbt(payout_keyspend_psbt, agg_pub_nonce)?; + // submit signed payout psbt back to coordinator + panic!("Payout to be implemented!"); } else { error!("Trade failed. Initiating escrow mode."); TradeObligationsUnsatisfied::request_escrow(&offer.offer_id_hex, maker_config)?; diff --git a/taptrade-cli-demo/trader/src/wallet/mod.rs b/taptrade-cli-demo/trader/src/wallet/mod.rs index 3acd051..450c6ab 100644 --- a/taptrade-cli-demo/trader/src/wallet/mod.rs +++ b/taptrade-cli-demo/trader/src/wallet/mod.rs @@ -7,6 +7,7 @@ use crate::{ cli::TraderSettings, communication::api::{BondRequirementResponse, OfferTakenResponse}, }; +use ::musig2::AggNonce; use anyhow::{anyhow, Result}; use bdk::{ bitcoin::{ @@ -197,4 +198,20 @@ impl TradingWallet { Ok(self) } + + pub fn validate_payout_psbt(&self, psbt: &PartiallySignedTransaction) -> Result<&Self> { + warn!("IMPLEMENT PAYOUT PSBT VALIDATION for production use!"); + // validate: change output address, amounts, fee + // tbd once the trade psbt is implemented on coordinator side + + Ok(self) + } + + pub fn sign_payout_psbt( + &self, + psbt: PartiallySignedTransaction, + agg_pub_nonce: AggNonce, + ) -> Result { + Ok(signed_psbt) + } }