mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-03-29 01:18:13 -04:00

Mimics what @thomaseizinger did here [1] and here [2].
This has the advantage that the consumer has more freedom to execute
`Action`s without having to implement particular traits. The error
handling required inside this protocol-executing function is also
reduced.
As discussed with Thomas, for this approach to work well, the
trait functions such as `receive_transfer_proof` should be infallible,
and the implementer should be forced to hide IO errors behind a retry
mechanism.
All of these asynchronous calls need to be "raced" against
the abort condition (determined by the `refund_timelock`), which is
missing in the current state of the implementation.
The initial handshake of the protocol has not been included here,
because it may not be easy to integrate this approach with libp2p, but
a couple of messages still need to exchanged. I need @tcharding to
tell me if it's feasible/good to do it like this.
[1]
https://github.com/comit-network/comit-rs/blob/move-nectar-swap-to-comit/nectar/src/swap/comit/herc20_hbit.rs#L57-L184.
[2] e584d2b14f/nectar/src/swap.rs (L716-L751)
.
201 lines
6.5 KiB
Rust
201 lines
6.5 KiB
Rust
#![warn(
|
|
unused_extern_crates,
|
|
missing_debug_implementations,
|
|
missing_copy_implementations,
|
|
rust_2018_idioms,
|
|
clippy::cast_possible_truncation,
|
|
clippy::cast_sign_loss,
|
|
clippy::fallible_impl_from,
|
|
clippy::cast_precision_loss,
|
|
clippy::cast_possible_wrap,
|
|
clippy::dbg_macro
|
|
)]
|
|
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
|
|
#![forbid(unsafe_code)]
|
|
#![allow(non_snake_case)]
|
|
|
|
#[macro_use]
|
|
mod utils {
|
|
|
|
macro_rules! impl_try_from_parent_enum {
|
|
($type:ident, $parent:ident) => {
|
|
impl TryFrom<$parent> for $type {
|
|
type Error = anyhow::Error;
|
|
fn try_from(from: $parent) -> Result<Self> {
|
|
if let $parent::$type(inner) = from {
|
|
Ok(inner)
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"Failed to convert parent state to child state"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! impl_from_child_enum {
|
|
($type:ident, $parent:ident) => {
|
|
impl From<$type> for $parent {
|
|
fn from(from: $type) -> Self {
|
|
$parent::$type(from)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
pub mod alice;
|
|
pub mod bitcoin;
|
|
pub mod bob;
|
|
pub mod monero;
|
|
pub mod transport;
|
|
|
|
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
|
|
use genawaiter::sync::{Gen, GenBoxed};
|
|
use sha2::Sha256;
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[derive(Debug)]
|
|
pub enum Action {
|
|
LockBitcoin(bitcoin::TxLock),
|
|
SendBitcoinRedeemEncsig(bitcoin::EncryptedSignature),
|
|
CreateMoneroWalletForOutput {
|
|
spend_key: monero::PrivateKey,
|
|
view_key: monero::PrivateViewKey,
|
|
},
|
|
RefundBitcoin {
|
|
tx_cancel: bitcoin::Transaction,
|
|
tx_refund: bitcoin::Transaction,
|
|
},
|
|
}
|
|
|
|
// TODO: This could be moved to the monero module
|
|
pub trait ReceiveTransferProof {
|
|
fn receive_transfer_proof(&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_bob<N, M, B>(
|
|
network: &'static N,
|
|
monero_ledger: &'static M,
|
|
bitcoin_ledger: &'static B,
|
|
// TODO: Replace this with a new, slimmer struct?
|
|
bob::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,
|
|
..
|
|
}: bob::State2,
|
|
) -> GenBoxed<Action, (), ()>
|
|
where
|
|
N: ReceiveTransferProof + Send + Sync,
|
|
M: monero::CheckTransfer + Send + Sync,
|
|
B: bitcoin::WatchForRawTransaction + Send + Sync,
|
|
{
|
|
Gen::new_boxed(|co| async move {
|
|
let swap_result: Result<(), ()> = {
|
|
co.yield_(Action::LockBitcoin(tx_lock.clone())).await;
|
|
|
|
// the source of this could be the database, this layer doesn't care
|
|
let transfer_proof = network.receive_transfer_proof();
|
|
|
|
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;
|
|
|
|
// TODO: We should require a specific number of confirmations on the lock
|
|
// transaction
|
|
monero_ledger
|
|
.check_transfer(S, v.public(), transfer_proof, xmr)
|
|
.await
|
|
.expect("TODO: implementor of this trait must make it infallible by retrying");
|
|
|
|
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::SendBitcoinRedeemEncsig(tx_redeem_encsig.clone()))
|
|
.await;
|
|
|
|
let tx_redeem_published = bitcoin_ledger
|
|
.watch_for_raw_transaction(tx_redeem.txid())
|
|
.await
|
|
.expect("TODO: implementor of this trait must make it infallible by retrying");
|
|
|
|
// NOTE: If any of this fails, Bob will never be able to take the monero.
|
|
// Therefore, there is no way to handle these errors other than aborting
|
|
let tx_redeem_sig = tx_redeem
|
|
.extract_signature_by_key(tx_redeem_published, b.public())
|
|
.expect("redeem transaction must include signature from us");
|
|
let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig).expect(
|
|
"alice can only produce our signature by decrypting our encrypted signature",
|
|
);
|
|
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::CreateMoneroWalletForOutput {
|
|
spend_key: s_a + s_b,
|
|
view_key: v,
|
|
})
|
|
.await;
|
|
|
|
Ok(())
|
|
};
|
|
|
|
// NOTE: swap result should only be `Err` if we have reached the
|
|
// `refund_timelock`. Therefore, we should always yield the refund action
|
|
if swap_result.is_err() {
|
|
let tx_cancel =
|
|
bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public());
|
|
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address);
|
|
|
|
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")
|
|
};
|
|
|
|
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::RefundBitcoin {
|
|
tx_cancel: signed_tx_cancel,
|
|
tx_refund: signed_tx_refund,
|
|
})
|
|
.await;
|
|
}
|
|
})
|
|
}
|