Execute Alice and Bob state machines concurrently

Previously we were testing the protocol by manually driving Alice and
Bob's state machines. This logic has now be moved to an async state
transition function that can take any possible state as input. The
state transition function is called in a loop until it returns the
desired state. This allows use to interrupt midway through the protocol
and perform refund and punish tests. This design was chosen over a
generator based implementation because the the generator based
implementation results in a impure state transition function that is
difficult to reason about and prone to bugs.

Test related code was extracted into the tests folder.

The 2b and 4b states were renamed to be consistent with the rest.

Macros were used to reduce code duplication when converting
child states to their parent states and vice versa.

Todos were added were neccessary.
This commit is contained in:
rishflab 2020-09-29 15:36:50 +10:00
parent 3d3864807d
commit 8754a9931b
15 changed files with 1211 additions and 503 deletions

View File

@ -17,6 +17,8 @@ monero = "0.9"
rand = "0.7" rand = "0.7"
sha2 = "0.9" sha2 = "0.9"
thiserror = "1" thiserror = "1"
tokio = { version = "0.2", default-features = false, features = ["time"] }
tracing = "0.1"
[dev-dependencies] [dev-dependencies]
base64 = "0.12" base64 = "0.12"
@ -25,3 +27,5 @@ monero-harness = { path = "../monero-harness" }
reqwest = { version = "0.10", default-features = false } reqwest = { version = "0.10", default-features = false }
testcontainers = "0.10" testcontainers = "0.10"
tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] } tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time", "rt-threaded"] }
tracing-subscriber = "0.2.12"
tracing = "0.1"

View File

