mirror of
https://github.com/RoboSats/taptrade-core.git
synced 2025-07-19 17:23:26 +00:00
implementation of partial signature aggregation
This commit is contained in:
@ -265,7 +265,12 @@ async fn submit_payout_signature(
|
|||||||
Json(payload): Json<PayoutSignatureRequest>,
|
Json(payload): Json<PayoutSignatureRequest>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
match handle_payout_signature(&payload, coordinator).await {
|
match handle_payout_signature(&payload, coordinator).await {
|
||||||
Ok(_) => Ok(StatusCode::OK.into_response()),
|
// received both sigs, published final tx
|
||||||
|
Ok(true) => Ok(StatusCode::OK.into_response()),
|
||||||
|
|
||||||
|
// this was the first signature
|
||||||
|
Ok(false) => Ok(StatusCode::ACCEPTED.into_response()),
|
||||||
|
|
||||||
// Err(RequestError::NotConfirmed) => {
|
// Err(RequestError::NotConfirmed) => {
|
||||||
// info!("Offer tx for final payout not confirmed");
|
// info!("Offer tx for final payout not confirmed");
|
||||||
// Ok(StatusCode::NOT_ACCEPTABLE.into_response())
|
// Ok(StatusCode::NOT_ACCEPTABLE.into_response())
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
use std::str::FromStr;
|
use super::*;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use bdk::{
|
use bdk::{
|
||||||
bitcoin::{key::XOnlyPublicKey, Address},
|
bitcoin::{
|
||||||
|
hashes::Hash,
|
||||||
|
key::XOnlyPublicKey,
|
||||||
|
psbt::{PartiallySignedTransaction, Prevouts},
|
||||||
|
sighash::{SighashCache, TapSighashType},
|
||||||
|
Address,
|
||||||
|
},
|
||||||
miniscript::Descriptor,
|
miniscript::Descriptor,
|
||||||
};
|
};
|
||||||
use musig2::BinaryEncoding;
|
use musig2::{BinaryEncoding, LiftedSignature};
|
||||||
|
use std::str::FromStr;
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PayoutProcessingResult {
|
pub enum PayoutProcessingResult {
|
||||||
@ -28,6 +32,92 @@ pub struct PayoutData {
|
|||||||
pub aggregated_musig_pubkey_ctx_hex: String,
|
pub aggregated_musig_pubkey_ctx_hex: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct KeyspendContext {
|
||||||
|
pub agg_sig: LiftedSignature,
|
||||||
|
pub agg_nonce: MusigAggNonce,
|
||||||
|
pub agg_keyspend_pk: KeyAggContext,
|
||||||
|
pub keyspend_psbt: PartiallySignedTransaction,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn agg_hex_musig_nonces(maker_nonce: &str, taker_nonce: &str) -> Result<MusigAggNonce> {
|
||||||
|
let musig_pub_nonce_maker = match MusigPubNonce::from_hex(maker_nonce) {
|
||||||
|
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(taker_nonce) {
|
||||||
|
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_nonce = musig2::AggNonce::sum([musig_pub_nonce_maker, musig_pub_nonce_taker]);
|
||||||
|
|
||||||
|
Ok(agg_nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyspendContext {
|
||||||
|
pub fn from_hex_str(
|
||||||
|
maker_sig: &str,
|
||||||
|
taker_sig: &str,
|
||||||
|
maker_nonce: &str,
|
||||||
|
taker_nonce: &str,
|
||||||
|
maker_pk: &str,
|
||||||
|
taker_pk: &str,
|
||||||
|
keyspend_psbt: &str,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let agg_keyspend_pk: musig2::KeyAggContext =
|
||||||
|
wallet::aggregate_musig_pubkeys(maker_pk, taker_pk)?;
|
||||||
|
let agg_nonce: MusigAggNonce =
|
||||||
|
coordinator_utils::agg_hex_musig_nonces(&maker_nonce, &taker_nonce)?;
|
||||||
|
let keyspend_psbt = PartiallySignedTransaction::from_str(keyspend_psbt)?;
|
||||||
|
|
||||||
|
let partial_maker_sig = PartialSignature::from_hex(maker_sig)?;
|
||||||
|
let partial_taker_sig = PartialSignature::from_hex(taker_sig)?;
|
||||||
|
let partial_signatures = vec![partial_maker_sig, partial_taker_sig];
|
||||||
|
|
||||||
|
// let msg = keyspend_psbt.
|
||||||
|
let msg = {
|
||||||
|
let mut sig_hash_cache = SighashCache::new(keyspend_psbt.unsigned_tx.clone());
|
||||||
|
|
||||||
|
let utxo = keyspend_psbt
|
||||||
|
.iter_funding_utxos()
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow!("No UTXO found in payout psbt"))??
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
// get the msg (sighash) to sign with the musig key
|
||||||
|
let binding = sig_hash_cache
|
||||||
|
.taproot_key_spend_signature_hash(0, &Prevouts::All(&[utxo]), TapSighashType::All)
|
||||||
|
.context("Failed to create keyspend sighash")?;
|
||||||
|
binding.as_byte_array().to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let agg_sig: LiftedSignature = musig2::aggregate_partial_signatures(
|
||||||
|
&agg_keyspend_pk,
|
||||||
|
&agg_nonce,
|
||||||
|
partial_signatures,
|
||||||
|
msg.as_slice(),
|
||||||
|
)
|
||||||
|
.context("Aggregating partial signatures failed")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
agg_sig,
|
||||||
|
agg_nonce,
|
||||||
|
agg_keyspend_pk,
|
||||||
|
keyspend_psbt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PayoutData {
|
impl PayoutData {
|
||||||
pub fn new_from_strings(
|
pub fn new_from_strings(
|
||||||
escrow_output_descriptor: &str,
|
escrow_output_descriptor: &str,
|
||||||
@ -40,31 +130,12 @@ impl PayoutData {
|
|||||||
musig_pk_hex_maker: &str,
|
musig_pk_hex_maker: &str,
|
||||||
musig_pk_hex_taker: &str,
|
musig_pk_hex_taker: &str,
|
||||||
) -> Result<Self> {
|
) -> 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 aggregated_musig_pubkey_ctx_hex = hex::encode(
|
let aggregated_musig_pubkey_ctx_hex = hex::encode(
|
||||||
aggregate_musig_pubkeys(musig_pk_hex_maker, musig_pk_hex_taker)?.to_bytes(),
|
aggregate_musig_pubkeys(musig_pk_hex_maker, musig_pk_hex_taker)?.to_bytes(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let agg_musig_nonce: MusigAggNonce =
|
let agg_musig_nonce: MusigAggNonce =
|
||||||
musig2::AggNonce::sum([musig_pub_nonce_maker, musig_pub_nonce_taker]);
|
agg_hex_musig_nonces(musig_pub_nonce_hex_maker, musig_pub_nonce_hex_taker)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
escrow_output_descriptor: Descriptor::from_str(escrow_output_descriptor)?,
|
escrow_output_descriptor: Descriptor::from_str(escrow_output_descriptor)?,
|
||||||
|
@ -6,6 +6,7 @@ pub mod tx_confirmation_monitoring;
|
|||||||
|
|
||||||
use self::coordinator_utils::*;
|
use self::coordinator_utils::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use musig2::{AggNonce, KeyAggContext, PartialSignature};
|
||||||
|
|
||||||
pub async fn process_order(
|
pub async fn process_order(
|
||||||
coordinator: Arc<Coordinator>,
|
coordinator: Arc<Coordinator>,
|
||||||
@ -326,39 +327,38 @@ pub async fn handle_final_payout(
|
|||||||
pub async fn handle_payout_signature(
|
pub async fn handle_payout_signature(
|
||||||
payload: &PayoutSignatureRequest,
|
payload: &PayoutSignatureRequest,
|
||||||
coordinator: Arc<Coordinator>,
|
coordinator: Arc<Coordinator>,
|
||||||
) -> Result<(), RequestError> {
|
) -> Result<bool, RequestError> {
|
||||||
let database = &coordinator.coordinator_db;
|
let database = &coordinator.coordinator_db;
|
||||||
let wallet = &coordinator.coordinator_wallet;
|
let wallet = &coordinator.coordinator_wallet;
|
||||||
|
|
||||||
check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?;
|
check_offer_and_confirmation(&payload.offer_id_hex, &payload.robohash_hex, database).await?;
|
||||||
|
|
||||||
let (maker_partial_sig_hex, taker_partial_sig_hex, payout_psbt_hex) = match database
|
database
|
||||||
.insert_partial_sig_and_fetch_if_both(
|
.insert_partial_sig(
|
||||||
&payload.partial_sig_hex,
|
&payload.partial_sig_hex,
|
||||||
&payload.offer_id_hex,
|
&payload.offer_id_hex,
|
||||||
&payload.robohash_hex,
|
&payload.robohash_hex,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.map_err(|e| RequestError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
let keyspend_information = if let Some(keyspend_context) = database
|
||||||
|
.fetch_keyspend_payout_information(&payload.offer_id_hex)
|
||||||
|
.await
|
||||||
|
.map_err(|e| RequestError::Database(e.to_string()))?
|
||||||
{
|
{
|
||||||
Ok(Some((maker_partial_sig, taker_partial_sig, payout_transaction_psbt_hex))) => (
|
keyspend_context
|
||||||
maker_partial_sig,
|
} else {
|
||||||
taker_partial_sig,
|
return Ok(false);
|
||||||
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::payout_tx::aggregate_partial_signatures(
|
||||||
|
// &maker_partial_sig_hex,
|
||||||
|
// &taker_partial_sig_hex,
|
||||||
|
// )
|
||||||
|
// .map_err(|e| RequestError::CoordinatorError(e.to_string()))?;
|
||||||
|
|
||||||
warn!("Use musig2 validate partial sig to validate sigs before using to blame users providing wrong sigs");
|
warn!("Use musig2 validate partial sig to validate sigs before using to blame users providing wrong sigs");
|
||||||
|
|
||||||
let aggregated_signature = wallet::payout_tx::aggregate_partial_signatures(
|
Ok(true)
|
||||||
&maker_partial_sig_hex,
|
|
||||||
&taker_partial_sig_hex,
|
|
||||||
)
|
|
||||||
.map_err(|e| RequestError::CoordinatorError(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
mod db_tests;
|
mod db_tests;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use coordinator::coordinator_utils::*;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use musig2::BinaryEncoding;
|
use musig2::PartialSignature;
|
||||||
use serde::de::IntoDeserializer;
|
use sha2::digest::typenum::marker_traits;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use bdk::bitcoin::address::Address;
|
use bdk::bitcoin::address::Address;
|
||||||
@ -907,12 +908,12 @@ impl CoordinatorDB {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_partial_sig_and_fetch_if_both(
|
pub async fn insert_partial_sig(
|
||||||
&self,
|
&self,
|
||||||
partial_sig_hex: &str,
|
partial_sig_hex: &str,
|
||||||
offer_id_hex: &str,
|
offer_id_hex: &str,
|
||||||
robohash_hex: &str,
|
robohash_hex: &str,
|
||||||
) -> Result<Option<(String, String, String)>> {
|
) -> Result<()> {
|
||||||
// first check if the escrow psbt has already been submitted
|
// first check if the escrow psbt has already been submitted
|
||||||
let is_maker = self
|
let is_maker = self
|
||||||
.is_maker_in_taken_offers(offer_id_hex, robohash_hex)
|
.is_maker_in_taken_offers(offer_id_hex, robohash_hex)
|
||||||
@ -957,16 +958,40 @@ impl CoordinatorDB {
|
|||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_keyspend_payout_information(
|
||||||
|
&self,
|
||||||
|
offer_id_hex: &str,
|
||||||
|
) -> Result<Option<KeyspendContext>> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
"SELECT musig_partial_sig_maker, musig_partial_sig_taker, payout_transaction_psbt_hex FROM taken_offers WHERE offer_id = ?",
|
"SELECT musig_partial_sig_maker, musig_partial_sig_taker,
|
||||||
|
musig_pubkey_compressed_hex_maker, musig_pubkey_compressed_hex_taker, musig_pub_nonce_hex_maker, musig_pub_nonce_hex_taker,
|
||||||
|
payout_transaction_psbt_hex FROM taken_offers WHERE offer_id = ?",
|
||||||
).bind(offer_id_hex).fetch_one(&*self.db_pool).await?;
|
).bind(offer_id_hex).fetch_one(&*self.db_pool).await?;
|
||||||
|
|
||||||
let maker_sig: Option<String> = row.try_get("musig_partial_sig_maker")?;
|
let maker_sig: Option<String> = row.try_get("musig_partial_sig_maker")?;
|
||||||
let taker_sig: Option<String> = row.try_get("musig_partial_sig_taker")?;
|
let taker_sig: Option<String> = row.try_get("musig_partial_sig_taker")?;
|
||||||
let payout_tx_hex: String = row.try_get("payout_transaction_psbt_hex")?;
|
|
||||||
|
let maker_pubkey: String = row.try_get("musig_pubkey_compressed_hex_maker")?;
|
||||||
|
let taker_pubkey: String = row.try_get("musig_pubkey_compressed_hex_taker")?;
|
||||||
|
|
||||||
|
let maker_nonce: String = row.try_get("musig_pub_nonce_hex_maker")?;
|
||||||
|
let taker_nonce: String = row.try_get("musig_pub_nonce_hex_taker")?;
|
||||||
|
|
||||||
|
let keyspend_psbt: String = row.try_get("payout_transaction_psbt_hex")?;
|
||||||
|
|
||||||
if let (Some(maker), Some(taker)) = (maker_sig, taker_sig) {
|
if let (Some(maker), Some(taker)) = (maker_sig, taker_sig) {
|
||||||
Ok(Some((maker, taker, payout_tx_hex)))
|
Ok(Some(KeyspendContext::from_hex_str(
|
||||||
|
&maker,
|
||||||
|
&taker,
|
||||||
|
&maker_nonce,
|
||||||
|
&taker_nonce,
|
||||||
|
&maker_pubkey,
|
||||||
|
&taker_pubkey,
|
||||||
|
&keyspend_psbt,
|
||||||
|
)?))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
/// construction of the transaction spending the escrow output after a successfull trade as keyspend transaction
|
/// construction of the transaction spending the escrow output after a successfull trade as keyspend transaction
|
||||||
use super::*;
|
use super::*;
|
||||||
use bdk::bitcoin::psbt::Input;
|
use bdk::bitcoin::psbt::Input;
|
||||||
use bdk::bitcoin::psbt::PartiallySignedTransaction;
|
|
||||||
use bdk::bitcoin::OutPoint;
|
use bdk::bitcoin::OutPoint;
|
||||||
use bdk::miniscript::Descriptor;
|
use bdk::miniscript::Descriptor;
|
||||||
use musig2::{AggNonce, KeyAggContext, PartialSignature};
|
|
||||||
|
|
||||||
pub struct KeyspendContext {
|
|
||||||
pub partial_maker_sig: PartialSignature,
|
|
||||||
pub partial_taker_sig: PartialSignature,
|
|
||||||
pub agg_nonce: AggNonce,
|
|
||||||
pub key_agg_context: KeyAggContext,
|
|
||||||
pub keyspend_psbt: PartiallySignedTransaction,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> {
|
fn get_tx_fees_abs_sat(blockchain_backend: &RpcBlockchain) -> Result<(u64, u64)> {
|
||||||
let feerate = blockchain_backend.estimate_fee(6)?;
|
let feerate = blockchain_backend.estimate_fee(6)?;
|
||||||
|
Reference in New Issue
Block a user