diff --git a/docs/TapTrade_obs/.obsidian/workspace.json b/docs/TapTrade_obs/.obsidian/workspace.json index b5c9702..3ced75d 100755 --- a/docs/TapTrade_obs/.obsidian/workspace.json +++ b/docs/TapTrade_obs/.obsidian/workspace.json @@ -13,11 +13,11 @@ "state": { "type": "canvas", "state": { - "file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas", + "file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas", "viewState": { - "x": 656.8883048329865, - "y": 805.4168609598363, - "zoom": -0.6000000000000001 + "x": -29.238865484157145, + "y": 404.69490907920806, + "zoom": 0.016298266873861295 } } } @@ -88,7 +88,7 @@ "state": { "type": "backlink", "state": { - "file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas", + "file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -105,7 +105,7 @@ "state": { "type": "outgoing-link", "state": { - "file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas", + "file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas", "linksCollapsed": false, "unlinkedCollapsed": true } @@ -128,7 +128,7 @@ "state": { "type": "outline", "state": { - "file": "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas" + "file": "Research/Trade Pipelines/new concepts/concept locking script 1.canvas" } } } @@ -151,11 +151,11 @@ }, "active": "bdb9fd88a01a8909", "lastOpenFiles": [ + "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas", "Research/Implementation/CLI demonstrator architecture/demonstrator architecture.canvas", "Research/Trade Pipelines/new concepts/concept locking script 1.canvas", "Research/Bitcoin fundamentals/Knowledge sources.md", "Research/Trade Pipelines/Existing research.md", - "Research/Trade Pipelines/new concepts/concept pipeline 1.canvas", "Research/Bitcoin fundamentals/Taproot output structure.canvas", "Research/Bitcoin fundamentals/Spending Taproot UTXOs.md", "Research/Implementation/Libraries.md", diff --git a/docs/TapTrade_obs/Research/Trade Pipelines/new concepts/concept locking script 1.canvas b/docs/TapTrade_obs/Research/Trade Pipelines/new concepts/concept locking script 1.canvas index a3f7f34..768adee 100755 --- a/docs/TapTrade_obs/Research/Trade Pipelines/new concepts/concept locking script 1.canvas +++ b/docs/TapTrade_obs/Research/Trade Pipelines/new concepts/concept locking script 1.canvas @@ -4,7 +4,7 @@ {"id":"9945f983ca9b2b3c","type":"text","text":"MSTB Escrow locking key\n(External key of taproot contract where trade participants lock their money to)","x":-678,"y":-690,"width":260,"height":160}, {"id":"4f4cec183e99cc39","type":"text","text":"Internal Key\nSigned by Taker, Maker and Coordinator via MuSig2 in case of complete trade or cooperative cancellation\n","x":-1025,"y":-490,"width":287,"height":185}, {"id":"c7d1840bae375d47","type":"text","text":"Check size, op_checksigadd 2of2 vs just checking 2 sigs. Maybe there is optimization possible.","x":140,"y":-770,"width":280,"height":130,"color":"1"}, - {"id":"cd16e3e9eda3242d","type":"text","text":"(Script B)\nCould also be keyspend MuSig2 spend!\n\nMUSIG(SIG(Maker) && SIG(Taker) && SIG(COORDINATOR))\n\n`and_v(v:pk(MAKER),and_v(v:pk(TAKER),pk(COORDINATOR)))`\n\nEscrow + Maker Bond to Maker.\n\nTaker Bond to Taker\n\nNeeds Coordinator signature to prevent Maker and Taker from stealing Fees of coordinator after successful trade.","x":-110,"y":-490,"width":250,"height":560}, + {"id":"cd16e3e9eda3242d","type":"text","text":"Seems obsolete\n\n(Script B)\nCould also be keyspend MuSig2 spend!\n\nMUSIG(SIG(Maker) && SIG(Taker) && SIG(COORDINATOR))\n\n`and_v(v:pk(MAKER),and_v(v:pk(TAKER),pk(COORDINATOR)))`\n\nEscrow + Maker Bond to Maker.\n\nTaker Bond to Taker\n\nNeeds Coordinator signature to prevent Maker and Taker from stealing Fees of coordinator after successful trade.","x":-110,"y":-490,"width":250,"height":560}, {"id":"f5fbfebcea40c384","type":"text","text":"SCRIPT C\n\nAND(SIG(Maker), SIG(COORDINATOR))\n`and_v(v:pk(MAKER),pk(COORDINATOR))`\n\nFees to coordinator.\nRemaining to Maker\n\n(could probably also be Musig spend)\n\n","x":227,"y":-490,"width":385,"height":230}, {"id":"341d50e0a5929e24","type":"text","text":"Script D\n\nAND(SIG(TAKER), SIG(COORDINATOR))\n`and_v(v:pk(TAKER),pk(COORDINATOR))`\n\nFees to coordinator.\nRemaining to Taker.\n\n(could probably also be Musig spend)","x":640,"y":-490,"width":447,"height":250}, {"id":"38bc9592ec0c29e2","type":"text","text":"SCRIPT F\nAND(TIMELOCK(2048), AND(SIG(TAKER), SIG(MAKER)))\n`and_v(and_v(v:pk(MAKER),v:pk(TAKER)),after(2048))`\n\nCooperative close without coordinator, in case coordinator vanishes or doesn't cosign.\n\nCould be used to prevent paying fees to coordinator after successful trade but both maker and taker would have to cooperate and wait at least some time.\n\nIf coordinator is offline this would need a direct communication layer between maker and taker to create the transaction, realistic? \n\n","x":-91,"y":120,"width":250,"height":720}, diff --git a/taptrade-cli-demo/coordinator/Cargo.lock b/taptrade-cli-demo/coordinator/Cargo.lock index 798ea8b..40653f4 100644 --- a/taptrade-cli-demo/coordinator/Cargo.lock +++ b/taptrade-cli-demo/coordinator/Cargo.lock @@ -208,6 +208,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base58ck" version = "0.1.0" @@ -464,6 +470,7 @@ dependencies = [ "hex", "log", "miniscript 12.0.0", + "musig2", "rand", "reqwest", "serde", @@ -1376,6 +1383,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "musig2" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed08befaac75bfb31ca5e87678c4e8490bcd21d0c98ccb4f12f4065a7567e83" +dependencies = [ + "base16ct", + "hmac", + "once_cell", + "secp", + "secp256k1 0.28.2", + "sha2", + "subtle", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -1992,6 +2014,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "secp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4754628ff9006f80c6abd1cd1e88c5ca6f5a60eab151ad2e16268aab3514d0" +dependencies = [ + "base16ct", + "once_cell", + "secp256k1 0.28.2", + "subtle", +] + [[package]] name = "secp256k1" version = "0.27.0" @@ -2004,6 +2038,15 @@ dependencies = [ "serde", ] +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys 0.9.2", +] + [[package]] name = "secp256k1" version = "0.29.0" @@ -2023,6 +2066,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.10.0" diff --git a/taptrade-cli-demo/coordinator/Cargo.toml b/taptrade-cli-demo/coordinator/Cargo.toml index 7bebac7..5ada21c 100644 --- a/taptrade-cli-demo/coordinator/Cargo.toml +++ b/taptrade-cli-demo/coordinator/Cargo.toml @@ -26,6 +26,7 @@ log = "0.4.22" env_logger = "0.11.3" sha2 = "0.10.8" validator = { version = "0.18.1", features = ["derive"] } +musig2 = "0.0.11" [profile.release] lto = true diff --git a/taptrade-cli-demo/coordinator/src/communication/api.rs b/taptrade-cli-demo/coordinator/src/communication/api.rs index ceb8c2c..b231184 100644 --- a/taptrade-cli-demo/coordinator/src/communication/api.rs +++ b/taptrade-cli-demo/coordinator/src/communication/api.rs @@ -27,6 +27,7 @@ pub struct BondSubmissionRequest { pub robohash_hex: String, pub signed_bond_hex: String, // signed bond transaction, hex encoded pub payout_address: String, // does this make sense here? + pub taproot_pubkey_hex: String, pub musig_pub_nonce_hex: String, pub musig_pubkey_hex: String, } diff --git a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs index 1f09068..145a431 100755 --- a/taptrade-cli-demo/coordinator/src/coordinator/mod.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/mod.rs @@ -133,17 +133,23 @@ pub async fn handle_taker_bond( } debug!("\nTaker bond validation successful"); - panic!("Trade contract PSBT not implemented!"); - let trade_contract_psbt_taker = "".to_string(); // implement psbt - let trade_contract_psbt_maker = "".to_string(); // implement psbt - let escrow_tx_txid: String = "".to_string(); // implement txid of psbt + let escrow_psbt_data = match wallet + .assemble_escrow_psbt(database, &payload.offer.offer_id_hex) + .await + { + Ok(escrow_psbt_data) => escrow_psbt_data, + Err(e) => { + return Err(BondError::CoordinatorError(e.to_string())); + } + }; if let Err(e) = database .add_taker_info_and_move_table( payload, - &trade_contract_psbt_maker, - &trade_contract_psbt_taker, - escrow_tx_txid, + &escrow_psbt_data.escrow_psbt_hex_maker, + &escrow_psbt_data.escrow_psbt_hex_taker, + escrow_psbt_data.escrow_psbt_txid, + escrow_psbt_data.coordinator_escrow_pk, ) .await { @@ -151,7 +157,7 @@ pub async fn handle_taker_bond( } Ok(OfferTakenResponse { - trade_psbt_hex_to_sign: trade_contract_psbt_taker, + trade_psbt_hex_to_sign: escrow_psbt_data.escrow_psbt_hex_taker, }) } diff --git a/taptrade-cli-demo/coordinator/src/database/db_tests.rs b/taptrade-cli-demo/coordinator/src/database/db_tests.rs index e2404ea..aa99347 100644 --- a/taptrade-cli-demo/coordinator/src/database/db_tests.rs +++ b/taptrade-cli-demo/coordinator/src/database/db_tests.rs @@ -1,5 +1,5 @@ use super::*; -#[cfg(test)] + use anyhow::Ok; #[allow(dead_code)] diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index 40509f5..1da964d 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -1,4 +1,5 @@ -pub mod db_tests; +#[cfg(test)] +mod db_tests; use anyhow::Context; use futures_util::StreamExt; @@ -36,6 +37,7 @@ struct AwaitingTakerOffer { bond_amount_sat: i64, bond_tx_hex_maker: String, payout_address_maker: String, + taproot_pubkey_hex_maker: String, musig_pub_nonce_hex_maker: String, musig_pubkey_hex_maker: String, } @@ -98,6 +100,7 @@ impl CoordinatorDB { bond_amount_sat INTEGER NOT NULL, bond_tx_hex TEXT NOT NULL, payout_address TEXT NOT NULL, + taproot_pubkey_hex_maker TEXT NOT NULL, musig_pub_nonce_hex TEXT NOT NULL, musig_pubkey_hex TEXT NOT NULL, taker_bond_address TEXT @@ -121,7 +124,9 @@ impl CoordinatorDB { bond_tx_hex_maker TEXT NOT NULL, bond_tx_hex_taker TEXT NOT NULL, payout_address_maker TEXT NOT NULL, + taproot_pubkey_hex_maker TEXT NOT NULL, payout_address_taker TEXT NOT NULL, + taproot_pubkey_hex_taker TEXT NOT NULL, musig_pub_nonce_hex_maker TEXT NOT NULL, musig_pubkey_hex_maker TEXT NOT NULL, musig_pub_nonce_hex_taker TEXT NOT NULL, @@ -133,7 +138,8 @@ impl CoordinatorDB { maker_happy INTEGER, taker_happy INTEGER, escrow_ongoing INTEGER NOT NULL, - escrow_winner_robohash TEXT + escrow_winner_robohash TEXT, + escrow_taproot_pk_coordinator TEXT )", // escrow_psbt_is_confirmed will be set 1 once the escrow psbt is confirmed onchain ) .execute(&db_pool) @@ -231,8 +237,8 @@ impl CoordinatorDB { ); sqlx::query( "INSERT OR REPLACE INTO active_maker_offers (offer_id, robohash, is_buy_order, amount_sat, - bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, musig_pub_nonce_hex, musig_pubkey_hex, taker_bond_address) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, taproot_pubkey_hex_maker, musig_pub_nonce_hex, musig_pubkey_hex, taker_bond_address) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(offer_id) .bind(hex::decode(&data.robohash_hex)?) @@ -244,6 +250,7 @@ impl CoordinatorDB { .bind(remaining_offer_information.bond_amount_sat as i64) .bind(data.signed_bond_hex.clone()) .bind(data.payout_address.clone()) + .bind(data.taproot_pubkey_hex.clone()) .bind(data.musig_pub_nonce_hex.clone()) .bind(data.musig_pubkey_hex.clone()) .bind(taker_bond_address) @@ -312,8 +319,8 @@ impl CoordinatorDB { &self, offer_id_hex: &str, ) -> Result { - let fetched_values = sqlx::query_as::<_, (Vec, i32, i64, i32, i64, String, i64, String, String, String, String)> ( - "SELECT robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, + let fetched_values = sqlx::query_as::<_, (Vec, i32, i64, i32, i64, String, i64, String, String, String, String, String)> ( + "SELECT robohash, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address, bond_amount_sat, bond_tx_hex, payout_address, taproot_pubkey_hex_maker, musig_pub_nonce_hex, musig_pubkey_hex FROM active_maker_offers WHERE offer_id = ?", ) .bind(offer_id_hex) @@ -337,8 +344,9 @@ impl CoordinatorDB { bond_amount_sat: fetched_values.6, bond_tx_hex_maker: fetched_values.7, payout_address_maker: fetched_values.8, - musig_pub_nonce_hex_maker: fetched_values.9, - musig_pubkey_hex_maker: fetched_values.10, + taproot_pubkey_hex_maker: fetched_values.9, + musig_pub_nonce_hex_maker: fetched_values.10, + musig_pubkey_hex_maker: fetched_values.11, }) } @@ -348,6 +356,7 @@ impl CoordinatorDB { trade_contract_psbt_maker: &str, trade_contract_psbt_taker: &str, trade_tx_txid: String, + escrow_taproot_pk_coordinator: String, ) -> Result<()> { let public_offer = self .fetch_and_delete_offer_from_public_offers_table( @@ -358,9 +367,10 @@ impl CoordinatorDB { sqlx::query( "INSERT OR REPLACE INTO taken_offers (offer_id, robohash_maker, robohash_taker, is_buy_order, amount_sat, bond_ratio, offer_duration_ts, bond_address_maker, bond_address_taker, bond_amount_sat, bond_tx_hex_maker, - bond_tx_hex_taker, payout_address_maker, payout_address_taker, musig_pub_nonce_hex_maker, musig_pubkey_hex_maker, - musig_pub_nonce_hex_taker, musig_pubkey_hex_taker, escrow_psbt_hex_maker, escrow_psbt_hex_taker, escrow_psbt_txid, escrow_psbt_is_confirmed, escrow_ongoing) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + bond_tx_hex_taker, payout_address_maker, payout_address_taker, taproot_pubkey_hex_maker, taproot_pubkey_hex_taker, musig_pub_nonce_hex_maker, musig_pubkey_hex_maker, + musig_pub_nonce_hex_taker, musig_pubkey_hex_taker, escrow_psbt_hex_maker, escrow_psbt_hex_taker, escrow_psbt_txid, escrow_psbt_is_confirmed, escrow_ongoing, + escrow_taproot_pk_coordinator) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(public_offer.offer_id) .bind(public_offer.robohash_maker) @@ -376,6 +386,8 @@ impl CoordinatorDB { .bind(trade_and_taker_info.trade_data.signed_bond_hex.clone()) .bind(public_offer.payout_address_maker) .bind(trade_and_taker_info.trade_data.payout_address.clone()) + .bind(public_offer.taproot_pubkey_hex_maker) + .bind(trade_and_taker_info.trade_data.taproot_pubkey_hex.clone()) .bind(public_offer.musig_pub_nonce_hex_maker) .bind(public_offer.musig_pubkey_hex_maker) .bind(trade_and_taker_info.trade_data.musig_pub_nonce_hex.clone()) @@ -385,6 +397,7 @@ impl CoordinatorDB { .bind(trade_tx_txid) .bind(0) .bind(0) + .bind(escrow_taproot_pk_coordinator) .execute(&*self.db_pool) .await?; @@ -662,4 +675,26 @@ impl CoordinatorDB { Ok(winner_robohash) } + + pub async fn fetch_escrow_tx_payout_data( + &self, + offer_id: &str, + ) -> Result { + let row = sqlx::query("SELECT taproot_pubkey_hex_maker, taproot_pubkey_hex_taker, musig_pubkey_hex_maker, musig_pubkey_hex_taker FROM taken_offers WHERE offer_id = ?") + .bind(offer_id) + .fetch_one(&*self.db_pool) + .await?; + + let taproot_pubkey_hex_maker: String = row.get("taproot_pubkey_hex_maker"); + let taproot_pubkey_hex_taker: String = row.get("taproot_pubkey_hex_taker"); + let musig_pubkey_hex_maker: String = row.get("musig_pubkey_hex_maker"); + let musig_pubkey_hex_taker: String = row.get("musig_pubkey_hex_taker"); + + Ok(EscrowPsbtConstructionData { + taproot_pubkey_hex_maker, + taproot_pubkey_hex_taker, + musig_pubkey_hex_maker, + musig_pubkey_hex_taker, + }) + } } diff --git a/taptrade-cli-demo/coordinator/src/main.rs b/taptrade-cli-demo/coordinator/src/main.rs index eab5f48..3718d97 100755 --- a/taptrade-cli-demo/coordinator/src/main.rs +++ b/taptrade-cli-demo/coordinator/src/main.rs @@ -21,7 +21,7 @@ use std::{ }; use tokio::sync::Mutex; use validator::{Validate, ValidationError}; -use wallet::*; +use wallet::{escrow_psbt::*, *}; pub struct Coordinator { pub coordinator_db: Arc, diff --git a/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs new file mode 100644 index 0000000..f12c19e --- /dev/null +++ b/taptrade-cli-demo/coordinator/src/wallet/escrow_psbt.rs @@ -0,0 +1,77 @@ +use super::*; +use bdk::{ + descriptor::Descriptor, + miniscript::{descriptor::TapTree, policy::Concrete, Tap}, +}; +use musig2::{secp256k1::PublicKey as MuSig2PubKey, KeyAggContext}; + +#[derive(Debug)] +pub struct EscrowPsbtConstructionData { + pub taproot_pubkey_hex_maker: String, + pub taproot_pubkey_hex_taker: String, + // pub taproot_pubkey_hex_coordinator: String, + pub musig_pubkey_hex_maker: String, + pub musig_pubkey_hex_taker: String, +} + +fn aggregate_musig_pubkeys(maker_musig_pubkey: &str, taker_musig_pubkey: &str) -> Result { + let pubkeys: [MuSig2PubKey; 2] = [maker_musig_pubkey.parse()?, taker_musig_pubkey.parse()?]; + + let key_agg_ctx = KeyAggContext::new(pubkeys)?; + let agg_pk: MuSig2PubKey = key_agg_ctx.aggregated_pubkey(); + + Ok(agg_pk.to_string()) +} + +pub fn build_escrow_transaction_output_descriptor( + escrow_data: &EscrowPsbtConstructionData, + coordinator_pk: &XOnlyPublicKey, +) -> Result { + let taproot_pubkey_hex_maker = escrow_data.taproot_pubkey_hex_maker.clone(); + let maker_pk = taproot_pubkey_hex_maker; + let taker_pk = escrow_data.taproot_pubkey_hex_taker.clone(); + let coordinator_pk = hex::encode(coordinator_pk.serialize()); + + // let script_a = format!("and(and(after({}),{}),{})", "144", maker_pk, coordinator_pk); + // let script_b = format!( + // "and_v(v:{},and_v(v:{},{}))", + // maker_pk, taker_pk, coordinator_pk + // ); + let script_c: String = format!("and(pk({}),pk({}))", maker_pk, coordinator_pk); + let script_d = format!("and(pk({}),pk({}))", taker_pk, coordinator_pk); + let script_e = format!("and({},after(12228))", maker_pk); + let script_f = format!("and_v(and_v(v:{},v:{}),after(2048))", maker_pk, taker_pk); + + // let compiled_a = Concrete::::from_str(&script_a)?.compile::()?; + // let compiled_b = Concrete::::from_str(&script_b)?.compile()?; + let compiled_c = Concrete::::from_str(&script_c)?.compile::()?; + let compiled_d = Concrete::::from_str(&script_d)?.compile::()?; + let compiled_e = Concrete::::from_str(&script_e)?.compile::()?; + let compiled_f = Concrete::::from_str(&script_f)?.compile::()?; + + // Create TapTree leaves + // let tap_leaf_a = TapTree::Leaf(Arc::new(compiled_a)); + // let tap_leaf_b = TapTree::Leaf(Arc::new(compiled_b)); + let tap_leaf_c = TapTree::Leaf(Arc::new(compiled_c)); + let tap_leaf_d = TapTree::Leaf(Arc::new(compiled_d)); + let tap_leaf_e = TapTree::Leaf(Arc::new(compiled_e)); + let tap_leaf_f = TapTree::Leaf(Arc::new(compiled_f)); + + let tap_node_cd = TapTree::Tree(Arc::new(tap_leaf_c), Arc::new(tap_leaf_d)); + let tap_node_ef = TapTree::Tree(Arc::new(tap_leaf_e), Arc::new(tap_leaf_f)); + + // Create the TapTree (example combining leaves, adjust as necessary), will be used for Script Path Spending (Alternative Spending Paths) in the descriptor + let final_tap_tree = TapTree::Tree(Arc::new(tap_node_cd), Arc::new(tap_node_ef)); + + // An internal key, that defines the way to spend the transaction directly, using Key Path Spending + let internal_agg_musig_key = aggregate_musig_pubkeys( + &escrow_data.musig_pubkey_hex_maker, + &escrow_data.musig_pubkey_hex_taker, + )?; + + // Create the descriptor + let descriptor = Descriptor::new_tr(internal_agg_musig_key, Some(final_tap_tree))?; + + debug!("Escrow descriptor: {}", descriptor.to_string()); + Ok(descriptor.to_string()) +} diff --git a/taptrade-cli-demo/coordinator/src/wallet/mod.rs b/taptrade-cli-demo/coordinator/src/wallet/mod.rs index 67254db..b5a53a3 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/mod.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/mod.rs @@ -1,12 +1,21 @@ +pub mod escrow_psbt; mod utils; // pub mod verify_tx; +#[cfg(test)] +mod wallet_tests; +use self::escrow_psbt::*; use super::*; use anyhow::Context; use bdk::{ bitcoin::{ - self, bip32::ExtendedPrivKey, consensus::encode::deserialize, key::secp256k1, - Network::Regtest, Transaction, + self, + address::Payload, + bip32::ExtendedPrivKey, + consensus::encode::deserialize, + key::{secp256k1, XOnlyPublicKey}, + Network::Regtest, + Transaction, }, bitcoincore_rpc::{Client, RawTx, RpcApi}, blockchain::{rpc::Auth, Blockchain, ConfigurableBlockchain, RpcBlockchain, RpcConfig}, @@ -29,6 +38,14 @@ pub struct CoordinatorWallet { pub mempool: Arc, } +#[derive(Debug)] +pub struct EscrowPsbt { + pub escrow_psbt_hex_maker: String, + pub escrow_psbt_hex_taker: String, + pub escrow_psbt_txid: String, + pub coordinator_escrow_pk: String, +} + #[derive(PartialEq, Debug, Clone)] pub struct BondRequirements { pub bond_address: String, @@ -214,6 +231,36 @@ impl CoordinatorWallet { blockchain.broadcast(&tx)?; Ok(()) } + + pub async fn assemble_escrow_psbt( + &self, + db: &Arc, + trade_id: &str, + ) -> Result { + let escrow_pubkeys = db.fetch_escrow_tx_payout_data(trade_id).await?; + let coordinator_escrow_pk = self.get_coordinator_taproot_pk().await?; + let escrow_descriptor = + build_escrow_transaction_output_descriptor(&escrow_pubkeys, &coordinator_escrow_pk)?; + + panic!("Dummy output"); + // Ok(EscrowPsbt { + // escrow_psbt_hex_maker: String::new(), + // escrow_psbt_hex_taker: String::new(), + // escrow_psbt_txid: String::new(), + // coordinator_escrow_pk, + // }) + } + + pub async fn get_coordinator_taproot_pk(&self) -> Result { + let wallet = self.wallet.lock().await; + let address = wallet.get_address(bdk::wallet::AddressIndex::New)?; + let pubkey = if let Payload::WitnessProgram(witness_program) = &address.payload { + witness_program.program().as_bytes() + } else { + return Err(anyhow!("Getting taproot pubkey failed")); + }; + Ok(XOnlyPublicKey::from_slice(pubkey)?) + } } fn search_monitoring_bond_by_txid( @@ -282,179 +329,3 @@ impl fmt::Debug for CoordinatorWallet { .finish() } } - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use bdk::bitcoin::Network; - use bdk::database::MemoryDatabase; - use bdk::{blockchain::RpcBlockchain, Wallet}; - async fn new_test_wallet(wallet_xprv: &str) -> CoordinatorWallet { - dotenv().ok(); - let wallet_xprv = ExtendedPrivKey::from_str(wallet_xprv).unwrap(); - let secp_context = secp256k1::Secp256k1::new(); - let rpc_config = RpcConfig { - url: env::var("BITCOIN_RPC_ADDRESS_PORT").unwrap().to_string(), - auth: Auth::UserPass { - username: env::var("BITCOIN_RPC_USER").unwrap(), - password: env::var("BITCOIN_RPC_PASSWORD").unwrap(), - }, - network: Regtest, - // wallet_name: env::var("BITCOIN_RPC_WALLET_NAME")?, - wallet_name: bdk::wallet::wallet_name_from_descriptor( - Bip86(wallet_xprv, KeychainKind::External), - Some(Bip86(wallet_xprv, KeychainKind::Internal)), - Network::Testnet, - &secp_context, - ) - .unwrap(), - sync_params: None, - }; - let json_rpc_client = - Arc::new(Client::new(&rpc_config.url, rpc_config.auth.clone().into()).unwrap()); - let backend = RpcBlockchain::from_config(&rpc_config).unwrap(); - - let wallet = Wallet::new( - Bip86(wallet_xprv, KeychainKind::External), - Some(Bip86(wallet_xprv, KeychainKind::Internal)), - Network::Testnet, - MemoryDatabase::new(), - ) - .unwrap(); - wallet.sync(&backend, SyncOptions::default()).unwrap(); - tokio::time::sleep(Duration::from_secs(16)).await; // fetch the mempool - CoordinatorWallet:: { - wallet: Arc::new(Mutex::new(wallet)), - backend: Arc::new(backend), - json_rpc_client: Arc::clone(&json_rpc_client), - mempool: Arc::new(MempoolHandler::new(json_rpc_client).await), - } - } - - // the transactions are testnet4 transactions, so run a testnet4 rpc node as backend - #[tokio::test] - async fn test_transaction_without_signature() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - let bond_without_signature = "02000000010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000fdffffff02998d0000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c6532364f860000"; - let requirements = BondRequirements { - min_input_sum_sat: 51000, - locking_amount_sat: 50000, - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet - .validate_bond_tx_hex(&bond_without_signature, &requirements) - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_transaction_with_invalid_signature() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - // assembled bond tx but with the signature of a different bond = invalid - let bond_with_invalid_signature = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb3801400000000001900000002aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c65323601401fddcc681a1d0324c8fdeabbc08a3b06c26741872363c0ddfc82f15b6abe43d37815bcdc2ce1fb2f70cac426f7fb269d322ac6a621886208d0c625335bba670800000000"; - let requirements = BondRequirements { - min_input_sum_sat: 51000, - locking_amount_sat: 50000, - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet - .validate_bond_tx_hex(&bond_with_invalid_signature, &requirements) - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_bond_with_spent_input() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - let bond_with_spent_input = "02000000000101f7d992795b0b43227ea83e296a7c2a91771ede3ef54f1eb5664393c79b9399080100000000fdffffff0250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236abc6010000000000225120b83c64b440203fb74a0c672cd829f387b957129835dd3b5c4e33fc71a146b3ae0140afdafbae5b76217f469790b211d7fbda427e5b4379c4603e9ae08c9ef5aaae30bfecfc16e5f636c737bea8e0e27974854d1cd0d094ed737aadfc679a974074574f860000"; - let requirements = BondRequirements { - min_input_sum_sat: 51000, - locking_amount_sat: 50000, - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet - .validate_bond_tx_hex(&bond_with_spent_input, &requirements) - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_valid_bond_tx() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000010000000250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c014010e19c8b915624bd4aa0ba4d094d26ca031a6f2d8f23fe51372c7ea50e05f3caf81c7e139f6fed3e9ffd20c03d79f78542acb3d8aed664898f1c4b2909c2188c00000000"; - let requirements = BondRequirements { - min_input_sum_sat: 100000, - locking_amount_sat: 50000, - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_invalid_bond_tx_low_input_sum() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000010000000250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c014010e19c8b915624bd4aa0ba4d094d26ca031a6f2d8f23fe51372c7ea50e05f3caf81c7e139f6fed3e9ffd20c03d79f78542acb3d8aed664898f1c4b2909c2188c00000000"; - let requirements = BondRequirements { - min_input_sum_sat: 2000000, // Set higher than the actual input sum - locking_amount_sat: 50000, - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Bond input sum too small")); - } - - #[tokio::test] - async fn test_invalid_bond_tx_low_output_sum() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000010000000250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c014010e19c8b915624bd4aa0ba4d094d26ca031a6f2d8f23fe51372c7ea50e05f3caf81c7e139f6fed3e9ffd20c03d79f78542acb3d8aed664898f1c4b2909c2188c00000000"; - let requirements = BondRequirements { - min_input_sum_sat: 100000, - locking_amount_sat: 1000000, // Set higher than the actual output sum - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Bond output sum too small")); - } - - #[tokio::test] - async fn test_invalid_bond_tx_low_fee_rate() { - let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; - let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000fdffffff0259b00000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c6532360140bee11f7f644cf09d5031683203bbe61109090b1e4be4626e13de7a889d6e5d2f154233a2bfaf9cb983f31ccf01b1be5db2cd37bb0cb9a395e2632bc50105b4583f860000"; - let requirements = BondRequirements { - min_input_sum_sat: 100000, - locking_amount_sat: 50000, - bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" - .to_string(), - }; - - let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Bond fee rate too low")); - } -} diff --git a/taptrade-cli-demo/coordinator/src/wallet/wallet_tests.rs b/taptrade-cli-demo/coordinator/src/wallet/wallet_tests.rs new file mode 100644 index 0000000..4dad8fc --- /dev/null +++ b/taptrade-cli-demo/coordinator/src/wallet/wallet_tests.rs @@ -0,0 +1,165 @@ +use std::time::Duration; + +use super::*; +use bdk::bitcoin::Network; +use bdk::database::MemoryDatabase; +use bdk::{blockchain::RpcBlockchain, Wallet}; +async fn new_test_wallet(wallet_xprv: &str) -> CoordinatorWallet { + dotenv().ok(); + let wallet_xprv = ExtendedPrivKey::from_str(wallet_xprv).unwrap(); + let secp_context = secp256k1::Secp256k1::new(); + let rpc_config = RpcConfig { + url: env::var("BITCOIN_RPC_ADDRESS_PORT").unwrap().to_string(), + auth: Auth::UserPass { + username: env::var("BITCOIN_RPC_USER").unwrap(), + password: env::var("BITCOIN_RPC_PASSWORD").unwrap(), + }, + network: Regtest, + // wallet_name: env::var("BITCOIN_RPC_WALLET_NAME")?, + wallet_name: bdk::wallet::wallet_name_from_descriptor( + Bip86(wallet_xprv, KeychainKind::External), + Some(Bip86(wallet_xprv, KeychainKind::Internal)), + Network::Testnet, + &secp_context, + ) + .unwrap(), + sync_params: None, + }; + let json_rpc_client = + Arc::new(Client::new(&rpc_config.url, rpc_config.auth.clone().into()).unwrap()); + let backend = RpcBlockchain::from_config(&rpc_config).unwrap(); + + let wallet = Wallet::new( + Bip86(wallet_xprv, KeychainKind::External), + Some(Bip86(wallet_xprv, KeychainKind::Internal)), + Network::Testnet, + MemoryDatabase::new(), + ) + .unwrap(); + wallet.sync(&backend, SyncOptions::default()).unwrap(); + tokio::time::sleep(Duration::from_secs(16)).await; // fetch the mempool + CoordinatorWallet:: { + wallet: Arc::new(Mutex::new(wallet)), + backend: Arc::new(backend), + json_rpc_client: Arc::clone(&json_rpc_client), + mempool: Arc::new(MempoolHandler::new(json_rpc_client).await), + } +} + +// the transactions are testnet4 transactions, so run a testnet4 rpc node as backend +#[tokio::test] +async fn test_transaction_without_signature() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond_without_signature = "02000000010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000fdffffff02998d0000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c6532364f860000"; + let requirements = BondRequirements { + min_input_sum_sat: 51000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet + .validate_bond_tx_hex(&bond_without_signature, &requirements) + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn test_transaction_with_invalid_signature() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + // assembled bond tx but with the signature of a different bond = invalid + let bond_with_invalid_signature = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb3801400000000001900000002aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c65323601401fddcc681a1d0324c8fdeabbc08a3b06c26741872363c0ddfc82f15b6abe43d37815bcdc2ce1fb2f70cac426f7fb269d322ac6a621886208d0c625335bba670800000000"; + let requirements = BondRequirements { + min_input_sum_sat: 51000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet + .validate_bond_tx_hex(&bond_with_invalid_signature, &requirements) + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn test_bond_with_spent_input() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond_with_spent_input = "02000000000101f7d992795b0b43227ea83e296a7c2a91771ede3ef54f1eb5664393c79b9399080100000000fdffffff0250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236abc6010000000000225120b83c64b440203fb74a0c672cd829f387b957129835dd3b5c4e33fc71a146b3ae0140afdafbae5b76217f469790b211d7fbda427e5b4379c4603e9ae08c9ef5aaae30bfecfc16e5f636c737bea8e0e27974854d1cd0d094ed737aadfc679a974074574f860000"; + let requirements = BondRequirements { + min_input_sum_sat: 51000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet + .validate_bond_tx_hex(&bond_with_spent_input, &requirements) + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn test_valid_bond_tx() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000010000000250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c014010e19c8b915624bd4aa0ba4d094d26ca031a6f2d8f23fe51372c7ea50e05f3caf81c7e139f6fed3e9ffd20c03d79f78542acb3d8aed664898f1c4b2909c2188c00000000"; + let requirements = BondRequirements { + min_input_sum_sat: 100000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; + assert!(result.is_ok()); +} + +#[tokio::test] +async fn test_invalid_bond_tx_low_input_sum() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000010000000250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c014010e19c8b915624bd4aa0ba4d094d26ca031a6f2d8f23fe51372c7ea50e05f3caf81c7e139f6fed3e9ffd20c03d79f78542acb3d8aed664898f1c4b2909c2188c00000000"; + let requirements = BondRequirements { + min_input_sum_sat: 2000000, // Set higher than the actual input sum + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Bond input sum too small")); +} + +#[tokio::test] +async fn test_invalid_bond_tx_low_output_sum() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000010000000250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c014010e19c8b915624bd4aa0ba4d094d26ca031a6f2d8f23fe51372c7ea50e05f3caf81c7e139f6fed3e9ffd20c03d79f78542acb3d8aed664898f1c4b2909c2188c00000000"; + let requirements = BondRequirements { + min_input_sum_sat: 100000, + locking_amount_sat: 1000000, // Set higher than the actual output sum + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Bond output sum too small")); +} + +#[tokio::test] +async fn test_invalid_bond_tx_low_fee_rate() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000fdffffff0259b00000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c6532360140bee11f7f644cf09d5031683203bbe61109090b1e4be4626e13de7a889d6e5d2f154233a2bfaf9cb983f31ccf01b1be5db2cd37bb0cb9a395e2632bc50105b4583f860000"; + let requirements = BondRequirements { + min_input_sum_sat: 100000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx".to_string(), + }; + + let result = test_wallet.validate_bond_tx_hex(&bond, &requirements).await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Bond fee rate too low")); +} diff --git a/taptrade-cli-demo/trader/src/communication/api.rs b/taptrade-cli-demo/trader/src/communication/api.rs index f6ce51f..ee6d20d 100644 --- a/taptrade-cli-demo/trader/src/communication/api.rs +++ b/taptrade-cli-demo/trader/src/communication/api.rs @@ -26,6 +26,7 @@ pub struct BondSubmissionRequest { pub robohash_hex: String, pub signed_bond_hex: String, // signed bond transaction, hex encoded pub payout_address: String, // does this make sense here? + pub taproot_pubkey_hex: String, pub musig_pub_nonce_hex: String, pub musig_pubkey_hex: String, } diff --git a/taptrade-cli-demo/trader/src/communication/mod.rs b/taptrade-cli-demo/trader/src/communication/mod.rs index c0af8ea..bf822a0 100644 --- a/taptrade-cli-demo/trader/src/communication/mod.rs +++ b/taptrade-cli-demo/trader/src/communication/mod.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use api::*; -use bdk::bitcoin::consensus::encode::serialize_hex; +use bdk::bitcoin::{consensus::encode::serialize_hex, key::XOnlyPublicKey}; use bdk::{ bitcoin::{consensus::Encodable, psbt::PartiallySignedTransaction}, wallet::AddressInfo, @@ -71,10 +71,12 @@ impl BondSubmissionRequest { payout_address: &AddressInfo, musig_data: &mut MuSigData, trader_config: &TraderSettings, + taproot_pubkey: &XOnlyPublicKey, ) -> Result { let signed_bond_hex = serialize_hex(&bond.to_owned().extract_tx()); let musig_pub_nonce_hex = hex::encode(musig_data.nonce.get_pub_for_sharing()?.serialize()); let musig_pubkey_hex = hex::encode(musig_data.public_key.0.serialize()); + let taproot_pubkey_hex = hex::encode(taproot_pubkey.serialize()); let request = BondSubmissionRequest { robohash_hex: trader_config.robosats_robohash_hex.clone(), @@ -82,6 +84,7 @@ impl BondSubmissionRequest { payout_address: payout_address.address.to_string(), musig_pub_nonce_hex, musig_pubkey_hex, + taproot_pubkey_hex, }; Ok(request) } @@ -92,8 +95,15 @@ impl BondSubmissionRequest { musig_data: &mut MuSigData, payout_address: &AddressInfo, trader_setup: &TraderSettings, + taproot_pubkey: &XOnlyPublicKey, ) -> Result { - let request = Self::prepare_bond_request(bond, payout_address, musig_data, trader_setup)?; + let request = Self::prepare_bond_request( + bond, + payout_address, + musig_data, + trader_setup, + taproot_pubkey, + )?; let client = reqwest::blocking::Client::new(); let res = client .post(format!( diff --git a/taptrade-cli-demo/trader/src/trading/maker_utils.rs b/taptrade-cli-demo/trader/src/trading/maker_utils.rs index 69a1d3a..132b93f 100644 --- a/taptrade-cli-demo/trader/src/trading/maker_utils.rs +++ b/taptrade-cli-demo/trader/src/trading/maker_utils.rs @@ -16,6 +16,7 @@ impl ActiveOffer { &mut musig_data, &payout_address, maker_config, + &trading_wallet.taproot_pubkey, )?; Ok(ActiveOffer { offer_id_hex: submission_result.offer_id_hex, diff --git a/taptrade-cli-demo/trader/src/trading/taker_utils.rs b/taptrade-cli-demo/trader/src/trading/taker_utils.rs index b058b4c..1d94dd1 100644 --- a/taptrade-cli-demo/trader/src/trading/taker_utils.rs +++ b/taptrade-cli-demo/trader/src/trading/taker_utils.rs @@ -27,6 +27,7 @@ impl ActiveOffer { &payout_address, &mut musig_data, taker_config, + &trading_wallet.taproot_pubkey, )?; let mut escrow_contract_psbt = OfferPsbtRequest::taker_request(offer, bond_submission_request, taker_config)?; diff --git a/taptrade-cli-demo/trader/src/wallet/mod.rs b/taptrade-cli-demo/trader/src/wallet/mod.rs index 56f8d8c..22c6983 100644 --- a/taptrade-cli-demo/trader/src/wallet/mod.rs +++ b/taptrade-cli-demo/trader/src/wallet/mod.rs @@ -6,7 +6,13 @@ use super::*; use crate::{cli::TraderSettings, communication::api::BondRequirementResponse}; use anyhow::{anyhow, Result}; use bdk::{ - bitcoin::{self, bip32::ExtendedPrivKey, psbt::PartiallySignedTransaction, Network}, + bitcoin::{ + self, + bip32::ExtendedPrivKey, + key::{KeyPair, Secp256k1, XOnlyPublicKey}, + psbt::PartiallySignedTransaction, + Network, + }, blockchain::ElectrumBlockchain, database::MemoryDatabase, electrum_client::Client, @@ -24,6 +30,7 @@ use wallet_utils::get_seed; pub struct TradingWallet { pub wallet: Wallet, pub backend: ElectrumBlockchain, + pub taproot_pubkey: XOnlyPublicKey, } pub fn get_wallet_xprv(xprv_input: Option) -> Result { @@ -49,10 +56,18 @@ impl TradingWallet { bitcoin::Network::Regtest, MemoryDatabase::default(), // non-permanent storage )?; + let taproot_pubkey = trader_config + .wallet_xprv + .to_keypair(&Secp256k1::new()) + .x_only_public_key(); wallet.sync(&backend, SyncOptions::default())?; dbg!("Balance: {} SAT", wallet.get_balance()?); - Ok(TradingWallet { wallet, backend }) + Ok(TradingWallet { + wallet, + backend, + taproot_pubkey: taproot_pubkey.0, + }) } // assemble bond and generate musig data for passed trade