Add Alice execution path

Consolidate and simplify swap execution. Generators are no longer
needed. Consolidate recovery and swap data structures. The
recursive calls can be replaced with a loop if returning prior to
completion is desired for testing purposes.

Fill out alice abort path

Move state machine executors into seperate files

Not compiling due to recursion/async issues

Fix async recursion compilation errors

Fix Bob swap execution

Remove check for ack message from Alice. Seems like a bad idea to
rely on an acknowledgement message instead of looking at the
blockchain.

Fix Bob abort

Fix warnings

Xmr lock complete

Add TxCancel submit to XmrLocked

Bob swap completed

Remove alice
This commit is contained in:
rishflab 2020-11-12 11:06:34 +11:00
parent e7682a42a4
commit dd07e2f882
10 changed files with 538 additions and 510 deletions

View File

@ -8,6 +8,7 @@ description = "XMR/BTC trustless atomic swaps."
[dependencies]
anyhow = "1"
async-trait = "0.1"
async-recursion = "0.3.1"
atty = "0.2"
backoff = { version = "0.2", features = ["tokio"] }
base64 = "0.12"

View File

@ -1,461 +0,0 @@
//! Run an XMR/BTC swap in the role of Alice.
//! Alice holds XMR and wishes receive BTC.
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use genawaiter::GeneratorState;
use libp2p::{
core::{identity::Keypair, Multiaddr},
request_response::ResponseChannel,
NetworkBehaviour, PeerId,
};
use rand::rngs::OsRng;
use std::{sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use uuid::Uuid;
mod amounts;
mod message0;
mod message1;
mod message2;
mod message3;
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
bitcoin,
bitcoin::TX_LOCK_MINE_TIMEOUT,
monero,
network::{
peer_tracker::{self, PeerTracker},
request_response::AliceToBob,
transport::SwapTransport,
TokioExecutor,
},
state,
storage::Database,
SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use xmr_btc::{
alice::{self, action_generator, Action, ReceiveBitcoinRedeemEncsig, State0},
bitcoin::BroadcastSignedTransaction,
bob,
monero::{CreateWalletForOutput, Transfer},
};
pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Database,
listen: Multiaddr,
transport: SwapTransport,
behaviour: Alice,
) -> Result<()> {
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
// to `ConstantBackoff`.
#[async_trait]
impl ReceiveBitcoinRedeemEncsig for Network {
async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature {
#[derive(Debug)]
struct UnexpectedMessage;
let encsig = (|| async {
let mut guard = self.swarm.lock().await;
let encsig = match guard.next().await {
OutEvent::Message3(msg) => msg.tx_redeem_encsig,
other => {
warn!("Expected Bob's Bitcoin redeem encsig, got: {:?}", other);
return Err(backoff::Error::Transient(UnexpectedMessage));
}
};
Result::<_, backoff::Error<UnexpectedMessage>>::Ok(encsig)
})
.retry(ConstantBackoff::new(Duration::from_secs(1)))
.await
.expect("transient errors to be retried");
info!("Received Bitcoin redeem encsig");
encsig
}
}
let mut swarm = new_swarm(listen, transport, behaviour)?;
let message0: bob::Message0;
let mut state0: Option<alice::State0> = None;
let mut last_amounts: Option<SwapAmounts> = None;
// TODO: This loop is a neat idea for local development, as it allows us to keep
// Alice up and let Bob keep trying to connect, request amounts and/or send the
// first message of the handshake, but it comes at the cost of needing to handle
// mutable state, which has already been the source of a bug at one point. This
// is an obvious candidate for refactoring
loop {
match swarm.next().await {
OutEvent::ConnectionEstablished(bob) => {
info!("Connection established with: {}", bob);
}
OutEvent::Request(amounts::OutEvent::Btc { btc, channel }) => {
let amounts = calculate_amounts(btc);
last_amounts = Some(amounts);
swarm.send_amounts(channel, amounts);
let SwapAmounts { btc, xmr } = amounts;
let redeem_address = bitcoin_wallet.as_ref().new_address().await?;
let punish_address = redeem_address.clone();
// TODO: Pass this in using <R: RngCore + CryptoRng>
let rng = &mut OsRng;
let state = State0::new(
rng,
btc,
xmr,
REFUND_TIMELOCK,
PUNISH_TIMELOCK,
redeem_address,
punish_address,
);
info!("Commencing handshake");
swarm.set_state0(state.clone());
state0 = Some(state)
}
OutEvent::Message0(msg) => {
// We don't want Bob to be able to crash us by sending an out of
// order message. Keep looping if Bob has not requested amounts.
if last_amounts.is_some() {
// TODO: We should verify the amounts and notify Bob if they have changed.
message0 = msg;
break;
}
}
other => panic!("Unexpected event: {:?}", other),
};
}
let state1 = state0.expect("to be set").receive(message0)?;
let (state2, channel) = match swarm.next().await {
OutEvent::Message1 { msg, channel } => {
let state2 = state1.receive(msg);
(state2, channel)
}
other => panic!("Unexpected event: {:?}", other),
};
let msg = state2.next_message();
swarm.send_message1(channel, msg);
let (state3, channel) = match swarm.next().await {
OutEvent::Message2 { msg, channel } => {
let state3 = state2.receive(msg)?;
(state3, channel)
}
other => panic!("Unexpected event: {:?}", other),
};
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state::Alice::Handshaken(state3.clone()).into())
.await?;
info!("Handshake complete, we now have State3 for Alice.");
let network = Arc::new(Mutex::new(Network {
swarm: Arc::new(Mutex::new(swarm)),
channel: Some(channel),
}));
let mut action_generator = action_generator(
network.clone(),
bitcoin_wallet.clone(),
state3.clone(),
TX_LOCK_MINE_TIMEOUT,
);
loop {
let state = action_generator.async_resume().await;
tracing::info!("Resumed execution of generator, got: {:?}", state);
match state {
GeneratorState::Yielded(Action::LockXmr {
amount,
public_spend_key,
public_view_key,
}) => {
db.insert_latest_state(swap_id, state::Alice::BtcLocked(state3.clone()).into())
.await?;
let (transfer_proof, _) = monero_wallet
.transfer(public_spend_key, public_view_key, amount)
.await?;
db.insert_latest_state(swap_id, state::Alice::XmrLocked(state3.clone()).into())
.await?;
let mut guard = network.as_ref().lock().await;
guard.send_message2(transfer_proof).await;
info!("Sent transfer proof");
}
GeneratorState::Yielded(Action::RedeemBtc(tx)) => {
db.insert_latest_state(
swap_id,
state::Alice::BtcRedeemable {
state: state3.clone(),
redeem_tx: tx.clone(),
}
.into(),
)
.await?;
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
}
GeneratorState::Yielded(Action::CancelBtc(tx)) => {
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
}
GeneratorState::Yielded(Action::PunishBtc(tx)) => {
db.insert_latest_state(swap_id, state::Alice::BtcPunishable(state3.clone()).into())
.await?;
let _ = bitcoin_wallet.broadcast_signed_transaction(tx).await?;
}
GeneratorState::Yielded(Action::CreateMoneroWalletForOutput {
spend_key,
view_key,
}) => {
db.insert_latest_state(
swap_id,
state::Alice::BtcRefunded {
state: state3.clone(),
spend_key,
view_key,
}
.into(),
)
.await?;
monero_wallet
.create_and_load_wallet_for_output(spend_key, view_key)
.await?;
}
GeneratorState::Complete(()) => {
db.insert_latest_state(swap_id, state::Alice::SwapComplete.into())
.await?;
return Ok(());
}
}
}
}
pub type Swarm = libp2p::Swarm<Alice>;
fn new_swarm(listen: Multiaddr, transport: SwapTransport, behaviour: Alice) -> Result<Swarm> {
use anyhow::Context as _;
let local_peer_id = behaviour.peer_id();
let mut swarm = libp2p::swarm::SwarmBuilder::new(transport, behaviour, local_peer_id.clone())
.executor(Box::new(TokioExecutor {
handle: tokio::runtime::Handle::current(),
}))
.build();
Swarm::listen_on(&mut swarm, listen.clone())
.with_context(|| format!("Address is not supported: {:#}", listen))?;
tracing::info!("Initialized swarm: {}", local_peer_id);
Ok(swarm)
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum OutEvent {
ConnectionEstablished(PeerId),
Request(amounts::OutEvent), // Not-uniform with Bob on purpose, ready for adding Xmr event.
Message0(bob::Message0),
Message1 {
msg: bob::Message1,
channel: ResponseChannel<AliceToBob>,
},
Message2 {
msg: bob::Message2,
channel: ResponseChannel<AliceToBob>,
},
Message3(bob::Message3),
}
impl From<peer_tracker::OutEvent> for OutEvent {
fn from(event: peer_tracker::OutEvent) -> Self {
match event {
peer_tracker::OutEvent::ConnectionEstablished(id) => {
OutEvent::ConnectionEstablished(id)
}
}
}
}
impl From<amounts::OutEvent> for OutEvent {
fn from(event: amounts::OutEvent) -> Self {
OutEvent::Request(event)
}
}
impl From<message0::OutEvent> for OutEvent {
fn from(event: message0::OutEvent) -> Self {
match event {
message0::OutEvent::Msg(msg) => OutEvent::Message0(msg),
}
}
}
impl From<message1::OutEvent> for OutEvent {
fn from(event: message1::OutEvent) -> Self {
match event {
message1::OutEvent::Msg { msg, channel } => OutEvent::Message1 { msg, channel },
}
}
}
impl From<message2::OutEvent> for OutEvent {
fn from(event: message2::OutEvent) -> Self {
match event {
message2::OutEvent::Msg { msg, channel } => OutEvent::Message2 { msg, channel },
}
}
}
impl From<message3::OutEvent> for OutEvent {
fn from(event: message3::OutEvent) -> Self {
match event {
message3::OutEvent::Msg(msg) => OutEvent::Message3(msg),
}
}
}
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Alice {
pt: PeerTracker,
amounts: Amounts,
message0: Message0,
message1: Message1,
message2: Message2,
message3: Message3,
#[behaviour(ignore)]
identity: Keypair,
}
impl Alice {
pub fn identity(&self) -> Keypair {
self.identity.clone()
}
pub fn peer_id(&self) -> PeerId {
PeerId::from(self.identity.public())
}
/// Alice always sends her messages as a response to a request from Bob.
pub fn send_amounts(&mut self, channel: ResponseChannel<AliceToBob>, amounts: SwapAmounts) {
let msg = AliceToBob::Amounts(amounts);
self.amounts.send(channel, msg);
info!("Sent amounts response");
}
/// Message0 gets sent within the network layer using this state0.
pub fn set_state0(&mut self, state: State0) {
debug!("Set state 0");
let _ = self.message0.set_state(state);
}
/// Send Message1 to Bob in response to receiving his Message1.
pub fn send_message1(
&mut self,
channel: ResponseChannel<AliceToBob>,
msg: xmr_btc::alice::Message1,
) {
self.message1.send(channel, msg);
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 Alice {
fn default() -> Self {
let identity = Keypair::generate_ed25519();
Self {
pt: PeerTracker::default(),
amounts: Amounts::default(),
message0: Message0::default(),
message1: Message1::default(),
message2: Message2::default(),
message3: Message3::default(),
identity,
}
}
}
fn calculate_amounts(btc: ::bitcoin::Amount) -> SwapAmounts {
// TODO: Get this from an exchange.
// This value corresponds to 100 XMR per BTC
const PICONERO_PER_SAT: u64 = 1_000_000;
let picos = btc.as_sat() * PICONERO_PER_SAT;
let xmr = monero::Amount::from_piconero(picos);
SwapAmounts { btc, xmr }
}
#[cfg(test)]
mod tests {
use super::*;
const ONE_BTC: u64 = 100_000_000;
const HUNDRED_XMR: u64 = 100_000_000_000_000;
#[test]
fn one_bitcoin_equals_a_hundred_moneroj() {
let btc = ::bitcoin::Amount::from_sat(ONE_BTC);
let want = monero::Amount::from_piconero(HUNDRED_XMR);
let SwapAmounts { xmr: got, .. } = calculate_amounts(btc);
assert_eq!(got, want);
}
}

View File

@ -0,0 +1,30 @@
use anyhow::Result;
use structopt::StructOpt;
use swap::{
bob_simple::{simple_swap, BobState},
cli::Options,
storage::Database,
};
#[tokio::main]
async fn main() -> Result<()> {
let opt = Options::from_args();
let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap();
let swarm = unimplemented!();
let bitcoin_wallet = unimplemented!();
let monero_wallet = unimplemented!();
let mut rng = unimplemented!();
let bob_state = unimplemented!();
match opt {
Options::Alice { .. } => {
simple_swap(bob_state, swarm, db, bitcoin_wallet, monero_wallet, rng).await?;
}
Options::Recover { .. } => {
let _stored_state: BobState = unimplemented!("io.get_state(uuid)?");
// abort(_stored_state, _io);
}
_ => {}
};
}

View File

@ -15,7 +15,6 @@
use anyhow::Result;
use futures::{channel::mpsc, StreamExt};
use libp2p::Multiaddr;
use log::LevelFilter;
use prettytable::{row, Table};
use std::{io, io::Write, process, sync::Arc};
use structopt::StructOpt;
@ -23,9 +22,11 @@ use swap::{
alice::{self, Alice},
bitcoin,
bob::{self, Bob},
cli::Options,
monero,
network::transport::{build, build_tor, SwapTransport},
recover::recover,
storage::Database,
Cmd, Rsp, SwapAmounts,
};
use tracing::info;
@ -33,20 +34,12 @@ use tracing::info;
#[macro_use]
extern crate prettytable;
mod cli;
mod trace;
use cli::Options;
use swap::storage::Database;
// TODO: Add root seed file instead of generating new seed each run.
#[tokio::main]
async fn main() -> Result<()> {
let opt = Options::from_args();
trace::init_tracing(LevelFilter::Debug)?;
// This currently creates the directory if it's not there in the first place
let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap();

View File

@ -110,6 +110,13 @@ impl WatchForRawTransaction for Wallet {
}
}
#[async_trait]
impl GetRawTransaction for Wallet {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
Ok(self.0.get_raw_transaction(txid).await?)
}
}
#[async_trait]
impl BlockHeight for Wallet {
async fn block_height(&self) -> u32 {

View File

@ -1,5 +1,18 @@
//! Run an XMR/BTC swap in the role of Bob.
//! Bob holds BTC and wishes receive XMR.
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
bitcoin::{self, TX_LOCK_MINE_TIMEOUT},
monero,
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
TokioExecutor,
},
state,
storage::Database,
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use anyhow::Result;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
@ -14,26 +27,6 @@ use std::{process, sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use uuid::Uuid;
mod amounts;
mod message0;
mod message1;
mod message2;
mod message3;
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
use crate::{
bitcoin::{self, TX_LOCK_MINE_TIMEOUT},
monero,
network::{
peer_tracker::{self, PeerTracker},
transport::SwapTransport,
TokioExecutor,
},
state,
storage::Database,
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use xmr_btc::{
alice,
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
@ -41,6 +34,12 @@ use xmr_btc::{
monero::CreateWalletForOutput,
};
mod amounts;
mod message0;
mod message1;
mod message2;
mod message3;
#[allow(clippy::too_many_arguments)]
pub async fn swap(
bitcoin_wallet: Arc<bitcoin::Wallet>,
@ -98,6 +97,9 @@ pub async fn swap(
swarm.request_amounts(alice.clone(), btc);
// What is going on here, shouldn't this be a simple req/resp??
// Why do we need mspc channels?
// Todo: simplify this code
let (btc, xmr) = match swarm.next().await {
OutEvent::Amounts(amounts) => {
info!("Got amounts from Alice: {:?}", amounts);
@ -108,7 +110,6 @@ pub async fn swap(
info!("User rejected amounts proposed by Alice, aborting...");
process::exit(0);
}
info!("User accepted amounts proposed by Alice");
(amounts.btc, amounts.xmr)
}

304
swap/src/bob_simple.rs Normal file
View File

@ -0,0 +1,304 @@
use crate::{
bitcoin::{self},
bob::{OutEvent, Swarm},
network::{transport::SwapTransport, TokioExecutor},
state,
storage::Database,
Cmd, Rsp, SwapAmounts, PUNISH_TIMELOCK, REFUND_TIMELOCK,
};
use anyhow::Result;
use async_recursion::async_recursion;
use async_trait::async_trait;
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
use futures::{
channel::mpsc::{Receiver, Sender},
future::Either,
FutureExt, StreamExt,
};
use genawaiter::GeneratorState;
use libp2p::{core::identity::Keypair, Multiaddr, NetworkBehaviour, PeerId};
use rand::{rngs::OsRng, CryptoRng, RngCore};
use std::{process, sync::Arc, time::Duration};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use uuid::Uuid;
use xmr_btc::{
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.
// This allows for a seamless transition from a failed swap to recovery.
pub enum BobState {
Started(Sender<Cmd>, Receiver<Rsp>, u64, PeerId),
Negotiated(bob::State2, PeerId),
BtcLocked(bob::State3, PeerId),
XmrLocked(bob::State4, PeerId),
EncSigSent(bob::State4, PeerId),
BtcRedeemed(bob::State5),
Cancelled(bob::State4),
BtcRefunded,
XmrRedeemed,
Punished,
SafelyAborted,
}
// State machine driver for swap execution
#[async_recursion]
pub async fn simple_swap(
state: BobState,
mut swarm: Swarm,
db: Database,
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
monero_wallet: Arc<crate::monero::Wallet>,
mut rng: OsRng,
) -> Result<BobState> {
match state {
BobState::Started(mut cmd_tx, mut rsp_rx, btc, alice_peer_id) => {
// todo: dial the swarm outside
// libp2p::Swarm::dial_addr(&mut swarm, addr)?;
let alice = match swarm.next().await {
OutEvent::ConnectionEstablished(alice) => alice,
other => panic!("unexpected event: {:?}", other),
};
info!("Connection established with: {}", alice);
swarm.request_amounts(alice.clone(), btc);
// todo: remove mspc channel
let (btc, xmr) = match swarm.next().await {
OutEvent::Amounts(amounts) => {
info!("Got amounts from Alice: {:?}", amounts);
let cmd = Cmd::VerifyAmounts(amounts);
cmd_tx.try_send(cmd)?;
let response = rsp_rx.next().await;
if response == Some(Rsp::Abort) {
info!("User rejected amounts proposed by Alice, aborting...");
process::exit(0);
}
info!("User accepted amounts proposed by Alice");
(amounts.btc, amounts.xmr)
}
other => panic!("unexpected event: {:?}", other),
};
let refund_address = bitcoin_wallet.new_address().await?;
let state0 = State0::new(
&mut rng,
btc,
xmr,
REFUND_TIMELOCK,
PUNISH_TIMELOCK,
refund_address,
);
info!("Commencing handshake");
swarm.send_message0(alice.clone(), state0.next_message(&mut rng));
let state1 = match swarm.next().await {
OutEvent::Message0(msg) => state0.receive(bitcoin_wallet.as_ref(), msg).await?,
other => panic!("unexpected event: {:?}", other),
};
swarm.send_message1(alice.clone(), state1.next_message());
let state2 = match swarm.next().await {
OutEvent::Message1(msg) => state1.receive(msg)?,
other => panic!("unexpected event: {:?}", other),
};
let swap_id = Uuid::new_v4();
db.insert_latest_state(swap_id, state::Bob::Handshaken(state2.clone()).into())
.await?;
swarm.send_message2(alice.clone(), state2.next_message());
info!("Handshake complete");
simple_swap(
BobState::Negotiated(state2, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
BobState::Negotiated(state2, alice_peer_id) => {
// Alice and Bob have exchanged info
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?;
simple_swap(
BobState::BtcLocked(state3, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
// Bob has locked Btc
// Watch for Alice to Lock Xmr or for t1 to elapse
BobState::BtcLocked(state3, alice_peer_id) => {
// todo: watch until t1, not indefinetely
let state4 = match swarm.next().await {
OutEvent::Message2(msg) => {
state3
.watch_for_lock_xmr(monero_wallet.as_ref(), msg)
.await?
}
other => panic!("unexpected event: {:?}", other),
};
simple_swap(
BobState::XmrLocked(state4, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
BobState::XmrLocked(state, alice_peer_id) => {
// Alice has locked Xmr
// Bob sends Alice his key
// let cloned = state.clone();
let tx_redeem_encsig = state.tx_redeem_encsig();
// Do we have to wait for a response?
// What if Alice fails to receive this? Should we always resend?
// todo: If we cannot dial Alice we should go to EncSigSent. Maybe dialing
// should happen in this arm?
swarm.send_message3(alice_peer_id.clone(), tx_redeem_encsig);
simple_swap(
BobState::EncSigSent(state, alice_peer_id),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
BobState::EncSigSent(state, ..) => {
// Watch for redeem
let redeem_watcher = state.watch_for_redeem_btc(bitcoin_wallet.as_ref());
let t1_timeout = state.wait_for_t1(bitcoin_wallet.as_ref());
tokio::select! {
val = redeem_watcher => {
simple_swap(
BobState::BtcRedeemed(val?),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
val = t1_timeout => {
// Check whether TxCancel has been published.
// 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 {
state.submit_tx_cancel(bitcoin_wallet.as_ref()).await;
}
simple_swap(
BobState::Cancelled(state),
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
}
}
BobState::BtcRedeemed(state) => {
// Bob redeems XMR using revealed s_a
state.claim_xmr(monero_wallet.as_ref()).await?;
simple_swap(
BobState::XmrRedeemed,
swarm,
db,
bitcoin_wallet,
monero_wallet,
rng,
)
.await
}
BobState::Cancelled(_state) => Ok(BobState::BtcRefunded),
BobState::BtcRefunded => Ok(BobState::BtcRefunded),
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),
// }
// }

View File

@ -1,9 +1,10 @@
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
pub mod alice;
pub mod bitcoin;
pub mod bob;
pub mod bob_simple;
pub mod cli;
pub mod monero;
pub mod network;
pub mod recover;

View File

@ -186,6 +186,11 @@ pub trait WatchForRawTransaction {
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
}
#[async_trait]
pub trait GetRawTransaction {
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
}
#[async_trait]
pub trait BlockHeight {
async fn block_height(&self) -> u32;

View File

@ -32,7 +32,11 @@ use tokio::{sync::Mutex, time::timeout};
use tracing::error;
pub mod message;
use crate::monero::{CreateWalletForOutput, WatchForTransfer};
use crate::{
bitcoin::{BlockHeight, GetRawTransaction, TransactionBlockHeight},
monero::{CreateWalletForOutput, WatchForTransfer},
};
use ::bitcoin::{Transaction, Txid};
pub use message::{Message, Message0, Message1, Message2, Message3};
#[allow(clippy::large_enum_variant)]
@ -679,23 +683,23 @@ impl State3 {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct State4 {
A: bitcoin::PublicKey,
b: bitcoin::SecretKey,
pub A: bitcoin::PublicKey,
pub b: bitcoin::SecretKey,
s_b: cross_curve_dleq::Scalar,
S_a_monero: monero::PublicKey,
S_a_bitcoin: bitcoin::PublicKey,
pub S_a_bitcoin: bitcoin::PublicKey,
v: monero::PrivateViewKey,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc: bitcoin::Amount,
xmr: monero::Amount,
refund_timelock: u32,
pub refund_timelock: u32,
punish_timelock: u32,
refund_address: bitcoin::Address,
redeem_address: bitcoin::Address,
pub redeem_address: bitcoin::Address,
punish_address: bitcoin::Address,
tx_lock: bitcoin::TxLock,
pub tx_lock: bitcoin::TxLock,
tx_cancel_sig_a: Signature,
tx_refund_encsig: EncryptedSignature,
}
@ -708,7 +712,77 @@ impl State4 {
Message3 { tx_redeem_encsig }
}
pub async fn watch_for_redeem_btc<W>(self, bitcoin_wallet: &W) -> Result<State5>
pub fn tx_redeem_encsig(&self) -> EncryptedSignature {
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
self.b.encsign(self.S_a_bitcoin.clone(), tx_redeem.digest())
}
pub async fn check_for_tx_cancel<W>(&self, bitcoin_wallet: &W) -> Result<Transaction>
where
W: GetRawTransaction,
{
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
// todo: check if this is correct
let sig_a = self.tx_cancel_sig_a.clone();
let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&self.tx_lock,
(self.A.clone(), sig_a),
(self.b.public(), sig_b),
)
.expect(
"sig_{a,b} to be valid signatures for
tx_cancel",
);
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
Ok(tx)
}
pub async fn submit_tx_cancel<W>(&self, bitcoin_wallet: &W) -> Result<Txid>
where
W: BroadcastSignedTransaction,
{
let tx_cancel = bitcoin::TxCancel::new(
&self.tx_lock,
self.refund_timelock,
self.A.clone(),
self.b.public(),
);
// todo: check if this is correct
let sig_a = self.tx_cancel_sig_a.clone();
let sig_b = self.b.sign(tx_cancel.digest());
let tx_cancel = tx_cancel
.clone()
.add_signatures(
&self.tx_lock,
(self.A.clone(), sig_a),
(self.b.public(), sig_b),
)
.expect(
"sig_{a,b} to be valid signatures for
tx_cancel",
);
let tx_id = bitcoin_wallet
.broadcast_signed_transaction(tx_cancel)
.await?;
Ok(tx_id)
}
pub async fn watch_for_redeem_btc<W>(&self, bitcoin_wallet: &W) -> Result<State5>
where
W: WatchForRawTransaction,
{
@ -725,25 +799,38 @@ impl State4 {
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
Ok(State5 {
A: self.A,
b: self.b,
A: self.A.clone(),
b: self.b.clone(),
s_a,
s_b: self.s_b,
S_a_monero: self.S_a_monero,
S_a_bitcoin: self.S_a_bitcoin,
S_a_bitcoin: self.S_a_bitcoin.clone(),
v: self.v,
btc: self.btc,
xmr: self.xmr,
refund_timelock: self.refund_timelock,
punish_timelock: self.punish_timelock,
refund_address: self.refund_address,
redeem_address: self.redeem_address,
punish_address: self.punish_address,
tx_lock: self.tx_lock,
tx_refund_encsig: self.tx_refund_encsig,
tx_cancel_sig: self.tx_cancel_sig_a,
refund_address: self.refund_address.clone(),
redeem_address: self.redeem_address.clone(),
punish_address: self.punish_address.clone(),
tx_lock: self.tx_lock.clone(),
tx_refund_encsig: self.tx_refund_encsig.clone(),
tx_cancel_sig: self.tx_cancel_sig_a.clone(),
})
}
pub async fn wait_for_t1<W>(&self, bitcoin_wallet: &W) -> Result<()>
where
W: WatchForRawTransaction + TransactionBlockHeight + BlockHeight,
{
let tx_id = self.tx_lock.txid().clone();
let tx_lock_height = bitcoin_wallet.transaction_block_height(tx_id).await;
let t1_timeout =
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + self.refund_timelock);
t1_timeout.await;
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -792,3 +879,63 @@ impl State5 {
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(),
})
}