@ -2,33 +2,178 @@ use anyhow::{anyhow, Result};
use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature}; use ecdsa_fun::adaptor::{Adaptor, EncryptedSignature};
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use crate::{bitcoin, bitcoin::GetRawTransaction, bob, monero, monero::ImportOutput}; use crate::{
use ecdsa_fun::{nonce::Deterministic, Signature}; bitcoin,
bitcoin::{BroadcastSignedTransaction, GetRawTransaction},
bob, monero,
monero::{ImportOutput, Transfer},
transport::SendReceive,
};
use ecdsa_fun::nonce::Deterministic;
use sha2::Sha256; use sha2::Sha256;
use std::convert::{TryFrom, TryInto};
#[derive(Debug)] pub mod message;
pub struct Message0 { pub use message::{Message, Message0, Message1, Message2, UnexpectedMessage};
pub(crate) A: bitcoin::PublicKey,
pub(crate) S_a_monero: monero::PublicKey, pub async fn next_state<
pub(crate) S_a_bitcoin: bitcoin::PublicKey, 'a,
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, R: RngCore + CryptoRng,
pub(crate) v_a: monero::PrivateViewKey, B: GetRawTransaction + BroadcastSignedTransaction,
pub(crate) redeem_address: bitcoin::Address, M: ImportOutput + Transfer,
pub(crate) punish_address: bitcoin::Address, T: SendReceive<Message, 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: bob::Message0 = transport.receive_message().await?.try_into()?;
let state1 = state0.receive(bob_message0)?;
Ok(state1.into())
}
State::State1(state1) => {
let bob_message1: bob::Message1 = transport.receive_message().await?.try_into()?;
let state2 = state1.receive(bob_message1);
let alice_message1: Message1 = state2.next_message();
transport.send_message(alice_message1.into()).await?;
Ok(state2.into())
}
State::State2(state2) => {
let bob_message2: bob::Message2 = transport.receive_message().await?.try_into()?;
let state3 = state2.receive(bob_message2)?;
tokio::time::delay_for(std::time::Duration::new(5, 0)).await;
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: bob::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.unwrap();
Ok(state6.into())
}
State::State6(state6) => Ok(state6.into()),
}
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Message1 { pub enum State {
pub(crate) tx_cancel_sig: Signature, State0(State0),
pub(crate) tx_refund_encsig: EncryptedSignature, State1(State1),
State2(State2),
State3(State3),
State4(State4),
State5(State5),
State6(State6),
} }
#[derive(Debug)] // TODO: use macro or generics
pub struct Message2 { pub fn is_state4(state: &State) -> bool {
pub(crate) tx_lock_proof: monero::TransferProof, match state {
State::State4 { .. } => true,
_ => false,
}
}
// TODO: use macro or generics
pub fn is_state5(state: &State) -> bool {
match state {
State::State5 { .. } => true,
_ => false,
}
}
// TODO: use macro or generics
pub fn is_state6(state: &State) -> bool {
match state {
State::State6 { .. } => true,
_ => false,
}
} }
#[derive(Debug)] macro_rules! impl_try_from_parent_state {
($type:ident) => {
impl TryFrom<State> for $type {
type Error = anyhow::Error;
fn try_from(from: State) -> Result<Self> {
if let State::$type(state) = from {
Ok(state)
} else {
Err(anyhow!("Failed to convert parent state to child state"))
}
}
}
};
}
impl_try_from_parent_state!(State0);
impl_try_from_parent_state!(State1);
impl_try_from_parent_state!(State2);
impl_try_from_parent_state!(State3);
impl_try_from_parent_state!(State4);
impl_try_from_parent_state!(State5);
impl_try_from_parent_state!(State6);
macro_rules! impl_from_child_state {
($type:ident) => {
impl From<$type> for State {
fn from(from: $type) -> Self {
State::$type(from)
}
}
};
}
impl_from_child_state!(State0);
impl_from_child_state!(State1);
impl_from_child_state!(State2);
impl_from_child_state!(State3);
impl_from_child_state!(State4);
impl_from_child_state!(State5);
impl_from_child_state!(State6);
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, Clone)]
pub struct State0 { pub struct State0 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar, s_a: cross_curve_dleq::Scalar,
@ -114,7 +259,7 @@ impl State0 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State1 { pub struct State1 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
B: bitcoin::PublicKey, B: bitcoin::PublicKey,
@ -152,7 +297,7 @@ impl State1 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State2 { pub struct State2 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
B: bitcoin::PublicKey, B: bitcoin::PublicKey,
@ -227,7 +372,7 @@ impl State2 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State3 { pub struct State3 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
B: bitcoin::PublicKey, B: bitcoin::PublicKey,
@ -252,10 +397,13 @@ impl State3 {
where where
W: bitcoin::GetRawTransaction, W: bitcoin::GetRawTransaction,
{ {
let _ = bitcoin_wallet tracing::info!("{}", self.tx_lock.txid());
let tx = bitcoin_wallet
.get_raw_transaction(self.tx_lock.txid()) .get_raw_transaction(self.tx_lock.txid())
.await?; .await?;
tracing::info!("{}", tx.txid());
Ok(State4 { Ok(State4 {
a: self.a, a: self.a,
B: self.B, B: self.B,
@ -277,7 +425,7 @@ impl State3 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State4 { pub struct State4 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
B: bitcoin::PublicKey, B: bitcoin::PublicKey,
@ -298,7 +446,7 @@ pub struct State4 {
} }
impl 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 where
W: monero::Transfer, W: monero::Transfer,
{ {
@ -311,8 +459,7 @@ impl State4 {
.transfer(S_a + S_b, self.v.public(), self.xmr) .transfer(S_a + S_b, self.v.public(), self.xmr)
.await?; .await?;
Ok(( Ok(State5 {
State4b {
a: self.a, a: self.a,
B: self.B, B: self.B,
s_a: self.s_a, s_a: self.s_a,
@ -330,9 +477,8 @@ impl State4 {
tx_lock_proof, tx_lock_proof,
tx_punish_sig_bob: self.tx_punish_sig_bob, tx_punish_sig_bob: self.tx_punish_sig_bob,
tx_cancel_sig_bob: self.tx_cancel_sig_bob, tx_cancel_sig_bob: self.tx_cancel_sig_bob,
}, lock_xmr_fee: fee,
fee, })
))
} }
pub async fn punish<W: bitcoin::BroadcastSignedTransaction>( pub async fn punish<W: bitcoin::BroadcastSignedTransaction>(
@ -382,8 +528,8 @@ impl State4 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State4b { pub struct State5 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
B: bitcoin::PublicKey, B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar, s_a: cross_curve_dleq::Scalar,
@ -401,17 +547,18 @@ pub struct State4b {
tx_lock_proof: monero::TransferProof, tx_lock_proof: monero::TransferProof,
tx_punish_sig_bob: bitcoin::Signature, tx_punish_sig_bob: bitcoin::Signature,
tx_cancel_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 { pub fn next_message(&self) -> Message2 {
Message2 { Message2 {
tx_lock_proof: self.tx_lock_proof.clone(), tx_lock_proof: self.tx_lock_proof.clone(),
} }
} }
pub fn receive(self, msg: bob::Message3) -> State5 { pub fn receive(self, msg: bob::Message3) -> State6 {
State5 { State6 {
a: self.a, a: self.a,
B: self.B, B: self.B,
s_a: self.s_a, s_a: self.s_a,
@ -428,6 +575,7 @@ impl State4b {
tx_lock: self.tx_lock, tx_lock: self.tx_lock,
tx_punish_sig_bob: self.tx_punish_sig_bob, tx_punish_sig_bob: self.tx_punish_sig_bob,
tx_redeem_encsig: msg.tx_redeem_encsig, tx_redeem_encsig: msg.tx_redeem_encsig,
lock_xmr_fee: self.lock_xmr_fee,
} }
} }
@ -469,8 +617,8 @@ impl State4b {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State5 { pub struct State6 {
a: bitcoin::SecretKey, a: bitcoin::SecretKey,
B: bitcoin::PublicKey, B: bitcoin::PublicKey,
s_a: cross_curve_dleq::Scalar, s_a: cross_curve_dleq::Scalar,
@ -487,9 +635,10 @@ pub struct State5 {
tx_lock: bitcoin::TxLock, tx_lock: bitcoin::TxLock,
tx_punish_sig_bob: bitcoin::Signature, tx_punish_sig_bob: bitcoin::Signature,
tx_redeem_encsig: EncryptedSignature, tx_redeem_encsig: EncryptedSignature,
lock_xmr_fee: monero::Amount,
} }
impl State5 { impl State6 {
pub async fn redeem_btc<W: bitcoin::BroadcastSignedTransaction>( pub async fn redeem_btc<W: bitcoin::BroadcastSignedTransaction>(
&self, &self,
bitcoin_wallet: &W, bitcoin_wallet: &W,
@ -513,4 +662,8 @@ impl State5 {
Ok(()) Ok(())
} }
pub fn lock_xmr_fee(&self) -> monero::Amount {
self.lock_xmr_fee
}
} }

View File

@ -0,0 +1,114 @@
use anyhow::Result;
use ecdsa_fun::adaptor::EncryptedSignature;
use crate::{bitcoin, monero};
use ecdsa_fun::Signature;
use std::convert::TryFrom;
#[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 From<Message0> for Message {
fn from(m: Message0) -> Self {
Message::Message0(m)
}
}
impl TryFrom<Message> for Message0 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message0(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create0".to_string(),
received: m,
}),
}
}
}
impl From<Message1> for Message {
fn from(m: Message1) -> Self {
Message::Message1(m)
}
}
impl TryFrom<Message> for Message1 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message1(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create1".to_string(),
received: m,
}),
}
}
}
impl From<Message2> for Message {
fn from(m: Message2) -> Self {
Message::Message2(m)
}
}
impl TryFrom<Message> for Message2 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message2(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create2".to_string(),
received: m,
}),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("expected message of type {expected_type}, got {received:?}")]
pub struct UnexpectedMessage {
expected_type: String,
received: Message,
}
impl UnexpectedMessage {
pub fn new<T>(received: Message) -> Self {
let expected_type = std::any::type_name::<T>();
Self {
expected_type: expected_type.to_string(),
received,
}
}
}

View File

@ -1,6 +1,4 @@
pub mod transactions; pub mod transactions;
#[cfg(test)]
pub mod wallet;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
@ -28,9 +26,6 @@ pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxR
pub use bitcoin::{Address, Amount, OutPoint, Txid}; pub use bitcoin::{Address, Amount, OutPoint, Txid};
pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; pub use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
#[cfg(test)]
pub use wallet::{make_wallet, Wallet};
pub const TX_FEE: u64 = 10_000; pub const TX_FEE: u64 = 10_000;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -1,7 +1,11 @@
use crate::{ use crate::{
alice, alice,
bitcoin::{self, BuildTxLockPsbt, GetRawTransaction, TxCancel}, bitcoin::{
self, BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxCancel,
},
monero, monero,
monero::{CheckTransfer, ImportOutput},
transport::SendReceive,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ecdsa_fun::{ use ecdsa_fun::{
@ -11,31 +15,132 @@ use ecdsa_fun::{
}; };
use rand::{CryptoRng, RngCore}; use rand::{CryptoRng, RngCore};
use sha2::Sha256; use sha2::Sha256;
use std::convert::{TryFrom, TryInto};
#[derive(Debug)] pub mod message;
pub struct Message0 { pub use message::{Message, Message0, Message1, Message2, Message3, UnexpectedMessage};
pub(crate) B: bitcoin::PublicKey,
pub(crate) S_b_monero: monero::PublicKey, pub async fn next_state<
pub(crate) S_b_bitcoin: bitcoin::PublicKey, 'a,
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, R: RngCore + CryptoRng,
pub(crate) v_b: monero::PrivateViewKey, B: GetRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction,
pub(crate) refund_address: bitcoin::Address, M: ImportOutput + CheckTransfer,
T: SendReceive<Message, 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: alice::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: alice::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: alice::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");
tokio::time::delay_for(std::time::Duration::new(5, 0)).await;
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)] #[derive(Debug)]
pub struct Message1 { pub enum State {
pub(crate) tx_lock: bitcoin::TxLock, State0(State0),
State1(State1),
State2(State2),
State3(State3),
State4(State4),
State5(State5),
} }
#[derive(Debug)] macro_rules! impl_try_from_parent_state {
pub struct Message2 { ($type:ident) => {
pub(crate) tx_punish_sig: Signature, impl TryFrom<State> for $type {
pub(crate) tx_cancel_sig: Signature, type Error = anyhow::Error;
fn try_from(from: State) -> Result<Self> {
if let State::$type(state) = from {
Ok(state)
} else {
Err(anyhow!("Failed to convert parent state to child state"))
}
}
}
};
} }
#[derive(Debug)] impl_try_from_parent_state!(State0);
pub struct Message3 { impl_try_from_parent_state!(State1);
pub(crate) tx_redeem_encsig: EncryptedSignature, impl_try_from_parent_state!(State2);
impl_try_from_parent_state!(State3);
impl_try_from_parent_state!(State4);
impl_try_from_parent_state!(State5);
macro_rules! impl_from_child_state {
($type:ident) => {
impl From<$type> for State {
fn from(from: $type) -> Self {
State::$type(from)
}
}
};
}
impl_from_child_state!(State0);
impl_from_child_state!(State1);
impl_from_child_state!(State2);
impl_from_child_state!(State3);
impl_from_child_state!(State4);
impl_from_child_state!(State5);
// TODO: use macro or generics
pub fn is_state5(state: &State) -> bool {
match state {
State::State5 { .. } => true,
_ => false,
}
}
// TODO: use macro or generics
pub fn is_state3(state: &State) -> bool {
match state {
State::State3 { .. } => true,
_ => false,
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -126,7 +231,7 @@ impl State0 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State1 { pub struct State1 {
A: bitcoin::PublicKey, A: bitcoin::PublicKey,
b: bitcoin::SecretKey, b: bitcoin::SecretKey,
@ -189,7 +294,7 @@ impl State1 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State2 { pub struct State2 {
A: bitcoin::PublicKey, A: bitcoin::PublicKey,
b: bitcoin::SecretKey, b: bitcoin::SecretKey,
@ -228,17 +333,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 where
W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction, W: bitcoin::SignTxLock + bitcoin::BroadcastSignedTransaction,
{ {
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?; let signed_tx_lock = bitcoin_wallet.sign_tx_lock(self.tx_lock.clone()).await?;
tracing::info!("{}", self.tx_lock.txid());
let _ = bitcoin_wallet let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_lock) .broadcast_signed_transaction(signed_tx_lock)
.await?; .await?;
Ok(State2b { Ok(State3 {
A: self.A, A: self.A,
b: self.b, b: self.b,
s_b: self.s_b, s_b: self.s_b,
@ -260,7 +366,7 @@ impl State2 {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct State2b { pub struct State3 {
A: bitcoin::PublicKey, A: bitcoin::PublicKey,
b: bitcoin::SecretKey, b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar, s_b: cross_curve_dleq::Scalar,
@ -279,8 +385,9 @@ pub struct State2b {
tx_refund_encsig: EncryptedSignature, tx_refund_encsig: EncryptedSignature,
} }
impl State2b { impl State3 {
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State3> // todo: loop until punish? timelock has expired
pub async fn watch_for_lock_xmr<W>(self, xmr_wallet: &W, msg: alice::Message2) -> Result<State4>
where where
W: monero::CheckTransfer, W: monero::CheckTransfer,
{ {
@ -293,7 +400,7 @@ impl State2b {
.check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr) .check_transfer(S, self.v.public(), msg.tx_lock_proof, self.xmr)
.await?; .await?;
Ok(State3 { Ok(State4 {
A: self.A, A: self.A,
b: self.b, b: self.b,
s_b: self.s_b, s_b: self.s_b,
@ -359,15 +466,13 @@ impl State2b {
} }
Ok(()) Ok(())
} }
#[cfg(test)]
pub fn tx_lock_id(&self) -> bitcoin::Txid { pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid() self.tx_lock.txid()
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct State3 { pub struct State4 {
A: bitcoin::PublicKey, A: bitcoin::PublicKey,
b: bitcoin::SecretKey, b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar, s_b: cross_curve_dleq::Scalar,
@ -386,7 +491,7 @@ pub struct State3 {
tx_refund_encsig: EncryptedSignature, tx_refund_encsig: EncryptedSignature,
} }
impl State3 { impl State4 {
pub fn next_message(&self) -> Message3 { pub fn next_message(&self) -> Message3 {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); 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_encsig = self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest());
@ -394,7 +499,7 @@ impl State3 {
Message3 { tx_redeem_encsig } 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 where
W: GetRawTransaction, W: GetRawTransaction,
{ {
@ -409,7 +514,7 @@ impl State3 {
let s_a = let s_a =
monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_a.to_bytes())); monero::PrivateKey::from_scalar(monero::Scalar::from_bytes_mod_order(s_a.to_bytes()));
Ok(State4 { Ok(State5 {
A: self.A, A: self.A,
b: self.b, b: self.b,
s_a, s_a,
@ -431,8 +536,8 @@ impl State3 {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State4 { pub struct State5 {
A: bitcoin::PublicKey, A: bitcoin::PublicKey,
b: bitcoin::SecretKey, b: bitcoin::SecretKey,
s_a: monero::PrivateKey, s_a: monero::PrivateKey,
@ -452,7 +557,7 @@ pub struct State4 {
tx_cancel_sig: Signature, tx_cancel_sig: Signature,
} }
impl State4 { impl State5 {
pub async fn claim_xmr<W>(&self, monero_wallet: &W) -> Result<()> pub async fn claim_xmr<W>(&self, monero_wallet: &W) -> Result<()>
where where
W: monero::ImportOutput, W: monero::ImportOutput,
@ -469,4 +574,7 @@ impl State4 {
Ok(()) Ok(())
} }
pub fn tx_lock_id(&self) -> bitcoin::Txid {
self.tx_lock.txid()
}
} }

137
xmr-btc/src/bob/message.rs Normal file
View File

@ -0,0 +1,137 @@
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 From<Message0> for Message {
fn from(m: Message0) -> Self {
Message::Message0(m)
}
}
impl TryFrom<Message> for Message0 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message0(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create0".to_string(),
received: m,
}),
}
}
}
impl From<Message1> for Message {
fn from(m: Message1) -> Self {
Message::Message1(m)
}
}
impl TryFrom<Message> for Message1 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message1(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create0".to_string(),
received: m,
}),
}
}
}
impl From<Message2> for Message {
fn from(m: Message2) -> Self {
Message::Message2(m)
}
}
impl TryFrom<Message> for Message2 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message2(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create0".to_string(),
received: m,
}),
}
}
}
impl From<Message3> for Message {
fn from(m: Message3) -> Self {
Message::Message3(m)
}
}
impl TryFrom<Message> for Message3 {
type Error = UnexpectedMessage;
fn try_from(m: Message) -> Result<Self, Self::Error> {
match m {
Message::Message3(m) => Ok(m),
_ => Err(UnexpectedMessage {
expected_type: "Create0".to_string(),
received: m,
}),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("expected message of type {expected_type}, got {received:?}")]
pub struct UnexpectedMessage {
expected_type: String,
received: Message,
}
impl UnexpectedMessage {
pub fn new<T>(received: Message) -> Self {
let expected_type = std::any::type_name::<T>();
Self {
expected_type: expected_type.to_string(),
received,
}
}
}

View File

@ -18,367 +18,4 @@ pub mod alice;
pub mod bitcoin; pub mod bitcoin;
pub mod bob; pub mod bob;
pub mod monero; pub mod monero;
pub mod transport;
#[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
);
}
}

View File

@ -1,6 +1,3 @@
#[cfg(test)]
pub mod wallet;
use std::ops::Add; use std::ops::Add;
use anyhow::Result; use anyhow::Result;
@ -16,9 +13,6 @@ pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
PrivateKey::from_scalar(scalar) PrivateKey::from_scalar(scalar)
} }
#[cfg(test)]
pub use wallet::{AliceWallet, BobWallet};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct PrivateViewKey(PrivateKey); pub struct PrivateViewKey(PrivateKey);
@ -69,6 +63,9 @@ impl Amount {
pub fn from_piconero(amount: u64) -> Self { pub fn from_piconero(amount: u64) -> Self {
Amount(amount) Amount(amount)
} }
pub fn as_piconero(&self) -> u64 {
self.0
}
} }
impl From<Amount> for u64 { impl From<Amount> for u64 {
@ -83,8 +80,21 @@ pub struct TransferProof {
tx_key: PrivateKey, 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)] #[derive(Clone, Debug)]
pub struct TxHash(String); pub struct TxHash(pub String);
impl From<TxHash> for String { impl From<TxHash> for String {
fn from(from: TxHash) -> Self { fn from(from: TxHash) -> Self {

8
xmr-btc/src/transport.rs Normal file
View File

@ -0,0 +1,8 @@
use anyhow::Result;
use async_trait::async_trait;
#[async_trait]
pub trait SendReceive<SendMsg, RecvMsg> {
async fn send_message(&mut self, message: SendMsg) -> Result<()>;
async fn receive_message(&mut self) -> Result<RecvMsg>;
}

389
xmr-btc/tests/e2e.rs Normal file
View File

@ -0,0 +1,389 @@
use crate::{
node::{AliceNode, BobNode},
transport::Transport,
};
use bitcoin_harness::Bitcoind;
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 node;
mod transport;
mod wallet;
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.clone(),
);
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,
)
}
#[cfg(test)]
mod tests {
use crate::{
init_bitcoind, init_test,
node::{run_alice_until, run_bob_until},
};
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(),
alice::is_state6,
&mut OsRng,
),
run_bob_until(&mut bob_node, bob_state0.into(), 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();
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
);
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_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(),
alice::is_state5,
&mut OsRng,
),
run_bob_until(&mut bob_node, bob_state0.into(), 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();
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
);
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();
// 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(),
alice::is_state4,
&mut OsRng,
),
run_bob_until(&mut bob_node, bob_state0.into(), 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
);
}
}

92
xmr-btc/tests/node.rs Normal file
View File

@ -0,0 +1,92 @@
use crate::{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,56 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use tokio::{
stream::StreamExt,
sync::mpsc::{Receiver, Sender},
};
use xmr_btc::{alice, bob, transport::SendReceive};
#[derive(Debug)]
pub struct Transport<SendMsg, RecvMsg> {
pub sender: Sender<SendMsg>,
pub receiver: Receiver<RecvMsg>,
}
#[async_trait]
impl SendReceive<alice::Message, bob::Message> for Transport<alice::Message, bob::Message> {
async fn send_message(&mut self, message: alice::Message) -> Result<()> {
let _ = self
.sender
.send(message)
.await
.map_err(|_| anyhow!("failed to send message"))?;
Ok(())
}
async fn receive_message(&mut self) -> Result<bob::Message> {
let message = self
.receiver
.next()
.await
.ok_or_else(|| anyhow!("failed to receive message"))?;
Ok(message)
}
}
#[async_trait]
impl SendReceive<bob::Message, alice::Message> for Transport<bob::Message, alice::Message> {
async fn send_message(&mut self, message: bob::Message) -> Result<()> {
let _ = self
.sender
.send(message)
.await
.map_err(|_| anyhow!("failed to send message"))?;
Ok(())
}
async fn receive_message(&mut self) -> Result<alice::Message> {
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 anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
@ -8,6 +5,9 @@ use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
use reqwest::Url; use reqwest::Url;
use std::time::Duration; use std::time::Duration;
use tokio::time; use tokio::time;
use xmr_btc::bitcoin::{
BroadcastSignedTransaction, BuildTxLockPsbt, GetRawTransaction, SignTxLock, TxLock,
};
#[derive(Debug)] #[derive(Debug)]
pub struct Wallet(pub bitcoin_harness::Wallet); pub struct Wallet(pub bitcoin_harness::Wallet);
@ -109,7 +109,10 @@ impl BroadcastSignedTransaction for Wallet {
#[async_trait] #[async_trait]
impl GetRawTransaction for Wallet { impl GetRawTransaction for Wallet {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> { async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
// TODO: put into loop instead of delaying
time::delay_for(Duration::from_millis(5000)).await;
let tx = self.0.get_raw_transaction(txid).await?; let tx = self.0.get_raw_transaction(txid).await?;
tracing::info!("{}", tx.txid());
Ok(tx) Ok(tx)
} }

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 anyhow::{bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
use monero::{Address, Network, PrivateKey}; use monero::{Address, Network, PrivateKey};
use monero_harness::Monero; use monero_harness::Monero;
use std::str::FromStr; use std::str::FromStr;
use xmr_btc::monero::{
Amount, CheckTransfer, ImportOutput, PrivateViewKey, PublicKey, PublicViewKey, Transfer,
TransferProof, TxHash,
};
#[derive(Debug)] #[derive(Debug)]
pub struct AliceWallet<'c>(pub &'c Monero<'c>); pub struct AliceWallet<'c>(pub &'c Monero<'c>);
@ -24,7 +24,7 @@ impl Transfer for AliceWallet<'_> {
let res = self let res = self
.0 .0
.transfer_from_alice(amount.0, &destination_address.to_string()) .transfer_from_alice(amount.as_piconero(), &destination_address.to_string())
.await?; .await?;
let tx_hash = TxHash(res.tx_hash); let tx_hash = TxHash(res.tx_hash);
@ -32,7 +32,33 @@ impl Transfer for AliceWallet<'_> {
let fee = Amount::from_piconero(res.fee); let fee = Amount::from_piconero(res.fee);
Ok((TransferProof { tx_hash, tx_key }, fee)) Ok((TransferProof::new(tx_hash, tx_key), fee))
}
}
#[async_trait]
impl ImportOutput for AliceWallet<'_> {
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
.alice_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
} }
} }
@ -54,8 +80,8 @@ impl CheckTransfer for BobWallet<'_> {
let res = cli let res = cli
.check_tx_key( .check_tx_key(
&String::from(transfer_proof.tx_hash), &String::from(transfer_proof.tx_hash()),
&transfer_proof.tx_key.to_string(), &transfer_proof.tx_key().to_string(),
&address.to_string(), &address.to_string(),
) )
.await?; .await?;
@ -97,29 +123,3 @@ impl ImportOutput for BobWallet<'_> {
Ok(()) Ok(())
} }
} }
#[async_trait]
impl ImportOutput for AliceWallet<'_> {
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
.alice_wallet_rpc_client()
.generate_from_keys(
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
)
.await?;
Ok(())
}
}