mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-27 11:10:31 -05:00
Test that both parties refund if Alice does not redeem
Also: - Move generator functions to `alice` and `bob` modules. This makes using `tracing` a lot easier, since the context of the file name let's us differentiate between Alice's and Bob's generator logs more clearly. - Accept 0 confirmations when watching for the Monero lock transaction. This should eventually be configured by the application, but in the tests it's making things unexpectedly slower.
This commit is contained in:
parent
964640154d
commit
e84c56378c
6 changed files with 735 additions and 591 deletions
|
|
@ -1,28 +1,258 @@
|
|||
use crate::{
|
||||
alice,
|
||||
bitcoin::{
|
||||
self, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxCancel,
|
||||
WatchForRawTransaction,
|
||||
self, poll_until_block_height_is_gte, BroadcastSignedTransaction, BuildTxLockPsbt,
|
||||
SignTxLock, TxCancel, WatchForRawTransaction,
|
||||
},
|
||||
monero,
|
||||
serde::monero_private_key,
|
||||
transport::{ReceiveMessage, SendMessage},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use ecdsa_fun::{
|
||||
adaptor::{Adaptor, EncryptedSignature},
|
||||
nonce::Deterministic,
|
||||
Signature,
|
||||
};
|
||||
use futures::{
|
||||
future::{select, Either},
|
||||
FutureExt,
|
||||
};
|
||||
use genawaiter::sync::{Gen, GenBoxed};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use tracing::error;
|
||||
|
||||
pub mod message;
|
||||
use crate::monero::{CreateWalletForOutput, WatchForTransfer};
|
||||
pub use message::{Message, Message0, Message1, Message2, Message3};
|
||||
|
||||
// TODO: Replace this with something configurable, such as an function argument.
|
||||
/// Time that Bob has to publish the Bitcoin lock transaction before both
|
||||
/// parties will abort, in seconds.
|
||||
pub const SECS_TO_ACT: u64 = 60;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum Action {
|
||||
LockBtc(bitcoin::TxLock),
|
||||
SendBtcRedeemEncsig(bitcoin::EncryptedSignature),
|
||||
CreateXmrWalletForOutput {
|
||||
spend_key: monero::PrivateKey,
|
||||
view_key: monero::PrivateViewKey,
|
||||
},
|
||||
CancelBtc(bitcoin::Transaction),
|
||||
RefundBtc(bitcoin::Transaction),
|
||||
}
|
||||
|
||||
// TODO: This could be moved to the monero module
|
||||
#[async_trait]
|
||||
pub trait ReceiveTransferProof {
|
||||
async fn receive_transfer_proof(&mut self) -> monero::TransferProof;
|
||||
}
|
||||
|
||||
/// Perform the on-chain protocol to swap monero and bitcoin as Bob.
|
||||
///
|
||||
/// This is called post handshake, after all the keys, addresses and most of the
|
||||
/// signatures have been exchanged.
|
||||
pub fn action_generator<N, M, B>(
|
||||
mut network: N,
|
||||
monero_client: Arc<M>,
|
||||
bitcoin_client: Arc<B>,
|
||||
// TODO: Replace this with a new, slimmer struct?
|
||||
State2 {
|
||||
A,
|
||||
b,
|
||||
s_b,
|
||||
S_a_monero,
|
||||
S_a_bitcoin,
|
||||
v,
|
||||
xmr,
|
||||
refund_timelock,
|
||||
redeem_address,
|
||||
refund_address,
|
||||
tx_lock,
|
||||
tx_cancel_sig_a,
|
||||
tx_refund_encsig,
|
||||
..
|
||||
}: State2,
|
||||
) -> GenBoxed<Action, (), ()>
|
||||
where
|
||||
N: ReceiveTransferProof + Send + Sync + 'static,
|
||||
M: monero::WatchForTransfer + Send + Sync + 'static,
|
||||
B: bitcoin::BlockHeight
|
||||
+ bitcoin::TransactionBlockHeight
|
||||
+ bitcoin::WatchForRawTransaction
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
#[derive(Debug)]
|
||||
enum SwapFailed {
|
||||
BeforeBtcLock,
|
||||
AfterBtcLock(Reason),
|
||||
AfterBtcRedeem(Reason),
|
||||
}
|
||||
|
||||
/// Reason why the swap has failed.
|
||||
#[derive(Debug)]
|
||||
enum Reason {
|
||||
/// The refund timelock has been reached.
|
||||
BtcExpired,
|
||||
/// Alice did not lock up enough monero in the shared output.
|
||||
InsufficientXmr(monero::InsufficientFunds),
|
||||
/// Could not find Bob's signature on the redeem transaction witness
|
||||
/// stack.
|
||||
BtcRedeemSignature,
|
||||
/// Could not recover secret `s_a` from Bob's redeem transaction
|
||||
/// signature.
|
||||
SecretRecovery,
|
||||
}
|
||||
|
||||
Gen::new_boxed(|co| async move {
|
||||
let swap_result: Result<(), SwapFailed> = async {
|
||||
co.yield_(Action::LockBtc(tx_lock.clone())).await;
|
||||
|
||||
timeout(
|
||||
Duration::from_secs(SECS_TO_ACT),
|
||||
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
|
||||
)
|
||||
.await
|
||||
.map(|tx| tx.txid())
|
||||
.map_err(|_| SwapFailed::BeforeBtcLock)?;
|
||||
|
||||
let tx_lock_height = bitcoin_client
|
||||
.transaction_block_height(tx_lock.txid())
|
||||
.await;
|
||||
let poll_until_btc_has_expired = poll_until_block_height_is_gte(
|
||||
bitcoin_client.as_ref(),
|
||||
tx_lock_height + refund_timelock,
|
||||
)
|
||||
.shared();
|
||||
futures::pin_mut!(poll_until_btc_has_expired);
|
||||
|
||||
let transfer_proof = match select(
|
||||
network.receive_transfer_proof(),
|
||||
poll_until_btc_has_expired.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::Left((proof, _)) => proof,
|
||||
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
|
||||
};
|
||||
|
||||
let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(
|
||||
s_b.into_ed25519(),
|
||||
));
|
||||
let S = S_a_monero + S_b_monero;
|
||||
|
||||
match select(
|
||||
monero_client.watch_for_transfer(S, v.public(), transfer_proof, xmr, 0),
|
||||
poll_until_btc_has_expired.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::Left((Err(e), _)) => {
|
||||
return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e)))
|
||||
}
|
||||
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address);
|
||||
let tx_redeem_encsig = b.encsign(S_a_bitcoin.clone(), tx_redeem.digest());
|
||||
|
||||
co.yield_(Action::SendBtcRedeemEncsig(tx_redeem_encsig.clone()))
|
||||
.await;
|
||||
|
||||
let tx_redeem_published = match select(
|
||||
bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()),
|
||||
poll_until_btc_has_expired,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Either::Left((tx, _)) => tx,
|
||||
Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)),
|
||||
};
|
||||
|
||||
let tx_redeem_sig = tx_redeem
|
||||
.extract_signature_by_key(tx_redeem_published, b.public())
|
||||
.map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?;
|
||||
let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)
|
||||
.map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?;
|
||||
let s_a = monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(
|
||||
s_a.to_bytes(),
|
||||
));
|
||||
|
||||
let s_b = monero::PrivateKey {
|
||||
scalar: s_b.into_ed25519(),
|
||||
};
|
||||
|
||||
co.yield_(Action::CreateXmrWalletForOutput {
|
||||
spend_key: s_a + s_b,
|
||||
view_key: v,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(ref err) = swap_result {
|
||||
error!("swap failed: {:?}", err);
|
||||
}
|
||||
|
||||
if let Err(SwapFailed::AfterBtcLock(_)) = swap_result {
|
||||
let tx_cancel =
|
||||
bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public());
|
||||
let tx_cancel_txid = tx_cancel.txid();
|
||||
let signed_tx_cancel = {
|
||||
let sig_a = tx_cancel_sig_a.clone();
|
||||
let sig_b = b.sign(tx_cancel.digest());
|
||||
|
||||
tx_cancel
|
||||
.clone()
|
||||
.add_signatures(&tx_lock, (A.clone(), sig_a), (b.public(), sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel")
|
||||
};
|
||||
|
||||
co.yield_(Action::CancelBtc(signed_tx_cancel)).await;
|
||||
|
||||
let _ = bitcoin_client
|
||||
.watch_for_raw_transaction(tx_cancel_txid)
|
||||
.await;
|
||||
|
||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
|
||||
let tx_refund_txid = tx_refund.txid();
|
||||
let signed_tx_refund = {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
let sig_a =
|
||||
adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone());
|
||||
let sig_b = b.sign(tx_refund.digest());
|
||||
|
||||
tx_refund
|
||||
.add_signatures(&tx_cancel, (A.clone(), sig_a), (b.public(), sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_refund")
|
||||
};
|
||||
|
||||
co.yield_(Action::RefundBtc(signed_tx_refund)).await;
|
||||
|
||||
let _ = bitcoin_client
|
||||
.watch_for_raw_transaction(tx_refund_txid)
|
||||
.await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// There are no guarantees that send_message and receive_massage do not block
|
||||
// the flow of execution. Therefore they must be paired between Alice/Bob, one
|
||||
// send to one receive in the correct order.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue