finish keyspend psbt creation on coordinator, begin signing on client

This commit is contained in:
fbock
2024-08-14 14:11:52 +02:00
parent ead749b7e7
commit b4b136e183
13 changed files with 201 additions and 28 deletions

View File

@ -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,

View File

@ -21,6 +21,7 @@ pub enum FetchEscrowConfirmationError {
pub enum RequestError {
Database(String),
NotConfirmed,
CoordinatorError(String),
NotFound,
PsbtAlreadySubmitted,
PsbtInvalid(String),

View File

@ -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");

View File

@ -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<XOnlyPublicKey>,
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<Self> {
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<u8> = rand::thread_rng()

View File

@ -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
{

View File

@ -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<PayoutData> {
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::<i64, _>("escrow_amount_maker_sat")? as u64;
let payout_amount_taker: u64 = row.try_get::<i64, _>("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,
)
}
}

View File

@ -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,

View File

@ -1,5 +1,4 @@
use super::*;
use axum::routing::trace;
use bdk::{
bitcoin::{psbt::PartiallySignedTransaction, PublicKey},
descriptor::{policy, Descriptor},

View File

@ -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<D: bdk::database::BatchDatabase> CoordinatorWallet<D> {
fn get_escrow_utxo(
&self,
descriptor: &Descriptor<XOnlyPublicKey>,
) -> 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<Address, u64>,
) -> Result<PartiallySignedTransaction> {
payout_information: &PayoutData,
) -> anyhow::Result<String> {
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())
}
}

View File

@ -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,

View File

@ -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<Option<PartiallySignedTransaction>> {
) -> 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::<PayoutPsbtResponse>()?.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))
}
}

View File

@ -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)?;

View File

@ -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<PartiallySignedTransaction> {
Ok(signed_psbt)
}
}