mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-18 04:14:32 -05:00
Merge pull request #2 from comit-network/async-squashed
Execute Alice and Bob state machines concurrently
This commit is contained in:
commit
36608657a5
147
.gitignore
vendored
147
.gitignore
vendored
@ -1,3 +1,148 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,clion+all,emacs
|
||||
|
||||
### CLion+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### CLion+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
@ -8,3 +153,5 @@ Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust,clion+all,emacs
|
||||
|
@ -17,11 +17,15 @@ monero = "0.9"
|
||||
rand = "0.7"
|
||||
sha2 = "0.9"
|
||||
thiserror = "1"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.12"
|
||||
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" }
|
||||
futures = "0.3"
|
||||
monero-harness = { path = "../monero-harness" }
|
||||
reqwest = { version = "0.10", default-features = false }
|
||||
testcontainers = "0.10"
|
||||
tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.2"
|
||||
|
@ -1,31 +1,129 @@
|
||||
use crate::{
|
||||
bitcoin,
|
||||
bitcoin::{BroadcastSignedTransaction, WatchForRawTransaction},
|
||||
bob, monero,
|
||||
monero::{CreateWalletForOutput, Transfer},
|
||||
transport::{ReceiveMessage, SendMessage},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature};
|
||||
use ecdsa_fun::{
|
||||
adaptor::{Adaptor, EncryptedSignature},
|
||||
nonce::Deterministic,
|
||||
};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{bitcoin, bitcoin::GetRawTransaction, bob, monero, monero::ImportOutput};
|
||||
use ecdsa_fun::{nonce::Deterministic, Signature};
|
||||
use sha2::Sha256;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message0 {
|
||||
pub(crate) A: bitcoin::PublicKey,
|
||||
pub(crate) S_a_monero: monero::PublicKey,
|
||||
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
|
||||
pub(crate) v_a: monero::PrivateViewKey,
|
||||
pub(crate) redeem_address: bitcoin::Address,
|
||||
pub(crate) punish_address: bitcoin::Address,
|
||||
pub mod message;
|
||||
pub use message::{Message, Message0, Message1, Message2};
|
||||
|
||||
pub async fn next_state<
|
||||
R: RngCore + CryptoRng,
|
||||
B: WatchForRawTransaction + BroadcastSignedTransaction,
|
||||
M: CreateWalletForOutput + Transfer,
|
||||
T: SendMessage<Message> + ReceiveMessage<bob::Message>,
|
||||
>(
|
||||
bitcoin_wallet: &B,
|
||||
monero_wallet: &M,
|
||||
transport: &mut T,
|
||||
state: State,
|
||||
rng: &mut R,
|
||||
) -> Result<State> {
|
||||
match state {
|
||||
State::State0(state0) => {
|
||||
transport
|
||||
.send_message(state0.next_message(rng).into())
|
||||
.await?;
|
||||
|
||||
let bob_message0 = transport.receive_message().await?.try_into()?;
|
||||
let state1 = state0.receive(bob_message0)?;
|
||||
Ok(state1.into())
|
||||
}
|
||||
State::State1(state1) => {
|
||||
let bob_message1 = transport.receive_message().await?.try_into()?;
|
||||
let state2 = state1.receive(bob_message1);
|
||||
let alice_message1 = state2.next_message();
|
||||
transport.send_message(alice_message1.into()).await?;
|
||||
Ok(state2.into())
|
||||
}
|
||||
State::State2(state2) => {
|
||||
let bob_message2 = transport.receive_message().await?.try_into()?;
|
||||
let state3 = state2.receive(bob_message2)?;
|
||||
Ok(state3.into())
|
||||
}
|
||||
State::State3(state3) => {
|
||||
tracing::info!("alice is watching for locked btc");
|
||||
let state4 = state3.watch_for_lock_btc(bitcoin_wallet).await?;
|
||||
Ok(state4.into())
|
||||
}
|
||||
State::State4(state4) => {
|
||||
let state5 = state4.lock_xmr(monero_wallet).await?;
|
||||
tracing::info!("alice has locked xmr");
|
||||
Ok(state5.into())
|
||||
}
|
||||
State::State5(state5) => {
|
||||
transport.send_message(state5.next_message().into()).await?;
|
||||
// todo: pass in state4b as a parameter somewhere in this call to prevent the
|
||||
// user from waiting for a message that wont be sent
|
||||
let message3 = transport.receive_message().await?.try_into()?;
|
||||
let state6 = state5.receive(message3);
|
||||
tracing::info!("alice has received bob message 3");
|
||||
tracing::info!("alice is redeeming btc");
|
||||
state6.redeem_btc(bitcoin_wallet).await?;
|
||||
Ok(state6.into())
|
||||
}
|
||||
State::State6(state6) => Ok(state6.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
pub(crate) tx_refund_encsig: EncryptedSignature,
|
||||
pub enum State {
|
||||
State0(State0),
|
||||
State1(State1),
|
||||
State2(State2),
|
||||
State3(State3),
|
||||
State4(State4),
|
||||
State5(State5),
|
||||
State6(State6),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message2 {
|
||||
pub(crate) tx_lock_proof: monero::TransferProof,
|
||||
impl_try_from_parent_enum!(State0, State);
|
||||
impl_try_from_parent_enum!(State1, State);
|
||||
impl_try_from_parent_enum!(State2, State);
|
||||
impl_try_from_parent_enum!(State3, State);
|
||||
impl_try_from_parent_enum!(State4, State);
|
||||
impl_try_from_parent_enum!(State5, State);
|
||||
impl_try_from_parent_enum!(State6, State);
|
||||
|
||||
impl_from_child_enum!(State0, State);
|
||||
impl_from_child_enum!(State1, State);
|
||||
impl_from_child_enum!(State2, State);
|
||||
impl_from_child_enum!(State3, State);
|
||||
impl_from_child_enum!(State4, State);
|
||||
impl_from_child_enum!(State5, State);
|
||||
impl_from_child_enum!(State6, State);
|
||||
|
||||
impl State {
|
||||
pub fn new<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
btc: bitcoin::Amount,
|
||||
xmr: monero::Amount,
|
||||
refund_timelock: u32,
|
||||
punish_timelock: u32,
|
||||
redeem_address: bitcoin::Address,
|
||||
punish_address: bitcoin::Address,
|
||||
) -> Self {
|
||||
Self::State0(State0::new(
|
||||
rng,
|
||||
btc,
|
||||
xmr,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -250,12 +348,15 @@ pub struct State3 {
|
||||
impl State3 {
|
||||
pub async fn watch_for_lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
|
||||
where
|
||||
W: bitcoin::GetRawTransaction,
|
||||
W: bitcoin::WatchForRawTransaction,
|
||||
{
|
||||
let _ = bitcoin_wallet
|
||||
.get_raw_transaction(self.tx_lock.txid())
|
||||
tracing::info!("watching for lock btc with txid: {}", self.tx_lock.txid());
|
||||
let tx = bitcoin_wallet
|
||||
.watch_for_raw_transaction(self.tx_lock.txid())
|
||||
.await?;
|
||||
|
||||
tracing::info!("tx lock seen with txid: {}", tx.txid());
|
||||
|
||||
Ok(State4 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
@ -298,7 +399,7 @@ pub struct State4 {
|
||||
}
|
||||
|
||||
impl State4 {
|
||||
pub async fn lock_xmr<W>(self, monero_wallet: &W) -> Result<(State4b, monero::Amount)>
|
||||
pub async fn lock_xmr<W>(self, monero_wallet: &W) -> Result<State5>
|
||||
where
|
||||
W: monero::Transfer,
|
||||
{
|
||||
@ -311,28 +412,26 @@ impl State4 {
|
||||
.transfer(S_a + S_b, self.v.public(), self.xmr)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
State4b {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
refund_timelock: self.refund_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_lock_proof,
|
||||
tx_punish_sig_bob: self.tx_punish_sig_bob,
|
||||
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
|
||||
},
|
||||
fee,
|
||||
))
|
||||
Ok(State5 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
S_b_monero: self.S_b_monero,
|
||||
S_b_bitcoin: self.S_b_bitcoin,
|
||||
v: self.v,
|
||||
btc: self.btc,
|
||||
xmr: self.xmr,
|
||||
refund_timelock: self.refund_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
punish_address: self.punish_address,
|
||||
tx_lock: self.tx_lock,
|
||||
tx_lock_proof,
|
||||
tx_punish_sig_bob: self.tx_punish_sig_bob,
|
||||
tx_cancel_sig_bob: self.tx_cancel_sig_bob,
|
||||
lock_xmr_fee: fee,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn punish<W: bitcoin::BroadcastSignedTransaction>(
|
||||
@ -383,7 +482,7 @@ impl State4 {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State4b {
|
||||
pub struct State5 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
@ -401,17 +500,18 @@ pub struct State4b {
|
||||
tx_lock_proof: monero::TransferProof,
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
tx_cancel_sig_bob: bitcoin::Signature,
|
||||
lock_xmr_fee: monero::Amount,
|
||||
}
|
||||
|
||||
impl State4b {
|
||||
impl State5 {
|
||||
pub fn next_message(&self) -> Message2 {
|
||||
Message2 {
|
||||
tx_lock_proof: self.tx_lock_proof.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive(self, msg: bob::Message3) -> State5 {
|
||||
State5 {
|
||||
pub fn receive(self, msg: bob::Message3) -> State6 {
|
||||
State6 {
|
||||
a: self.a,
|
||||
B: self.B,
|
||||
s_a: self.s_a,
|
||||
@ -428,14 +528,15 @@ impl State4b {
|
||||
tx_lock: self.tx_lock,
|
||||
tx_punish_sig_bob: self.tx_punish_sig_bob,
|
||||
tx_redeem_encsig: msg.tx_redeem_encsig,
|
||||
lock_xmr_fee: self.lock_xmr_fee,
|
||||
}
|
||||
}
|
||||
|
||||
// watch for refund on btc, recover s_b and refund xmr
|
||||
pub async fn refund_xmr<B, M>(self, bitcoin_wallet: &B, monero_wallet: &M) -> Result<()>
|
||||
where
|
||||
B: GetRawTransaction,
|
||||
M: ImportOutput,
|
||||
B: WatchForRawTransaction,
|
||||
M: CreateWalletForOutput,
|
||||
{
|
||||
let tx_cancel = bitcoin::TxCancel::new(
|
||||
&self.tx_lock,
|
||||
@ -448,7 +549,9 @@ impl State4b {
|
||||
|
||||
let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin.clone(), tx_refund.digest());
|
||||
|
||||
let tx_refund_candidate = bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
|
||||
let tx_refund_candidate = bitcoin_wallet
|
||||
.watch_for_raw_transaction(tx_refund.txid())
|
||||
.await?;
|
||||
|
||||
let tx_refund_sig =
|
||||
tx_refund.extract_signature_by_key(tx_refund_candidate, self.a.public())?;
|
||||
@ -462,7 +565,7 @@ impl State4b {
|
||||
// NOTE: This actually generates and opens a new wallet, closing the currently
|
||||
// open one.
|
||||
monero_wallet
|
||||
.import_output(monero::PrivateKey::from_scalar(s), self.v)
|
||||
.create_and_load_wallet_for_output(monero::PrivateKey::from_scalar(s), self.v)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@ -470,7 +573,7 @@ impl State4b {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State5 {
|
||||
pub struct State6 {
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
s_a: cross_curve_dleq::Scalar,
|
||||
@ -487,9 +590,10 @@ pub struct State5 {
|
||||
tx_lock: bitcoin::TxLock,
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
tx_redeem_encsig: EncryptedSignature,
|
||||
lock_xmr_fee: monero::Amount,
|
||||
}
|
||||
|
||||
impl State5 {
|
||||
impl State6 {
|
||||
pub async fn redeem_btc<W: bitcoin::BroadcastSignedTransaction>(
|
||||
&self,
|
||||
bitcoin_wallet: &W,
|
||||
@ -513,4 +617,8 @@ impl State5 {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lock_xmr_fee(&self) -> monero::Amount {
|
||||
self.lock_xmr_fee
|
||||
}
|
||||
}
|
||||
|
42
xmr-btc/src/alice/message.rs
Normal file
42
xmr-btc/src/alice/message.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use anyhow::Result;
|
||||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::{bitcoin, monero};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Message0(Message0),
|
||||
Message1(Message1),
|
||||
Message2(Message2),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message0 {
|
||||
pub(crate) A: bitcoin::PublicKey,
|
||||
pub(crate) S_a_monero: monero::PublicKey,
|
||||
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
|
||||
pub(crate) v_a: monero::PrivateViewKey,
|
||||
pub(crate) redeem_address: bitcoin::Address,
|
||||
pub(crate) punish_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
pub(crate) tx_refund_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message2 {
|
||||
pub(crate) tx_lock_proof: monero::TransferProof,
|
||||
}
|
||||
|
||||
impl_try_from_parent_enum!(Message0, Message);
|
||||
impl_try_from_parent_enum!(Message1, Message);
|
||||
impl_try_from_parent_enum!(Message2, Message);
|
||||
|
||||
impl_from_child_enum!(Message0, Message);
|
||||
impl_from_child_enum!(Message1, Message);
|
||||
impl_from_child_enum!(Message2, Message);
|
@ -1,6 +1,4 @@
|
||||
pub mod transactions;
|
||||
#[cfg(test)]
|
||||
pub mod wallet;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
@ -10,6 +8,7 @@ use bitcoin::{
|
||||
util::psbt::PartiallySignedTransaction,
|
||||
SigHash, Transaction,
|
||||
};
|
||||
pub use bitcoin::{Address, Amount, OutPoint, Txid};
|
||||
use ecdsa_fun::{
|
||||
adaptor::Adaptor,
|
||||
fun::{
|
||||
@ -19,17 +18,13 @@ use ecdsa_fun::{
|
||||
nonce::Deterministic,
|
||||
ECDSA,
|
||||
};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
use miniscript::{Descriptor, Segwitv0};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use sha2::Sha256;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
||||
pub use bitcoin::{Address, Amount, OutPoint, Txid};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
|
||||
#[cfg(test)]
|
||||
pub use wallet::{make_wallet, Wallet};
|
||||
|
||||
pub const TX_FEE: u64 = 10_000;
|
||||
|
||||
@ -193,8 +188,8 @@ pub trait BroadcastSignedTransaction {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait GetRawTransaction {
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||
pub trait WatchForRawTransaction {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
|
@ -1,7 +1,12 @@
|
||||
use crate::{
|
||||
alice,
|
||||
bitcoin::{self, BuildTxLockPsbt, GetRawTransaction, TxCancel},
|
||||
bitcoin::{
|
||||
self, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxCancel,
|
||||
WatchForRawTransaction,
|
||||
},
|
||||
monero,
|
||||
monero::{CheckTransfer, CreateWalletForOutput},
|
||||
transport::{ReceiveMessage, SendMessage},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ecdsa_fun::{
|
||||
@ -11,32 +16,88 @@ use ecdsa_fun::{
|
||||
};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use sha2::Sha256;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message0 {
|
||||
pub(crate) B: bitcoin::PublicKey,
|
||||
pub(crate) S_b_monero: monero::PublicKey,
|
||||
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
|
||||
pub(crate) v_b: monero::PrivateViewKey,
|
||||
pub(crate) refund_address: bitcoin::Address,
|
||||
pub mod message;
|
||||
pub use message::{Message, Message0, Message1, Message2, Message3};
|
||||
|
||||
pub async fn next_state<
|
||||
R: RngCore + CryptoRng,
|
||||
B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction,
|
||||
M: CreateWalletForOutput + CheckTransfer,
|
||||
T: SendMessage<Message> + ReceiveMessage<alice::Message>,
|
||||
>(
|
||||
bitcoin_wallet: &B,
|
||||
monero_wallet: &M,
|
||||
transport: &mut T,
|
||||
state: State,
|
||||
rng: &mut R,
|
||||
) -> Result<State> {
|
||||
match state {
|
||||
State::State0(state0) => {
|
||||
transport
|
||||
.send_message(state0.next_message(rng).into())
|
||||
.await?;
|
||||
let message0 = transport.receive_message().await?.try_into()?;
|
||||
let state1 = state0.receive(bitcoin_wallet, message0).await?;
|
||||
Ok(state1.into())
|
||||
}
|
||||
State::State1(state1) => {
|
||||
transport.send_message(state1.next_message().into()).await?;
|
||||
|
||||
let message1 = transport.receive_message().await?.try_into()?;
|
||||
let state2 = state1.receive(message1)?;
|
||||
Ok(state2.into())
|
||||
}
|
||||
State::State2(state2) => {
|
||||
let message2 = state2.next_message();
|
||||
let state3 = state2.lock_btc(bitcoin_wallet).await?;
|
||||
tracing::info!("bob has locked btc");
|
||||
transport.send_message(message2.into()).await?;
|
||||
Ok(state3.into())
|
||||
}
|
||||
State::State3(state3) => {
|
||||
let message2 = transport.receive_message().await?.try_into()?;
|
||||
let state4 = state3.watch_for_lock_xmr(monero_wallet, message2).await?;
|
||||
tracing::info!("bob has seen that alice has locked xmr");
|
||||
Ok(state4.into())
|
||||
}
|
||||
State::State4(state4) => {
|
||||
transport.send_message(state4.next_message().into()).await?;
|
||||
tracing::info!("bob is watching for redeem_btc");
|
||||
let state5 = state4.watch_for_redeem_btc(bitcoin_wallet).await?;
|
||||
tracing::info!("bob has seen that alice has redeemed btc");
|
||||
state5.claim_xmr(monero_wallet).await?;
|
||||
tracing::info!("bob has claimed xmr");
|
||||
Ok(state5.into())
|
||||
}
|
||||
State::State5(state5) => Ok(state5.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_lock: bitcoin::TxLock,
|
||||
pub enum State {
|
||||
State0(State0),
|
||||
State1(State1),
|
||||
State2(State2),
|
||||
State3(State3),
|
||||
State4(State4),
|
||||
State5(State5),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message2 {
|
||||
pub(crate) tx_punish_sig: Signature,
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
}
|
||||
impl_try_from_parent_enum!(State0, State);
|
||||
impl_try_from_parent_enum!(State1, State);
|
||||
impl_try_from_parent_enum!(State2, State);
|
||||
impl_try_from_parent_enum!(State3, State);
|
||||
impl_try_from_parent_enum!(State4, State);
|
||||
impl_try_from_parent_enum!(State5, State);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message3 {
|
||||
pub(crate) tx_redeem_encsig: EncryptedSignature,
|
||||
}
|
||||
impl_from_child_enum!(State0, State);
|
||||
impl_from_child_enum!(State1, State);
|
||||
impl_from_child_enum!(State2, State);
|
||||
impl_from_child_enum!(State3, State);
|
||||
impl_from_child_enum!(State4, State);
|
||||
impl_from_child_enum!(State5, State);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State0 {
|
||||
@ -228,17 +289,18 @@ impl State2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State2b>
|
||||
pub async fn lock_btc<W>(self, bitcoin_wallet: &W) -> Result<State3>
|
||||
where
|
||||
W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction,
|
||||
{
|
||||
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?;
|
||||
|
||||
tracing::info!("{}", self.tx_lock.txid());
|
||||
let _ = bitcoin_wallet
|
||||
.broadcast_signed_transaction(signed_tx_lock)
|
||||
.await?;
|
||||
|
||||
Ok(State2b {
|
||||
Ok(State3 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
@ -259,8 +321,8 @@ impl State2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State2b {
|
||||
#[derive(Debug)]
|
||||
pub struct State3 {
|
||||
A: bitcoin::PublicKey,
|
||||
b: bitcoin::SecretKey,
|
||||
s_b: cross_curve_dleq::Scalar,
|
||||
@ -279,8 +341,8 @@ pub struct State2b {
|
||||
tx_refund_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
impl State2b {
|
||||
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State3>
|
||||
impl State3 {
|
||||
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State4>
|
||||
where
|
||||
W: monero::CheckTransfer,
|
||||
{
|
||||
@ -293,7 +355,7 @@ impl State2b {
|
||||
.check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr)
|
||||
.await?;
|
||||
|
||||
Ok(State3 {
|
||||
Ok(State4 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
@ -359,15 +421,13 @@ impl State2b {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State3 {
|
||||
#[derive(Debug)]
|
||||
pub struct State4 {
|
||||
A: bitcoin::PublicKey,
|
||||
b: bitcoin::SecretKey,
|
||||
s_b: cross_curve_dleq::Scalar,
|
||||
@ -386,7 +446,7 @@ pub struct State3 {
|
||||
tx_refund_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
impl State3 {
|
||||
impl State4 {
|
||||
pub fn next_message(&self) -> Message3 {
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
|
||||
@ -394,14 +454,16 @@ impl State3 {
|
||||
Message3 { tx_redeem_encsig }
|
||||
}
|
||||
|
||||
pub async fn watch_for_redeem_btc<W>(self, bitcoin_wallet: &W) -> Result<State4>
|
||||
pub async fn watch_for_redeem_btc<W>(self, bitcoin_wallet: &W) -> Result<State5>
|
||||
where
|
||||
W: GetRawTransaction,
|
||||
W: WatchForRawTransaction,
|
||||
{
|
||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
|
||||
|
||||
let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?;
|
||||
let tx_redeem_candidate = bitcoin_wallet
|
||||
.watch_for_raw_transaction(tx_redeem.txid())
|
||||
.await?;
|
||||
|
||||
let tx_redeem_sig =
|
||||
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
|
||||
@ -409,7 +471,7 @@ impl State3 {
|
||||
let s_a =
|
||||
monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_a.to_bytes()));
|
||||
|
||||
Ok(State4 {
|
||||
Ok(State5 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_a,
|
||||
@ -432,7 +494,7 @@ impl State3 {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State4 {
|
||||
pub struct State5 {
|
||||
A: bitcoin::PublicKey,
|
||||
b: bitcoin::SecretKey,
|
||||
s_a: monero::PrivateKey,
|
||||
@ -452,10 +514,10 @@ pub struct State4 {
|
||||
tx_cancel_sig: Signature,
|
||||
}
|
||||
|
||||
impl State4 {
|
||||
impl State5 {
|
||||
pub async fn claim_xmr<W>(&self, monero_wallet: &W) -> Result<()>
|
||||
where
|
||||
W: monero::ImportOutput,
|
||||
W: monero::CreateWalletForOutput,
|
||||
{
|
||||
let s_b = monero::PrivateKey {
|
||||
scalar: self.s_b.into_ed25519(),
|
||||
@ -465,8 +527,13 @@ impl State4 {
|
||||
|
||||
// NOTE: This actually generates and opens a new wallet, closing the currently
|
||||
// open one.
|
||||
monero_wallet.import_output(s, self.v).await?;
|
||||
monero_wallet
|
||||
.create_and_load_wallet_for_output(s, self.v)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||
self.tx_lock.txid()
|
||||
}
|
||||
}
|
||||
|
48
xmr-btc/src/bob/message.rs
Normal file
48
xmr-btc/src/bob/message.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::Result;
|
||||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Message0(Message0),
|
||||
Message1(Message1),
|
||||
Message2(Message2),
|
||||
Message3(Message3),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message0 {
|
||||
pub(crate) B: bitcoin::PublicKey,
|
||||
pub(crate) S_b_monero: monero::PublicKey,
|
||||
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
|
||||
pub(crate) v_b: monero::PrivateViewKey,
|
||||
pub(crate) refund_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_lock: bitcoin::TxLock,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message2 {
|
||||
pub(crate) tx_punish_sig: Signature,
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Message3 {
|
||||
pub(crate) tx_redeem_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
impl_try_from_parent_enum!(Message0, Message);
|
||||
impl_try_from_parent_enum!(Message1, Message);
|
||||
impl_try_from_parent_enum!(Message2, Message);
|
||||
impl_try_from_parent_enum!(Message3, Message);
|
||||
|
||||
impl_from_child_enum!(Message0, Message);
|
||||
impl_from_child_enum!(Message1, Message);
|
||||
impl_from_child_enum!(Message2, Message);
|
||||
impl_from_child_enum!(Message3, Message);
|
@ -14,371 +14,39 @@
|
||||
#![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;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
alice, bitcoin,
|
||||
bitcoin::{Amount, TX_FEE},
|
||||
bob, monero,
|
||||
};
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use monero_harness::Monero;
|
||||
use rand::rngs::OsRng;
|
||||
use testcontainers::clients::Cli;
|
||||
|
||||
const TEN_XMR: u64 = 10_000_000_000_000;
|
||||
|
||||
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
|
||||
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
|
||||
let _ = bitcoind.init(5).await;
|
||||
|
||||
bitcoind
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn happy_path() {
|
||||
let cli = Cli::default();
|
||||
let monero = Monero::new(&cli);
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
// must be bigger than our hardcoded fee of 10_000
|
||||
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
|
||||
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let fund_alice = TEN_XMR;
|
||||
let fund_bob = 0;
|
||||
monero.init(fund_alice, fund_bob).await.unwrap();
|
||||
|
||||
let alice_monero_wallet = monero::AliceWallet(&monero);
|
||||
let bob_monero_wallet = monero::BobWallet(&monero);
|
||||
|
||||
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
|
||||
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
|
||||
|
||||
let alice_initial_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
|
||||
let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
|
||||
let punish_address = redeem_address.clone();
|
||||
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
||||
|
||||
let refund_timelock = 1;
|
||||
let punish_timelock = 1;
|
||||
|
||||
let alice_state0 = alice::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
);
|
||||
let bob_state0 = bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
refund_address.clone(),
|
||||
);
|
||||
|
||||
let alice_message0 = alice_state0.next_message(&mut OsRng);
|
||||
let bob_message0 = bob_state0.next_message(&mut OsRng);
|
||||
|
||||
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_btc_wallet, alice_message0)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob_message1 = bob_state1.next_message();
|
||||
let alice_state2 = alice_state1.receive(bob_message1);
|
||||
let alice_message1 = alice_state2.next_message();
|
||||
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
|
||||
|
||||
let bob_message2 = bob_state2.next_message();
|
||||
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
|
||||
|
||||
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
|
||||
let lock_txid = bob_state2b.tx_lock_id();
|
||||
|
||||
let alice_state4 = alice_state3
|
||||
.watch_for_lock_btc(&alice_btc_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (alice_state4b, lock_tx_monero_fee) =
|
||||
alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap();
|
||||
|
||||
let alice_message2 = alice_state4b.next_message();
|
||||
|
||||
let bob_state3 = bob_state2b
|
||||
.watch_for_lock_xmr(&bob_monero_wallet, alice_message2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob_message3 = bob_state3.next_message();
|
||||
let alice_state5 = alice_state4b.receive(bob_message3);
|
||||
|
||||
alice_state5.redeem_btc(&alice_btc_wallet).await.unwrap();
|
||||
let bob_state4 = bob_state3
|
||||
.watch_for_redeem_btc(&bob_btc_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
bob_state4.claim_xmr(&bob_monero_wallet).await.unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
|
||||
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet.transaction_fee(lock_txid).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
alice_initial_btc_balance + btc_amount - bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee
|
||||
);
|
||||
|
||||
let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
|
||||
bob_monero_wallet
|
||||
.0
|
||||
.wait_for_bob_wallet_block_height()
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
alice_final_xmr_balance,
|
||||
alice_initial_xmr_balance - u64::from(xmr_amount) - u64::from(lock_tx_monero_fee)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_xmr_balance,
|
||||
bob_initial_xmr_balance + u64::from(xmr_amount)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn both_refund() {
|
||||
let cli = Cli::default();
|
||||
let monero = Monero::new(&cli);
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
// must be bigger than our hardcoded fee of 10_000
|
||||
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
|
||||
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fund_alice = TEN_XMR;
|
||||
let fund_bob = 0;
|
||||
|
||||
monero.init(fund_alice, fund_bob).await.unwrap();
|
||||
let alice_monero_wallet = monero::AliceWallet(&monero);
|
||||
let bob_monero_wallet = monero::BobWallet(&monero);
|
||||
|
||||
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
|
||||
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
|
||||
|
||||
let bob_initial_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
|
||||
let punish_address = redeem_address.clone();
|
||||
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
||||
|
||||
let refund_timelock = 1;
|
||||
let punish_timelock = 1;
|
||||
|
||||
let alice_state0 = alice::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
);
|
||||
let bob_state0 = bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
refund_address.clone(),
|
||||
);
|
||||
|
||||
let alice_message0 = alice_state0.next_message(&mut OsRng);
|
||||
let bob_message0 = bob_state0.next_message(&mut OsRng);
|
||||
|
||||
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_btc_wallet, alice_message0)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob_message1 = bob_state1.next_message();
|
||||
let alice_state2 = alice_state1.receive(bob_message1);
|
||||
let alice_message1 = alice_state2.next_message();
|
||||
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
|
||||
|
||||
let bob_message2 = bob_state2.next_message();
|
||||
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
|
||||
|
||||
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
|
||||
|
||||
let alice_state4 = alice_state3
|
||||
.watch_for_lock_btc(&alice_btc_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (alice_state4b, _lock_tx_monero_fee) =
|
||||
alice_state4.lock_xmr(&alice_monero_wallet).await.unwrap();
|
||||
|
||||
bob_state2b.refund_btc(&bob_btc_wallet).await.unwrap();
|
||||
|
||||
alice_state4b
|
||||
.refund_xmr(&alice_btc_wallet, &alice_monero_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state2b.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(alice_final_btc_balance, alice_initial_btc_balance);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
|
||||
bob_initial_btc_balance - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee
|
||||
);
|
||||
|
||||
alice_monero_wallet
|
||||
.0
|
||||
.wait_for_alice_wallet_block_height()
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_final_xmr_balance = alice_monero_wallet.0.get_balance_alice().await.unwrap();
|
||||
let bob_final_xmr_balance = bob_monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
// Because we create a new wallet when claiming Monero, we can only assert on
|
||||
// this new wallet owning all of `xmr_amount` after refund
|
||||
assert_eq!(alice_final_xmr_balance, u64::from(xmr_amount));
|
||||
assert_eq!(bob_final_xmr_balance, bob_initial_xmr_balance);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn alice_punishes() {
|
||||
let cli = Cli::default();
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
// must be bigger than our hardcoded fee of 10_000
|
||||
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
|
||||
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let alice_btc_wallet = bitcoin::Wallet::new("alice", &bitcoind.node_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_btc_wallet = bitcoin::make_wallet("bob", &bitcoind, btc_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_initial_btc_balance = alice_btc_wallet.balance().await.unwrap();
|
||||
let bob_initial_btc_balance = bob_btc_wallet.balance().await.unwrap();
|
||||
|
||||
let redeem_address = alice_btc_wallet.new_address().await.unwrap();
|
||||
let punish_address = redeem_address.clone();
|
||||
let refund_address = bob_btc_wallet.new_address().await.unwrap();
|
||||
|
||||
let refund_timelock = 1;
|
||||
let punish_timelock = 1;
|
||||
|
||||
let alice_state0 = alice::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
redeem_address,
|
||||
punish_address,
|
||||
);
|
||||
let bob_state0 = bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
refund_timelock,
|
||||
punish_timelock,
|
||||
refund_address.clone(),
|
||||
);
|
||||
|
||||
let alice_message0 = alice_state0.next_message(&mut OsRng);
|
||||
let bob_message0 = bob_state0.next_message(&mut OsRng);
|
||||
|
||||
let alice_state1 = alice_state0.receive(bob_message0).unwrap();
|
||||
let bob_state1 = bob_state0
|
||||
.receive(&bob_btc_wallet, alice_message0)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob_message1 = bob_state1.next_message();
|
||||
let alice_state2 = alice_state1.receive(bob_message1);
|
||||
let alice_message1 = alice_state2.next_message();
|
||||
let bob_state2 = bob_state1.receive(alice_message1).unwrap();
|
||||
|
||||
let bob_message2 = bob_state2.next_message();
|
||||
let alice_state3 = alice_state2.receive(bob_message2).unwrap();
|
||||
|
||||
let bob_state2b = bob_state2.lock_btc(&bob_btc_wallet).await.unwrap();
|
||||
|
||||
let alice_state4 = alice_state3
|
||||
.watch_for_lock_btc(&alice_btc_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
alice_state4.punish(&alice_btc_wallet).await.unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_btc_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_btc_wallet.balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_btc_wallet
|
||||
.transaction_fee(bob_state2b.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
alice_initial_btc_balance + btc_amount - Amount::from_sat(2 * TX_FEE)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
bob_initial_btc_balance - btc_amount - lock_tx_bitcoin_fee
|
||||
);
|
||||
}
|
||||
}
|
||||
pub mod transport;
|
||||
|
@ -1,14 +1,9 @@
|
||||
#[cfg(test)]
|
||||
pub mod wallet;
|
||||
|
||||
use std::ops::Add;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use monero::{Address, PrivateKey, PublicKey};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::ops::Add;
|
||||
|
||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||
let scalar = Scalar::random(rng);
|
||||
@ -16,9 +11,6 @@ pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||
PrivateKey::from_scalar(scalar)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub use wallet::{AliceWallet, BobWallet};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PrivateViewKey(PrivateKey);
|
||||
|
||||
@ -69,6 +61,9 @@ impl Amount {
|
||||
pub fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for u64 {
|
||||
@ -83,8 +78,21 @@ pub struct TransferProof {
|
||||
tx_key: PrivateKey,
|
||||
}
|
||||
|
||||
impl TransferProof {
|
||||
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
|
||||
Self { tx_hash, tx_key }
|
||||
}
|
||||
pub fn tx_hash(&self) -> TxHash {
|
||||
self.tx_hash.clone()
|
||||
}
|
||||
pub fn tx_key(&self) -> PrivateKey {
|
||||
self.tx_key
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add constructor/ change String to fixed length byte array
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TxHash(String);
|
||||
pub struct TxHash(pub String);
|
||||
|
||||
impl From<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
@ -114,8 +122,8 @@ pub trait CheckTransfer {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ImportOutput {
|
||||
async fn import_output(
|
||||
pub trait CreateWalletForOutput {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
|
12
xmr-btc/src/transport.rs
Normal file
12
xmr-btc/src/transport.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SendMessage<SendMsg> {
|
||||
async fn send_message(&mut self, message: SendMsg) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ReceiveMessage<RecvMsg> {
|
||||
async fn receive_message(&mut self) -> Result<RecvMsg>;
|
||||
}
|
398
xmr-btc/tests/e2e.rs
Normal file
398
xmr-btc/tests/e2e.rs
Normal file
@ -0,0 +1,398 @@
|
||||
use crate::harness::wallet;
|
||||
use bitcoin_harness::Bitcoind;
|
||||
use harness::{
|
||||
node::{AliceNode, BobNode},
|
||||
transport::Transport,
|
||||
};
|
||||
use monero_harness::Monero;
|
||||
use rand::rngs::OsRng;
|
||||
use testcontainers::clients::Cli;
|
||||
use tokio::sync::{
|
||||
mpsc,
|
||||
mpsc::{Receiver, Sender},
|
||||
};
|
||||
use xmr_btc::{alice, bitcoin, bob, monero};
|
||||
|
||||
mod harness;
|
||||
|
||||
const TEN_XMR: u64 = 10_000_000_000_000;
|
||||
const RELATIVE_REFUND_TIMELOCK: u32 = 1;
|
||||
const RELATIVE_PUNISH_TIMELOCK: u32 = 1;
|
||||
|
||||
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
|
||||
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
|
||||
let _ = bitcoind.init(5).await;
|
||||
|
||||
bitcoind
|
||||
}
|
||||
|
||||
pub struct InitialBalances {
|
||||
alice_xmr: u64,
|
||||
alice_btc: bitcoin::Amount,
|
||||
bob_xmr: u64,
|
||||
bob_btc: bitcoin::Amount,
|
||||
}
|
||||
|
||||
pub struct SwapAmounts {
|
||||
xmr: monero::Amount,
|
||||
btc: bitcoin::Amount,
|
||||
}
|
||||
|
||||
pub fn init_alice_and_bob_transports() -> (
|
||||
Transport<alice::Message, bob::Message>,
|
||||
Transport<bob::Message, alice::Message>,
|
||||
) {
|
||||
let (a_sender, b_receiver): (Sender<alice::Message>, Receiver<alice::Message>) =
|
||||
mpsc::channel(5);
|
||||
let (b_sender, a_receiver): (Sender<bob::Message>, Receiver<bob::Message>) = mpsc::channel(5);
|
||||
|
||||
let a_transport = Transport {
|
||||
sender: a_sender,
|
||||
receiver: a_receiver,
|
||||
};
|
||||
|
||||
let b_transport = Transport {
|
||||
sender: b_sender,
|
||||
receiver: b_receiver,
|
||||
};
|
||||
|
||||
(a_transport, b_transport)
|
||||
}
|
||||
|
||||
pub async fn init_test<'a>(
|
||||
monero: &'a Monero<'a>,
|
||||
bitcoind: &Bitcoind<'_>,
|
||||
) -> (
|
||||
alice::State0,
|
||||
bob::State0,
|
||||
AliceNode<'a>,
|
||||
BobNode<'a>,
|
||||
InitialBalances,
|
||||
SwapAmounts,
|
||||
) {
|
||||
// must be bigger than our hardcoded fee of 10_000
|
||||
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
|
||||
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
|
||||
|
||||
let swap_amounts = SwapAmounts {
|
||||
xmr: xmr_amount,
|
||||
btc: btc_amount,
|
||||
};
|
||||
|
||||
let fund_alice = TEN_XMR;
|
||||
let fund_bob = 0;
|
||||
monero.init(fund_alice, fund_bob).await.unwrap();
|
||||
|
||||
let alice_monero_wallet = wallet::monero::AliceWallet(&monero);
|
||||
let bob_monero_wallet = wallet::monero::BobWallet(&monero);
|
||||
|
||||
let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (alice_transport, bob_transport) = init_alice_and_bob_transports();
|
||||
let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet);
|
||||
|
||||
let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet);
|
||||
|
||||
let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap();
|
||||
let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap();
|
||||
|
||||
let alice_initial_xmr_balance = alice.monero_wallet.0.get_balance_alice().await.unwrap();
|
||||
let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap();
|
||||
let punish_address = redeem_address.clone();
|
||||
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
|
||||
|
||||
let alice_state0 = alice::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
RELATIVE_REFUND_TIMELOCK,
|
||||
RELATIVE_PUNISH_TIMELOCK,
|
||||
redeem_address.clone(),
|
||||
punish_address.clone(),
|
||||
);
|
||||
let bob_state0 = bob::State0::new(
|
||||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
RELATIVE_REFUND_TIMELOCK,
|
||||
RELATIVE_PUNISH_TIMELOCK,
|
||||
refund_address,
|
||||
);
|
||||
let initial_balances = InitialBalances {
|
||||
alice_xmr: alice_initial_xmr_balance,
|
||||
alice_btc: alice_initial_btc_balance,
|
||||
bob_xmr: bob_initial_xmr_balance,
|
||||
bob_btc: bob_initial_btc_balance,
|
||||
};
|
||||
(
|
||||
alice_state0,
|
||||
bob_state0,
|
||||
alice,
|
||||
bob,
|
||||
initial_balances,
|
||||
swap_amounts,
|
||||
)
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use crate::{
|
||||
harness,
|
||||
harness::node::{run_alice_until, run_bob_until},
|
||||
init_bitcoind, init_test,
|
||||
};
|
||||
use futures::future;
|
||||
use monero_harness::Monero;
|
||||
use rand::rngs::OsRng;
|
||||
use std::convert::TryInto;
|
||||
use testcontainers::clients::Cli;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use xmr_btc::{
|
||||
alice, bitcoin,
|
||||
bitcoin::{Amount, TX_FEE},
|
||||
bob,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn happy_path() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let monero = Monero::new(&cli);
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
let (
|
||||
alice_state0,
|
||||
bob_state0,
|
||||
mut alice_node,
|
||||
mut bob_node,
|
||||
initial_balances,
|
||||
swap_amounts,
|
||||
) = init_test(&monero, &bitcoind).await;
|
||||
|
||||
let (alice_state, bob_state) = future::try_join(
|
||||
run_alice_until(
|
||||
&mut alice_node,
|
||||
alice_state0.into(),
|
||||
harness::alice::is_state6,
|
||||
&mut OsRng,
|
||||
),
|
||||
run_bob_until(
|
||||
&mut bob_node,
|
||||
bob_state0.into(),
|
||||
harness::bob::is_state5,
|
||||
&mut OsRng,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_state6: alice::State6 = alice_state.try_into().unwrap();
|
||||
let bob_state5: bob::State5 = bob_state.try_into().unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap();
|
||||
|
||||
let lock_tx_bitcoin_fee = bob_node
|
||||
.bitcoin_wallet
|
||||
.transaction_fee(bob_state5.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_final_xmr_balance = alice_node
|
||||
.monero_wallet
|
||||
.0
|
||||
.get_balance_alice()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
bob_node
|
||||
.monero_wallet
|
||||
.0
|
||||
.wait_for_bob_wallet_block_height()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
initial_balances.alice_btc + swap_amounts.btc
|
||||
- bitcoin::Amount::from_sat(bitcoin::TX_FEE)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
alice_final_xmr_balance,
|
||||
initial_balances.alice_xmr
|
||||
- u64::from(swap_amounts.xmr)
|
||||
- u64::from(alice_state6.lock_xmr_fee())
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_xmr_balance,
|
||||
initial_balances.bob_xmr + u64::from(swap_amounts.xmr)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn both_refund() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let monero = Monero::new(&cli);
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
let (
|
||||
alice_state0,
|
||||
bob_state0,
|
||||
mut alice_node,
|
||||
mut bob_node,
|
||||
initial_balances,
|
||||
swap_amounts,
|
||||
) = init_test(&monero, &bitcoind).await;
|
||||
|
||||
let (alice_state, bob_state) = future::try_join(
|
||||
run_alice_until(
|
||||
&mut alice_node,
|
||||
alice_state0.into(),
|
||||
harness::alice::is_state5,
|
||||
&mut OsRng,
|
||||
),
|
||||
run_bob_until(
|
||||
&mut bob_node,
|
||||
bob_state0.into(),
|
||||
harness::bob::is_state3,
|
||||
&mut OsRng,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_state5: alice::State5 = alice_state.try_into().unwrap();
|
||||
let bob_state3: bob::State3 = bob_state.try_into().unwrap();
|
||||
|
||||
bob_state3
|
||||
.refund_btc(&bob_node.bitcoin_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
alice_state5
|
||||
.refund_xmr(&alice_node.bitcoin_wallet, &alice_node.monero_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_node
|
||||
.bitcoin_wallet
|
||||
.transaction_fee(bob_state3.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
alice_node
|
||||
.monero_wallet
|
||||
.0
|
||||
.wait_for_alice_wallet_block_height()
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_final_xmr_balance = alice_node
|
||||
.monero_wallet
|
||||
.0
|
||||
.get_balance_alice()
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance_bob().await.unwrap();
|
||||
|
||||
assert_eq!(alice_final_btc_balance, initial_balances.alice_btc);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
// The 2 * TX_FEE corresponds to tx_refund and tx_cancel.
|
||||
initial_balances.bob_btc - Amount::from_sat(2 * TX_FEE) - lock_tx_bitcoin_fee
|
||||
);
|
||||
|
||||
// Because we create a new wallet when claiming Monero, we can only assert on
|
||||
// this new wallet owning all of `xmr_amount` after refund
|
||||
assert_eq!(alice_final_xmr_balance, u64::from(swap_amounts.xmr));
|
||||
assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn alice_punishes() {
|
||||
let _guard = tracing_subscriber::fmt()
|
||||
.with_env_filter("info")
|
||||
.set_default();
|
||||
|
||||
let cli = Cli::default();
|
||||
let monero = Monero::new(&cli);
|
||||
let bitcoind = init_bitcoind(&cli).await;
|
||||
|
||||
let (
|
||||
alice_state0,
|
||||
bob_state0,
|
||||
mut alice_node,
|
||||
mut bob_node,
|
||||
initial_balances,
|
||||
swap_amounts,
|
||||
) = init_test(&monero, &bitcoind).await;
|
||||
|
||||
let (alice_state, bob_state) = future::try_join(
|
||||
run_alice_until(
|
||||
&mut alice_node,
|
||||
alice_state0.into(),
|
||||
harness::alice::is_state4,
|
||||
&mut OsRng,
|
||||
),
|
||||
run_bob_until(
|
||||
&mut bob_node,
|
||||
bob_state0.into(),
|
||||
harness::bob::is_state3,
|
||||
&mut OsRng,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_state4: alice::State4 = alice_state.try_into().unwrap();
|
||||
let bob_state3: bob::State3 = bob_state.try_into().unwrap();
|
||||
|
||||
alice_state4
|
||||
.punish(&alice_node.bitcoin_wallet)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_final_btc_balance = alice_node.bitcoin_wallet.balance().await.unwrap();
|
||||
let bob_final_btc_balance = bob_node.bitcoin_wallet.balance().await.unwrap();
|
||||
|
||||
// lock_tx_bitcoin_fee is determined by the wallet, it is not necessarily equal
|
||||
// to TX_FEE
|
||||
let lock_tx_bitcoin_fee = bob_node
|
||||
.bitcoin_wallet
|
||||
.transaction_fee(bob_state3.tx_lock_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
alice_final_btc_balance,
|
||||
initial_balances.alice_btc + swap_amounts.btc - Amount::from_sat(2 * TX_FEE)
|
||||
);
|
||||
assert_eq!(
|
||||
bob_final_btc_balance,
|
||||
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
|
||||
);
|
||||
}
|
||||
}
|
36
xmr-btc/tests/harness/mod.rs
Normal file
36
xmr-btc/tests/harness/mod.rs
Normal file
@ -0,0 +1,36 @@
|
||||
pub mod node;
|
||||
pub mod transport;
|
||||
pub mod wallet;
|
||||
|
||||
pub mod bob {
|
||||
use xmr_btc::bob::State;
|
||||
|
||||
// TODO: use macro or generics
|
||||
pub fn is_state5(state: &State) -> bool {
|
||||
matches!(state, State::State5 { .. })
|
||||
}
|
||||
|
||||
// TODO: use macro or generics
|
||||
pub fn is_state3(state: &State) -> bool {
|
||||
matches!(state, State::State3 { .. })
|
||||
}
|
||||
}
|
||||
|
||||
pub mod alice {
|
||||
use xmr_btc::alice::State;
|
||||
|
||||
// TODO: use macro or generics
|
||||
pub fn is_state4(state: &State) -> bool {
|
||||
matches!(state, State::State4 { .. })
|
||||
}
|
||||
|
||||
// TODO: use macro or generics
|
||||
pub fn is_state5(state: &State) -> bool {
|
||||
matches!(state, State::State5 { .. })
|
||||
}
|
||||
|
||||
// TODO: use macro or generics
|
||||
pub fn is_state6(state: &State) -> bool {
|
||||
matches!(state, State::State6 { .. })
|
||||
}
|
||||
}
|
92
xmr-btc/tests/harness/node.rs
Normal file
92
xmr-btc/tests/harness/node.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use crate::harness::{transport::Transport, wallet};
|
||||
use anyhow::Result;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use xmr_btc::{alice, bob};
|
||||
|
||||
// TODO: merge this with bob node
|
||||
// This struct is responsible for I/O
|
||||
pub struct AliceNode<'a> {
|
||||
transport: Transport<alice::Message, bob::Message>,
|
||||
pub bitcoin_wallet: wallet::bitcoin::Wallet,
|
||||
pub monero_wallet: wallet::monero::AliceWallet<'a>,
|
||||
}
|
||||
|
||||
impl<'a> AliceNode<'a> {
|
||||
pub fn new(
|
||||
transport: Transport<alice::Message, bob::Message>,
|
||||
bitcoin_wallet: wallet::bitcoin::Wallet,
|
||||
monero_wallet: wallet::monero::AliceWallet<'a>,
|
||||
) -> AliceNode<'a> {
|
||||
Self {
|
||||
transport,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_alice_until<'a, R: RngCore + CryptoRng>(
|
||||
alice: &mut AliceNode<'a>,
|
||||
initial_state: alice::State,
|
||||
is_state: fn(&alice::State) -> bool,
|
||||
rng: &mut R,
|
||||
) -> Result<alice::State> {
|
||||
let mut result = initial_state;
|
||||
loop {
|
||||
result = alice::next_state(
|
||||
&alice.bitcoin_wallet,
|
||||
&alice.monero_wallet,
|
||||
&mut alice.transport,
|
||||
result,
|
||||
rng,
|
||||
)
|
||||
.await?;
|
||||
if is_state(&result) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: merge this with alice node
|
||||
// This struct is responsible for I/O
|
||||
pub struct BobNode<'a> {
|
||||
transport: Transport<bob::Message, alice::Message>,
|
||||
pub bitcoin_wallet: wallet::bitcoin::Wallet,
|
||||
pub monero_wallet: wallet::monero::BobWallet<'a>,
|
||||
}
|
||||
|
||||
impl<'a> BobNode<'a> {
|
||||
pub fn new(
|
||||
transport: Transport<bob::Message, alice::Message>,
|
||||
bitcoin_wallet: wallet::bitcoin::Wallet,
|
||||
monero_wallet: wallet::monero::BobWallet<'a>,
|
||||
) -> BobNode<'a> {
|
||||
Self {
|
||||
transport,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_bob_until<'a, R: RngCore + CryptoRng>(
|
||||
bob: &mut BobNode<'a>,
|
||||
initial_state: bob::State,
|
||||
is_state: fn(&bob::State) -> bool,
|
||||
rng: &mut R,
|
||||
) -> Result<bob::State> {
|
||||
let mut result = initial_state;
|
||||
loop {
|
||||
result = bob::next_state(
|
||||
&bob.bitcoin_wallet,
|
||||
&bob.monero_wallet,
|
||||
&mut bob.transport,
|
||||
result,
|
||||
rng,
|
||||
)
|
||||
.await?;
|
||||
if is_state(&result) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
45
xmr-btc/tests/harness/transport.rs
Normal file
45
xmr-btc/tests/harness/transport.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use tokio::{
|
||||
stream::StreamExt,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use xmr_btc::transport::{ReceiveMessage, SendMessage};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport<SendMsg, RecvMsg> {
|
||||
pub sender: Sender<SendMsg>,
|
||||
pub receiver: Receiver<RecvMsg>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<SendMsg, RecvMsg> SendMessage<SendMsg> for Transport<SendMsg, RecvMsg>
|
||||
where
|
||||
SendMsg: Send + Sync,
|
||||
RecvMsg: std::marker::Send,
|
||||
{
|
||||
async fn send_message(&mut self, message: SendMsg) -> Result<()> {
|
||||
let _ = self
|
||||
.sender
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(|_| anyhow!("failed to send message"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<SendMsg, RecvMsg> ReceiveMessage<RecvMsg> for Transport<SendMsg, RecvMsg>
|
||||
where
|
||||
SendMsg: std::marker::Send,
|
||||
RecvMsg: Send + Sync,
|
||||
{
|
||||
async fn receive_message(&mut self) -> Result<RecvMsg> {
|
||||
let message = self
|
||||
.receiver
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("failed to receive message"))?;
|
||||
Ok(message)
|
||||
}
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
use crate::bitcoin::{
|
||||
BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxLock,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
|
||||
@ -8,6 +5,9 @@ use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use tokio::time;
|
||||
use xmr_btc::bitcoin::{
|
||||
BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet(pub bitcoin_harness::Wallet);
|
||||
@ -107,10 +107,13 @@ impl BroadcastSignedTransaction for Wallet {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetRawTransaction for Wallet {
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
let tx = self.0.get_raw_transaction(txid).await?;
|
||||
|
||||
Ok(tx)
|
||||
impl WatchForRawTransaction for Wallet {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
loop {
|
||||
if let Ok(tx) = self.0.get_raw_transaction(txid).await {
|
||||
return Ok(tx);
|
||||
}
|
||||
time::delay_for(Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
}
|
2
xmr-btc/tests/harness/wallet/mod.rs
Normal file
2
xmr-btc/tests/harness/wallet/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod bitcoin;
|
||||
pub mod monero;
|
@ -1,12 +1,12 @@
|
||||
use crate::monero::{
|
||||
Amount, CheckTransfer, ImportOutput, PrivateViewKey, PublicKey, PublicViewKey, Transfer,
|
||||
TransferProof, TxHash,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use monero::{Address, Network, PrivateKey};
|
||||
use monero_harness::Monero;
|
||||
use std::str::FromStr;
|
||||
use xmr_btc::monero::{
|
||||
Amount, CheckTransfer, CreateWalletForOutput, PrivateViewKey, PublicKey, PublicViewKey,
|
||||
Transfer, TransferProof, TxHash,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AliceWallet<'c>(pub &'c Monero<'c>);
|
||||
@ -24,7 +24,7 @@ impl Transfer for AliceWallet<'_> {
|
||||
|
||||
let res = self
|
||||
.0
|
||||
.transfer_from_alice(amount.0, &destination_address.to_string())
|
||||
.transfer_from_alice(amount.as_piconero(), &destination_address.to_string())
|
||||
.await?;
|
||||
|
||||
let tx_hash = TxHash(res.tx_hash);
|
||||
@ -32,75 +32,13 @@ impl Transfer for AliceWallet<'_> {
|
||||
|
||||
let fee = Amount::from_piconero(res.fee);
|
||||
|
||||
Ok((TransferProof { tx_hash, tx_key }, fee))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BobWallet<'c>(pub &'c Monero<'c>);
|
||||
|
||||
#[async_trait]
|
||||
impl CheckTransfer for BobWallet<'_> {
|
||||
async fn check_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
amount: Amount,
|
||||
) -> Result<()> {
|
||||
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
|
||||
|
||||
let cli = self.0.bob_wallet_rpc_client();
|
||||
|
||||
let res = cli
|
||||
.check_tx_key(
|
||||
&String::from(transfer_proof.tx_hash),
|
||||
&transfer_proof.tx_key.to_string(),
|
||||
&address.to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if res.received != u64::from(amount) {
|
||||
bail!(
|
||||
"tx_lock doesn't pay enough: expected {:?}, got {:?}",
|
||||
res.received,
|
||||
amount
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok((TransferProof::new(tx_hash, tx_key), fee))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImportOutput for BobWallet<'_> {
|
||||
async fn import_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> Result<()> {
|
||||
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||
|
||||
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key);
|
||||
|
||||
let _ = self
|
||||
.0
|
||||
.bob_wallet_rpc_client()
|
||||
.generate_from_keys(
|
||||
&address.to_string(),
|
||||
&private_spend_key.to_string(),
|
||||
&PrivateKey::from(private_view_key).to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImportOutput for AliceWallet<'_> {
|
||||
async fn import_output(
|
||||
impl CreateWalletForOutput for AliceWallet<'_> {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
@ -123,3 +61,65 @@ impl ImportOutput for AliceWallet<'_> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BobWallet<'c>(pub &'c Monero<'c>);
|
||||
|
||||
#[async_trait]
|
||||
impl CheckTransfer for BobWallet<'_> {
|
||||
async fn check_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
amount: Amount,
|
||||
) -> Result<()> {
|
||||
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key.into());
|
||||
|
||||
let cli = self.0.bob_wallet_rpc_client();
|
||||
|
||||
let res = cli
|
||||
.check_tx_key(
|
||||
&String::from(transfer_proof.tx_hash()),
|
||||
&transfer_proof.tx_key().to_string(),
|
||||
&address.to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if res.received != u64::from(amount) {
|
||||
bail!(
|
||||
"tx_lock doesn't pay enough: expected {:?}, got {:?}",
|
||||
res.received,
|
||||
amount
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CreateWalletForOutput for BobWallet<'_> {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> Result<()> {
|
||||
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||
|
||||
let address = Address::standard(Network::Mainnet, public_spend_key, public_view_key);
|
||||
|
||||
let _ = self
|
||||
.0
|
||||
.bob_wallet_rpc_client()
|
||||
.generate_from_keys(
|
||||
&address.to_string(),
|
||||
&private_spend_key.to_string(),
|
||||
&PrivateKey::from(private_view_key).to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user