Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/bin/taker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use log::LevelFilter;
use serde_json::{json, to_string_pretty};
use std::{path::PathBuf, str::FromStr};

#[cfg(feature = "integration-test")]
use coinswap::taker::api2::TakerBehavior as TaprootTakerBehavior;
#[cfg(feature = "integration-test")]
use coinswap::taker::TakerBehavior;

Expand Down Expand Up @@ -200,6 +202,8 @@ fn main() -> Result<(), TakerError> {
args.tor_auth,
args.zmq,
None,
#[cfg(feature = "integration-test")]
TaprootTakerBehavior::Normal,
)?;
taproot_taker.recover_from_swap()?;
}
Expand All @@ -214,6 +218,8 @@ fn main() -> Result<(), TakerError> {
args.tor_auth,
args.zmq,
None,
#[cfg(feature = "integration-test")]
TaprootTakerBehavior::Normal,
)?;

let taproot_swap_params = coinswap::taker::api2::SwapParams {
Expand Down
16 changes: 16 additions & 0 deletions src/maker/api2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub enum MakerBehavior {
/// Close connection after sweeping incoming contract but before completing handover
/// This allows maker to recover their coins but forces taker to recover via hashlock/timelock
CloseAfterSweep,
/// Close connection after accepting Swap offer from taker and sending it AckResponse message.
CloseAfterAckResponse,
}

/// Interval for health checks on a stable RPC connection with bitcoind.
Expand Down Expand Up @@ -918,6 +920,20 @@ impl Maker {
.contract_tx
.compute_txid();

for (vout, _) in connection_state
.incoming_contract
.contract_tx
.output
.iter()
.enumerate()
{
let outpoint = OutPoint {
txid: incoming_txid,
vout: vout as u32,
};
self.watch_service.unwatch(outpoint);
}

// Record the swept coin to track swap balance
let output_scriptpubkey = spending_tx.output[0].script_pubkey.clone();
// [TODO] Look into the key value pair later, it shouldn't be both sriptpubkey
Expand Down
12 changes: 12 additions & 0 deletions src/maker/handlers2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ fn handle_swap_details(
our_fee
);

// Check for CloseAfterAckResponse behavior
#[cfg(feature = "integration-test")]
if maker.behavior == super::api2::MakerBehavior::CloseAfterAckResponse {
log::warn!(
"[{}] Maker behavior: CloseAfterAckResponse - Closing connection after sending Ack Response message to taker",
maker.config.network_port
);
return Err(MakerError::General(
"Maker closing connection after sending AckResponse to taker (test behavior)",
));
}

// Send acknowledgment
Ok(Some(MakerToTakerMessage::AckResponse(AckResponse::Ack)))
}
Expand Down
85 changes: 82 additions & 3 deletions src/taker/api2.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::error::TakerError;
use crate::{
protocol::{
contract2::{
Expand Down Expand Up @@ -51,7 +52,20 @@ use std::{
time::Duration,
};

use super::error::TakerError;
/// Represents different behaviors taker can have during the swap.
/// Used for testing various possible scenarios that can happen during a swap.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg(feature = "integration-test")]
pub enum TakerBehavior {
/// Normal behaviour
Normal,
/// Close connection after receiving AckResponse message from the maker. (no outgoing/incoming contract created)
CloseAtAckResponse,
/// Close connection after sending SendersContract (outgoing contract created)
CloseAtSendersContract,
/// Close connection after receiving SendersContract from maker (outgoing contract created).
CloseAtSendersContractFromMaker,
}

/// Represents how a taproot contract output was spent
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -321,10 +335,13 @@ pub struct Taker {
ongoing_swap_state: OngoingSwapState,
data_dir: PathBuf,
watch_service: WatchService,
#[cfg(feature = "integration-test")]
behavior: TakerBehavior,
}

impl Taker {
/// Initialize a new Taker instance
#[allow(clippy::too_many_arguments)]
pub fn init(
data_dir: Option<PathBuf>,
wallet_file_name: Option<String>,
Expand All @@ -333,6 +350,7 @@ impl Taker {
tor_auth_password: Option<String>,
zmq_addr: String,
password: Option<String>,
#[cfg(feature = "integration-test")] behavior: TakerBehavior,
) -> Result<Taker, TakerError> {
let data_dir = data_dir.unwrap_or_else(get_taker_dir);

Expand Down Expand Up @@ -418,6 +436,8 @@ impl Taker {
ongoing_swap_state: OngoingSwapState::default(),
data_dir,
watch_service,
#[cfg(feature = "integration-test")]
behavior,
})
}

Expand Down Expand Up @@ -466,6 +486,16 @@ impl Taker {
self.choose_makers_for_swap(swap_params)?;
self.setup_contract_keys_and_scripts()?;

#[cfg(feature = "integration-test")]
{
if self.behavior == TakerBehavior::CloseAtAckResponse {
log::error!(
"Dropping Swap Process after receiving Ack Response message from maker"
);
return Err(TakerError::General("Taker Dropping after receiving AckResponse message from maker. (Test behavior)".to_string(),));
}
}

let outgoing_signed_contract_transactions = self.create_outgoing_contract_transactions()?;
let tx = match outgoing_signed_contract_transactions.first() {
Some(tx) => tx,
Expand Down Expand Up @@ -498,7 +528,6 @@ impl Taker {
for tx in &outgoing_signed_contract_transactions {
self.wallet.wait_for_tx_confirmation(tx.compute_txid())?;
}

match self
.negotiate_with_makers_and_coordinate_sweep(&outgoing_signed_contract_transactions)
{
Expand All @@ -509,6 +538,7 @@ impl Taker {

// Store swap state before reset for report generation
let prereset_swapstate = self.ongoing_swap_state.clone();
self.ongoing_swap_state = OngoingSwapState::default();

// Sync wallet and generate report
self.wallet.sync_and_save()?;
Expand Down Expand Up @@ -1234,6 +1264,16 @@ impl Taker {

let msg = TakerToMakerMessage::SendersContract(senders_contract.clone());
let response = self.send_to_maker_and_get_response(&first_maker.address, msg)?;
#[cfg(feature = "integration-test")]
{
if self.behavior == TakerBehavior::CloseAtSendersContract {
log::error!("Dropping connection after sending SendersContract to Maker");
return Err(TakerError::General(
"Taker dropping of after sending Senders Contract to Maker (test behavior) "
.to_string(),
));
}
}

match response {
MakerToTakerMessage::SenderContractFromMaker(incoming_contract) => {
Expand Down Expand Up @@ -1310,6 +1350,17 @@ impl Taker {

match maker_response {
MakerToTakerMessage::SenderContractFromMaker(maker_contract) => {
#[cfg(feature = "integration-test")]
{
if self.behavior == TakerBehavior::CloseAtSendersContractFromMaker {
log::error!(
"Dropping connection after receiving Sender Contract from Maker"
);
return Err(TakerError::General(
"Taker dropping of after receiving Senders Contract from Maker (test behavior) ".to_string(),));
}
}

if maker_index == maker_count - 1 {
// This is the last maker - store its response as final contract data
self.store_final_contract_data(&maker_contract)?;
Expand Down Expand Up @@ -1454,14 +1505,27 @@ impl Taker {

// Send to maker and get their outgoing key in response
let response = self.send_to_maker_and_get_response(&maker_address, privkey_msg)?;

// remove taker's outgoing swapcoin since we've handed over the key
if maker_index == 0 {
let outgoing_txid = self
.ongoing_swap_state
.outgoing_contract
.contract_tx
.compute_txid();
for (vout, _) in self
.ongoing_swap_state
.outgoing_contract
.contract_tx
.output
.iter()
.enumerate()
{
let outpoint = OutPoint {
txid: outgoing_txid,
vout: vout as u32,
};
self.watch_service.unwatch(outpoint);
}
self.wallet.remove_outgoing_swapcoin_v2(&outgoing_txid);
log::info!(
"Removed taker's outgoing swapcoin {} after sending PrivateKeyHandover",
Expand Down Expand Up @@ -1693,6 +1757,21 @@ impl Taker {
incoming_contract_txid
);

for (vout, _) in self
.ongoing_swap_state
.incoming_contract
.contract_tx
.output
.iter()
.enumerate()
{
let outpoint = OutPoint {
txid: incoming_contract_txid,
vout: vout as u32,
};
self.watch_service.unwatch(outpoint);
}

// Remove the incoming swapcoin since we've successfully swept it
self.wallet
.remove_incoming_swapcoin_v2(&incoming_contract_txid);
Expand Down
70 changes: 2 additions & 68 deletions tests/taproot_hashlock_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@

use bitcoin::Amount;
use coinswap::{
maker::{start_maker_server_taproot, TaprootMaker, TaprootMakerBehavior as MakerBehavior},
taker::{
api2::{SwapParams, Taker},
TakerBehavior,
},
maker::{start_maker_server_taproot, TaprootMakerBehavior as MakerBehavior},
taker::api2::{SwapParams, TakerBehavior},
};
use std::sync::Arc;

mod test_framework;
use test_framework::*;
Expand Down Expand Up @@ -230,65 +226,3 @@ fn test_taproot_hashlock_recovery_end_to_end() {
test_framework.stop();
block_generation_handle.join().unwrap();
}

/// Fund taproot makers and verify their balances
fn fund_taproot_makers(
makers: &[Arc<TaprootMaker>],
bitcoind: &bitcoind::BitcoinD,
utxo_count: u32,
utxo_value: Amount,
) {
for maker in makers {
let mut wallet = maker.wallet().write().unwrap();

// Fund with regular UTXOs
for _ in 0..utxo_count {
let addr = wallet.get_next_external_address().unwrap();
send_to_address(bitcoind, &addr, utxo_value);
}

generate_blocks(bitcoind, 1);
wallet.sync_and_save().unwrap();

// Verify balances
let balances = wallet.get_balances().unwrap();
let expected_regular = utxo_value * utxo_count.into();

assert_eq!(balances.regular, expected_regular);

info!(
"Taproot Maker funded successfully. Regular: {}, Fidelity: {}",
balances.regular, balances.fidelity
);
}
}

/// Fund taproot taker and verify balance
fn fund_taproot_taker(
taker: &mut Taker,
bitcoind: &bitcoind::BitcoinD,
utxo_count: u32,
utxo_value: Amount,
) -> Amount {
// Fund with regular UTXOs
for _ in 0..utxo_count {
let addr = taker.get_wallet_mut().get_next_external_address().unwrap();
send_to_address(bitcoind, &addr, utxo_value);
}

generate_blocks(bitcoind, 1);
taker.get_wallet_mut().sync_and_save().unwrap();

// Verify balances
let balances = taker.get_wallet().get_balances().unwrap();
let expected_regular = utxo_value * utxo_count.into();

assert_eq!(balances.regular, expected_regular);

info!(
"Taproot Taker funded successfully. Regular: {}, Spendable: {}",
balances.regular, balances.spendable
);

balances.spendable
}
Loading