376: ASB resumes unfinished swaps after startup r=da-kami a=da-kami

Fixes #374 

- [x] Save Bob peer-id in database for Alice
- [x]  Alice: Wait for `10` Monero confirmations in `BtcRefunded` instead of `XmrLocked` (requires extending the RPC to distinguish locked / unlocked balance)
- [x] Save Alice peer-id in database for Bob ~~(+ multiaddress and remove params from resume)~~
- [ ] ~~Refactor Bob in test setup (handle event-loop in test setup similar to Alice)~~

I decided against refactoring Bob in the test setup, because eventually we might still want to add concurrent swap tests with multiple Bobs. The refactoring I had in mind would not allow such kind of tests. 
Generally, the current state of the changes already contains enough added value to open the PR :)

Follow ups out of scope

- [ ] Parametrize database with role (Alice / Bob) and remove all the (currently useless) mapping between DB and protocol types.
- [ ]  Alice: Wait for transfer proof ack before transitioning to new `XmrLocked`

Co-authored-by: Daniel Karzel <daniel@comit.network>
This commit is contained in:
bors[bot] 2021-04-01 06:49:56 +00:00 committed by GitHub
commit d144405182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 769 additions and 185 deletions

View File

@ -110,10 +110,13 @@ jobs:
happy_path, happy_path,
happy_path_restart_bob_after_xmr_locked, happy_path_restart_bob_after_xmr_locked,
happy_path_restart_bob_before_xmr_locked, happy_path_restart_bob_before_xmr_locked,
happy_path_restart_alice_after_xmr_locked,
bob_refunds_using_cancel_and_refund_command, bob_refunds_using_cancel_and_refund_command,
bob_refunds_using_cancel_and_refund_command_timelock_not_expired, bob_refunds_using_cancel_and_refund_command_timelock_not_expired,
bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force, bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force,
punish punish,
alice_punishes_after_restart_punish_timelock_expired,
alice_refunds_after_restart_bob_refunded
] ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- The `resume` command of the `swap` CLI no longer require the `--seller-peer-id` parameter.
This information is now saved in the database.
### Added ### Added
- A changelog file. - A changelog file.
- Automatic resume of unfinished swaps for the `asb` upon startup.
Unfinished swaps from earlier versions will be skipped.
### Fixed ### Fixed

14
Cargo.lock generated
View File

@ -1552,6 +1552,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.7" version = "0.4.7"
@ -2547,7 +2556,7 @@ checksum = "32d3ebd75ac2679c2af3a92246639f9fcc8a442ee420719cc4fe195b98dd5fa3"
dependencies = [ dependencies = [
"bytes 1.0.1", "bytes 1.0.1",
"heck", "heck",
"itertools", "itertools 0.9.0",
"log 0.4.14", "log 0.4.14",
"multimap", "multimap",
"petgraph", "petgraph",
@ -2564,7 +2573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4" checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools", "itertools 0.9.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -3522,6 +3531,7 @@ dependencies = [
"futures", "futures",
"get-port", "get-port",
"hyper 0.14.5", "hyper 0.14.5",
"itertools 0.10.0",
"libp2p", "libp2p",
"libp2p-async-await", "libp2p-async-await",
"miniscript", "miniscript",

View File

@ -8,9 +8,12 @@ status = [
"test (macos-latest)", "test (macos-latest)",
"docker_tests (happy_path)", "docker_tests (happy_path)",
"docker_tests (happy_path_restart_bob_after_xmr_locked)", "docker_tests (happy_path_restart_bob_after_xmr_locked)",
"docker_tests (happy_path_restart_alice_after_xmr_locked)",
"docker_tests (happy_path_restart_bob_before_xmr_locked)", "docker_tests (happy_path_restart_bob_before_xmr_locked)",
"docker_tests (bob_refunds_using_cancel_and_refund_command)", "docker_tests (bob_refunds_using_cancel_and_refund_command)",
"docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force)", "docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired_force)",
"docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired)", "docker_tests (bob_refunds_using_cancel_and_refund_command_timelock_not_expired)",
"docker_tests (punish)" "docker_tests (punish)",
"docker_tests (alice_punishes_after_restart_punish_timelock_expired)",
"docker_tests (alice_refunds_after_restart_bob_refunded)"
] ]

View File

@ -25,6 +25,7 @@ dialoguer = "0.8"
directories-next = "2" directories-next = "2"
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", features = ["libsecp_compat", "serde"] } ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", features = ["libsecp_compat", "serde"] }
futures = { version = "0.3", default-features = false } futures = { version = "0.3", default-features = false }
itertools = "0.10"
libp2p = { version = "0.36", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response"] } libp2p = { version = "0.36", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response"] }
libp2p-async-await = { git = "https://github.com/comit-network/rust-libp2p-async-await" } libp2p-async-await = { git = "https://github.com/comit-network/rust-libp2p-async-await" }
miniscript = { version = "5", features = ["serde"] } miniscript = { version = "5", features = ["serde"] }

View File

