use map_err for coordinator mod error handling, add endpoints for partitial sig

This commit is contained in:
fbock
2024-08-15 15:41:04 +02:00
parent 6a5a10546a
commit 4fa3b5fd49
8 changed files with 239 additions and 115 deletions

View File

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

View File

@ -260,6 +260,31 @@ async fn poll_final_payout(
}
}
async fn submit_payout_signature(
Extension(coordinator): Extension<Arc<Coordinator>>,
Json(payload): Json<PayoutSignatureRequest>,
) -> Result<Response, AppError> {
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<Coordinator>) -> 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

View File

@ -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<PublicOffers, FetchOffersError> {
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<PayoutProcessingResult, RequestError> {
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<Coordinator>,
) -> 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(())
}

View File

@ -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<Option<(String, String, String)>> {
// 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::<Option<String>, _>("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::<Option<String>, _>("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<String> = row.try_get("musig_partitial_sig_maker")?;
let taker_sig: Option<String> = 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)
}
}
}

View File

@ -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<String> {
Ok(())
}
impl<D: bdk::database::BatchDatabase> CoordinatorWallet<D> {
fn get_escrow_utxo(
&self,

View File

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

View File

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

View File

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