Resolve rebase issues, restructure code and fix warnings

This commit is contained in:
rishflab 2020-11-27 11:30:07 +11:00
parent 437c1cbb80
commit 3b005bd15c
10 changed files with 557 additions and 713 deletions

View File

@ -2,9 +2,8 @@
//! Alice holds XMR and wishes receive BTC. //! Alice holds XMR and wishes receive BTC.
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{ use crate::{
alice::execution::{lock_xmr, negotiate},
bitcoin, bitcoin,
bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT}, bitcoin::TX_LOCK_MINE_TIMEOUT,
monero, monero,
network::{ network::{
peer_tracker::{self, PeerTracker}, peer_tracker::{self, PeerTracker},
@ -16,34 +15,23 @@ use crate::{
storage::Database, storage::Database,
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::Result;
use async_recursion::async_recursion;
use async_trait::async_trait; use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{
future::{select, Either},
pin_mut,
};
use genawaiter::GeneratorState; use genawaiter::GeneratorState;
use libp2p::{ use libp2p::{
core::{identity::Keypair, Multiaddr}, core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel, request_response::ResponseChannel,
NetworkBehaviour, PeerId, NetworkBehaviour, PeerId,
}; };
use rand::{rngs::OsRng, CryptoRng, RngCore}; use rand::rngs::OsRng;
use sha2::Sha256;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use tokio::{sync::Mutex, time::timeout}; use tokio::sync::Mutex;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use uuid::Uuid; use uuid::Uuid;
use xmr_btc::{ use xmr_btc::{
alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0, State3}, alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0},
bitcoin::{ bitcoin::BroadcastSignedTransaction,
poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction,
TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality,
WatchForRawTransaction,
},
bob, cross_curve_dleq, bob, cross_curve_dleq,
monero::{CreateWalletForOutput, Transfer}, monero::{CreateWalletForOutput, Transfer},
}; };
@ -54,440 +42,7 @@ mod message0;
mod message1; mod message1;
mod message2; mod message2;
mod message3; mod message3;
pub mod swap;
trait Rng: RngCore + CryptoRng + Send {}
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
// The same data structure is used for swap execution and recovery.
// This allows for a seamless transition from a failed swap to recovery.
pub enum AliceState {
Started {
amounts: SwapAmounts,
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
},
Negotiated {
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
},
BtcLocked {
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
},
XmrLocked {
state3: State3,
},
EncSignLearned {
state3: State3,
encrypted_signature: EncryptedSignature,
},
BtcRedeemed,
BtcCancelled {
state3: State3,
tx_cancel: TxCancel,
},
BtcRefunded {
tx_refund: TxRefund,
published_refund_tx: ::bitcoin::Transaction,
state3: State3,
},
BtcPunishable {
tx_refund: TxRefund,
state3: State3,
},
BtcPunished {
tx_refund: TxRefund,
punished_tx_id: bitcoin::Txid,
state3: State3,
},
XmrRefunded,
WaitingToCancel {
state3: State3,
},
Punished,
SafelyAborted,
}
// State machine driver for swap execution
#[async_recursion]
pub async fn simple_swap(
state: AliceState,
mut swarm: Swarm,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
) -> Result<AliceState> {
match state {
AliceState::Started {
amounts,
a,
s_a,
v_a,
} => {
let (channel, amounts, state3) =
negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?;
simple_swap(
AliceState::Negotiated {
channel,
amounts,
state3,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::Negotiated {
state3,
channel,
amounts,
} => {
timeout(
Duration::from_secs(TX_LOCK_MINE_TIMEOUT),
bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()),
)
.await
.context("Timed out, Bob did not lock Bitcoin in time")?;
simple_swap(
AliceState::BtcLocked {
channel,
amounts,
state3,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::BtcLocked {
channel,
amounts,
state3,
} => {
lock_xmr(
channel,
amounts,
state3.clone(),
&mut swarm,
monero_wallet.clone(),
)
.await?;
simple_swap(
AliceState::XmrLocked { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::XmrLocked { state3 } => {
let encsig = timeout(
// Give a set arbitrary time to Bob to send us `tx_redeem_encsign`
Duration::from_secs(TX_LOCK_MINE_TIMEOUT),
async {
match swarm.next().await {
OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig),
other => Err(anyhow!(
"Expected Bob's Bitcoin redeem encsig, got: {:?}",
other
)),
}
},
)
.await
.context("Timed out, Bob did not send redeem encsign in time");
match encsig {
Err(_timeout_error) => {
// TODO(Franck): Insert in DB
simple_swap(
AliceState::WaitingToCancel { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
Ok(Err(_unexpected_msg_error)) => {
// TODO(Franck): Insert in DB
simple_swap(
AliceState::WaitingToCancel { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
Ok(Ok(encrypted_signature)) => {
// TODO(Franck): Insert in DB
simple_swap(
AliceState::EncSignLearned {
state3,
encrypted_signature,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
}
}
AliceState::EncSignLearned {
state3,
encrypted_signature,
} => {
let (signed_tx_redeem, _tx_redeem_txid) = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address);
bitcoin::verify_encsig(
state3.B.clone(),
state3.s_a.into_secp256k1().into(),
&tx_redeem.digest(),
&encrypted_signature,
)
.context("Invalid encrypted signature received")?;
let sig_a = state3.a.sign(tx_redeem.digest());
let sig_b = adaptor
.decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone());
let tx = tx_redeem
.add_signatures(
&state3.tx_lock,
(state3.a.public(), sig_a),
(state3.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_redeem");
let txid = tx.txid();
(tx, txid)
};
// TODO(Franck): Insert in db
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_redeem)
.await?;
// TODO(Franck) Wait for confirmations
simple_swap(
AliceState::BtcRedeemed,
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::WaitingToCancel { state3 } => {
let tx_lock_height = bitcoin_wallet
.transaction_block_height(state3.tx_lock.txid())
.await;
poll_until_block_height_is_gte(
bitcoin_wallet.as_ref(),
tx_lock_height + state3.refund_timelock,
)
.await;
let tx_cancel = bitcoin::TxCancel::new(
&state3.tx_lock,
state3.refund_timelock,
state3.a.public(),
state3.B.clone(),
);
if let None = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await {
let sig_a = state3.a.sign(tx_cancel.digest());
let sig_b = state3.tx_cancel_sig_bob.clone();
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&state3.tx_lock,
(state3.a.public(), sig_a),
(state3.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
}
simple_swap(
AliceState::BtcCancelled { state3, tx_cancel },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::BtcCancelled { state3, tx_cancel } => {
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await;
let reached_t2 = poll_until_block_height_is_gte(
bitcoin_wallet.as_ref(),
tx_cancel_height + state3.punish_timelock,
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address);
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(reached_t2);
pin_mut!(seen_refund_tx);
match select(reached_t2, seen_refund_tx).await {
Either::Left(_) => {
simple_swap(
AliceState::BtcPunishable { tx_refund, state3 },
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
Either::Right((published_refund_tx, _)) => {
simple_swap(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
}
}
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
} => {
let s_a = monero::PrivateKey {
scalar: state3.s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(published_refund_tx, state3.a.public())
.context("Failed to extract signature from Bitcoin refund tx")?;
let tx_refund_encsig = state3
.a
.encsign(state3.S_b_bitcoin.clone(), tx_refund.digest());
let s_b = bitcoin::recover(state3.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.context("Failed to recover Monero secret key from Bitcoin signature")?;
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
let spend_key = s_a + s_b;
let view_key = state3.v;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
Ok(AliceState::XmrRefunded)
}
AliceState::BtcPunishable { tx_refund, state3 } => {
let tx_cancel = bitcoin::TxCancel::new(
&state3.tx_lock,
state3.refund_timelock,
state3.a.public(),
state3.B.clone(),
);
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock);
let punished_tx_id = tx_punish.txid();
let sig_a = state3.a.sign(tx_punish.digest());
let sig_b = state3.tx_punish_sig_bob.clone();
let signed_tx_punish = tx_punish
.add_signatures(
&tx_cancel,
(state3.a.public(), sig_a),
(state3.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_punish)
.await?;
simple_swap(
AliceState::BtcPunished {
tx_refund,
punished_tx_id,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
AliceState::BtcPunished {
punished_tx_id,
tx_refund,
state3,
} => {
let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id);
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen);
match select(punish_tx_finalised, refund_tx_seen).await {
Either::Left(_) => {
simple_swap(
AliceState::Punished,
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
Either::Right((published_refund_tx, _)) => {
simple_swap(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
}
}
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::Punished => Ok(AliceState::Punished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
}
}
pub async fn swap( pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>, bitcoin_wallet: Arc<bitcoin::Wallet>,
@ -497,7 +52,25 @@ pub async fn swap(
transport: SwapTransport, transport: SwapTransport,
behaviour: Behaviour, behaviour: Behaviour,
) -> Result<()> { ) -> Result<()> {
struct Network(Arc<Mutex<Swarm>>); struct Network {
swarm: Arc<Mutex<Swarm>>,
channel: Option<ResponseChannel<AliceToBob>>,
}
impl Network {
pub async fn send_message2(&mut self, proof: monero::TransferProof) {
match self.channel.take() {
None => warn!("Channel not found, did you call this twice?"),
Some(channel) => {
let mut guard = self.swarm.lock().await;
guard.send_message2(channel, alice::Message2 {
tx_lock_proof: proof,
});
info!("Sent transfer proof");
}
}
}
}
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed // TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`. // to `ConstantBackoff`.
@ -508,7 +81,7 @@ pub async fn swap(
struct UnexpectedMessage; struct UnexpectedMessage;
let encsig = (|| async { let encsig = (|| async {
let mut guard = self.0.lock().await; let mut guard = self.swarm.lock().await;
let encsig = match guard.next().await { let encsig = match guard.next().await {
OutEvent::Message3(msg) => msg.tx_redeem_encsig, OutEvent::Message3(msg) => msg.tx_redeem_encsig,
other => { other => {
@ -602,8 +175,11 @@ pub async fn swap(
let msg = state2.next_message(); let msg = state2.next_message();
swarm.send_message1(channel, msg); swarm.send_message1(channel, msg);
let state3 = match swarm.next().await { let (state3, channel) = match swarm.next().await {
OutEvent::Message2(msg) => state2.receive(msg)?, OutEvent::Message2 { msg, channel } => {
let state3 = state2.receive(msg)?;
(state3, channel)
}
other => panic!("Unexpected event: {:?}", other), other => panic!("Unexpected event: {:?}", other),
}; };
@ -613,10 +189,13 @@ pub async fn swap(
info!("Handshake complete, we now have State3 for Alice."); info!("Handshake complete, we now have State3 for Alice.");
let network = Arc::new(Mutex::new(Network(unimplemented!()))); let network = Arc::new(Mutex::new(Network {
swarm: Arc::new(Mutex::new(swarm)),
channel: Some(channel),
}));
let mut action_generator = action_generator( let mut action_generator = action_generator(
network, network.clone(),
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
state3.clone(), state3.clone(),
TX_LOCK_MINE_TIMEOUT, TX_LOCK_MINE_TIMEOUT,
@ -636,12 +215,16 @@ pub async fn swap(
db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into()) db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into())
.await?; .await?;
let _ = monero_wallet let (transfer_proof, _) = monero_wallet
.transfer(public_spend_key, public_view_key, amount) .transfer(public_spend_key, public_view_key, amount)
.await?; .await?;
db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into()) db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into())
.await?; .await?;
let mut guard = network.as_ref().lock().await;
guard.send_message2(transfer_proof).await;
info!("Sent transfer proof");
} }
GeneratorState::Yielded(Action::RedeemBtc(tx)) => { GeneratorState::Yielded(Action::RedeemBtc(tx)) => {
@ -728,7 +311,10 @@ pub enum OutEvent {
msg: bob::Message1, msg: bob::Message1,
channel: ResponseChannel<AliceToBob>, channel: ResponseChannel<AliceToBob>,
}, },
Message2(bob::Message2), Message2 {
msg: bob::Message2,
channel: ResponseChannel<AliceToBob>,
},
Message3(bob::Message3), Message3(bob::Message3),
} }
@ -767,7 +353,7 @@ impl From<message1::OutEvent> for OutEvent {
impl From<message2::OutEvent> for OutEvent { impl From<message2::OutEvent> for OutEvent {
fn from(event: message2::OutEvent) -> Self { fn from(event: message2::OutEvent) -> Self {
match event { match event {
message2::OutEvent::Msg { msg, .. } => OutEvent::Message2(msg), message2::OutEvent::Msg { msg, channel } => OutEvent::Message2 { msg, channel },
} }
} }
} }
@ -827,6 +413,16 @@ impl Behaviour {
self.message1.send(channel, msg); self.message1.send(channel, msg);
debug!("Sent Message1"); debug!("Sent Message1");
} }
/// Send Message2 to Bob in response to receiving his Message2.
pub fn send_message2(
&mut self,
channel: ResponseChannel<AliceToBob>,
msg: xmr_btc::alice::Message2,
) {
self.message2.send(channel, msg);
debug!("Sent Message2");
}
} }
impl Default for Behaviour { impl Default for Behaviour {

472
swap/src/alice/swap.rs Normal file
View File

@ -0,0 +1,472 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use crate::{
alice::{
execution::{lock_xmr, negotiate},
OutEvent, Swarm,
},
bitcoin,
bitcoin::{EncryptedSignature, TX_LOCK_MINE_TIMEOUT},
monero,
network::request_response::AliceToBob,
SwapAmounts,
};
use anyhow::{anyhow, Context, Result};
use async_recursion::async_recursion;
use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic};
use futures::{
future::{select, Either},
pin_mut,
};
use libp2p::request_response::ResponseChannel;
use rand::{CryptoRng, RngCore};
use sha2::Sha256;
use std::{sync::Arc, time::Duration};
use tokio::time::timeout;
use xmr_btc::{
alice::State3,
bitcoin::{
poll_until_block_height_is_gte, BroadcastSignedTransaction, GetRawTransaction,
TransactionBlockHeight, TxCancel, TxRefund, WaitForTransactionFinality,
WatchForRawTransaction,
},
cross_curve_dleq,
monero::CreateWalletForOutput,
};
trait Rng: RngCore + CryptoRng + Send {}
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
// The same data structure is used for swap execution and recovery.
// This allows for a seamless transition from a failed swap to recovery.
pub enum AliceState {
Started {
amounts: SwapAmounts,
a: bitcoin::SecretKey,
s_a: cross_curve_dleq::Scalar,
v_a: monero::PrivateViewKey,
},
Negotiated {
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
},
BtcLocked {
channel: ResponseChannel<AliceToBob>,
amounts: SwapAmounts,
state3: State3,
},
XmrLocked {
state3: State3,
},
EncSignLearned {
state3: State3,
encrypted_signature: EncryptedSignature,
},
BtcRedeemed,
BtcCancelled {
state3: State3,
tx_cancel: TxCancel,
},
BtcRefunded {
tx_refund: TxRefund,
published_refund_tx: ::bitcoin::Transaction,
state3: State3,
},
BtcPunishable {
tx_refund: TxRefund,
state3: State3,
},
BtcPunished {
tx_refund: TxRefund,
punished_tx_id: bitcoin::Txid,
state3: State3,
},
XmrRefunded,
WaitingToCancel {
state3: State3,
},
Punished,
SafelyAborted,
}
// State machine driver for swap execution
#[async_recursion]
pub async fn swap(
state: AliceState,
mut swarm: Swarm,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
) -> Result<AliceState> {
match state {
AliceState::Started {
amounts,
a,
s_a,
v_a,
} => {
let (channel, amounts, state3) =
negotiate(amounts, a, s_a, v_a, &mut swarm, bitcoin_wallet.clone()).await?;
swap(
AliceState::Negotiated {
channel,
amounts,
state3,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::Negotiated {
state3,
channel,
amounts,
} => {
timeout(
Duration::from_secs(TX_LOCK_MINE_TIMEOUT),
bitcoin_wallet.wait_for_transaction_finality(state3.tx_lock.txid()),
)
.await
.context("Timed out, Bob did not lock Bitcoin in time")?;
swap(
AliceState::BtcLocked {
channel,
amounts,
state3,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::BtcLocked {
channel,
amounts,
state3,
} => {
lock_xmr(
channel,
amounts,
state3.clone(),
&mut swarm,
monero_wallet.clone(),
)
.await?;
swap(
AliceState::XmrLocked { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::XmrLocked { state3 } => {
let encsig = timeout(
// Give a set arbitrary time to Bob to send us `tx_redeem_encsign`
Duration::from_secs(TX_LOCK_MINE_TIMEOUT),
async {
match swarm.next().await {
OutEvent::Message3(msg) => Ok(msg.tx_redeem_encsig),
other => Err(anyhow!(
"Expected Bob's Bitcoin redeem encsig, got: {:?}",
other
)),
}
},
)
.await
.context("Timed out, Bob did not send redeem encsign in time");
match encsig {
Err(_timeout_error) => {
// TODO(Franck): Insert in DB
swap(
AliceState::WaitingToCancel { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
Ok(Err(_unexpected_msg_error)) => {
// TODO(Franck): Insert in DB
swap(
AliceState::WaitingToCancel { state3 },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
Ok(Ok(encrypted_signature)) => {
// TODO(Franck): Insert in DB
swap(
AliceState::EncSignLearned {
state3,
encrypted_signature,
},
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
}
}
AliceState::EncSignLearned {
state3,
encrypted_signature,
} => {
let (signed_tx_redeem, _tx_redeem_txid) = {
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
let tx_redeem = bitcoin::TxRedeem::new(&state3.tx_lock, &state3.redeem_address);
bitcoin::verify_encsig(
state3.B.clone(),
state3.s_a.into_secp256k1().into(),
&tx_redeem.digest(),
&encrypted_signature,
)
.context("Invalid encrypted signature received")?;
let sig_a = state3.a.sign(tx_redeem.digest());
let sig_b = adaptor
.decrypt_signature(&state3.s_a.into_secp256k1(), encrypted_signature.clone());
let tx = tx_redeem
.add_signatures(
&state3.tx_lock,
(state3.a.public(), sig_a),
(state3.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_redeem");
let txid = tx.txid();
(tx, txid)
};
// TODO(Franck): Insert in db
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_redeem)
.await?;
// TODO(Franck) Wait for confirmations
swap(
AliceState::BtcRedeemed,
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::WaitingToCancel { state3 } => {
let tx_lock_height = bitcoin_wallet
.transaction_block_height(state3.tx_lock.txid())
.await;
poll_until_block_height_is_gte(
bitcoin_wallet.as_ref(),
tx_lock_height + state3.refund_timelock,
)
.await;
let tx_cancel = bitcoin::TxCancel::new(
&state3.tx_lock,
state3.refund_timelock,
state3.a.public(),
state3.B.clone(),
);
if let Err(_e) = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await {
let sig_a = state3.a.sign(tx_cancel.digest());
let sig_b = state3.tx_cancel_sig_bob.clone();
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&state3.tx_lock,
(state3.a.public(), sig_a),
(state3.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
}
swap(
AliceState::BtcCancelled { state3, tx_cancel },
swarm,
bitcoin_wallet,
monero_wallet,
)
.await
}
AliceState::BtcCancelled { state3, tx_cancel } => {
let tx_cancel_height = bitcoin_wallet
.transaction_block_height(tx_cancel.txid())
.await;
let reached_t2 = poll_until_block_height_is_gte(
bitcoin_wallet.as_ref(),
tx_cancel_height + state3.punish_timelock,
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state3.refund_address);
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(reached_t2);
pin_mut!(seen_refund_tx);
match select(reached_t2, seen_refund_tx).await {
Either::Left(_) => {
swap(
AliceState::BtcPunishable { tx_refund, state3 },
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
Either::Right((published_refund_tx, _)) => {
swap(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
}
}
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
} => {
let s_a = monero::PrivateKey {
scalar: state3.s_a.into_ed25519(),
};
let tx_refund_sig = tx_refund
.extract_signature_by_key(published_refund_tx, state3.a.public())
.context("Failed to extract signature from Bitcoin refund tx")?;
let tx_refund_encsig = state3
.a
.encsign(state3.S_b_bitcoin.clone(), tx_refund.digest());
let s_b = bitcoin::recover(state3.S_b_bitcoin, tx_refund_sig, tx_refund_encsig)
.context("Failed to recover Monero secret key from Bitcoin signature")?;
let s_b = monero::private_key_from_secp256k1_scalar(s_b.into());
let spend_key = s_a + s_b;
let view_key = state3.v;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
Ok(AliceState::XmrRefunded)
}
AliceState::BtcPunishable { tx_refund, state3 } => {
let tx_cancel = bitcoin::TxCancel::new(
&state3.tx_lock,
state3.refund_timelock,
state3.a.public(),
state3.B.clone(),
);
let tx_punish =
bitcoin::TxPunish::new(&tx_cancel, &state3.punish_address, state3.punish_timelock);
let punished_tx_id = tx_punish.txid();
let sig_a = state3.a.sign(tx_punish.digest());
let sig_b = state3.tx_punish_sig_bob.clone();
let signed_tx_punish = tx_punish
.add_signatures(
&tx_cancel,
(state3.a.public(), sig_a),
(state3.B.clone(), sig_b),
)
.expect("sig_{a,b} to be valid signatures for tx_cancel");
let _ = bitcoin_wallet
.broadcast_signed_transaction(signed_tx_punish)
.await?;
swap(
AliceState::BtcPunished {
tx_refund,
punished_tx_id,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
AliceState::BtcPunished {
punished_tx_id,
tx_refund,
state3,
} => {
let punish_tx_finalised = bitcoin_wallet.wait_for_transaction_finality(punished_tx_id);
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
pin_mut!(punish_tx_finalised);
pin_mut!(refund_tx_seen);
match select(punish_tx_finalised, refund_tx_seen).await {
Either::Left(_) => {
swap(
AliceState::Punished,
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
Either::Right((published_refund_tx, _)) => {
swap(
AliceState::BtcRefunded {
tx_refund,
published_refund_tx,
state3,
},
swarm,
bitcoin_wallet.clone(),
monero_wallet,
)
.await
}
}
}
AliceState::XmrRefunded => Ok(AliceState::XmrRefunded),
AliceState::BtcRedeemed => Ok(AliceState::BtcRedeemed),
AliceState::Punished => Ok(AliceState::Punished),
AliceState::SafelyAborted => Ok(AliceState::SafelyAborted),
}
}

View File

@ -1,11 +1,6 @@
use anyhow::Result; use anyhow::Result;
use structopt::StructOpt; use structopt::StructOpt;
use swap::{ use swap::{alice::swap::swap, bob::swap::BobState, cli::Options, storage::Database};
bob_simple::{simple_swap, BobState},
cli::Options,
storage::Database,
};
use uuid::Uuid;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@ -20,16 +15,7 @@ async fn main() -> Result<()> {
match opt { match opt {
Options::Alice { .. } => { Options::Alice { .. } => {
simple_swap( swap(bob_state, swarm, bitcoin_wallet, monero_wallet).await?;
bob_state,
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
Uuid::new_v4(),
)
.await?;
} }
Options::Recover { .. } => { Options::Recover { .. } => {
let _stored_state: BobState = unimplemented!("io.get_state(uuid)?"); let _stored_state: BobState = unimplemented!("io.get_state(uuid)?");

View File

@ -99,7 +99,6 @@ impl BroadcastSignedTransaction for Wallet {
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed // TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
// to `ConstantBackoff`. // to `ConstantBackoff`.
#[async_trait] #[async_trait]
impl WatchForRawTransaction for Wallet { impl WatchForRawTransaction for Wallet {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
@ -112,6 +111,7 @@ impl WatchForRawTransaction for Wallet {
#[async_trait] #[async_trait]
impl GetRawTransaction for Wallet { impl GetRawTransaction for Wallet {
// todo: potentially replace with option
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> { async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
Ok(self.0.get_raw_transaction(txid).await?) Ok(self.0.get_raw_transaction(txid).await?)
} }
@ -154,13 +154,6 @@ impl TransactionBlockHeight for Wallet {
} }
} }
#[async_trait]
impl GetRawTransaction for Wallet {
async fn get_raw_transaction(&self, _txid: Txid) -> Option<Transaction> {
todo!()
}
}
#[async_trait] #[async_trait]
impl WaitForTransactionFinality for Wallet { impl WaitForTransactionFinality for Wallet {
async fn wait_for_transaction_finality(&self, _txid: Txid) { async fn wait_for_transaction_finality(&self, _txid: Txid) {

View File

@ -1,7 +1,7 @@
//! Run an XMR/BTC swap in the role of Bob. //! Run an XMR/BTC swap in the role of Bob.
//! Bob holds BTC and wishes receive XMR. //! Bob holds BTC and wishes receive XMR.
use anyhow::Result; use anyhow::Result;
use async_recursion::async_recursion;
use async_trait::async_trait; use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use futures::{ use futures::{
@ -21,11 +21,11 @@ mod message0;
mod message1; mod message1;
mod message2; mod message2;
mod message3; mod message3;
pub mod swap;
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{ use crate::{
bitcoin::{self, TX_LOCK_MINE_TIMEOUT}, bitcoin::{self, TX_LOCK_MINE_TIMEOUT},
io::Io,
monero, monero,
network::{ network::{
peer_tracker::{self, PeerTracker}, peer_tracker::{self, PeerTracker},
@ -43,129 +43,6 @@ use xmr_btc::{
monero::CreateWalletForOutput, monero::CreateWalletForOutput,
}; };
// The same data structure is used for swap execution and recovery.
// This allows for a seamless transition from a failed swap to recovery.
pub enum BobState {
Started,
Negotiated,
BtcLocked,
XmrLocked,
BtcRedeemed,
BtcRefunded,
XmrRedeemed,
Cancelled,
Punished,
SafelyAborted,
}
// State machine driver for swap execution
#[async_recursion]
pub async fn simple_swap(state: BobState, io: Io) -> Result<BobState> {
match state {
BobState::Started => {
// Alice and Bob exchange swap info
// Todo: Poll the swarm here until Alice and Bob have exchanged info
simple_swap(BobState::Negotiated, io).await
}
BobState::Negotiated => {
// Alice and Bob have exchanged info
// Bob Locks Btc
simple_swap(BobState::BtcLocked, io).await
}
BobState::BtcLocked => {
// Bob has locked Btc
// Watch for Alice to Lock Xmr
simple_swap(BobState::XmrLocked, io).await
}
BobState::XmrLocked => {
// Alice has locked Xmr
// Bob sends Alice his key
// Todo: This should be a oneshot
if unimplemented!("Redeemed before t1") {
simple_swap(BobState::BtcRedeemed, io).await
} else {
// submit TxCancel
simple_swap(BobState::Cancelled, io).await
}
}
BobState::Cancelled => {
if unimplemented!("<t2") {
// submit TxRefund
simple_swap(BobState::BtcRefunded, io).await
} else {
simple_swap(BobState::Punished, io).await
}
}
BobState::BtcRefunded => Ok(BobState::BtcRefunded),
BobState::BtcRedeemed => {
// Bob redeems XMR using revealed s_a
simple_swap(BobState::XmrRedeemed, io).await
}
BobState::Punished => Ok(BobState::Punished),
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
}
}
// State machine driver for recovery execution
#[async_recursion]
pub async fn abort(state: BobState, io: Io) -> Result<BobState> {
match state {
BobState::Started => {
// Nothing has been commited by either party, abort swap.
abort(BobState::SafelyAborted, io).await
}
BobState::Negotiated => {
// Nothing has been commited by either party, abort swap.
abort(BobState::SafelyAborted, io).await
}
BobState::BtcLocked => {
// Bob has locked BTC and must refund it
// Bob waits for alice to publish TxRedeem or t1
if unimplemented!("TxRedeemSeen") {
// Alice has redeemed revealing s_a
abort(BobState::BtcRedeemed, io).await
} else if unimplemented!("T1Elapsed") {
// publish TxCancel or see if it has been published
abort(BobState::Cancelled, io).await
} else {
Err(unimplemented!())
}
}
BobState::XmrLocked => {
// Alice has locked Xmr
// Wait until t1
if unimplemented!(">t1 and <t2") {
// Bob publishes TxCancel
abort(BobState::Cancelled, io).await
} else {
// >t2
// submit TxCancel
abort(BobState::Punished, io).await
}
}
BobState::Cancelled => {
// Bob has cancelled the swap
// If <t2 Bob refunds
if unimplemented!("<t2") {
// Submit TxRefund
abort(BobState::BtcRefunded, io).await
} else {
// Bob failed to refund in time and has been punished
abort(BobState::Punished, io).await
}
}
BobState::BtcRedeemed => {
// Bob uses revealed s_a to redeem XMR
abort(BobState::XmrRedeemed, io).await
}
BobState::BtcRefunded => Ok(BobState::BtcRefunded),
BobState::Punished => Ok(BobState::Punished),
BobState::SafelyAborted => Ok(BobState::SafelyAborted),
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
}
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn swap( pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>, bitcoin_wallet: Arc<bitcoin::Wallet>,

View File

@ -1,36 +1,24 @@
use crate::{ use crate::{
bitcoin::{self},
bob::{OutEvent, Swarm}, bob::{OutEvent, Swarm},
network::{transport::SwapTransport, TokioExecutor},
state, state,
storage::Database, storage::Database,
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK, Cmd, Rsp, PUNISH_TIMELOCK, REFUND_TIMELOCK,
}; };
use anyhow::Result; use anyhow::Result;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use futures::{ use futures::{
channel::mpsc::{Receiver, Sender}, channel::mpsc::{Receiver, Sender},
future::Either, StreamExt,
FutureExt, StreamExt,
}; };
use genawaiter::GeneratorState;
use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId}; use libp2p::PeerId;
use rand::{rngs::OsRng, CryptoRng, RngCore}; use rand::rngs::OsRng;
use std::{process, sync::Arc, time::Duration}; use std::{process, sync::Arc};
use tokio::sync::Mutex;
use tracing::{debug, info, warn}; use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use xmr_btc::{ use xmr_btc::bob::{self, State0};
alice,
bitcoin::{
poll_until_block_height_is_gte, BroadcastSignedTransaction, EncryptedSignature, SignTxLock,
TransactionBlockHeight,
},
bob::{self, action_generator, ReceiveTransferProof, State0},
monero::CreateWalletForOutput,
};
// The same data structure is used for swap execution and recovery. // The same data structure is used for swap execution and recovery.
// This allows for a seamless transition from a failed swap to recovery. // This allows for a seamless transition from a failed swap to recovery.
@ -50,7 +38,7 @@ pub enum BobState {
// State machine driver for swap execution // State machine driver for swap execution
#[async_recursion] #[async_recursion]
pub async fn simple_swap( pub async fn swap(
state: BobState, state: BobState,
mut swarm: Swarm, mut swarm: Swarm,
db: Database, db: Database,
@ -122,7 +110,7 @@ pub async fn simple_swap(
info!("Handshake complete"); info!("Handshake complete");
simple_swap( swap(
BobState::Negotiated(state2, alice_peer_id), BobState::Negotiated(state2, alice_peer_id),
swarm, swarm,
db, db,
@ -137,7 +125,7 @@ pub async fn simple_swap(
// Alice and Bob have exchanged info // Alice and Bob have exchanged info
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?; let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?;
// db.insert_latest_state(state); // db.insert_latest_state(state);
simple_swap( swap(
BobState::BtcLocked(state3, alice_peer_id), BobState::BtcLocked(state3, alice_peer_id),
swarm, swarm,
db, db,
@ -160,7 +148,7 @@ pub async fn simple_swap(
} }
other => panic!("unexpected event: {:?}", other), other => panic!("unexpected event: {:?}", other),
}; };
simple_swap( swap(
BobState::XmrLocked(state4, alice_peer_id), BobState::XmrLocked(state4, alice_peer_id),
swarm, swarm,
db, db,
@ -182,7 +170,7 @@ pub async fn simple_swap(
// should happen in this arm? // should happen in this arm?
swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig); swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig);
simple_swap( swap(
BobState::EncSigSent(state, alice_peer_id), BobState::EncSigSent(state, alice_peer_id),
swarm, swarm,
db, db,
@ -200,7 +188,7 @@ pub async fn simple_swap(
tokio::select! { tokio::select! {
val = redeem_watcher => { val = redeem_watcher => {
simple_swap( swap(
BobState::BtcRedeemed(val?), BobState::BtcRedeemed(val?),
swarm, swarm,
db, db,
@ -211,14 +199,14 @@ pub async fn simple_swap(
) )
.await .await
} }
val = t1_timeout => { _ = t1_timeout => {
// Check whether TxCancel has been published. // Check whether TxCancel has been published.
// We should not fail if the transaction is already on the blockchain // We should not fail if the transaction is already on the blockchain
if let Err(_e) = state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await { if let Err(_) = state.check_for_tx_cancel(bitcoin_wallet.as_ref()).await {
state.submit_tx_cancel(bitcoin_wallet.as_ref()).await; state.submit_tx_cancel(bitcoin_wallet.as_ref()).await?;
} }
simple_swap( swap(
BobState::Cancelled(state), BobState::Cancelled(state),
swarm, swarm,
db, db,
@ -235,7 +223,7 @@ pub async fn simple_swap(
BobState::BtcRedeemed(state) => { BobState::BtcRedeemed(state) => {
// Bob redeems XMR using revealed s_a // Bob redeems XMR using revealed s_a
state.claim_xmr(monero_wallet.as_ref()).await?; state.claim_xmr(monero_wallet.as_ref()).await?;
simple_swap( swap(
BobState::XmrRedeemed, BobState::XmrRedeemed,
swarm, swarm,
db, db,
@ -253,6 +241,7 @@ pub async fn simple_swap(
BobState::XmrRedeemed => Ok(BobState::XmrRedeemed), BobState::XmrRedeemed => Ok(BobState::XmrRedeemed),
} }
} }
// // State machine driver for recovery execution // // State machine driver for recovery execution
// #[async_recursion] // #[async_recursion]
// pub async fn abort(state: BobState, io: Io) -> Result<BobState> { // pub async fn abort(state: BobState, io: Io) -> Result<BobState> {

View File

@ -1,8 +0,0 @@
// This struct contains all the I/O required to execute a swap
pub struct Io {
// swarm: libp2p::Swarm<>,
// bitcoind_rpc: _,
// monerod_rpc: _,
// monero_wallet_rpc: _,
// db: _,
}

View File

@ -6,9 +6,7 @@ use std::fmt::{self, Display};
pub mod alice; pub mod alice;
pub mod bitcoin; pub mod bitcoin;
pub mod bob; pub mod bob;
pub mod bob_simple;
pub mod cli; pub mod cli;
pub mod io;
pub mod monero; pub mod monero;
pub mod network; pub mod network;
pub mod recover; pub mod recover;

View File

@ -186,6 +186,7 @@ pub trait WatchForRawTransaction {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
} }
#[async_trait]
pub trait WaitForTransactionFinality { pub trait WaitForTransactionFinality {
async fn wait_for_transaction_finality(&self, txid: Txid); async fn wait_for_transaction_finality(&self, txid: Txid);
} }

View File

@ -879,63 +879,3 @@ impl State5 {
self.tx_lock.txid() self.tx_lock.txid()
} }
} }
/// Watch for the refund transaction on the blockchain. Watch until t2 has
/// elapsed.
pub async fn watch_for_refund_btc<W>(state: State5, bitcoin_wallet: &W) -> Result<()>
where
W: WatchForRawTransaction,
{
let tx_cancel = bitcoin::TxCancel::new(
&state.tx_lock,
state.refund_timelock,
state.A.clone(),
state.b.public(),
);
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &state.refund_address);
let tx_refund_watcher = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
Ok(())
}
// Watch for refund transaction on the blockchain
pub async fn watch_for_redeem_btc<W>(state: State4, bitcoin_wallet: &W) -> Result<State5>
where
W: WatchForRawTransaction,
{
let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address);
let tx_redeem_encsig = state
.b
.encsign(state.S_a_bitcoin.clone(), tx_redeem.digest());
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, state.b.public())?;
let s_a = bitcoin::recover(state.S_a_bitcoin.clone(), tx_redeem_sig, tx_redeem_encsig)?;
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
Ok(State5 {
A: state.A.clone(),
b: state.b.clone(),
s_a,
s_b: state.s_b,
S_a_monero: state.S_a_monero,
S_a_bitcoin: state.S_a_bitcoin.clone(),
v: state.v,
btc: state.btc,
xmr: state.xmr,
refund_timelock: state.refund_timelock,
punish_timelock: state.punish_timelock,
refund_address: state.refund_address.clone(),
redeem_address: state.redeem_address.clone(),
punish_address: state.punish_address.clone(),
tx_lock: state.tx_lock.clone(),
tx_refund_encsig: state.tx_refund_encsig.clone(),
tx_cancel_sig: state.tx_cancel_sig_a.clone(),
})
}