@ -1,6 +1,6 @@
use crate::asb::Rate; use crate::asb::Rate;
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug)]
pub struct FixedRate(Rate); pub struct FixedRate(Rate);
impl FixedRate { impl FixedRate {

View File

@ -130,7 +130,7 @@ async fn main() -> Result<()> {
table.add_row(row!["SWAP ID", "STATE"]); table.add_row(row!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all()? { for (swap_id, state) in db.all_alice()? {
table.add_row(row![swap_id, state]); table.add_row(row![swap_id, state]);
} }

View File

@ -21,7 +21,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use structopt::StructOpt; use structopt::StructOpt;
use swap::bitcoin::{Amount, TxLock}; use swap::bitcoin::{Amount, TxLock};
use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams}; use swap::cli::command::{AliceMultiaddress, Arguments, Command, Data, MoneroParams};
use swap::database::Database; use swap::database::Database;
use swap::env::{Config, GetConfig}; use swap::env::{Config, GetConfig};
use swap::network::quote::BidQuote; use swap::network::quote::BidQuote;
@ -81,9 +81,9 @@ async fn main() -> Result<()> {
match args.cmd { match args.cmd {
Command::BuyXmr { Command::BuyXmr {
connect_params: alice_peer_id,
AliceConnectParams { alice_multi_addr:
peer_id: alice_peer_id, AliceMultiaddress {
multiaddr: alice_addr, multiaddr: alice_addr,
}, },
monero_params: monero_params:
@ -131,9 +131,12 @@ async fn main() -> Result<()> {
) )
.await?; .await?;
let swap_id = Uuid::new_v4();
db.insert_peer_id(swap_id, alice_peer_id).await?;
let swap = Builder::new( let swap = Builder::new(
db, db,
Uuid::new_v4(), swap_id,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
Arc::new(monero_wallet), Arc::new(monero_wallet),
env_config, env_config,
@ -158,7 +161,7 @@ async fn main() -> Result<()> {
table.add_row(row!["SWAP ID", "STATE"]); table.add_row(row!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all()? { for (swap_id, state) in db.all_bob()? {
table.add_row(row![swap_id, state]); table.add_row(row![swap_id, state]);
} }
@ -167,9 +170,8 @@ async fn main() -> Result<()> {
} }
Command::Resume { Command::Resume {
swap_id, swap_id,
connect_params: alice_multi_addr:
AliceConnectParams { AliceMultiaddress {
peer_id: alice_peer_id,
multiaddr: alice_addr, multiaddr: alice_addr,
}, },
monero_params: monero_params:
@ -189,6 +191,7 @@ async fn main() -> Result<()> {
init_monero_wallet(data_dir, monero_daemon_host, env_config).await?; init_monero_wallet(data_dir, monero_daemon_host, env_config).await?;
let bitcoin_wallet = Arc::new(bitcoin_wallet); let bitcoin_wallet = Arc::new(bitcoin_wallet);
let alice_peer_id = db.get_peer_id(swap_id)?;
let mut swarm = swarm::new::<Behaviour>(&seed)?; let mut swarm = swarm::new::<Behaviour>(&seed)?;
swarm.add_address(alice_peer_id, alice_addr); swarm.add_address(alice_peer_id, alice_addr);

View File

@ -37,8 +37,15 @@ pub struct Arguments {
pub enum Command { pub enum Command {
/// Start a XMR for BTC swap /// Start a XMR for BTC swap
BuyXmr { BuyXmr {
#[structopt(
long = "seller-peer-id",
default_value = DEFAULT_ALICE_PEER_ID,
help = "The peer id of a specific swap partner can be optionally provided"
)]
alice_peer_id: PeerId,
#[structopt(flatten)] #[structopt(flatten)]
connect_params: AliceConnectParams, alice_multi_addr: AliceMultiaddress,
#[structopt(long = "electrum-rpc", #[structopt(long = "electrum-rpc",
help = "Provide the Bitcoin Electrum RPC URL", help = "Provide the Bitcoin Electrum RPC URL",
@ -60,7 +67,7 @@ pub enum Command {
swap_id: Uuid, swap_id: Uuid,
#[structopt(flatten)] #[structopt(flatten)]
connect_params: AliceConnectParams, alice_multi_addr: AliceMultiaddress,
#[structopt(long = "electrum-rpc", #[structopt(long = "electrum-rpc",
help = "Provide the Bitcoin Electrum RPC URL", help = "Provide the Bitcoin Electrum RPC URL",
@ -108,14 +115,7 @@ pub enum Command {
} }
#[derive(structopt::StructOpt, Debug)] #[derive(structopt::StructOpt, Debug)]
pub struct AliceConnectParams { pub struct AliceMultiaddress {
#[structopt(
long = "seller-peer-id",
default_value = DEFAULT_ALICE_PEER_ID,
help = "The peer id of a specific swap partner can be optionally provided"
)]
pub peer_id: PeerId,
#[structopt( #[structopt(
long = "seller-addr", long = "seller-addr",
default_value = DEFAULT_ALICE_MULTIADDR, default_value = DEFAULT_ALICE_MULTIADDR,

View File

@ -2,10 +2,13 @@ pub use alice::Alice;
pub use bob::Bob; pub use bob::Bob;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use itertools::Itertools;
use libp2p::PeerId;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Display; use std::fmt::Display;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
mod alice; mod alice;
@ -38,16 +41,34 @@ impl Display for Swap {
} }
} }
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
#[error("Not in the role of Alice")]
struct NotAlice;
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
#[error("Not in the role of Bob")]
struct NotBob;
impl Swap { impl Swap {
pub fn try_into_alice(self) -> Result<Alice> {
match self {
Swap::Alice(alice) => Ok(alice),
Swap::Bob(_) => bail!(NotAlice),
}
}
pub fn try_into_bob(self) -> Result<Bob> { pub fn try_into_bob(self) -> Result<Bob> {
match self { match self {
Swap::Bob(bob) => Ok(bob), Swap::Bob(bob) => Ok(bob),
Swap::Alice(_) => bail!("Swap instance is not Bob"), Swap::Alice(_) => bail!(NotBob),
} }
} }
} }
pub struct Database(sled::Db); pub struct Database {
swaps: sled::Tree,
peers: sled::Tree,
}
impl Database { impl Database {
pub fn open(path: &Path) -> Result<Self> { pub fn open(path: &Path) -> Result<Self> {
@ -56,22 +77,51 @@ impl Database {
let db = let db =
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?; sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
Ok(Database(db)) let swaps = db.open_tree("swaps")?;
let peers = db.open_tree("peers")?;
Ok(Database { swaps, peers })
}
pub async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()> {
let peer_id_str = peer_id.to_string();
let key = serialize(&swap_id)?;
let value = serialize(&peer_id_str).context("Could not serialize peer-id")?;
self.peers.insert(key, value)?;
self.peers
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_peer_id(&self, swap_id: Uuid) -> Result<PeerId> {
let key = serialize(&swap_id)?;
let encoded = self
.peers
.get(&key)?
.ok_or_else(|| anyhow!("No peer-id found for swap id {} in database", swap_id))?;
let peer_id: String = deserialize(&encoded).context("Could not deserialize peer-id")?;
Ok(PeerId::from_str(peer_id.as_str())?)
} }
pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> { pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> {
let key = serialize(&swap_id)?; let key = serialize(&swap_id)?;
let new_value = serialize(&state).context("Could not serialize new state value")?; let new_value = serialize(&state).context("Could not serialize new state value")?;
let old_value = self.0.get(&key)?; let old_value = self.swaps.get(&key)?;
self.0 self.swaps
.compare_and_swap(key, old_value, Some(new_value)) .compare_and_swap(key, old_value, Some(new_value))
.context("Could not write in the DB")? .context("Could not write in the DB")?
.context("Stored swap somehow changed, aborting saving")?; .context("Stored swap somehow changed, aborting saving")?;
// TODO: see if this can be done through sled config self.swaps
self.0
.flush_async() .flush_async()
.await .await
.map(|_| ()) .map(|_| ())
@ -82,7 +132,7 @@ impl Database {
let key = serialize(&swap_id)?; let key = serialize(&swap_id)?;
let encoded = self let encoded = self
.0 .swaps
.get(&key)? .get(&key)?
.ok_or_else(|| anyhow!("Swap with id {} not found in database", swap_id))?; .ok_or_else(|| anyhow!("Swap with id {} not found in database", swap_id))?;
@ -90,22 +140,42 @@ impl Database {
Ok(state) Ok(state)
} }
pub fn all(&self) -> Result<Vec<(Uuid, Swap)>> { pub fn all_alice(&self) -> Result<Vec<(Uuid, Alice)>> {
self.0 self.all_alice_iter().collect()
.iter() }
.map(|item| match item {
Ok((key, value)) => {
let swap_id = deserialize::<Uuid>(&key);
let swap = deserialize::<Swap>(&value).context("Failed to deserialize swap");
match (swap_id, swap) { fn all_alice_iter(&self) -> impl Iterator<Item = Result<(Uuid, Alice)>> {
(Ok(swap_id), Ok(swap)) => Ok((swap_id, swap)), self.all_swaps_iter().map(|item| {
(Ok(_), Err(err)) => Err(err), let (swap_id, swap) = item?;
_ => bail!("Failed to deserialize swap"), Ok((swap_id, swap.try_into_alice()?))
}
}
Err(err) => Err(err).context("Failed to retrieve swap from DB"),
}) })
}
pub fn all_bob(&self) -> Result<Vec<(Uuid, Bob)>> {
self.all_bob_iter().collect()
}
fn all_bob_iter(&self) -> impl Iterator<Item = Result<(Uuid, Bob)>> {
self.all_swaps_iter().map(|item| {
let (swap_id, swap) = item?;
Ok((swap_id, swap.try_into_bob()?))
})
}
fn all_swaps_iter(&self) -> impl Iterator<Item = Result<(Uuid, Swap)>> {
self.swaps.iter().map(|item| {
let (key, value) = item.context("Failed to retrieve swap from DB")?;
let swap_id = deserialize::<Uuid>(&key)?;
let swap = deserialize::<Swap>(&value).context("Failed to deserialize swap")?;
Ok((swap_id, swap))
})
}
pub fn unfinished_alice(&self) -> Result<Vec<(Uuid, Alice)>> {
self.all_alice_iter()
.filter_ok(|(_swap_id, alice)| !matches!(alice, Alice::Done(_)))
.collect() .collect()
} }
} }
@ -187,26 +257,106 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn can_fetch_all_keys() { async fn all_swaps_as_alice() {
let db_dir = tempfile::tempdir().unwrap(); let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap(); let db = Database::open(db_dir.path()).unwrap();
let state_1 = Swap::Alice(Alice::Done(AliceEndState::BtcPunished)); let alice_state = Alice::Done(AliceEndState::BtcPunished);
let swap_id_1 = Uuid::new_v4(); let alice_swap = Swap::Alice(alice_state.clone());
db.insert_latest_state(swap_id_1, state_1.clone()) let alice_swap_id = Uuid::new_v4();
db.insert_latest_state(alice_swap_id, alice_swap)
.await .await
.expect("Failed to save second state"); .expect("Failed to save alice state 1");
let state_2 = Swap::Bob(Bob::Done(BobEndState::SafelyAborted)); let alice_swaps = db.all_alice().unwrap();
let swap_id_2 = Uuid::new_v4(); assert_eq!(alice_swaps.len(), 1);
db.insert_latest_state(swap_id_2, state_2.clone()) assert!(alice_swaps.contains(&(alice_swap_id, alice_state)));
let bob_state = Bob::Done(BobEndState::SafelyAborted);
let bob_swap = Swap::Bob(bob_state);
let bob_swap_id = Uuid::new_v4();
db.insert_latest_state(bob_swap_id, bob_swap)
.await .await
.expect("Failed to save first state"); .expect("Failed to save bob state 1");
let swaps = db.all().unwrap(); let err = db.all_alice().unwrap_err();
assert_eq!(swaps.len(), 2); assert_eq!(err.downcast_ref::<NotAlice>().unwrap(), &NotAlice);
assert!(swaps.contains(&(swap_id_1, state_1))); }
assert!(swaps.contains(&(swap_id_2, state_2)));
#[tokio::test]
async fn all_swaps_as_bob() {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let bob_state = Bob::Done(BobEndState::SafelyAborted);
let bob_swap = Swap::Bob(bob_state.clone());
let bob_swap_id = Uuid::new_v4();
db.insert_latest_state(bob_swap_id, bob_swap)
.await
.expect("Failed to save bob state 1");
let bob_swaps = db.all_bob().unwrap();
assert_eq!(bob_swaps.len(), 1);
assert!(bob_swaps.contains(&(bob_swap_id, bob_state)));
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state);
let alice_swap_id = Uuid::new_v4();
db.insert_latest_state(alice_swap_id, alice_swap)
.await
.expect("Failed to save alice state 1");
let err = db.all_bob().unwrap_err();
assert_eq!(err.downcast_ref::<NotBob>().unwrap(), &NotBob);
}
#[tokio::test]
async fn can_save_swap_state_and_peer_id_with_same_swap_id() -> Result<()> {
let db_dir = tempfile::tempdir().unwrap();
let db = Database::open(db_dir.path()).unwrap();
let alice_id = Uuid::new_v4();
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state);
let peer_id = PeerId::random();
db.insert_latest_state(alice_id, alice_swap.clone()).await?;
db.insert_peer_id(alice_id, peer_id).await?;
let loaded_swap = db.get_state(alice_id)?;
let loaded_peer_id = db.get_peer_id(alice_id)?;
assert_eq!(alice_swap, loaded_swap);
assert_eq!(peer_id, loaded_peer_id);
Ok(())
}
#[tokio::test]
async fn test_reopen_db() -> Result<()> {
let db_dir = tempfile::tempdir().unwrap();
let alice_id = Uuid::new_v4();
let alice_state = Alice::Done(AliceEndState::BtcPunished);
let alice_swap = Swap::Alice(alice_state);
let peer_id = PeerId::random();
{
let db = Database::open(db_dir.path()).unwrap();
db.insert_latest_state(alice_id, alice_swap.clone()).await?;
db.insert_peer_id(alice_id, peer_id).await?;
}
let db = Database::open(db_dir.path()).unwrap();
let loaded_swap = db.get_state(alice_id)?;
let loaded_peer_id = db.get_peer_id(alice_id)?;
assert_eq!(alice_swap, loaded_swap);
assert_eq!(peer_id, loaded_peer_id);
Ok(())
} }
} }

View File

@ -1,6 +1,6 @@
use crate::bitcoin::EncryptedSignature; use crate::bitcoin::EncryptedSignature;
use crate::monero; use crate::monero;
use crate::monero::monero_private_key; use crate::monero::{monero_private_key, TransferProof};
use crate::protocol::alice; use crate::protocol::alice;
use crate::protocol::alice::AliceState; use crate::protocol::alice::AliceState;
use ::bitcoin::hashes::core::fmt::Display; use ::bitcoin::hashes::core::fmt::Display;
@ -18,29 +18,45 @@ pub enum Alice {
BtcLocked { BtcLocked {
state3: alice::State3, state3: alice::State3,
}, },
XmrLockTransactionSent {
monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3,
},
XmrLocked { XmrLocked {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3,
},
XmrLockTransferProofSent {
monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3, state3: alice::State3,
}, },
EncSigLearned { EncSigLearned {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
encrypted_signature: EncryptedSignature, encrypted_signature: EncryptedSignature,
state3: alice::State3, state3: alice::State3,
}, },
CancelTimelockExpired { CancelTimelockExpired {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3, state3: alice::State3,
}, },
BtcCancelled { BtcCancelled {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3, state3: alice::State3,
}, },
BtcPunishable { BtcPunishable {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3, state3: alice::State3,
}, },
BtcRefunded { BtcRefunded {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: alice::State3, state3: alice::State3,
#[serde(with = "monero_private_key")] #[serde(with = "monero_private_key")]
spend_key: monero::PrivateKey, spend_key: monero::PrivateKey,
@ -65,54 +81,82 @@ impl From<&AliceState> for Alice {
AliceState::BtcLocked { state3 } => Alice::BtcLocked { AliceState::BtcLocked { state3 } => Alice::BtcLocked {
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
}, },
AliceState::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => Alice::XmrLockTransactionSent {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(),
},
AliceState::XmrLocked { AliceState::XmrLocked {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
} => Alice::XmrLocked { } => Alice::XmrLocked {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(),
},
AliceState::XmrLockTransferProofSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => Alice::XmrLockTransferProofSent {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
}, },
AliceState::EncSigLearned { AliceState::EncSigLearned {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
encrypted_signature, encrypted_signature,
} => Alice::EncSigLearned { } => Alice::EncSigLearned {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
encrypted_signature: *encrypted_signature.clone(), encrypted_signature: *encrypted_signature.clone(),
}, },
AliceState::BtcRedeemed => Alice::Done(AliceEndState::BtcRedeemed), AliceState::BtcRedeemed => Alice::Done(AliceEndState::BtcRedeemed),
AliceState::BtcCancelled { AliceState::BtcCancelled {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
..
} => Alice::BtcCancelled { } => Alice::BtcCancelled {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
}, },
AliceState::BtcRefunded { AliceState::BtcRefunded {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
spend_key, spend_key,
state3, state3,
} => Alice::BtcRefunded { } => Alice::BtcRefunded {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
spend_key: *spend_key, spend_key: *spend_key,
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
}, },
AliceState::BtcPunishable { AliceState::BtcPunishable {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
..
} => Alice::BtcPunishable { } => Alice::BtcPunishable {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
}, },
AliceState::XmrRefunded => Alice::Done(AliceEndState::XmrRefunded), AliceState::XmrRefunded => Alice::Done(AliceEndState::XmrRefunded),
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
} => Alice::CancelTimelockExpired { } => Alice::CancelTimelockExpired {
monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight, monero_wallet_restore_blockheight: *monero_wallet_restore_blockheight,
transfer_proof: transfer_proof.clone(),
state3: state3.as_ref().clone(), state3: state3.as_ref().clone(),
}, },
AliceState::BtcPunished => Alice::Done(AliceEndState::BtcPunished), AliceState::BtcPunished => Alice::Done(AliceEndState::BtcPunished),
@ -130,50 +174,80 @@ impl From<Alice> for AliceState {
Alice::BtcLocked { state3 } => AliceState::BtcLocked { Alice::BtcLocked { state3 } => AliceState::BtcLocked {
state3: Box::new(state3), state3: Box::new(state3),
}, },
Alice::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => AliceState::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state3),
},
Alice::XmrLocked { Alice::XmrLocked {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
} => AliceState::XmrLocked { } => AliceState::XmrLocked {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state3),
},
Alice::XmrLockTransferProofSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => AliceState::XmrLockTransferProofSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state3), state3: Box::new(state3),
}, },
Alice::EncSigLearned { Alice::EncSigLearned {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3: state, state3: state,
encrypted_signature, encrypted_signature,
} => AliceState::EncSigLearned { } => AliceState::EncSigLearned {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state), state3: Box::new(state),
encrypted_signature: Box::new(encrypted_signature), encrypted_signature: Box::new(encrypted_signature),
}, },
Alice::CancelTimelockExpired { Alice::CancelTimelockExpired {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
} => AliceState::CancelTimelockExpired { } => AliceState::CancelTimelockExpired {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state3), state3: Box::new(state3),
}, },
Alice::BtcCancelled { Alice::BtcCancelled {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
} => AliceState::BtcCancelled { } => AliceState::BtcCancelled {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state3), state3: Box::new(state3),
}, },
Alice::BtcPunishable { Alice::BtcPunishable {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3, state3,
} => AliceState::BtcPunishable { } => AliceState::BtcPunishable {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3: Box::new(state3), state3: Box::new(state3),
}, },
Alice::BtcRefunded { Alice::BtcRefunded {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
state3, transfer_proof,
spend_key, spend_key,
state3,
} => AliceState::BtcRefunded { } => AliceState::BtcRefunded {
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
spend_key, spend_key,
state3: Box::new(state3), state3: Box::new(state3),
}, },
@ -192,7 +266,11 @@ impl Display for Alice {
match self { match self {
Alice::Started { .. } => write!(f, "Started"), Alice::Started { .. } => write!(f, "Started"),
Alice::BtcLocked { .. } => f.write_str("Bitcoin locked"), Alice::BtcLocked { .. } => f.write_str("Bitcoin locked"),
Alice::XmrLockTransactionSent { .. } => f.write_str("Monero lock transaction sent"),
Alice::XmrLocked { .. } => f.write_str("Monero locked"), Alice::XmrLocked { .. } => f.write_str("Monero locked"),
Alice::XmrLockTransferProofSent { .. } => {
f.write_str("Monero lock transfer proof sent")
}
Alice::CancelTimelockExpired { .. } => f.write_str("Cancel timelock is expired"), Alice::CancelTimelockExpired { .. } => f.write_str("Cancel timelock is expired"),
Alice::BtcCancelled { .. } => f.write_str("Bitcoin cancel transaction published"), Alice::BtcCancelled { .. } => f.write_str("Bitcoin cancel transaction published"),
Alice::BtcPunishable { .. } => f.write_str("Bitcoin punishable"), Alice::BtcPunishable { .. } => f.write_str("Bitcoin punishable"),

View File

@ -84,6 +84,43 @@ where
// terminate forever. // terminate forever.
self.send_transfer_proof.push(future::pending().boxed()); self.send_transfer_proof.push(future::pending().boxed());
let unfinished_swaps = match self.db.unfinished_alice() {
Ok(unfinished_swaps) => unfinished_swaps,
Err(_) => {
tracing::error!("Failed to load unfinished swaps");
return;
}
};
for (swap_id, state) in unfinished_swaps {
let peer_id = match self.db.get_peer_id(swap_id) {
Ok(peer_id) => peer_id,
Err(_) => {
tracing::warn!(%swap_id, "Resuming swap skipped because no peer-id found for swap in database");
continue;
}
};
let handle = self.new_handle(peer_id);
let swap = Swap {
event_loop_handle: handle,
bitcoin_wallet: self.bitcoin_wallet.clone(),
monero_wallet: self.monero_wallet.clone(),
env_config: self.env_config,
db: self.db.clone(),
state: state.into(),
swap_id,
};
match self.swap_sender.send(swap).await {
Ok(_) => tracing::info!(%swap_id, "Resuming swap"),
Err(_) => {
tracing::warn!(%swap_id, "Failed to resume swap because receiver has been dropped")
}
}
}
loop { loop {
tokio::select! { tokio::select! {
swarm_event = self.swarm.next_event() => { swarm_event = self.swarm.next_event() => {
@ -264,10 +301,20 @@ where
swap_id, swap_id,
}; };
// TODO: Consider adding separate components for start/rsume of swaps
// swaps save peer id so we can resume
match self.db.insert_peer_id(swap_id, bob_peer_id).await {
Ok(_) => {
if let Err(error) = self.swap_sender.send(swap).await { if let Err(error) = self.swap_sender.send(swap).await {
tracing::warn!(%swap_id, "Swap cannot be spawned: {}", error); tracing::warn!(%swap_id, "Swap cannot be spawned: {}", error);
} }
} }
Err(error) => {
tracing::warn!(%swap_id, "Unable to save peer-id, swap cannot be spawned: {}", error);
}
}
}
/// Create a new [`EventLoopHandle`] that is scoped for communication with /// Create a new [`EventLoopHandle`] that is scoped for communication with
/// the given peer. /// the given peer.

View File

@ -22,32 +22,48 @@ pub enum AliceState {
BtcLocked { BtcLocked {
state3: Box<State3>, state3: Box<State3>,
}, },
XmrLockTransactionSent {
monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: Box<State3>,
},
XmrLocked { XmrLocked {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: Box<State3>,
},
XmrLockTransferProofSent {
monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: Box<State3>, state3: Box<State3>,
}, },
EncSigLearned { EncSigLearned {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
encrypted_signature: Box<bitcoin::EncryptedSignature>, encrypted_signature: Box<bitcoin::EncryptedSignature>,
state3: Box<State3>, state3: Box<State3>,
}, },
BtcRedeemed, BtcRedeemed,
BtcCancelled { BtcCancelled {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: Box<State3>, state3: Box<State3>,
}, },
BtcRefunded { BtcRefunded {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
spend_key: monero::PrivateKey, spend_key: monero::PrivateKey,
state3: Box<State3>, state3: Box<State3>,
}, },
BtcPunishable { BtcPunishable {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: Box<State3>, state3: Box<State3>,
}, },
XmrRefunded, XmrRefunded,
CancelTimelockExpired { CancelTimelockExpired {
monero_wallet_restore_blockheight: BlockHeight, monero_wallet_restore_blockheight: BlockHeight,
transfer_proof: TransferProof,
state3: Box<State3>, state3: Box<State3>,
}, },
BtcPunished, BtcPunished,
@ -59,7 +75,11 @@ impl fmt::Display for AliceState {
match self { match self {
AliceState::Started { .. } => write!(f, "started"), AliceState::Started { .. } => write!(f, "started"),
AliceState::BtcLocked { .. } => write!(f, "btc is locked"), AliceState::BtcLocked { .. } => write!(f, "btc is locked"),
AliceState::XmrLockTransactionSent { .. } => write!(f, "xmr lock transaction sent"),
AliceState::XmrLocked { .. } => write!(f, "xmr is locked"), AliceState::XmrLocked { .. } => write!(f, "xmr is locked"),
AliceState::XmrLockTransferProofSent { .. } => {
write!(f, "xmr lock transfer proof sent")
}
AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"), AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"),
AliceState::BtcRedeemed => write!(f, "btc is redeemed"), AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"), AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),

View File

@ -5,39 +5,25 @@ use crate::env::Config;
use crate::protocol::alice; use crate::protocol::alice;
use crate::protocol::alice::event_loop::EventLoopHandle; use crate::protocol::alice::event_loop::EventLoopHandle;
use crate::protocol::alice::AliceState; use crate::protocol::alice::AliceState;
use crate::protocol::alice::AliceState::XmrLockTransferProofSent;
use crate::{bitcoin, database, monero}; use crate::{bitcoin, database, monero};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use rand::{CryptoRng, RngCore};
use tokio::select; use tokio::select;
use tokio::time::timeout; use tokio::time::timeout;
use tracing::{error, info}; use tracing::{error, info};
trait Rng: RngCore + CryptoRng + Send {}
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
pub fn is_complete(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrRefunded
| AliceState::BtcRedeemed
| AliceState::BtcPunished
| AliceState::SafelyAborted
)
}
pub async fn run(swap: alice::Swap) -> Result<AliceState> { pub async fn run(swap: alice::Swap) -> Result<AliceState> {
run_until(swap, is_complete).await run_until(swap, |_| false).await
} }
#[tracing::instrument(name = "swap", skip(swap,is_target_state), fields(id = %swap.swap_id))] #[tracing::instrument(name = "swap", skip(swap,exit_early), fields(id = %swap.swap_id))]
pub async fn run_until( pub async fn run_until(
mut swap: alice::Swap, mut swap: alice::Swap,
is_target_state: fn(&AliceState) -> bool, exit_early: fn(&AliceState) -> bool,
) -> Result<AliceState> { ) -> Result<AliceState> {
let mut current_state = swap.state; let mut current_state = swap.state;
while !is_target_state(&current_state) { while !is_complete(&current_state) && !exit_early(&current_state) {
current_state = next_state( current_state = next_state(
current_state, current_state,
&mut swap.event_loop_handle, &mut swap.event_loop_handle,
@ -89,6 +75,8 @@ async fn next_state(
} }
} }
AliceState::BtcLocked { state3 } => { AliceState::BtcLocked { state3 } => {
match state3.expired_timelocks(bitcoin_wallet).await? {
ExpiredTimelocks::None => {
// Record the current monero wallet block height so we don't have to scan from // Record the current monero wallet block height so we don't have to scan from
// block 0 for scenarios where we create a refund wallet. // block 0 for scenarios where we create a refund wallet.
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
@ -97,30 +85,64 @@ async fn next_state(
.transfer(state3.lock_xmr_transfer_request()) .transfer(state3.lock_xmr_transfer_request())
.await?; .await?;
AliceState::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
}
}
_ => AliceState::SafelyAborted,
}
}
AliceState::XmrLockTransactionSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => match state3.expired_timelocks(bitcoin_wallet).await? {
ExpiredTimelocks::None => {
monero_wallet monero_wallet
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
.await?; .await?;
// TODO: Waiting for XMR confirmations should be done in a separate AliceState::XmrLocked {
// state! We have to record that Alice has already sent the transaction. monero_wallet_restore_blockheight,
// Otherwise Alice might publish the lock tx twice! transfer_proof,
state3,
}
}
_ => AliceState::CancelTimelockExpired {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
},
},
AliceState::XmrLocked {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => match state3.expired_timelocks(bitcoin_wallet).await? {
ExpiredTimelocks::None => {
event_loop_handle event_loop_handle
.send_transfer_proof(transfer_proof.clone()) .send_transfer_proof(transfer_proof.clone())
.await?; .await?;
monero_wallet XmrLockTransferProofSent {
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10))
.await?;
AliceState::XmrLocked {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} }
} }
AliceState::XmrLocked { _ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
},
},
AliceState::XmrLockTransferProofSent {
monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => { } => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
@ -129,31 +151,35 @@ async fn next_state(
select! { select! {
_ = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => { _ = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock) => {
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} }
} }
enc_sig = event_loop_handle.recv_encrypted_signature() => { enc_sig = event_loop_handle.recv_encrypted_signature() => {
tracing::info!("Received encrypted signature"); tracing::info!("Received encrypted signature");
AliceState::EncSigLearned { AliceState::EncSigLearned {
state3,
encrypted_signature: Box::new(enc_sig?),
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
encrypted_signature: Box::new(enc_sig?),
state3,
} }
} }
} }
} }
_ => AliceState::CancelTimelockExpired { _ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
}, },
} }
} }
AliceState::EncSigLearned { AliceState::EncSigLearned {
state3,
encrypted_signature,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
encrypted_signature,
state3,
} => match state3.expired_timelocks(bitcoin_wallet).await? { } => match state3.expired_timelocks(bitcoin_wallet).await? {
ExpiredTimelocks::None => { ExpiredTimelocks::None => {
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
@ -172,8 +198,9 @@ async fn next_state(
.await?; .await?;
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} }
} }
}, },
@ -184,20 +211,23 @@ async fn next_state(
.await?; .await?;
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} }
} }
} }
} }
_ => AliceState::CancelTimelockExpired { _ => AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
}, },
}, },
AliceState::CancelTimelockExpired { AliceState::CancelTimelockExpired {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => { } => {
let transaction = state3.signed_cancel_transaction()?; let transaction = state3.signed_cancel_transaction()?;
@ -219,13 +249,15 @@ async fn next_state(
} }
AliceState::BtcCancelled { AliceState::BtcCancelled {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} }
} }
AliceState::BtcCancelled { AliceState::BtcCancelled {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => { } => {
let tx_refund_status = bitcoin_wallet.subscribe_to(state3.tx_refund()).await; let tx_refund_status = bitcoin_wallet.subscribe_to(state3.tx_refund()).await;
let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await; let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await;
@ -238,26 +270,35 @@ async fn next_state(
let spend_key = state3.extract_monero_private_key(published_refund_tx)?; let spend_key = state3.extract_monero_private_key(published_refund_tx)?;
AliceState::BtcRefunded { AliceState::BtcRefunded {
monero_wallet_restore_blockheight,
transfer_proof,
spend_key, spend_key,
state3, state3,
monero_wallet_restore_blockheight,
} }
} }
_ = tx_cancel_status.wait_until_confirmed_with(state3.punish_timelock) => { _ = tx_cancel_status.wait_until_confirmed_with(state3.punish_timelock) => {
AliceState::BtcPunishable { AliceState::BtcPunishable {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} }
} }
} }
} }
AliceState::BtcRefunded { AliceState::BtcRefunded {
monero_wallet_restore_blockheight,
transfer_proof,
spend_key, spend_key,
state3, state3,
monero_wallet_restore_blockheight,
} => { } => {
let view_key = state3.v; let view_key = state3.v;
// Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations
// on the lock transaction
monero_wallet
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10))
.await?;
monero_wallet monero_wallet
.create_from(spend_key, view_key, monero_wallet_restore_blockheight) .create_from(spend_key, view_key, monero_wallet_restore_blockheight)
.await?; .await?;
@ -265,8 +306,9 @@ async fn next_state(
AliceState::XmrRefunded AliceState::XmrRefunded
} }
AliceState::BtcPunishable { AliceState::BtcPunishable {
state3,
monero_wallet_restore_blockheight, monero_wallet_restore_blockheight,
transfer_proof,
state3,
} => { } => {
let signed_tx_punish = state3.signed_punish_transaction()?; let signed_tx_punish = state3.signed_punish_transaction()?;
@ -301,9 +343,10 @@ async fn next_state(
let spend_key = state3.extract_monero_private_key(published_refund_tx)?; let spend_key = state3.extract_monero_private_key(published_refund_tx)?;
AliceState::BtcRefunded { AliceState::BtcRefunded {
monero_wallet_restore_blockheight,
transfer_proof,
spend_key, spend_key,
state3, state3,
monero_wallet_restore_blockheight,
} }
} }
} }
@ -314,3 +357,13 @@ async fn next_state(
AliceState::SafelyAborted => AliceState::SafelyAborted, AliceState::SafelyAborted => AliceState::SafelyAborted,
}) })
} }
fn is_complete(state: &AliceState) -> bool {
matches!(
state,
AliceState::XmrRefunded
| AliceState::BtcRedeemed
| AliceState::BtcPunished
| AliceState::SafelyAborted
)
}

