Merge pull request #2 from comit-network/async-squashed

Execute Alice and Bob state machines concurrently
This commit is contained in:
rishflab 2020-10-09 12:14:45 +11:00 committed by GitHub
commit 36608657a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1234 additions and 559 deletions

147
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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
}
}

View 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);

View File

@ -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> {

View File

@ -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()
}
}

View 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);

View File

@ -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;

View File

@ -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
View 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
View 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
);
}
}

View 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 { .. })
}
}

View 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);
}
}
}

View 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)
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,2 @@
pub mod bitcoin;
pub mod monero;

View File

@ -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(())
}
}