diff --git a/src/lib.rs b/src/lib.rs index a5f6cac5..2da09de2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub extern crate bitcoind; pub mod error; pub mod fee_estimation; pub mod maker; +pub mod nostr_coinswap; pub mod protocol; pub mod security; pub mod taker; diff --git a/src/maker/api.rs b/src/maker/api.rs index 32f8b35e..f9ed6bd3 100644 --- a/src/maker/api.rs +++ b/src/maker/api.rs @@ -18,11 +18,8 @@ use crate::{ }, wallet::{RPCConfig, SwapCoin, WalletSwapCoin}, watch_tower::{ - registry_storage::FileRegistry, - rpc_backend::BitcoinRpc, - service::WatchService, - watcher::{Role, Watcher, WatcherEvent}, - zmq_backend::ZmqBackend, + service::{start_maker_watch_service, WatchService}, + watcher::{Role, WatcherEvent}, }, }; use bitcoin::{ @@ -38,9 +35,9 @@ use std::{ path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering::Relaxed}, - mpsc, Arc, Mutex, RwLock, + Arc, Mutex, RwLock, }, - thread::{self, JoinHandle}, + thread::JoinHandle, time::{Duration, Instant}, }; @@ -297,25 +294,8 @@ impl Maker { config.network_port = port; } - // ## TODO: Encapsulate these initialization inside the watcher and - // pollute the client declaration. - let backend = ZmqBackend::new(&zmq_addr); - let rpc_backend = BitcoinRpc::new(rpc_config.clone())?; - let blockchain_info = rpc_backend.get_blockchain_info()?; - let file_registry = data_dir - .join(format!(".maker_{}_watcher", config.network_port)) - .join(blockchain_info.chain.to_string()); - let registry = FileRegistry::load(file_registry); - let (tx_requests, rx_requests) = mpsc::channel(); - let (tx_events, rx_responses) = mpsc::channel(); - let rpc_config_watcher = rpc_config.clone(); - - let mut watcher = Watcher::::new(backend, registry, rx_requests, tx_events); - _ = thread::Builder::new() - .name("Watcher thread".to_string()) - .spawn(move || watcher.run(rpc_config_watcher)); - - let watch_service = WatchService::new(tx_requests, rx_responses); + let watch_service = + start_maker_watch_service(&zmq_addr, &rpc_config, &data_dir, config.network_port)?; let mut wallet = Wallet::load_or_init_wallet(&wallet_path, &rpc_config, password)?; diff --git a/src/maker/api2.rs b/src/maker/api2.rs index 659a816c..24564e91 100644 --- a/src/maker/api2.rs +++ b/src/maker/api2.rs @@ -25,11 +25,8 @@ use crate::{ WalletError, }, watch_tower::{ - registry_storage::FileRegistry, - rpc_backend::BitcoinRpc, - service::WatchService, - watcher::{Role, Watcher}, - zmq_backend::ZmqBackend, + service::{start_maker_watch_service, WatchService}, + watcher::Role, }, }; use bitcoin::{ @@ -42,9 +39,9 @@ use std::{ path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering::Relaxed}, - mpsc, Arc, Mutex, RwLock, + Arc, Mutex, RwLock, }, - thread::{self, JoinHandle}, + thread::JoinHandle, time::{Duration, Instant}, }; @@ -238,7 +235,7 @@ pub struct Maker { /// Map of IP address to Connection State + last Connected instant pub(crate) ongoing_swap_state: Mutex>, /// Highest Value Fidelity Proof - pub(crate) highest_fidelity_proof: RwLock>, + pub(crate) highest_fidelity_proof: RwLock>, /// Is setup complete pub is_setup_complete: AtomicBool, /// Path for the data directory. @@ -283,25 +280,8 @@ impl Maker { config.network_port = port; } - // ## TODO: Encapsulate these initialization inside the watcher and - // pollute the client declaration. - let backend = ZmqBackend::new(&zmq_addr); - let rpc_backend = BitcoinRpc::new(rpc_config.clone())?; - let blockchain_info = rpc_backend.get_blockchain_info()?; - let file_registry = data_dir - .join(format!(".maker_{}_watcher", config.network_port)) - .join(blockchain_info.chain.to_string()); - let registry = FileRegistry::load(file_registry); - let (tx_requests, rx_requests) = mpsc::channel(); - let (tx_events, rx_responses) = mpsc::channel(); - let rpc_config_watcher = rpc_config.clone(); - - let mut watcher = Watcher::::new(backend, registry, rx_requests, tx_events); - _ = thread::Builder::new() - .name("Watcher thread".to_string()) - .spawn(move || watcher.run(rpc_config_watcher)); - - let watch_service = WatchService::new(tx_requests, rx_responses); + let watch_service = + start_maker_watch_service(&zmq_addr, &rpc_config, &data_dir, config.network_port)?; let mut wallet = if wallet_path.exists() { let wallet = Wallet::load(&wallet_path, &rpc_config, password)?; @@ -372,10 +352,9 @@ impl Maker { connection_state: &mut ConnectionState, ) -> Result { let wallet = self.wallet.read()?; - let (incoming_contract_my_privkey, incoming_contract_my_pubkey) = - wallet.get_tweakable_keypair()?; - connection_state.incoming_contract.my_privkey = Some(incoming_contract_my_privkey); - connection_state.incoming_contract.my_pubkey = Some(incoming_contract_my_pubkey); + // Create a temporary incoming contract pubkey here, replace with the actual pubkey after sending AckResponse msg to taker. + let (_, incoming_contract_temporary_pubkey) = wallet.get_tweakable_keypair()?; + connection_state.incoming_contract.my_pubkey = Some(incoming_contract_temporary_pubkey); log::info!( "[{}] create_offer: Set my_privkey for incoming contract, is_some={}", self.config.network_port, @@ -397,7 +376,8 @@ impl Maker { let max_size = max_size.to_sat(); Ok(Offer { - tweakable_point: incoming_contract_my_pubkey, + // send temporary pubkey to taker for now + tweakable_point: incoming_contract_temporary_pubkey, base_fee: BASE_FEE, amount_relative_fee: AMOUNT_RELATIVE_FEE_PCT, time_relative_fee: TIME_RELATIVE_FEE_PCT, diff --git a/src/maker/handlers2.rs b/src/maker/handlers2.rs index f599498a..6585f024 100644 --- a/src/maker/handlers2.rs +++ b/src/maker/handlers2.rs @@ -7,14 +7,19 @@ use std::sync::Arc; +use bitcoin::Amount; + use super::{ api2::{ConnectionState, Maker}, error::MakerError, }; -use crate::protocol::messages2::{ - AckResponse, GetOffer, MakerToTakerMessage, PrivateKeyHandover, SendersContract, SwapDetails, - TakerToMakerMessage, +use crate::protocol::{ + self, + messages2::{ + GetOffer, MakerToTakerMessage, PrivateKeyHandover, SendersContract, SwapDetails, + TakerToMakerMessage, + }, }; /// The Global Handle Message function for taproot protocol. Takes in a [`Arc`] and handles @@ -82,27 +87,31 @@ fn handle_swap_details( swap_details.no_of_tx ); - // Reject if GetOffer wasn't received first (my_privkey must be set) - // This ensures the taker has a fresh offer with a valid tweakable_point - if connection_state.incoming_contract.my_privkey.is_none() { + // Reject if GetOffer wasn't received first (temporary_pubkey must be set) + // This ensures the taker has a fresh offer. + if connection_state.incoming_contract.my_pubkey.is_none() { log::warn!( "[{}] Rejecting SwapDetails - GetOffer must be sent first to establish keypair", maker.config.network_port ); - return Ok(Some(MakerToTakerMessage::AckResponse(AckResponse::Nack))); } - // Reject if there's already an active swap in progress for this connection - // This prevents an attacker from resetting another taker's swap state - // [TODO] Remove this once we have a way to handle multiple swaps using swap_id - // if connection_state.swap_amount > Amount::ZERO { - // log::warn!( - // "[{}] Rejecting SwapDetails - swap already in progress with amount {}", - // maker.config.network_port, - // connection_state.swap_amount - // ); - // return Ok(Some(MakerToTakerMessage::AckResponse(AckResponse::Nack))); - // } + let (privkey, pubkey) = maker.wallet.read()?.get_tweakable_keypair()?; + connection_state.incoming_contract.my_privkey = Some(privkey); + connection_state.incoming_contract.my_pubkey = Some(pubkey); + + if connection_state.swap_amount > Amount::ZERO { + log::warn!( + "[{}] Rejecting SwapDetails - swap already in progress with amount {}", + maker.config.network_port, + connection_state.swap_amount + ); + return Ok(Some(MakerToTakerMessage::AckResponse( + protocol::messages2::AckResponse { + tweakable_point: None, + }, + ))); + } // Validate swap parameters using api2 maker.validate_swap_parameters(&swap_details)?; @@ -132,7 +141,11 @@ fn handle_swap_details( } // Send acknowledgment - Ok(Some(MakerToTakerMessage::AckResponse(AckResponse::Ack))) + Ok(Some(MakerToTakerMessage::AckResponse( + protocol::messages2::AckResponse { + tweakable_point: Some(pubkey), + }, + ))) } /// Handles SendersContract message and creates our receiver contract diff --git a/src/maker/server.rs b/src/maker/server.rs index 5a44e0aa..72193edf 100644 --- a/src/maker/server.rs +++ b/src/maker/server.rs @@ -4,19 +4,9 @@ //! The server maintains the thread pool for P2P Connection, Watchtower, Bitcoin Backend, and RPC Client Request. //! The server listens at two ports: 6102 for P2P, and 6103 for RPC Client requests. -use crate::{ - protocol::messages::FidelityProof, - utill::{COINSWAP_KIND, NOSTR_RELAYS}, -}; +use crate::protocol::messages::FidelityProof; use bitcoin::{absolute::LockTime, Amount}; use bitcoind::bitcoincore_rpc::RpcApi; -use nostr::{ - event::{EventBuilder, Kind}, - key::{Keys, SecretKey}, - message::{ClientMessage, RelayMessage}, - util::JsonUtil, -}; -use tungstenite::Message; use std::{ io::ErrorKind, @@ -44,6 +34,7 @@ use crate::{ handlers::handle_message, rpc::start_rpc_server, }, + nostr_coinswap::broadcast_bond_on_nostr, protocol::messages::TakerToMakerMessage, utill::{read_message, send_message, HEART_BEAT_INTERVAL, MIN_FEE_RATE}, wallet::{AddressType, WalletError}, @@ -146,97 +137,6 @@ fn spawn_nostr_broadcast_task( Ok(()) } -// ##TODO: Make this part of nostr module and improve error handing -// ##TODO: Try retry in case relay doesn't accept the event -fn broadcast_bond_on_nostr(fidelity: FidelityProof) -> Result<(), MakerError> { - let outpoint = fidelity.bond.outpoint; - let content = format!("{}:{}", outpoint.txid, outpoint.vout); - - // ##TODO: Don't use ephemeral keys - let secret_key = SecretKey::generate(); - let keys = Keys::new(secret_key); - - let event = EventBuilder::new(Kind::Custom(COINSWAP_KIND), content) - .build(keys.public_key) - .sign_with_keys(&keys) - .expect("Event should be signed"); - - let msg = ClientMessage::Event(std::borrow::Cow::Owned(event)); - - log::debug!("nostr wire msg: {}", msg.as_json()); - - let mut success = false; - - for relay in NOSTR_RELAYS { - match broadcast_to_relay(relay, &msg) { - Ok(()) => { - success = true; - } - Err(e) => { - log::warn!("failed to broadcast to {}: {:?}", relay, e); - } - } - } - - if !success { - log::warn!("nostr event was not accepted by any relay"); - } - - Ok(()) -} - -fn broadcast_to_relay(relay: &str, msg: &ClientMessage) -> Result<(), MakerError> { - let (mut socket, _) = tungstenite::connect(relay).map_err(|e| { - log::warn!("failed to connect to nostr relay {}: {}", relay, e); - MakerError::General("failed to connect to nostr relay") - })?; - - socket - .write(Message::Text(msg.as_json().into())) - .map_err(|e| { - log::warn!("nostr relay write failed: {}", e); - MakerError::General("failed to write to nostr relay") - })?; - socket.flush().ok(); - - match socket.read() { - Ok(Message::Text(text)) => { - if let Ok(relay_msg) = RelayMessage::from_json(&text) { - match relay_msg { - RelayMessage::Ok { - event_id, - status: true, - .. - } => { - log::info!("nostr relay {} accepted event {}", relay, event_id); - return Ok(()); - } - RelayMessage::Ok { - event_id, - status: false, - message, - } => { - log::warn!( - "nostr relay {} rejected event {}: {}", - relay, - event_id, - message - ); - } - _ => {} - } - } - } - Ok(_) => {} - Err(e) => { - log::warn!("nostr relay {} read error: {}", relay, e); - } - } - log::warn!("nostr relay {} did not confirm event", relay); - - Err(MakerError::General("nostr relay did not confirm event")) -} - /// Ensures the wallet has a valid fidelity bond. If no active bond exists, it creates a new one. /// /// ### NOTE ON VALID FIDELITY BOND: diff --git a/src/maker/server2.rs b/src/maker/server2.rs index 209a4425..7abb5a36 100644 --- a/src/maker/server2.rs +++ b/src/maker/server2.rs @@ -7,12 +7,6 @@ use bitcoin::Amount; use bitcoind::bitcoincore_rpc::RpcApi; -use nostr::{ - event::{EventBuilder, Kind}, - key::{Keys, SecretKey}, - message::{ClientMessage, RelayMessage}, - util::JsonUtil, -}; use std::{ io::ErrorKind, net::{Ipv4Addr, TcpListener, TcpStream}, @@ -23,7 +17,6 @@ use std::{ thread::{self, sleep}, time::{Duration, Instant}, }; -use tungstenite::Message; pub(crate) use super::api2::{Maker, RPC_PING_INTERVAL}; @@ -34,11 +27,12 @@ use crate::{ handlers2::handle_message_taproot, rpc::start_rpc_server, }, - protocol::messages2::{FidelityProof, MakerToTakerMessage, TakerToMakerMessage}, - utill::{ - get_tor_hostname, read_message, send_message, COINSWAP_KIND, HEART_BEAT_INTERVAL, - MIN_FEE_RATE, NOSTR_RELAYS, + nostr_coinswap::broadcast_bond_on_nostr, + protocol::{ + messages::FidelityProof, + messages2::{MakerToTakerMessage, TakerToMakerMessage}, }, + utill::{get_tor_hostname, read_message, send_message, HEART_BEAT_INTERVAL, MIN_FEE_RATE}, wallet::{AddressType, WalletError}, }; @@ -136,102 +130,12 @@ fn spawn_nostr_broadcast_task( Ok(()) } -// ##TODO: Make this part of nostr module and improve error handing -// ##TODO: Try retry in case relay doesn't accept the event -fn broadcast_bond_on_nostr(fidelity: FidelityProof) -> Result<(), MakerError> { - let outpoint = fidelity.bond.outpoint; - let content = format!("{}:{}", outpoint.txid, outpoint.vout); - - let secret_key = SecretKey::generate(); - let keys = Keys::new(secret_key); - - let event = EventBuilder::new(Kind::Custom(COINSWAP_KIND), content) - .build(keys.public_key) - .sign_with_keys(&keys) - .expect("Event should be signed"); - - let msg = ClientMessage::Event(std::borrow::Cow::Owned(event)); - - log::debug!("nostr wire msg: {}", msg.as_json()); - - let mut success = false; - - for relay in NOSTR_RELAYS { - match broadcast_to_relay(relay, &msg) { - Ok(()) => { - success = true; - } - Err(e) => { - log::warn!("failed to broadcast to {}: {:?}", relay, e); - } - } - } - - if !success { - log::warn!("nostr event was not accepted by any relay"); - } - - Ok(()) -} - -fn broadcast_to_relay(relay: &str, msg: &ClientMessage) -> Result<(), MakerError> { - let (mut socket, _) = tungstenite::connect(relay).map_err(|e| { - log::warn!("failed to connect to nostr relay {}: {}", relay, e); - MakerError::General("failed to connect to nostr relay") - })?; - - socket - .write(Message::Text(msg.as_json().into())) - .map_err(|e| { - log::warn!("nostr relay write failed: {}", e); - MakerError::General("failed to write to nostr relay") - })?; - socket.flush().ok(); - - match socket.read() { - Ok(Message::Text(text)) => { - if let Ok(relay_msg) = RelayMessage::from_json(&text) { - match relay_msg { - RelayMessage::Ok { - event_id, - status: true, - .. - } => { - log::info!("nostr relay {} accepted event {}", relay, event_id); - return Ok(()); - } - RelayMessage::Ok { - event_id, - status: false, - message, - } => { - log::warn!( - "nostr relay {} rejected event {}: {}", - relay, - event_id, - message - ); - } - _ => {} - } - } - } - Ok(_) => {} - Err(e) => { - log::warn!("nostr relay {} read error: {}", relay, e); - } - } - log::warn!("nostr relay {} did not confirm event", relay); - - Err(MakerError::General("nostr relay did not confirm event")) -} - /// Ensures the wallet has a valid fidelity bond for taproot operations. /// This follows the same pattern as the regular maker setup_fidelity_bond function. fn setup_fidelity_bond_taproot( maker: &Maker, maker_address: &str, -) -> Result { +) -> Result { use crate::wallet::WalletError; use bitcoin::absolute::LockTime; use std::thread; @@ -252,10 +156,9 @@ fn setup_fidelity_bond_taproot( .read()? .generate_fidelity_proof(i, maker_address)?; - // Convert to messages2::FidelityProof - let highest_proof = crate::protocol::messages2::FidelityProof { + let highest_proof = crate::protocol::messages::FidelityProof { bond: proof_message.bond.clone(), - cert_hash: *bitcoin::hashes::sha256::Hash::from_bytes_ref( + cert_hash: *bitcoin::hashes::sha256d::Hash::from_bytes_ref( proof_message.cert_hash.as_ref(), ), cert_sig: proof_message.cert_sig, @@ -376,9 +279,9 @@ fn setup_fidelity_bond_taproot( .generate_fidelity_proof(i, maker_address)?; // Convert to messages2::FidelityProof - let highest_proof = crate::protocol::messages2::FidelityProof { + let highest_proof = crate::protocol::messages::FidelityProof { bond: proof_message.bond, - cert_hash: *bitcoin::hashes::sha256::Hash::from_bytes_ref( + cert_hash: *bitcoin::hashes::sha256d::Hash::from_bytes_ref( proof_message.cert_hash.as_ref(), ), cert_sig: proof_message.cert_sig, @@ -524,29 +427,39 @@ fn handle_client_taproot(maker: &Arc, stream: &mut TcpStream) -> Result<( TakerToMakerMessage::GetOffer(msg) => Some(msg.id.clone()), TakerToMakerMessage::SwapDetails(msg) => Some(msg.id.clone()), TakerToMakerMessage::SendersContract(msg) => Some(msg.id.clone()), - TakerToMakerMessage::PrivateKeyHandover(msg) => msg.id.clone(), + TakerToMakerMessage::PrivateKeyHandover(msg) => Some(msg.id.clone()), } .ok_or_else(|| MakerError::General("Message missing swap_id"))?; // Get or create connection state for this swap id let mut connection_state = { - let mut ongoing_swaps = maker.ongoing_swap_state.lock()?; + let ongoing_swaps = maker.ongoing_swap_state.lock()?; match &message { TakerToMakerMessage::GetOffer(_) => { - // TODO: Right now sync_offerbook is sending GetOffer with an empty string swap_id - // Refactor to not create a connection state when swap id is an empty string - // Just return the Offer - let (state, _) = ongoing_swaps.entry(swap_id.clone()).or_insert_with(|| { + log::info!( + "[{}] Using temporary connection state for GetOffer from {:?}", + maker.config.network_port, + &swap_id + ); + ConnectionState::default() + } + TakerToMakerMessage::SwapDetails(_) => match ongoing_swaps.get(&swap_id) { + Some((state, _)) => { log::info!( - "[{}] Creating new connection state for {:?}", - maker.config.network_port, - &swap_id + "[{}] Found existing connection state for SwapDetails", + maker.config.network_port ); - (ConnectionState::default(), Instant::now()) - }); - state.clone() - } + state.clone() + } + None => { + log::info!( + "[{}] Creating new connection state for SwapDetails", + maker.config.network_port + ); + ConnectionState::default() + } + }, _ => match ongoing_swaps.get(&swap_id) { Some((state, _)) => state.clone(), None => { @@ -585,10 +498,12 @@ fn handle_client_taproot(maker: &Arc, stream: &mut TcpStream) -> Result<( break; } }; - { + + // Persist connection state after sending AckResponse message only. + if !matches!(response, Some(MakerToTakerMessage::RespOffer(_))) { let mut ongoing_swaps = maker.ongoing_swap_state.lock()?; log::info!( - "[{}] Saving connection state for {}: swap_amount={}, my_privkey_is_some={}", + "[{}] Persisting connection state for {}: swap_amount={}, my_privkey_is_some={}", maker.config.network_port, ip, connection_state.swap_amount, diff --git a/src/nostr_coinswap.rs b/src/nostr_coinswap.rs new file mode 100644 index 00000000..40627b90 --- /dev/null +++ b/src/nostr_coinswap.rs @@ -0,0 +1,132 @@ +//! Nostr integration for Maker announcements and coordination. +//! +//! This module provides a minimal interface for publishing Maker-related +//! events over the Nostr protocol. It is primarily used to broadcast +//! fidelity bond information and other coordination signals required +//! by the Coinswap protocol. + +use nostr::{ + event::{EventBuilder, Kind}, + key::{Keys, SecretKey}, + message::{ClientMessage, RelayMessage}, + util::JsonUtil, +}; +use tungstenite::Message; + +use crate::{maker::MakerError, protocol::messages::FidelityProof}; + +/// nost url for coinswap +#[cfg(not(feature = "integration-test"))] +pub const NOSTR_RELAYS: &[&str] = &["wss://nos.lol"]; +/// nostr url for coinswap +#[cfg(feature = "integration-test")] +pub const NOSTR_RELAYS: &[&str] = &["ws://127.0.0.1:8000"]; + +/// coinswap nostr event kind +pub const COINSWAP_KIND: u16 = 37777; + +/// Broadcasts a fidelity bond announcement over Nostr. +pub fn broadcast_bond_on_nostr(fidelity: FidelityProof) -> Result<(), MakerError> { + let outpoint = fidelity.bond.outpoint; + let content = format!("{}:{}", outpoint.txid, outpoint.vout); + + let secret_key = SecretKey::generate(); + let keys = Keys::new(secret_key); + + let event = EventBuilder::new(Kind::Custom(COINSWAP_KIND), content) + .build(keys.public_key) + .sign_with_keys(&keys) + .expect("Event should be signed"); + + let msg = ClientMessage::Event(std::borrow::Cow::Owned(event)); + + log::debug!("nostr wire msg: {}", msg.as_json()); + + const MAX_RETRIES: usize = 3; + const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2); + + let mut success = false; + + for relay in NOSTR_RELAYS { + for attempt in 1..=MAX_RETRIES { + match broadcast_to_relay(relay, &msg) { + Ok(()) => { + success = true; + break; + } + Err(e) => { + log::warn!( + "failed to broadcast to {} (attempt {}/{}): {:?}", + relay, + attempt, + MAX_RETRIES, + e + ); + + if attempt < MAX_RETRIES { + std::thread::sleep(RETRY_DELAY); + } + } + } + } + } + + if !success { + log::warn!("nostr event was not accepted by any relay"); + } + + Ok(()) +} + +/// Sends a Nostr event to a single relay and waits for confirmation. +fn broadcast_to_relay(relay: &str, msg: &ClientMessage) -> Result<(), MakerError> { + let (mut socket, _) = tungstenite::connect(relay).map_err(|e| { + log::warn!("failed to connect to nostr relay {}: {}", relay, e); + MakerError::General("failed to connect to nostr relay") + })?; + + socket + .write(Message::Text(msg.as_json().into())) + .map_err(|e| { + log::warn!("nostr relay write failed: {}", e); + MakerError::General("failed to write to nostr relay") + })?; + socket.flush().ok(); + + match socket.read() { + Ok(Message::Text(text)) => { + if let Ok(relay_msg) = RelayMessage::from_json(&text) { + match relay_msg { + RelayMessage::Ok { + event_id, + status: true, + .. + } => { + log::info!("nostr relay {} accepted event {}", relay, event_id); + return Ok(()); + } + RelayMessage::Ok { + event_id, + status: false, + message, + } => { + log::warn!( + "nostr relay {} rejected event {}: {}", + relay, + event_id, + message + ); + } + _ => {} + } + } + } + Ok(_) => {} + Err(e) => { + log::warn!("nostr relay {} read error: {}", relay, e); + } + } + log::warn!("nostr relay {} did not confirm event", relay); + + Err(MakerError::General("nostr relay did not confirm event")) +} diff --git a/src/protocol/messages2.rs b/src/protocol/messages2.rs index 2e29a54d..2672e8d6 100644 --- a/src/protocol/messages2.rs +++ b/src/protocol/messages2.rs @@ -1,6 +1,6 @@ //! This module defines the messages communicated between the parties(Taker, Maker) -use crate::wallet::FidelityBond; -use bitcoin::{hashes::sha256::Hash, Amount, PublicKey, ScriptBuf, Txid}; +use crate::protocol::messages::FidelityProof; +use bitcoin::{Amount, PublicKey, ScriptBuf, Txid}; use secp256k1::musig::{PartialSignature, PublicNonce}; use serde::{Deserialize, Serialize}; use std::{convert::TryInto, fmt::Display}; @@ -87,14 +87,6 @@ impl From for PartialSignature { } } -/// Represents a fidelity proof in the Coinswap protocol -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct FidelityProof { - pub(crate) bond: FidelityBond, - pub(crate) cert_hash: Hash, - pub(crate) cert_sig: bitcoin::secp256k1::ecdsa::Signature, -} - /// Private key handover message exchanged during taproot coinswap sweeps /// /// After contract transactions are established and broadcasted, parties exchange @@ -103,7 +95,7 @@ pub struct FidelityProof { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PrivateKeyHandover { /// Unique 8 byte ID to identify each swap process separately. Always generated by the Takers. - pub(crate) id: Option, + pub(crate) id: String, /// The outgoing contract private key pub(crate) secret_key: bitcoin::secp256k1::SecretKey, } @@ -162,10 +154,11 @@ pub(crate) struct SwapDetails { pub(crate) timelock: u16, } +/// `tweakable_point = Some(_)` → swap accepted (Ack) +/// `tweakable_point = None` → swap rejected (Nack) #[derive(Debug, Serialize, Deserialize)] -pub(crate) enum AckResponse { - Ack, - Nack, +pub(crate) struct AckResponse { + pub tweakable_point: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/taker/api2.rs b/src/taker/api2.rs index 1c53ca64..0ba8dec8 100644 --- a/src/taker/api2.rs +++ b/src/taker/api2.rs @@ -7,8 +7,8 @@ use crate::{ }, error::ProtocolError, messages2::{ - AckResponse, GetOffer, MakerToTakerMessage, Preimage, PrivateKeyHandover, - SenderContractFromMaker, SendersContract, SwapDetails, TakerToMakerMessage, + GetOffer, MakerToTakerMessage, Preimage, PrivateKeyHandover, SenderContractFromMaker, + SendersContract, SwapDetails, TakerToMakerMessage, }, musig_interface::{ aggregate_partial_signatures_compat, generate_new_nonce_pair_compat, @@ -796,14 +796,16 @@ impl Taker { TakerToMakerMessage::GetOffer(get_offer_msg), )?; match get_offer_response { - MakerToTakerMessage::RespOffer(fresh_offer) => { + MakerToTakerMessage::RespOffer(_fresh_offer) => { log::info!( - "Received fresh offer from maker: {:?}, updating tweakable_point", + "Received fresh offer from maker: {:?}", suitable_maker.address ); - // TODO: Update entire offer instead of just tweakable_point. + // TODO: Update entire offer other than tweakable_point + // as the maker is sending temporary tweakable point in + // RespOffer message. // This requires OfferAndAddress to use messages2::Offer for taproot swaps. - suitable_maker.offer.tweakable_point = fresh_offer.tweakable_point; + //suitable_maker.offer.tweakable_point = fresh_offer.tweakable_point; } _ => { log::warn!( @@ -839,15 +841,21 @@ impl Taker { let msg = TakerToMakerMessage::SwapDetails(swap_details); let response = self.send_to_maker_and_get_response(&suitable_maker.address, msg); match response { - Ok(MakerToTakerMessage::AckResponse(AckResponse::Ack)) => { - log::info!("Received AckResponse from maker: {:?}", suitable_maker); - self.ongoing_swap_state - .chosen_makers - .push(suitable_maker.clone()); - } - Ok(MakerToTakerMessage::AckResponse(AckResponse::Nack)) => { - log::warn!("Maker {:?} did not accept the swap request", suitable_maker); - continue; + Ok(MakerToTakerMessage::AckResponse(response)) => { + if response.tweakable_point.is_some() { + log::info!( + "Received AckResponse from maker: {:?}, updating tweakable_point.", + suitable_maker + ); + // Update the tweakable point + suitable_maker.offer.tweakable_point = response.tweakable_point.unwrap(); + self.ongoing_swap_state + .chosen_makers + .push(suitable_maker.clone()); + } else { + log::warn!("Maker {:?} did not accept the swap request", suitable_maker); + continue; + } } _ => { self.offerbook.add_bad_maker(suitable_maker); @@ -1392,7 +1400,7 @@ impl Taker { // Create private key handover message let privkey_msg = TakerToMakerMessage::PrivateKeyHandover(PrivateKeyHandover { - id: Some(self.ongoing_swap_state.id.clone()), + id: self.ongoing_swap_state.id.clone(), secret_key: outgoing_privkey, }); diff --git a/src/utill.rs b/src/utill.rs index 48537565..fd295032 100644 --- a/src/utill.rs +++ b/src/utill.rs @@ -40,15 +40,6 @@ use crossterm::{ use std::str::FromStr; static LOGGER: OnceLock<()> = OnceLock::new(); -/// nost url for coinswap -#[cfg(not(feature = "integration-test"))] -pub const NOSTR_RELAYS: &[&str] = &["wss://nos.lol"]; -/// nostr url for coinswap -#[cfg(feature = "integration-test")] -pub const NOSTR_RELAYS: &[&str] = &["ws://127.0.0.1:8000"]; - -/// coinswap nostr event kind -pub const COINSWAP_KIND: u16 = 37777; use crate::{ error::NetError, diff --git a/src/watch_tower/rpc_backend.rs b/src/watch_tower/rpc_backend.rs index fc9deded..1865083b 100644 --- a/src/watch_tower/rpc_backend.rs +++ b/src/watch_tower/rpc_backend.rs @@ -23,7 +23,7 @@ use nostr::{ use tungstenite::{stream::MaybeTlsStream, Message}; use crate::{ - utill::{COINSWAP_KIND, NOSTR_RELAYS}, + nostr_coinswap::{COINSWAP_KIND, NOSTR_RELAYS}, wallet::RPCConfig, watch_tower::{ registry_storage::FileRegistry, diff --git a/src/watch_tower/service.rs b/src/watch_tower/service.rs index 2e77e2f4..7074db1a 100644 --- a/src/watch_tower/service.rs +++ b/src/watch_tower/service.rs @@ -1,12 +1,26 @@ //! Public watchtower service for sending commands to and receiving events from the watcher. use bitcoin::OutPoint; -use std::sync::{ - mpsc::{Receiver, Sender}, - Arc, Mutex, +use std::{ + path::Path, + sync::{ + mpsc::{self, Receiver, Sender}, + Arc, Mutex, + }, + thread, }; -use crate::watch_tower::watcher::{WatcherCommand, WatcherEvent}; +use crate::{ + maker::Maker, + wallet::RPCConfig, + watch_tower::{ + registry_storage::FileRegistry, + rpc_backend::BitcoinRpc, + watcher::{Watcher, WatcherCommand, WatcherEvent}, + watcher_error::WatcherError, + zmq_backend::ZmqBackend, + }, +}; /// Client-facing service for sending watcher commands and receiving events. #[derive(Clone)] @@ -63,3 +77,37 @@ impl WatchService { let _ = self.tx.send(WatcherCommand::Shutdown); } } + +/// Starts the Maker Watch Service +pub fn start_maker_watch_service( + zmq_addr: &str, + rpc_config: &RPCConfig, + data_dir: &Path, + network_port: u16, +) -> Result { + // Backends + let backend = ZmqBackend::new(zmq_addr); + let rpc_backend = BitcoinRpc::new(rpc_config.clone())?; + let blockchain_info = rpc_backend.get_blockchain_info()?; + + // Registry + let file_registry = data_dir + .join(format!(".maker_{}_watcher", network_port)) + .join(blockchain_info.chain.to_string()); + let registry = FileRegistry::load(file_registry); + + // Channels + let (tx_requests, rx_requests) = mpsc::channel(); + let (tx_events, rx_responses) = mpsc::channel(); + + // Watcher + let rpc_config_watcher = rpc_config.clone(); + let mut watcher = Watcher::::new(backend, registry, rx_requests, tx_events); + + thread::Builder::new() + .name("Watcher thread".to_string()) + .spawn(move || watcher.run(rpc_config_watcher)) + .expect("failed to spawn watcher thread"); + + Ok(WatchService::new(tx_requests, rx_responses)) +}