View File

@ -26,7 +26,14 @@ pub async fn cancel(
BobState::XmrLocked(state4) => state4.cancel(), BobState::XmrLocked(state4) => state4.cancel(),
BobState::EncSigSent(state4) => state4.cancel(), BobState::EncSigSent(state4) => state4.cancel(),
BobState::CancelTimelockExpired(state6) => state6, BobState::CancelTimelockExpired(state6) => state6,
_ => bail!( BobState::Started { .. }
| BobState::ExecutionSetupDone(_)
| BobState::BtcRedeemed(_)
| BobState::BtcCancelled(_)
| BobState::BtcRefunded(_)
| BobState::XmrRedeemed { .. }
| BobState::BtcPunished { .. }
| BobState::SafelyAborted => bail!(
"Cannot cancel swap {} because it is in state {} which is not refundable.", "Cannot cancel swap {} because it is in state {} which is not refundable.",
swap_id, swap_id,
state state

View File

@ -24,7 +24,13 @@ pub async fn refund(
BobState::EncSigSent(state4) => state4.cancel(), BobState::EncSigSent(state4) => state4.cancel(),
BobState::CancelTimelockExpired(state6) => state6, BobState::CancelTimelockExpired(state6) => state6,
BobState::BtcCancelled(state6) => state6, BobState::BtcCancelled(state6) => state6,
_ => bail!( BobState::Started { .. }
| BobState::ExecutionSetupDone(_)
| BobState::BtcRedeemed(_)
| BobState::BtcRefunded(_)
| BobState::XmrRedeemed { .. }
| BobState::BtcPunished { .. }
| BobState::SafelyAborted => bail!(
"Cannot refund swap {} because it is in state {} which is not refundable.", "Cannot refund swap {} because it is in state {} which is not refundable.",
swap_id, swap_id,
state state

View File

@ -0,0 +1,62 @@
pub mod harness;
use harness::alice_run_until::is_xmr_lock_transaction_sent;
use harness::bob_run_until::is_btc_locked;
use harness::FastPunishConfig;
use swap::protocol::alice::AliceState;
use swap::protocol::bob::BobState;
use swap::protocol::{alice, bob};
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
/// the encsig and fail to refund or redeem. Alice punishes.
#[tokio::test]
async fn alice_punishes_after_restart_if_punish_timelock_expired() {
harness::setup_test(FastPunishConfig, |mut ctx| async move {
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
let alice_swap = ctx.alice_next_swap().await;
let alice_bitcoin_wallet = alice_swap.bitcoin_wallet.clone();
let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
let bob_state = bob_swap.await??;
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
let alice_state = alice_swap.await??;
// Ensure punish timelock is expired
if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state {
alice_bitcoin_wallet
.subscribe_to(state3.tx_lock)
.await
.wait_until_confirmed_with(state3.punish_timelock)
.await?;
} else {
panic!(
"\
Alice in unexpected state {}",
alice_state
);
}
ctx.restart_alice().await;
let alice_swap = ctx.alice_next_swap().await;
let alice_swap = tokio::spawn(alice::run(alice_swap));
let alice_state = alice_swap.await??;
ctx.assert_alice_punished(alice_state).await;
// Restart Bob after Alice punished to ensure Bob transitions to
// punished and does not run indefinitely
let (bob_swap, _) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await;
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
let bob_state = bob::run(bob_swap).await?;
ctx.assert_bob_punished(bob_state).await;
Ok(())
})
.await;
}

View File

@ -0,0 +1,38 @@
pub mod harness;
use harness::alice_run_until::is_xmr_lock_transaction_sent;
use harness::FastCancelConfig;
use swap::protocol::alice::AliceState;
use swap::protocol::{alice, bob};
/// Bob locks Btc and Alice locks Xmr. Alice does not act so Bob refunds.
/// Eventually Alice comes back online and refunds as well.
#[tokio::test]
async fn alice_refunds_after_restart_if_bob_already_refunded() {
harness::setup_test(FastCancelConfig, |mut ctx| async move {
let (bob_swap, _) = ctx.bob_swap().await;
let bob_swap = tokio::spawn(bob::run(bob_swap));
let alice_swap = ctx.alice_next_swap().await;
let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
let bob_state = bob_swap.await??;
ctx.assert_bob_refunded(bob_state).await;
let alice_state = alice_swap.await??;
assert!(matches!(
alice_state,
AliceState::XmrLockTransactionSent { .. }
));
ctx.restart_alice().await;
let alice_swap = ctx.alice_next_swap().await;
let alice_swap = tokio::spawn(alice::run(alice_swap));
let alice_state = alice_swap.await??;
ctx.assert_alice_refunded(alice_state).await;
Ok(())
})
.await;
}

View File

@ -19,7 +19,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await; let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await;
// Ensure Bob's timelock is expired // Ensure cancel timelock is expired
if let BobState::BtcLocked(state3) = bob_swap.state.clone() { if let BobState::BtcLocked(state3) = bob_swap.state.clone() {
bob_swap bob_swap
.bitcoin_wallet .bitcoin_wallet

View File

@ -0,0 +1,40 @@
pub mod harness;
use harness::alice_run_until::is_xmr_lock_transaction_sent;
use harness::SlowCancelConfig;
use swap::protocol::alice::AliceState;
use swap::protocol::{alice, bob};
#[tokio::test]
async fn given_alice_restarts_after_xmr_is_locked_resume_swap() {
harness::setup_test(SlowCancelConfig, |mut ctx| async move {
let (bob_swap, _) = ctx.bob_swap().await;
let bob_swap = tokio::spawn(bob::run(bob_swap));
let alice_swap = ctx.alice_next_swap().await;
let alice_swap = tokio::spawn(alice::run_until(alice_swap, is_xmr_lock_transaction_sent));
let alice_state = alice_swap.await??;
assert!(matches!(
alice_state,
AliceState::XmrLockTransactionSent { .. }
));
ctx.restart_alice().await;
let alice_swap = ctx.alice_next_swap().await;
assert!(matches!(
alice_swap.state,
AliceState::XmrLockTransactionSent { .. }
));
let alice_state = alice::run(alice_swap).await?;
ctx.assert_alice_redeemed(alice_state).await;
let bob_state = bob_swap.await??;
ctx.assert_bob_redeemed(bob_state).await;
Ok(())
})
.await;
}

View File

@ -29,8 +29,9 @@ use tempfile::tempdir;
use testcontainers::clients::Cli; use testcontainers::clients::Cli;
use testcontainers::{Container, Docker, RunArgs}; use testcontainers::{Container, Docker, RunArgs};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::Receiver;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio::time::interval; use tokio::time::{interval, timeout};
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@ -79,24 +80,40 @@ impl BobParams {
} }
} }
pub struct BobEventLoopJoinHandle(JoinHandle<()>); pub struct BobApplicationHandle(JoinHandle<()>);
impl BobEventLoopJoinHandle { impl BobApplicationHandle {
pub fn abort(&self) { pub fn abort(&self) {
self.0.abort() self.0.abort()
} }
} }
pub struct AliceEventLoopJoinHandle(JoinHandle<()>); pub struct AliceApplicationHandle {
handle: JoinHandle<()>,
peer_id: PeerId,
}
impl AliceApplicationHandle {
pub fn abort(&self) {
self.handle.abort()
}
}
pub struct TestContext { pub struct TestContext {
env_config: Config,
btc_amount: bitcoin::Amount, btc_amount: bitcoin::Amount,
xmr_amount: monero::Amount, xmr_amount: monero::Amount,
alice_seed: Seed,
alice_db_path: PathBuf,
alice_listen_address: Multiaddr,
alice_starting_balances: StartingBalances, alice_starting_balances: StartingBalances,
alice_bitcoin_wallet: Arc<bitcoin::Wallet>, alice_bitcoin_wallet: Arc<bitcoin::Wallet>,
alice_monero_wallet: Arc<monero::Wallet>, alice_monero_wallet: Arc<monero::Wallet>,
alice_swap_handle: mpsc::Receiver<Swap>, alice_swap_handle: mpsc::Receiver<Swap>,
alice_handle: AliceApplicationHandle,
bob_params: BobParams, bob_params: BobParams,
bob_starting_balances: StartingBalances, bob_starting_balances: StartingBalances,
@ -105,11 +122,30 @@ pub struct TestContext {
} }
impl TestContext { impl TestContext {
pub async fn alice_next_swap(&mut self) -> alice::Swap { pub async fn restart_alice(&mut self) {
self.alice_swap_handle.recv().await.unwrap() self.alice_handle.abort();
let (alice_handle, alice_swap_handle) = start_alice(
&self.alice_seed,
self.alice_db_path.clone(),
self.alice_listen_address.clone(),
self.env_config,
self.alice_bitcoin_wallet.clone(),
self.alice_monero_wallet.clone(),
);
self.alice_handle = alice_handle;
self.alice_swap_handle = alice_swap_handle;
} }
pub async fn bob_swap(&mut self) -> (bob::Swap, BobEventLoopJoinHandle) { pub async fn alice_next_swap(&mut self) -> alice::Swap {
timeout(Duration::from_secs(10), self.alice_swap_handle.recv())
.await
.expect("No Alice swap within 10 seconds, aborting because this test is waiting for a swap forever...")
.unwrap()
}
pub async fn bob_swap(&mut self) -> (bob::Swap, BobApplicationHandle) {
let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap(); let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap();
let swap = self let swap = self
@ -123,13 +159,13 @@ impl TestContext {
let join_handle = tokio::spawn(event_loop.run()); let join_handle = tokio::spawn(event_loop.run());
(swap, BobEventLoopJoinHandle(join_handle)) (swap, BobApplicationHandle(join_handle))
} }
pub async fn stop_and_resume_bob_from_db( pub async fn stop_and_resume_bob_from_db(
&mut self, &mut self,
join_handle: BobEventLoopJoinHandle, join_handle: BobApplicationHandle,
) -> (bob::Swap, BobEventLoopJoinHandle) { ) -> (bob::Swap, BobApplicationHandle) {
join_handle.abort(); join_handle.abort();
let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap(); let (event_loop, event_loop_handle) = self.bob_params.new_eventloop().unwrap();
@ -144,7 +180,7 @@ impl TestContext {
let join_handle = tokio::spawn(event_loop.run()); let join_handle = tokio::spawn(event_loop.run());
(swap, BobEventLoopJoinHandle(join_handle)) (swap, BobApplicationHandle(join_handle))
} }
pub async fn assert_alice_redeemed(&mut self, state: AliceState) { pub async fn assert_alice_redeemed(&mut self, state: AliceState) {
@ -462,20 +498,12 @@ where
btc: bitcoin::Amount::ZERO, btc: bitcoin::Amount::ZERO,
}; };
let port = get_port().expect("Failed to find a free port");
let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port)
.parse()
.expect("failed to parse Alice's address");
let electrs_rpc_port = containers let electrs_rpc_port = containers
.electrs .electrs
.get_host_port(harness::electrs::RPC_PORT) .get_host_port(harness::electrs::RPC_PORT)
.expect("Could not map electrs rpc port"); .expect("Could not map electrs rpc port");
let alice_seed = Seed::random().unwrap(); let alice_seed = Seed::random().unwrap();
let bob_seed = Seed::random().unwrap();
let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets(
MONERO_WALLET_NAME_ALICE, MONERO_WALLET_NAME_ALICE,
containers.bitcoind_url.clone(), containers.bitcoind_url.clone(),
@ -483,16 +511,27 @@ where
alice_starting_balances.clone(), alice_starting_balances.clone(),
tempdir().unwrap().path(), tempdir().unwrap().path(),
electrs_rpc_port, electrs_rpc_port,
alice_seed, &alice_seed,
env_config, env_config,
) )
.await; .await;
let db_path = tempdir().unwrap(); let alice_listen_port = get_port().expect("Failed to find a free port");
let alice_db = Arc::new(Database::open(db_path.path()).unwrap()); let alice_listen_address: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", alice_listen_port)
.parse()
.expect("failed to parse Alice's address");
let alice_seed = Seed::random().unwrap(); let alice_db_path = tempdir().unwrap().into_path();
let (alice_handle, alice_swap_handle) = start_alice(
&alice_seed,
alice_db_path.clone(),
alice_listen_address.clone(),
env_config,
alice_bitcoin_wallet.clone(),
alice_monero_wallet.clone(),
);
let bob_seed = Seed::random().unwrap();
let bob_starting_balances = StartingBalances { let bob_starting_balances = StartingBalances {
xmr: monero::Amount::ZERO, xmr: monero::Amount::ZERO,
btc: btc_amount * 10, btc: btc_amount * 10,
@ -505,47 +544,34 @@ where
bob_starting_balances.clone(), bob_starting_balances.clone(),
tempdir().unwrap().path(), tempdir().unwrap().path(),
electrs_rpc_port, electrs_rpc_port,
bob_seed, &bob_seed,
env_config, env_config,
) )
.await; .await;
let mut alice_swarm = swarm::new::<alice::Behaviour>(&alice_seed).unwrap();
Swarm::listen_on(&mut alice_swarm, alice_listen_address.clone()).unwrap();
let (alice_event_loop, alice_swap_handle) = alice::EventLoop::new(
alice_swarm,
env_config,
alice_bitcoin_wallet.clone(),
alice_monero_wallet.clone(),
alice_db,
FixedRate::default(),
bitcoin::Amount::ONE_BTC,
)
.unwrap();
let alice_peer_id = alice_event_loop.peer_id();
tokio::spawn(alice_event_loop.run());
let bob_params = BobParams { let bob_params = BobParams {
seed: Seed::random().unwrap(), seed: Seed::random().unwrap(),
db_path: tempdir().unwrap().path().to_path_buf(), db_path: tempdir().unwrap().path().to_path_buf(),
swap_id: Uuid::new_v4(), swap_id: Uuid::new_v4(),
bitcoin_wallet: bob_bitcoin_wallet.clone(), bitcoin_wallet: bob_bitcoin_wallet.clone(),
monero_wallet: bob_monero_wallet.clone(), monero_wallet: bob_monero_wallet.clone(),
alice_address: alice_listen_address, alice_address: alice_listen_address.clone(),
alice_peer_id, alice_peer_id: alice_handle.peer_id,
env_config, env_config,
}; };
let test = TestContext { let test = TestContext {
env_config,
btc_amount, btc_amount,
xmr_amount, xmr_amount,
alice_seed,
alice_db_path,
alice_listen_address,
alice_starting_balances, alice_starting_balances,
alice_bitcoin_wallet, alice_bitcoin_wallet,
alice_monero_wallet, alice_monero_wallet,
alice_swap_handle, alice_swap_handle,
alice_handle,
bob_params, bob_params,
bob_starting_balances, bob_starting_balances,
bob_bitcoin_wallet, bob_bitcoin_wallet,
@ -555,6 +581,36 @@ where
testfn(test).await.unwrap() testfn(test).await.unwrap()
} }
fn start_alice(
seed: &Seed,
db_path: PathBuf,
listen_address: Multiaddr,
env_config: Config,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
) -> (AliceApplicationHandle, Receiver<alice::Swap>) {
let db = Arc::new(Database::open(db_path.as_path()).unwrap());
let mut swarm = swarm::new::<alice::Behaviour>(&seed).unwrap();
Swarm::listen_on(&mut swarm, listen_address).unwrap();
let (event_loop, swap_handle) = alice::EventLoop::new(
swarm,
env_config,
bitcoin_wallet,
monero_wallet,
db,
FixedRate::default(),
bitcoin::Amount::ONE_BTC,
)
.unwrap();
let peer_id = event_loop.peer_id();
let handle = tokio::spawn(event_loop.run());
(AliceApplicationHandle { handle, peer_id }, swap_handle)
}
fn random_prefix() -> String { fn random_prefix() -> String {
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -714,7 +770,7 @@ async fn init_test_wallets(
starting_balances: StartingBalances, starting_balances: StartingBalances,
datadir: &Path, datadir: &Path,
electrum_rpc_port: u16, electrum_rpc_port: u16,
seed: Seed, seed: &Seed,
env_config: Config, env_config: Config,
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) { ) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
monero monero
@ -790,8 +846,8 @@ struct Containers<'a> {
pub mod alice_run_until { pub mod alice_run_until {
use swap::protocol::alice::AliceState; use swap::protocol::alice::AliceState;
pub fn is_xmr_locked(state: &AliceState) -> bool { pub fn is_xmr_lock_transaction_sent(state: &AliceState) -> bool {
matches!(state, AliceState::XmrLocked { .. }) matches!(state, AliceState::XmrLockTransactionSent { .. })
} }
pub fn is_encsig_learned(state: &AliceState) -> bool { pub fn is_encsig_learned(state: &AliceState) -> bool {
@ -835,7 +891,7 @@ pub struct FastCancelConfig;
impl GetConfig for FastCancelConfig { impl GetConfig for FastCancelConfig {
fn get_config() -> Config { fn get_config() -> Config {
Config { Config {
bitcoin_cancel_timelock: CancelTimelock::new(1), bitcoin_cancel_timelock: CancelTimelock::new(10),
..env::Regtest::get_config() ..env::Regtest::get_config()
} }
} }
@ -846,8 +902,8 @@ pub struct FastPunishConfig;
impl GetConfig for FastPunishConfig { impl GetConfig for FastPunishConfig {
fn get_config() -> Config { fn get_config() -> Config {
Config { Config {
bitcoin_cancel_timelock: CancelTimelock::new(1), bitcoin_cancel_timelock: CancelTimelock::new(10),
bitcoin_punish_timelock: PunishTimelock::new(1), bitcoin_punish_timelock: PunishTimelock::new(10),
..env::Regtest::get_config() ..env::Regtest::get_config()
} }
} }