diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f86eebf..c25de3f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,6 @@ jobs: - name: Check Cargo.toml formatting run: | cargo tomlfmt -d -p Cargo.toml - cargo tomlfmt -d -p xmr-btc/Cargo.toml cargo tomlfmt -d -p monero-harness/Cargo.toml cargo tomlfmt -d -p swap/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 620b74b2..fcff9e5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,36 +1089,6 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" -[[package]] -name = "genawaiter" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" -dependencies = [ - "genawaiter-macro", - "genawaiter-proc-macro", - "proc-macro-hack", -] - -[[package]] -name = "genawaiter-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" - -[[package]] -name = "genawaiter-proc-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" -dependencies = [ - "proc-macro-error 0.4.12", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "generator" version = "0.6.23" @@ -2379,42 +2349,16 @@ dependencies = [ "uint 0.8.5", ] -[[package]] -name = "proc-macro-error" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" -dependencies = [ - "proc-macro-error-attr 0.4.12", - "proc-macro2", - "quote", - "syn", - "version_check", -] - [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "proc-macro-error-attr 1.0.4", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" -dependencies = [ + "proc-macro-error-attr", "proc-macro2", "quote", "syn", - "syn-mid", "version_check", ] @@ -3283,7 +3227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" dependencies = [ "heck", - "proc-macro-error 1.0.4", + "proc-macro-error", "proc-macro2", "quote", "syn", @@ -3335,21 +3279,25 @@ dependencies = [ "bitcoin", "bitcoin-harness", "conquer-once", + "cross-curve-dleq", + "curve25519-dalek 2.1.0", "derivative", "ecdsa_fun", + "ed25519-dalek", "futures", - "genawaiter", "get-port", "hyper", "libp2p", "libp2p-tokio-socks5", "log", + "miniscript", "monero", "monero-harness", "port_check", "prettytable-rs", "rand 0.7.3", "reqwest", + "rust_decimal", "serde", "serde_cbor", "serde_derive", @@ -3361,6 +3309,7 @@ dependencies = [ "strum", "tempfile", "testcontainers", + "thiserror", "time", "tokio", "tracing", @@ -3371,7 +3320,6 @@ dependencies = [ "url", "uuid", "void", - "xmr-btc", ] [[package]] @@ -3385,17 +3333,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "syn-mid" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42823f0ff906a3eb8109610e825221b07fb1456d45c7d01cf18cb581b23ecfb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "synstructure" version = "0.12.4" @@ -4044,32 +3981,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "xmr-btc" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "bitcoin", - "conquer-once", - "cross-curve-dleq", - "curve25519-dalek 2.1.0", - "ecdsa_fun", - "ed25519-dalek", - "futures", - "genawaiter", - "miniscript", - "monero", - "rand 0.7.3", - "rust_decimal", - "serde", - "serde_cbor", - "sha2 0.9.2", - "thiserror", - "tokio", - "tracing", -] - [[package]] name = "yamux" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 9d880136..976a7b53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["monero-harness", "xmr-btc", "swap"] +members = ["monero-harness", "swap"] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index c4119c9d..28e4bf27 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -15,18 +15,22 @@ base64 = "0.12" bitcoin = { version = "0.25", features = ["rand", "use-serde"] } bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "864b55fcba2e770105f135781dd2e3002c503d12" } conquer-once = "0.3" +cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] } +curve25519-dalek = "2" derivative = "2" ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] } +ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3 futures = { version = "0.3", default-features = false } -genawaiter = "0.99.1" libp2p = { version = "0.29", default-features = false, features = ["tcp-tokio", "yamux", "mplex", "dns", "noise", "request-response"] } libp2p-tokio-socks5 = "0.4" log = { version = "0.4", features = ["serde"] } +miniscript = { version = "4", features = ["serde"] } monero = { version = "0.9", features = ["serde_support"] } monero-harness = { path = "../monero-harness" } prettytable-rs = "0.8" rand = "0.7" reqwest = { version = "0.10", default-features = false, features = ["socks"] } +rust_decimal = "1.8" serde = { version = "1", features = ["derive"] } serde_cbor = "0.11" serde_derive = "1.0" @@ -36,6 +40,7 @@ sled = "0.34" structopt = "0.3" strum = { version = "0.20", features = ["derive"] } tempfile = "3" +thiserror = "1" time = "0.2" tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] } tracing = { version = "0.1", features = ["attributes"] } @@ -46,12 +51,12 @@ tracing-subscriber = { version = "0.2", default-features = false, features = ["f url = "2.1" uuid = { version = "0.8", features = ["serde", "v4"] } void = "1" -xmr-btc = { path = "../xmr-btc" } [dev-dependencies] get-port = "3" hyper = "0.13" port_check = "0.1" +serde_cbor = "0.11" spectral = "0.6" tempfile = "3" testcontainers = "0.11" diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 0f76064f..b6070a05 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -1,202 +1,288 @@ -use anyhow::{Context, Result}; +use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; -use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; -use bitcoin::util::psbt::PartiallySignedTransaction; -use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi}; -use reqwest::Url; -use std::time::Duration; -use tokio::time::interval; -use xmr_btc::{ - bitcoin::{ - BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, SignTxLock, - TransactionBlockHeight, WatchForRawTransaction, - }, - config::Config, -}; - -pub use ::bitcoin::{Address, Transaction}; -pub use xmr_btc::bitcoin::*; - -pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600; - -#[derive(Debug)] -pub struct Wallet { - pub inner: bitcoin_harness::Wallet, - pub network: bitcoin::Network, -} - -impl Wallet { - pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result { - let wallet = bitcoin_harness::Wallet::new(name, url).await?; - - Ok(Self { - inner: wallet, - network, - }) +use bitcoin::hashes::{hex::ToHex, Hash}; +use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA}; +use miniscript::{Descriptor, Segwitv0}; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::str::FromStr; + +use crate::{config::Config, ExpiredTimelocks}; + +use crate::bitcoin::timelocks::{BlockHeight, Timelock}; +pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund}; +pub use ::bitcoin::{util::psbt::PartiallySignedTransaction, *}; +pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature}; +pub use wallet::Wallet; + +pub mod timelocks; +pub mod transactions; +pub mod wallet; + +// TODO: Configurable tx-fee (note: parties have to agree prior to swapping) +// Current reasoning: +// tx with largest weight (as determined by get_weight() upon broadcast in e2e +// test) = 609 assuming segwit and 60 sat/vB: +// (609 / 4) * 60 (sat/vB) = 9135 sats +// Recommended: Overpay a bit to ensure we don't have to wait too long for test +// runs. +pub const TX_FEE: u64 = 15_000; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct SecretKey { + inner: Scalar, + public: Point, +} + +impl SecretKey { + pub fn new_random(rng: &mut R) -> Self { + let scalar = Scalar::random(rng); + + let ecdsa = ECDSA::<()>::default(); + let public = ecdsa.verification_key_for(&scalar); + + Self { + inner: scalar, + public, + } } - pub async fn balance(&self) -> Result { - let balance = self.inner.balance().await?; - Ok(balance) + pub fn public(&self) -> PublicKey { + PublicKey(self.public) } - pub async fn new_address(&self) -> Result
{ - self.inner.new_address().await.map_err(Into::into) + pub fn to_bytes(&self) -> [u8; 32] { + self.inner.to_bytes() } - pub async fn transaction_fee(&self, txid: Txid) -> Result { - let fee = self - .inner - .get_wallet_transaction(txid) - .await - .map(|res| { - res.fee.map(|signed_amount| { - signed_amount - .abs() - .to_unsigned() - .expect("Absolute value is always positive") - }) - })? - .context("Rpc response did not contain a fee")?; - - Ok(fee) + pub fn sign(&self, digest: SigHash) -> Signature { + let ecdsa = ECDSA::>::default(); + + ecdsa.sign(&self.inner, &digest.into_inner()) } -} -#[async_trait] -impl BuildTxLockPsbt for Wallet { - async fn build_tx_lock_psbt( - &self, - output_address: Address, - output_amount: Amount, - ) -> Result { - let psbt = self.inner.fund_psbt(output_address, output_amount).await?; - let as_hex = base64::decode(psbt)?; + // TxRefund encsigning explanation: + // + // A and B, are the Bitcoin Public Keys which go on the joint output for + // TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the + // joint output for TxLock_Monero + + // tx_refund: multisig(A, B), published by bob + // bob can produce sig on B for tx_refund using b + // alice sends over an encrypted signature on A for tx_refund using a encrypted + // with S_b we want to leak s_b - let psbt = bitcoin::consensus::deserialize(&as_hex)?; + // produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really + // subtraction, it's recover) - Ok(psbt) + // self = a, Y = S_b, digest = tx_refund + pub fn encsign(&self, Y: PublicKey, digest: SigHash) -> EncryptedSignature { + let adaptor = Adaptor::>::default(); + + adaptor.encrypted_sign(&self.inner, &Y.0, &digest.into_inner()) } } -#[async_trait] -impl SignTxLock for Wallet { - async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result { - let psbt = PartiallySignedTransaction::from(tx_lock); +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] +pub struct PublicKey(Point); - let psbt = bitcoin::consensus::serialize(&psbt); - let as_base64 = base64::encode(psbt); +impl From for Point { + fn from(from: PublicKey) -> Self { + from.0 + } +} - let psbt = self - .inner - .wallet_process_psbt(PsbtBase64(as_base64)) - .await?; - let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt); +impl From for SecretKey { + fn from(scalar: Scalar) -> Self { + let ecdsa = ECDSA::<()>::default(); + let public = ecdsa.verification_key_for(&scalar); - let as_hex = base64::decode(signed_psbt)?; - let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?; + Self { + inner: scalar, + public, + } + } +} - let tx = psbt.extract_tx(); +impl From for Scalar { + fn from(sk: SecretKey) -> Self { + sk.inner + } +} - Ok(tx) +impl From for PublicKey { + fn from(scalar: Scalar) -> Self { + let ecdsa = ECDSA::<()>::default(); + PublicKey(ecdsa.verification_key_for(&scalar)) } } -#[async_trait] -impl BroadcastSignedTransaction for Wallet { - async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { - let txid = self.inner.send_raw_transaction(transaction).await?; - tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid); - Ok(txid) +pub fn verify_sig( + verification_key: &PublicKey, + transaction_sighash: &SigHash, + sig: &Signature, +) -> Result<()> { + let ecdsa = ECDSA::verify_only(); + + if ecdsa.verify(&verification_key.0, &transaction_sighash.into_inner(), &sig) { + Ok(()) + } else { + bail!(InvalidSignature) } } -// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed -// to `ConstantBackoff`. -#[async_trait] -impl WatchForRawTransaction for Wallet { - async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { - (|| async { Ok(self.inner.get_raw_transaction(txid).await?) }) - .retry(ConstantBackoff::new(Duration::from_secs(1))) - .await - .expect("transient errors to be retried") +#[derive(Debug, Clone, Copy, thiserror::Error)] +#[error("signature is invalid")] +pub struct InvalidSignature; + +pub fn verify_encsig( + verification_key: PublicKey, + encryption_key: PublicKey, + digest: &SigHash, + encsig: &EncryptedSignature, +) -> Result<()> { + let adaptor = Adaptor::>::default(); + + if adaptor.verify_encrypted_signature( + &verification_key.0, + &encryption_key.0, + &digest.into_inner(), + &encsig, + ) { + Ok(()) + } else { + bail!(InvalidEncryptedSignature) } } +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("encrypted signature is invalid")] +pub struct InvalidEncryptedSignature; + +pub fn build_shared_output_descriptor(A: Point, B: Point) -> Descriptor { + const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; + + // NOTE: This shouldn't be a source of error, but maybe it is + let A = ToHex::to_hex(&secp256k1::PublicKey::from(A)); + let B = ToHex::to_hex(&secp256k1::PublicKey::from(B)); + + let miniscript = MINISCRIPT_TEMPLATE.replace("A", &A).replace("B", &B); + + let miniscript = miniscript::Miniscript::::from_str(&miniscript) + .expect("a valid miniscript"); + + Descriptor::Wsh(miniscript) +} + #[async_trait] -impl GetRawTransaction for Wallet { - // todo: potentially replace with option - async fn get_raw_transaction(&self, txid: Txid) -> Result { - Ok(self.inner.get_raw_transaction(txid).await?) - } +pub trait BuildTxLockPsbt { + async fn build_tx_lock_psbt( + &self, + output_address: Address, + output_amount: Amount, + ) -> Result; } #[async_trait] -impl GetBlockHeight for Wallet { - async fn get_block_height(&self) -> BlockHeight { - let height = (|| async { Ok(self.inner.client.getblockcount().await?) }) - .retry(ConstantBackoff::new(Duration::from_secs(1))) - .await - .expect("transient errors to be retried"); - - BlockHeight::new(height) - } +pub trait SignTxLock { + async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result; } #[async_trait] -impl TransactionBlockHeight for Wallet { - async fn transaction_block_height(&self, txid: Txid) -> BlockHeight { - #[derive(Debug)] - enum Error { - Io, - NotYetMined, - } +pub trait BroadcastSignedTransaction { + async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result; +} - let height = (|| async { - let block_height = self - .inner - .transaction_block_height(txid) - .await - .map_err(|_| backoff::Error::Transient(Error::Io))?; +#[async_trait] +pub trait WatchForRawTransaction { + async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; +} - let block_height = - block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?; +#[async_trait] +pub trait WaitForTransactionFinality { + async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>; +} - Result::<_, backoff::Error>::Ok(block_height) - }) - .retry(ConstantBackoff::new(Duration::from_secs(1))) - .await - .expect("transient errors to be retried"); +#[async_trait] +pub trait GetBlockHeight { + async fn get_block_height(&self) -> BlockHeight; +} - BlockHeight::new(height) - } +#[async_trait] +pub trait TransactionBlockHeight { + async fn transaction_block_height(&self, txid: Txid) -> BlockHeight; } #[async_trait] -impl WaitForTransactionFinality for Wallet { - async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> { - // TODO(Franck): This assumes that bitcoind runs with txindex=1 - - // Divide by 4 to not check too often yet still be aware of the new block early - // on. - let mut interval = interval(config.bitcoin_avg_block_time / 4); - - loop { - let tx = self.inner.client.get_raw_transaction_verbose(txid).await?; - if let Some(confirmations) = tx.confirmations { - if confirmations >= config.bitcoin_finality_confirmations { - break; - } - } - interval.tick().await; - } +pub trait WaitForBlockHeight { + async fn wait_for_block_height(&self, height: BlockHeight); +} - Ok(()) +#[async_trait] +pub trait GetRawTransaction { + async fn get_raw_transaction(&self, txid: Txid) -> Result; +} + +#[async_trait] +pub trait Network { + fn get_network(&self) -> bitcoin::Network; +} + +pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { + let adaptor = Adaptor::>::default(); + + let s = adaptor + .recover_decryption_key(&S.0, &sig, &encsig) + .map(SecretKey::from) + .ok_or_else(|| anyhow!("secret recovery failure"))?; + + Ok(s) +} + +pub async fn poll_until_block_height_is_gte(client: &B, target: BlockHeight) +where + B: GetBlockHeight, +{ + while client.get_block_height().await < target { + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; } } -impl Network for Wallet { - fn get_network(&self) -> bitcoin::Network { - self.network +pub async fn current_epoch( + bitcoin_wallet: &W, + cancel_timelock: Timelock, + punish_timelock: Timelock, + lock_tx_id: ::bitcoin::Txid, +) -> anyhow::Result +where + W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight, +{ + let current_block_height = bitcoin_wallet.get_block_height().await; + let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await; + let cancel_timelock_height = lock_tx_height + cancel_timelock; + let punish_timelock_height = cancel_timelock_height + punish_timelock; + + match ( + current_block_height < cancel_timelock_height, + current_block_height < punish_timelock_height, + ) { + (true, _) => Ok(ExpiredTimelocks::None), + (false, true) => Ok(ExpiredTimelocks::Cancel), + (false, false) => Ok(ExpiredTimelocks::Punish), } } + +pub async fn wait_for_cancel_timelock_to_expire( + bitcoin_wallet: &W, + cancel_timelock: Timelock, + lock_tx_id: ::bitcoin::Txid, +) -> Result<()> +where + W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight, +{ + let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await; + + poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await; + Ok(()) +} diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs new file mode 100644 index 00000000..0f594afb --- /dev/null +++ b/swap/src/bitcoin/timelocks.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use std::ops::Add; + +/// Represent a timelock, expressed in relative block height as defined in +/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). +/// E.g. The timelock expires 10 blocks after the reference transaction is +/// mined. +#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(transparent)] +pub struct Timelock(u32); + +impl Timelock { + pub const fn new(number_of_blocks: u32) -> Self { + Self(number_of_blocks) + } +} + +impl From for u32 { + fn from(timelock: Timelock) -> Self { + timelock.0 + } +} + +/// Represent a block height, or block number, expressed in absolute block +/// count. E.g. The transaction was included in block #655123, 655123 block +/// after the genesis block. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)] +#[serde(transparent)] +pub struct BlockHeight(u32); + +impl From for u32 { + fn from(height: BlockHeight) -> Self { + height.0 + } +} + +impl BlockHeight { + pub const fn new(block_height: u32) -> Self { + Self(block_height) + } +} + +impl Add for BlockHeight { + type Output = BlockHeight; + + fn add(self, rhs: Timelock) -> Self::Output { + BlockHeight(self.0 + rhs.0) + } +} diff --git a/xmr-btc/src/bitcoin/transactions.rs b/swap/src/bitcoin/transactions.rs similarity index 99% rename from xmr-btc/src/bitcoin/transactions.rs rename to swap/src/bitcoin/transactions.rs index 8966cd8b..54bba2cc 100644 --- a/xmr-btc/src/bitcoin/transactions.rs +++ b/swap/src/bitcoin/transactions.rs @@ -1,7 +1,3 @@ -use crate::bitcoin::{ - build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, Network, OutPoint, PublicKey, - Timelock, Txid, TX_FEE, -}; use anyhow::{bail, Context, Result}; use bitcoin::{ util::{bip143::SigHashCache, psbt::PartiallySignedTransaction}, @@ -12,6 +8,11 @@ use miniscript::{Descriptor, NullCtx}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::bitcoin::{ + build_shared_output_descriptor, timelocks::Timelock, verify_sig, BuildTxLockPsbt, Network, + OutPoint, PublicKey, Txid, TX_FEE, +}; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TxLock { inner: Transaction, diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs new file mode 100644 index 00000000..295833c3 --- /dev/null +++ b/swap/src/bitcoin/wallet.rs @@ -0,0 +1,201 @@ +use ::bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid}; +use anyhow::{Context, Result}; +use async_trait::async_trait; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; +use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi}; +use reqwest::Url; +use std::time::Duration; +use tokio::time::interval; + +use crate::{ + bitcoin::{ + timelocks::BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, + GetRawTransaction, Network, SignTxLock, TransactionBlockHeight, TxLock, + WaitForTransactionFinality, WatchForRawTransaction, + }, + config::Config, +}; + +pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600; + +#[derive(Debug)] +pub struct Wallet { + pub inner: bitcoin_harness::Wallet, + pub network: bitcoin::Network, +} + +impl Wallet { + pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result { + let wallet = bitcoin_harness::Wallet::new(name, url).await?; + + Ok(Self { + inner: wallet, + network, + }) + } + + pub async fn balance(&self) -> Result { + let balance = self.inner.balance().await?; + Ok(balance) + } + + pub async fn new_address(&self) -> Result
{ + self.inner.new_address().await.map_err(Into::into) + } + + pub async fn transaction_fee(&self, txid: Txid) -> Result { + let fee = self + .inner + .get_wallet_transaction(txid) + .await + .map(|res| { + res.fee.map(|signed_amount| { + signed_amount + .abs() + .to_unsigned() + .expect("Absolute value is always positive") + }) + })? + .context("Rpc response did not contain a fee")?; + + Ok(fee) + } +} + +#[async_trait] +impl BuildTxLockPsbt for Wallet { + async fn build_tx_lock_psbt( + &self, + output_address: Address, + output_amount: Amount, + ) -> Result { + let psbt = self.inner.fund_psbt(output_address, output_amount).await?; + let as_hex = base64::decode(psbt)?; + + let psbt = bitcoin::consensus::deserialize(&as_hex)?; + + Ok(psbt) + } +} + +#[async_trait] +impl SignTxLock for Wallet { + async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result { + let psbt = PartiallySignedTransaction::from(tx_lock); + + let psbt = bitcoin::consensus::serialize(&psbt); + let as_base64 = base64::encode(psbt); + + let psbt = self + .inner + .wallet_process_psbt(PsbtBase64(as_base64)) + .await?; + let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt); + + let as_hex = base64::decode(signed_psbt)?; + let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?; + + let tx = psbt.extract_tx(); + + Ok(tx) + } +} + +#[async_trait] +impl BroadcastSignedTransaction for Wallet { + async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { + let txid = self.inner.send_raw_transaction(transaction).await?; + tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid); + Ok(txid) + } +} + +// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed +// to `ConstantBackoff`. +#[async_trait] +impl WatchForRawTransaction for Wallet { + async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { + (|| async { Ok(self.inner.get_raw_transaction(txid).await?) }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient errors to be retried") + } +} + +#[async_trait] +impl GetRawTransaction for Wallet { + // todo: potentially replace with option + async fn get_raw_transaction(&self, txid: Txid) -> Result { + Ok(self.inner.get_raw_transaction(txid).await?) + } +} + +#[async_trait] +impl GetBlockHeight for Wallet { + async fn get_block_height(&self) -> BlockHeight { + let height = (|| async { Ok(self.inner.client.getblockcount().await?) }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient errors to be retried"); + + BlockHeight::new(height) + } +} + +#[async_trait] +impl TransactionBlockHeight for Wallet { + async fn transaction_block_height(&self, txid: Txid) -> BlockHeight { + #[derive(Debug)] + enum Error { + Io, + NotYetMined, + } + + let height = (|| async { + let block_height = self + .inner + .transaction_block_height(txid) + .await + .map_err(|_| backoff::Error::Transient(Error::Io))?; + + let block_height = + block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?; + + Result::<_, backoff::Error>::Ok(block_height) + }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await + .expect("transient errors to be retried"); + + BlockHeight::new(height) + } +} + +#[async_trait] +impl WaitForTransactionFinality for Wallet { + async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> { + // TODO(Franck): This assumes that bitcoind runs with txindex=1 + + // Divide by 4 to not check too often yet still be aware of the new block early + // on. + let mut interval = interval(config.bitcoin_avg_block_time / 4); + + loop { + let tx = self.inner.client.get_raw_transaction_verbose(txid).await?; + if let Some(confirmations) = tx.confirmations { + if confirmations >= config.bitcoin_finality_confirmations { + break; + } + } + interval.tick().await; + } + + Ok(()) + } +} + +impl Network for Wallet { + fn get_network(&self) -> bitcoin::Network { + self.network + } +} diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 6e1412f4..c0f97504 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -2,6 +2,8 @@ use libp2p::{core::Multiaddr, PeerId}; use url::Url; use uuid::Uuid; +use crate::monero; + #[derive(structopt::StructOpt, Debug)] pub struct Options { // TODO: Default value should points to proper configuration folder in home folder @@ -13,7 +15,7 @@ pub struct Options { } #[derive(structopt::StructOpt, Debug)] -#[structopt(name = "xmr-btc-swap", about = "XMR BTC atomic swap")] +#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] pub enum Command { SellXmr { #[structopt(long = "bitcoind-rpc", default_value = "http://127.0.0.1:8332")] @@ -32,10 +34,10 @@ pub enum Command { listen_addr: Multiaddr, #[structopt(long = "send-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))] - send_monero: xmr_btc::monero::Amount, + send_monero: monero::Amount, #[structopt(long = "receive-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] - receive_bitcoin: bitcoin::Amount, + receive_bitcoin: ::bitcoin::Amount, }, BuyXmr { #[structopt(long = "connect-peer-id")] @@ -57,10 +59,10 @@ pub enum Command { monero_wallet_rpc_url: Url, #[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] - send_bitcoin: bitcoin::Amount, + send_bitcoin: ::bitcoin::Amount, #[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))] - receive_monero: xmr_btc::monero::Amount, + receive_monero: monero::Amount, }, History, Resume(Resume), @@ -116,7 +118,7 @@ fn parse_btc(str: &str) -> anyhow::Result { Ok(amount) } -fn parse_xmr(str: &str) -> anyhow::Result { - let amount = xmr_btc::monero::Amount::parse_monero(str)?; +fn parse_xmr(str: &str) -> anyhow::Result { + let amount = monero::Amount::parse_monero(str)?; Ok(amount) } diff --git a/xmr-btc/src/config.rs b/swap/src/config.rs similarity index 99% rename from xmr-btc/src/config.rs rename to swap/src/config.rs index c88cd45c..8e3ebccd 100644 --- a/xmr-btc/src/config.rs +++ b/swap/src/config.rs @@ -1,4 +1,4 @@ -use crate::bitcoin::Timelock; +use crate::bitcoin::timelocks::Timelock; use conquer_once::Lazy; use std::time::Duration; diff --git a/swap/src/database.rs b/swap/src/database.rs index 49d61930..e1e4cbd5 100644 --- a/swap/src/database.rs +++ b/swap/src/database.rs @@ -3,13 +3,39 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{fmt::Display, path::Path}; use uuid::Uuid; -mod alice; -mod bob; +pub mod alice; +pub mod bob; -pub use alice::*; -pub use bob::*; +pub use alice::Alice; +pub use bob::Bob; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub enum Swap { + Alice(Alice), + Bob(Bob), +} + +impl From for Swap { + fn from(from: Alice) -> Self { + Swap::Alice(from) + } +} + +impl From for Swap { + fn from(from: Bob) -> Self { + Swap::Bob(from) + } +} + +impl Display for Swap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Swap::Alice(alice) => Display::fmt(alice, f), + Swap::Bob(bob) => Display::fmt(bob, f), + } + } +} -#[derive(Debug)] pub struct Database(sled::Db); impl Database { @@ -85,37 +111,13 @@ where Ok(serde_cbor::from_slice(&v)?) } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -pub enum Swap { - Alice(Alice), - Bob(Bob), -} - -impl From for Swap { - fn from(from: Alice) -> Self { - Swap::Alice(from) - } -} - -impl From for Swap { - fn from(from: Bob) -> Self { - Swap::Bob(from) - } -} - -impl Display for Swap { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Swap::Alice(alice) => Display::fmt(alice, f), - Swap::Bob(bob) => Display::fmt(bob, f), - } - } -} - #[cfg(test)] mod tests { use super::*; - use crate::database::{Alice, AliceEndState, Bob, BobEndState}; + use crate::database::{ + alice::{Alice, AliceEndState}, + bob::{Bob, BobEndState}, + }; #[tokio::test] async fn can_write_and_read_to_multiple_keys() { diff --git a/swap/src/database/alice.rs b/swap/src/database/alice.rs index f2069d67..96e740aa 100644 --- a/swap/src/database/alice.rs +++ b/swap/src/database/alice.rs @@ -1,14 +1,15 @@ -use crate::{alice::swap::AliceState, SwapAmounts}; use bitcoin::hashes::core::fmt::Display; use serde::{Deserialize, Serialize}; -use xmr_btc::{ - alice, + +use crate::{ bitcoin::{EncryptedSignature, TxCancel, TxRefund}, monero, + protocol::{alice, alice::swap::AliceState}, serde::monero_private_key, + SwapAmounts, }; -// Large enum variant is fine because this is only used for storage +// Large enum variant is fine because this is only used for database // and is dropped once written in DB. #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] diff --git a/swap/src/database/bob.rs b/swap/src/database/bob.rs index 0f462138..f92371f1 100644 --- a/swap/src/database/bob.rs +++ b/swap/src/database/bob.rs @@ -1,7 +1,10 @@ -use crate::{bob::swap::BobState, SwapAmounts}; use bitcoin::hashes::core::fmt::Display; use serde::{Deserialize, Serialize}; -use xmr_btc::bob; + +use crate::{ + protocol::{bob, bob::swap::BobState}, + SwapAmounts, +}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub enum Bob { diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 04a66898..e76ee7af 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -1,7 +1,5 @@ #![warn( unused_extern_crates, - missing_debug_implementations, - missing_copy_implementations, rust_2018_idioms, clippy::cast_possible_truncation, clippy::cast_sign_loss, @@ -10,19 +8,25 @@ clippy::cast_possible_wrap, clippy::dbg_macro )] +#![cfg_attr(not(test), warn(clippy::unwrap_used))] #![forbid(unsafe_code)] -#![allow(non_snake_case)] +#![allow( + non_snake_case, + missing_debug_implementations, + missing_copy_implementations +)] use ::serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; -pub mod alice; pub mod bitcoin; -pub mod bob; pub mod cli; +pub mod config; pub mod database; pub mod monero; pub mod network; +pub mod protocol; +pub mod serde; pub mod trace; pub type Never = std::convert::Infallible; @@ -48,7 +52,7 @@ pub struct SwapAmounts { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] pub btc: bitcoin::Amount, /// Amount of XMR to swap. - #[serde(with = "xmr_btc::serde::monero_amount")] + #[serde(with = "serde::monero_amount")] pub xmr: monero::Amount, } @@ -63,3 +67,10 @@ impl Display for SwapAmounts { ) } } + +#[derive(Debug, Clone, Copy)] +pub enum ExpiredTimelocks { + None, + Cancel, + Punish, +} diff --git a/swap/src/main.rs b/swap/src/main.rs index 64291698..66db10ae 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -20,20 +20,18 @@ use rand::rngs::OsRng; use std::sync::Arc; use structopt::StructOpt; use swap::{ - alice, - alice::swap::AliceState, - bitcoin, bob, - bob::swap::BobState, + bitcoin, cli::{Command, Options, Resume}, + config::Config, database::{Database, Swap}, monero, network::transport::build, + protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState}, trace::init_tracing, SwapAmounts, }; use tracing::{info, log::LevelFilter}; use uuid::Uuid; -use xmr_btc::{alice::State0, config::Config, cross_curve_dleq}; #[macro_use] extern crate prettytable; @@ -76,10 +74,10 @@ async fn main() -> Result<()> { let rng = &mut OsRng; let a = bitcoin::SecretKey::new_random(rng); let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); + let v_a = monero::PrivateViewKey::new_random(rng); let redeem_address = bitcoin_wallet.as_ref().new_address().await?; let punish_address = redeem_address.clone(); - let state0 = State0::new( + let state0 = alice::state::State0::new( a, s_a, v_a, @@ -129,7 +127,7 @@ async fn main() -> Result<()> { .await?; let refund_address = bitcoin_wallet.new_address().await?; - let state0 = xmr_btc::bob::State0::new( + let state0 = bob::state::State0::new( &mut OsRng, send_bitcoin, receive_monero, @@ -248,9 +246,10 @@ async fn setup_wallets( bitcoin_wallet_name: &str, monero_wallet_rpc_url: url::Url, config: Config, -) -> Result<(Arc, Arc)> { +) -> Result<(Arc, Arc)> { let bitcoin_wallet = - bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network).await?; + swap::bitcoin::Wallet::new(bitcoin_wallet_name, bitcoind_url, config.bitcoin_network) + .await?; let bitcoin_balance = bitcoin_wallet.balance().await?; info!( "Connection to Bitcoin wallet succeeded, balance: {}", @@ -273,8 +272,8 @@ async fn alice_swap( swap_id: Uuid, state: AliceState, listen_addr: Multiaddr, - bitcoin_wallet: Arc, - monero_wallet: Arc, + bitcoin_wallet: Arc, + monero_wallet: Arc, config: Config, db: Database, ) -> Result { @@ -306,8 +305,8 @@ async fn alice_swap( async fn bob_swap( swap_id: Uuid, state: BobState, - bitcoin_wallet: Arc, - monero_wallet: Arc, + bitcoin_wallet: Arc, + monero_wallet: Arc, db: Database, alice_peer_id: PeerId, alice_addr: Multiaddr, diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 51111003..03448be8 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,143 +1,281 @@ +pub mod wallet; + +use ::bitcoin::hashes::core::fmt::Formatter; use anyhow::Result; use async_trait::async_trait; -use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; -use monero_harness::rpc::wallet; -use std::{str::FromStr, time::Duration}; -use url::Url; +use rand::{CryptoRng, RngCore}; +use rust_decimal::{ + prelude::{FromPrimitive, ToPrimitive}, + Decimal, +}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::Display, + ops::{Add, Mul, Sub}, + str::FromStr, +}; + +use crate::{bitcoin, serde::monero_private_key}; + +pub use curve25519_dalek::scalar::Scalar; +pub use monero::*; +pub use wallet::Wallet; -pub use xmr_btc::monero::*; +pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; -#[derive(Debug)] -pub struct Wallet { - pub inner: wallet::Client, - pub network: Network, +pub fn random_private_key(rng: &mut R) -> PrivateKey { + let scalar = Scalar::random(rng); + + PrivateKey::from_scalar(scalar) } -impl Wallet { - pub fn new(url: Url, network: Network) -> Self { - Self { - inner: wallet::Client::new(url), - network, - } +pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey { + let mut bytes = scalar.to_bytes(); + + // we must reverse the bytes because a secp256k1 scalar is big endian, whereas a + // ed25519 scalar is little endian + bytes.reverse(); + + PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes)) +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey); + +impl PrivateViewKey { + pub fn new_random(rng: &mut R) -> Self { + let scalar = Scalar::random(rng); + let private_key = PrivateKey::from_scalar(scalar); + + Self(private_key) + } + + pub fn public(&self) -> PublicViewKey { + PublicViewKey(PublicKey::from_private_key(&self.0)) } +} - /// Get the balance of the primary account. - pub async fn get_balance(&self) -> Result { - let amount = self.inner.get_balance(0).await?; +impl Add for PrivateViewKey { + type Output = Self; - Ok(Amount::from_piconero(amount)) + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) } } -#[async_trait] -impl Transfer for Wallet { - async fn transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - amount: Amount, - ) -> Result<(TransferProof, Amount)> { - let destination_address = - Address::standard(self.network, public_spend_key, public_view_key.into()); +impl From for PrivateKey { + fn from(from: PrivateViewKey) -> Self { + from.0 + } +} + +impl From for PublicKey { + fn from(from: PublicViewKey) -> Self { + from.0 + } +} - let res = self - .inner - .transfer(0, amount.as_piconero(), &destination_address.to_string()) - .await?; +#[derive(Clone, Copy, Debug)] +pub struct PublicViewKey(PublicKey); - let tx_hash = TxHash(res.tx_hash); - tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash); - let tx_key = PrivateKey::from_str(&res.tx_key)?; +#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)] +pub struct Amount(u64); - let fee = Amount::from_piconero(res.fee); +impl Amount { + pub const ZERO: Self = Self(0); + /// Create an [Amount] with piconero precision and the given number of + /// piconeros. + /// + /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. + pub fn from_piconero(amount: u64) -> Self { + Amount(amount) + } - let transfer_proof = TransferProof::new(tx_hash, tx_key); - tracing::debug!(" Transfer proof: {:?}", transfer_proof); + pub fn as_piconero(&self) -> u64 { + self.0 + } - Ok((transfer_proof, fee)) + pub fn parse_monero(amount: &str) -> Result { + let decimal = Decimal::from_str(amount)?; + let piconeros_dec = + decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); + let piconeros = piconeros_dec + .to_u64() + .ok_or_else(|| OverflowError(amount.to_owned()))?; + Ok(Amount(piconeros)) } } -#[async_trait] -impl CreateWalletForOutput for Wallet { - async fn create_and_load_wallet_for_output( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - ) -> Result<()> { - let public_spend_key = PublicKey::from_private_key(&private_spend_key); - let public_view_key = PublicKey::from_private_key(&private_view_key.into()); +impl Add for Amount { + type Output = Amount; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for Amount { + type Output = Amount; - let address = Address::standard(self.network, public_spend_key, public_view_key); + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} - let _ = self - .inner - .generate_from_keys( - &address.to_string(), - &private_spend_key.to_string(), - &PrivateKey::from(private_view_key).to_string(), - ) - .await?; +impl Mul for Amount { + type Output = Amount; - Ok(()) + fn mul(self, rhs: u64) -> Self::Output { + Self(self.0 * rhs) } } -// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed -// to `ConstantBackoff`. +impl From for u64 { + fn from(from: Amount) -> u64 { + from.0 + } +} + +impl Display for Amount { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut decimal = Decimal::from(self.0); + decimal + .set_scale(12) + .expect("12 is smaller than max precision of 28"); + write!(f, "{} XMR", decimal) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransferProof { + tx_hash: TxHash, + #[serde(with = "monero_private_key")] + tx_key: PrivateKey, +} + +impl TransferProof { + pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self { + Self { tx_hash, tx_key } + } + pub fn tx_hash(&self) -> TxHash { + self.tx_hash.clone() + } + pub fn tx_key(&self) -> PrivateKey { + self.tx_key + } +} + +// TODO: add constructor/ change String to fixed length byte array +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TxHash(pub String); + +impl From for String { + fn from(from: TxHash) -> Self { + from.0 + } +} + +#[async_trait] +pub trait Transfer { + async fn transfer( + &self, + public_spend_key: PublicKey, + public_view_key: PublicViewKey, + amount: Amount, + ) -> anyhow::Result<(TransferProof, Amount)>; +} #[async_trait] -impl WatchForTransfer for Wallet { +pub trait WatchForTransfer { async fn watch_for_transfer( &self, public_spend_key: PublicKey, public_view_key: PublicViewKey, transfer_proof: TransferProof, - expected_amount: Amount, + amount: Amount, expected_confirmations: u32, - ) -> Result<(), InsufficientFunds> { - enum Error { - TxNotFound, - InsufficientConfirmations, - InsufficientFunds { expected: Amount, actual: Amount }, - } - - let address = Address::standard(self.network, public_spend_key, public_view_key.into()); - - let res = (|| async { - // NOTE: Currently, this is conflating IO errors with the transaction not being - // in the blockchain yet, or not having enough confirmations on it. All these - // errors warrant a retry, but the strategy should probably differ per case - let proof = self - .inner - .check_tx_key( - &String::from(transfer_proof.tx_hash()), - &transfer_proof.tx_key().to_string(), - &address.to_string(), - ) - .await - .map_err(|_| backoff::Error::Transient(Error::TxNotFound))?; - - if proof.received != expected_amount.as_piconero() { - return Err(backoff::Error::Permanent(Error::InsufficientFunds { - expected: expected_amount, - actual: Amount::from_piconero(proof.received), - })); - } - - if proof.confirmations < expected_confirmations { - return Err(backoff::Error::Transient(Error::InsufficientConfirmations)); - } - - Ok(proof) - }) - .retry(ConstantBackoff::new(Duration::from_secs(1))) - .await; - - if let Err(Error::InsufficientFunds { expected, actual }) = res { - return Err(InsufficientFunds { expected, actual }); - }; - - Ok(()) + ) -> Result<(), InsufficientFunds>; +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")] +pub struct InsufficientFunds { + pub expected: Amount, + pub actual: Amount, +} + +#[async_trait] +pub trait CreateWalletForOutput { + async fn create_and_load_wallet_for_output( + &self, + private_spend_key: PrivateKey, + private_view_key: PrivateViewKey, + ) -> anyhow::Result<()>; +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Overflow, cannot convert {0} to u64")] +pub struct OverflowError(pub String); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_monero_min() { + let min_pics = 1; + let amount = Amount::from_piconero(min_pics); + let monero = amount.to_string(); + assert_eq!("0.000000000001 XMR", monero); + } + + #[test] + fn display_monero_one() { + let min_pics = 1000000000000; + let amount = Amount::from_piconero(min_pics); + let monero = amount.to_string(); + assert_eq!("1.000000000000 XMR", monero); + } + + #[test] + fn display_monero_max() { + let max_pics = 18_446_744_073_709_551_615; + let amount = Amount::from_piconero(max_pics); + let monero = amount.to_string(); + assert_eq!("18446744.073709551615 XMR", monero); + } + + #[test] + fn parse_monero_min() { + let monero_min = "0.000000000001"; + let amount = Amount::parse_monero(monero_min).unwrap(); + let pics = amount.0; + assert_eq!(1, pics); + } + + #[test] + fn parse_monero() { + let monero = "123"; + let amount = Amount::parse_monero(monero).unwrap(); + let pics = amount.0; + assert_eq!(123000000000000, pics); + } + + #[test] + fn parse_monero_max() { + let monero = "18446744.073709551615"; + let amount = Amount::parse_monero(monero).unwrap(); + let pics = amount.0; + assert_eq!(18446744073709551615, pics); + } + + #[test] + fn parse_monero_overflows() { + let overflow_pics = "18446744.073709551616"; + let error = Amount::parse_monero(overflow_pics).unwrap_err(); + assert_eq!( + error.downcast_ref::().unwrap(), + &OverflowError(overflow_pics.to_owned()) + ); } } diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs new file mode 100644 index 00000000..b4cc0766 --- /dev/null +++ b/swap/src/monero/wallet.rs @@ -0,0 +1,147 @@ +use anyhow::Result; +use async_trait::async_trait; +use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _}; +use monero::{Address, Network, PrivateKey, PublicKey}; +use monero_harness::rpc::wallet; +use std::{str::FromStr, time::Duration}; +use url::Url; + +use crate::monero::{ + Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicViewKey, Transfer, + TransferProof, TxHash, WatchForTransfer, +}; + +#[derive(Debug)] +pub struct Wallet { + pub inner: wallet::Client, + pub network: Network, +} + +impl Wallet { + pub fn new(url: Url, network: Network) -> Self { + Self { + inner: wallet::Client::new(url), + network, + } + } + + /// Get the balance of the primary account. + pub async fn get_balance(&self) -> Result { + let amount = self.inner.get_balance(0).await?; + + Ok(Amount::from_piconero(amount)) + } +} + +#[async_trait] +impl Transfer for Wallet { + async fn transfer( + &self, + public_spend_key: PublicKey, + public_view_key: PublicViewKey, + amount: Amount, + ) -> Result<(TransferProof, Amount)> { + let destination_address = + Address::standard(self.network, public_spend_key, public_view_key.into()); + + let res = self + .inner + .transfer(0, amount.as_piconero(), &destination_address.to_string()) + .await?; + + let tx_hash = TxHash(res.tx_hash); + tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash); + let tx_key = PrivateKey::from_str(&res.tx_key)?; + + let fee = Amount::from_piconero(res.fee); + + let transfer_proof = TransferProof::new(tx_hash, tx_key); + tracing::debug!(" Transfer proof: {:?}", transfer_proof); + + Ok((transfer_proof, fee)) + } +} + +#[async_trait] +impl CreateWalletForOutput for Wallet { + async fn create_and_load_wallet_for_output( + &self, + private_spend_key: PrivateKey, + private_view_key: PrivateViewKey, + ) -> Result<()> { + let public_spend_key = PublicKey::from_private_key(&private_spend_key); + let public_view_key = PublicKey::from_private_key(&private_view_key.into()); + + let address = Address::standard(self.network, public_spend_key, public_view_key); + + let _ = self + .inner + .generate_from_keys( + &address.to_string(), + &private_spend_key.to_string(), + &PrivateKey::from(private_view_key).to_string(), + ) + .await?; + + Ok(()) + } +} + +// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed +// to `ConstantBackoff`. + +#[async_trait] +impl WatchForTransfer for Wallet { + async fn watch_for_transfer( + &self, + public_spend_key: PublicKey, + public_view_key: PublicViewKey, + transfer_proof: TransferProof, + expected_amount: Amount, + expected_confirmations: u32, + ) -> Result<(), InsufficientFunds> { + enum Error { + TxNotFound, + InsufficientConfirmations, + InsufficientFunds { expected: Amount, actual: Amount }, + } + + let address = Address::standard(self.network, public_spend_key, public_view_key.into()); + + let res = (|| async { + // NOTE: Currently, this is conflating IO errors with the transaction not being + // in the blockchain yet, or not having enough confirmations on it. All these + // errors warrant a retry, but the strategy should probably differ per case + let proof = self + .inner + .check_tx_key( + &String::from(transfer_proof.tx_hash()), + &transfer_proof.tx_key().to_string(), + &address.to_string(), + ) + .await + .map_err(|_| backoff::Error::Transient(Error::TxNotFound))?; + + if proof.received != expected_amount.as_piconero() { + return Err(backoff::Error::Permanent(Error::InsufficientFunds { + expected: expected_amount, + actual: Amount::from_piconero(proof.received), + })); + } + + if proof.confirmations < expected_confirmations { + return Err(backoff::Error::Transient(Error::InsufficientConfirmations)); + } + + Ok(proof) + }) + .retry(ConstantBackoff::new(Duration::from_secs(1))) + .await; + + if let Err(Error::InsufficientFunds { expected, actual }) = res { + return Err(InsufficientFunds { expected, actual }); + }; + + Ok(()) + } +} diff --git a/swap/src/network/request_response.rs b/swap/src/network/request_response.rs index e58d81ad..28e4d254 100644 --- a/swap/src/network/request_response.rs +++ b/swap/src/network/request_response.rs @@ -1,3 +1,4 @@ +use crate::monero; use async_trait::async_trait; use futures::prelude::*; use libp2p::{ @@ -8,8 +9,10 @@ use serde::{Deserialize, Serialize}; use std::{fmt::Debug, io, marker::PhantomData}; use tracing::debug; -use crate::SwapAmounts; -use xmr_btc::{alice, bob, monero}; +use crate::{ + protocol::{alice, bob}, + SwapAmounts, +}; /// Time to wait for a response back once we send a request. pub const TIMEOUT: u64 = 3600; // One hour. diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs new file mode 100644 index 00000000..9de27854 --- /dev/null +++ b/swap/src/protocol.rs @@ -0,0 +1,2 @@ +pub mod alice; +pub mod bob; diff --git a/swap/src/alice.rs b/swap/src/protocol/alice.rs similarity index 86% rename from swap/src/alice.rs rename to swap/src/protocol/alice.rs index 903d22da..af5826bb 100644 --- a/swap/src/alice.rs +++ b/swap/src/protocol/alice.rs @@ -1,6 +1,14 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. -use self::{amounts::*, message0::*, message1::*, message2::*, message3::*}; +pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*}; +use anyhow::Result; +use libp2p::{ + core::{identity::Keypair, Multiaddr}, + request_response::ResponseChannel, + NetworkBehaviour, PeerId, +}; +use tracing::{debug, info}; + use crate::{ network::{ peer_tracker::{self, PeerTracker}, @@ -8,16 +16,9 @@ use crate::{ transport::SwapTransport, TokioExecutor, }, + protocol::bob, SwapAmounts, }; -use anyhow::Result; -use libp2p::{ - core::{identity::Keypair, Multiaddr}, - request_response::ResponseChannel, - NetworkBehaviour, PeerId, -}; -use tracing::{debug, info}; -use xmr_btc::bob; mod amounts; pub mod event_loop; @@ -25,6 +26,7 @@ mod message0; mod message1; mod message2; mod message3; +pub mod state; mod steps; pub mod swap; @@ -133,10 +135,10 @@ impl From for OutEvent { pub struct Behaviour { pt: PeerTracker, amounts: Amounts, - message0: Message0, - message1: Message1, - message2: Message2, - message3: Message3, + message0: Message0Behaviour, + message1: Message1Behaviour, + message2: Message2Behaviour, + message3: Message3Behaviour, #[behaviour(ignore)] identity: Keypair, } @@ -158,31 +160,19 @@ impl Behaviour { } /// Send Message0 to Bob in response to receiving his Message0. - pub fn send_message0( - &mut self, - channel: ResponseChannel, - msg: xmr_btc::alice::Message0, - ) { + pub fn send_message0(&mut self, channel: ResponseChannel, msg: Message0) { self.message0.send(channel, msg); debug!("Sent Message0"); } /// Send Message1 to Bob in response to receiving his Message1. - pub fn send_message1( - &mut self, - channel: ResponseChannel, - msg: xmr_btc::alice::Message1, - ) { + pub fn send_message1(&mut self, channel: ResponseChannel, msg: 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, - msg: xmr_btc::alice::Message2, - ) { + pub fn send_message2(&mut self, channel: ResponseChannel, msg: Message2) { self.message2.send(channel, msg); debug!("Sent Message2"); } @@ -195,10 +185,10 @@ impl Default for Behaviour { Self { pt: PeerTracker::default(), amounts: Amounts::default(), - message0: Message0::default(), - message1: Message1::default(), - message2: Message2::default(), - message3: Message3::default(), + message0: Message0Behaviour::default(), + message1: Message1Behaviour::default(), + message2: Message2Behaviour::default(), + message3: Message3Behaviour::default(), identity, } } diff --git a/swap/src/alice/amounts.rs b/swap/src/protocol/alice/amounts.rs similarity index 99% rename from swap/src/alice/amounts.rs rename to swap/src/protocol/alice/amounts.rs index f1b01cb0..a3079352 100644 --- a/swap/src/alice/amounts.rs +++ b/swap/src/protocol/alice/amounts.rs @@ -14,8 +14,8 @@ use std::{ use tracing::{debug, error}; use crate::{ - alice::amounts, network::request_response::{AliceToBob, AmountsProtocol, BobToAlice, Codec, TIMEOUT}, + protocol::alice::amounts, }; #[derive(Debug)] diff --git a/swap/src/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs similarity index 96% rename from swap/src/alice/event_loop.rs rename to swap/src/protocol/alice/event_loop.rs index f650932d..3b2f655b 100644 --- a/swap/src/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -1,15 +1,19 @@ -use crate::{ - alice::{Behaviour, OutEvent}, - network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor}, - SwapAmounts, -}; use anyhow::{anyhow, Context, Result}; use futures::FutureExt; use libp2p::{ core::Multiaddr, futures::StreamExt, request_response::ResponseChannel, PeerId, Swarm, }; use tokio::sync::mpsc::{Receiver, Sender}; -use xmr_btc::{alice, bob}; + +use crate::{ + network::{request_response::AliceToBob, transport::SwapTransport, TokioExecutor}, + protocol::{ + alice, + alice::{Behaviour, OutEvent}, + bob, + }, + SwapAmounts, +}; #[allow(missing_debug_implementations)] pub struct Channels { @@ -36,7 +40,7 @@ pub struct EventLoopHandle { msg1: Receiver<(bob::Message1, ResponseChannel)>, msg2: Receiver<(bob::Message2, ResponseChannel)>, msg3: Receiver, - request: Receiver, + request: Receiver, conn_established: Receiver, send_amounts: Sender<(ResponseChannel, SwapAmounts)>, send_msg0: Sender<(ResponseChannel, alice::Message0)>, @@ -80,7 +84,7 @@ impl EventLoopHandle { .ok_or_else(|| anyhow!("Failed to receive Bitcoin encrypted signature from Bob")) } - pub async fn recv_request(&mut self) -> Result { + pub async fn recv_request(&mut self) -> Result { self.request .recv() .await @@ -131,7 +135,7 @@ pub struct EventLoop { msg1: Sender<(bob::Message1, ResponseChannel)>, msg2: Sender<(bob::Message2, ResponseChannel)>, msg3: Sender, - request: Sender, + request: Sender, conn_established: Sender, send_amounts: Receiver<(ResponseChannel, SwapAmounts)>, send_msg0: Receiver<(ResponseChannel, alice::Message0)>, diff --git a/swap/src/alice/message0.rs b/swap/src/protocol/alice/message0.rs similarity index 78% rename from swap/src/alice/message0.rs rename to swap/src/protocol/alice/message0.rs index 92ca7a43..d9270b3a 100644 --- a/swap/src/alice/message0.rs +++ b/swap/src/protocol/alice/message0.rs @@ -1,11 +1,12 @@ use libp2p::{ request_response::{ handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, - RequestResponseEvent, RequestResponseMessage, + RequestResponseEvent, RequestResponseMessage, ResponseChannel, }, swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -13,9 +14,11 @@ use std::{ }; use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT}; -use libp2p::request_response::ResponseChannel; -use xmr_btc::bob; +use crate::{ + bitcoin, monero, + network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT}, + protocol::bob, +}; #[derive(Debug)] pub enum OutEvent { @@ -25,18 +28,29 @@ pub enum OutEvent { }, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message0 { + pub(crate) A: bitcoin::PublicKey, + pub(crate) S_a_monero: monero::PublicKey, + pub(crate) S_a_bitcoin: bitcoin::PublicKey, + pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, + pub(crate) v_a: monero::PrivateViewKey, + pub(crate) redeem_address: bitcoin::Address, + pub(crate) punish_address: bitcoin::Address, +} + /// A `NetworkBehaviour` that represents send/recv of message 0. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message0 { +pub struct Message0Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message0 { - pub fn send(&mut self, channel: ResponseChannel, msg: xmr_btc::alice::Message0) { +impl Message0Behaviour { + pub fn send(&mut self, channel: ResponseChannel, msg: Message0) { let msg = AliceToBob::Message0(Box::new(msg)); self.rr.send_response(channel, msg); } @@ -53,7 +67,7 @@ impl Message0 { } } -impl Default for Message0 { +impl Default for Message0Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -70,7 +84,9 @@ impl Default for Message0 { } } -impl NetworkBehaviourEventProcess> for Message0 { +impl NetworkBehaviourEventProcess> + for Message0Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/swap/src/alice/message1.rs b/swap/src/protocol/alice/message1.rs similarity index 85% rename from swap/src/alice/message1.rs rename to swap/src/protocol/alice/message1.rs index cf6d2bd2..94ff9be1 100644 --- a/swap/src/alice/message1.rs +++ b/swap/src/protocol/alice/message1.rs @@ -1,3 +1,4 @@ +use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; use libp2p::{ request_response::{ handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, @@ -6,6 +7,7 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -13,8 +15,10 @@ use std::{ }; use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT}; -use xmr_btc::bob; +use crate::{ + network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT}, + protocol::bob, +}; #[derive(Debug)] pub enum OutEvent { @@ -26,18 +30,24 @@ pub enum OutEvent { }, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message1 { + pub(crate) tx_cancel_sig: Signature, + pub(crate) tx_refund_encsig: EncryptedSignature, +} + /// A `NetworkBehaviour` that represents send/recv of message 1. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message1 { +pub struct Message1Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message1 { - pub fn send(&mut self, channel: ResponseChannel, msg: xmr_btc::alice::Message1) { +impl Message1Behaviour { + pub fn send(&mut self, channel: ResponseChannel, msg: Message1) { let msg = AliceToBob::Message1(Box::new(msg)); self.rr.send_response(channel, msg); } @@ -55,7 +65,7 @@ impl Message1 { } } -impl Default for Message1 { +impl Default for Message1Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -72,7 +82,9 @@ impl Default for Message1 { } } -impl NetworkBehaviourEventProcess> for Message1 { +impl NetworkBehaviourEventProcess> + for Message1Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/swap/src/alice/message2.rs b/swap/src/protocol/alice/message2.rs similarity index 87% rename from swap/src/alice/message2.rs rename to swap/src/protocol/alice/message2.rs index bab62d3e..27e7f965 100644 --- a/swap/src/alice/message2.rs +++ b/swap/src/protocol/alice/message2.rs @@ -6,6 +6,7 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -13,8 +14,11 @@ use std::{ }; use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT}; -use xmr_btc::bob; +use crate::{ + monero, + network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT}, + protocol::bob, +}; #[derive(Debug)] pub enum OutEvent { @@ -26,18 +30,23 @@ pub enum OutEvent { }, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message2 { + pub tx_lock_proof: monero::TransferProof, +} + /// A `NetworkBehaviour` that represents receiving of message 2 from Bob. #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message2 { +pub struct Message2Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message2 { - pub fn send(&mut self, channel: ResponseChannel, msg: xmr_btc::alice::Message2) { +impl Message2Behaviour { + pub fn send(&mut self, channel: ResponseChannel, msg: Message2) { let msg = AliceToBob::Message2(msg); self.rr.send_response(channel, msg); } @@ -55,7 +64,7 @@ impl Message2 { } } -impl Default for Message2 { +impl Default for Message2Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -72,7 +81,9 @@ impl Default for Message2 { } } -impl NetworkBehaviourEventProcess> for Message2 { +impl NetworkBehaviourEventProcess> + for Message2Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/swap/src/alice/message3.rs b/swap/src/protocol/alice/message3.rs similarity index 91% rename from swap/src/alice/message3.rs rename to swap/src/protocol/alice/message3.rs index 0e4bd773..cf859685 100644 --- a/swap/src/alice/message3.rs +++ b/swap/src/protocol/alice/message3.rs @@ -13,8 +13,10 @@ use std::{ }; use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT}; -use xmr_btc::bob; +use crate::{ + network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT}, + protocol::bob, +}; #[derive(Debug)] pub enum OutEvent { @@ -25,13 +27,13 @@ pub enum OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message3 { +pub struct Message3Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message3 { +impl Message3Behaviour { fn poll( &mut self, _: &mut Context<'_>, @@ -45,7 +47,7 @@ impl Message3 { } } -impl Default for Message3 { +impl Default for Message3Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -62,7 +64,9 @@ impl Default for Message3 { } } -impl NetworkBehaviourEventProcess> for Message3 { +impl NetworkBehaviourEventProcess> + for Message3Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/xmr-btc/src/alice.rs b/swap/src/protocol/alice/state.rs similarity index 52% rename from xmr-btc/src/alice.rs rename to swap/src/protocol/alice/state.rs index ce52a320..8f4c4cf8 100644 --- a/xmr-btc/src/alice.rs +++ b/swap/src/protocol/alice/state.rs @@ -1,439 +1,25 @@ -use crate::{ - bitcoin, - bitcoin::{poll_until_block_height_is_gte, BroadcastSignedTransaction, WatchForRawTransaction}, - bob, monero, - monero::{CreateWalletForOutput, Transfer}, - transport::{ReceiveMessage, SendMessage}, - ExpiredTimelocks, -}; use anyhow::{anyhow, Result}; -use async_trait::async_trait; use ecdsa_fun::{ adaptor::{Adaptor, EncryptedSignature}, nonce::Deterministic, }; -use futures::{ - future::{select, Either}, - pin_mut, FutureExt, -}; -use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - time::Duration, -}; -use tokio::{sync::Mutex, time::timeout}; -use tracing::{error, info}; -pub mod message; -use crate::bitcoin::{ - current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, Timelock, - TransactionBlockHeight, -}; -pub use message::{Message, Message0, Message1, Message2}; - -#[derive(Debug)] -pub enum Action { - // This action also includes proving to Bob that this has happened, given that our current - // protocol requires a transfer proof to verify that the coins have been locked on Monero - LockXmr { - amount: monero::Amount, - public_spend_key: monero::PublicKey, - public_view_key: monero::PublicViewKey, - }, - RedeemBtc(bitcoin::Transaction), - CreateMoneroWalletForOutput { - spend_key: monero::PrivateKey, - view_key: monero::PrivateViewKey, - }, - CancelBtc(bitcoin::Transaction), - PunishBtc(bitcoin::Transaction), -} - -// TODO: This could be moved to the bitcoin module -#[async_trait] -pub trait ReceiveBitcoinRedeemEncsig { - async fn receive_bitcoin_redeem_encsig(&mut self) -> bitcoin::EncryptedSignature; -} - -/// Perform the on-chain protocol to swap monero and bitcoin as Alice. -/// -/// This is called post handshake, after all the keys, addresses and most of the -/// signatures have been exchanged. -/// -/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will -/// wait for Bob, the counterparty, to lock up the bitcoin. -pub fn action_generator( - network: Arc>, - bitcoin_client: Arc, - // TODO: Replace this with a new, slimmer struct? - State3 { - a, - B, - s_a, - S_b_monero, - S_b_bitcoin, - v, - xmr, - cancel_timelock, - punish_timelock, - refund_address, - redeem_address, - punish_address, - tx_lock, - tx_punish_sig_bob, - tx_cancel_sig_bob, - .. - }: State3, - bitcoin_tx_lock_timeout: u64, -) -> GenBoxed -where - N: ReceiveBitcoinRedeemEncsig + Send + 'static, - B: bitcoin::GetBlockHeight - + bitcoin::TransactionBlockHeight - + bitcoin::WatchForRawTransaction - + Send - + Sync - + 'static, -{ - #[derive(Debug)] - enum SwapFailed { - BeforeBtcLock(Reason), - AfterXmrLock(Reason), - } - - /// Reason why the swap has failed. - #[derive(Debug)] - enum Reason { - /// Bob was too slow to lock the bitcoin. - InactiveBob, - /// Bob's encrypted signature on the Bitcoin redeem transaction is - /// invalid. - InvalidEncryptedSignature, - /// The refund timelock has been reached. - BtcExpired, - } - #[derive(Debug)] - enum RefundFailed { - BtcPunishable, - /// Could not find Alice's signature on the refund transaction witness - /// stack. - BtcRefundSignature, - /// Could not recover secret `s_b` from Alice's refund transaction - /// signature. - SecretRecovery, - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - timeout( - Duration::from_secs(bitcoin_tx_lock_timeout), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; - - let tx_lock_height = bitcoin_client - .transaction_block_height(tx_lock.txid()) - .await; - let poll_until_btc_has_expired = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_lock_height + cancel_timelock, - ) - .shared(); - pin_mut!(poll_until_btc_has_expired); - - let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { - scalar: s_a.into_ed25519(), - }); - - co.yield_(Action::LockXmr { - amount: xmr, - public_spend_key: S_a + S_b_monero, - public_view_key: v.public(), - }) - .await; - - // TODO: Watch for LockXmr using watch-only wallet. Doing so will prevent Alice - // from cancelling/refunding unnecessarily. - - let tx_redeem_encsig = { - let mut guard = network.as_ref().lock().await; - let tx_redeem_encsig = match select( - guard.receive_bitcoin_redeem_encsig(), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((encsig, _)) => encsig, - Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), - }; - - tracing::debug!("select returned redeem encsig from message"); - - tx_redeem_encsig - }; - - let (signed_tx_redeem, tx_redeem_txid) = { - let adaptor = Adaptor::>::default(); - - let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); - - bitcoin::verify_encsig( - B, - s_a.into_secp256k1().into(), - &tx_redeem.digest(), - &tx_redeem_encsig, - ) - .map_err(|_| SwapFailed::AfterXmrLock(Reason::InvalidEncryptedSignature))?; - - let sig_a = a.sign(tx_redeem.digest()); - let sig_b = - adaptor.decrypt_signature(&s_a.into_secp256k1(), tx_redeem_encsig.clone()); - - let tx = tx_redeem - .add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_redeem"); - let txid = tx.txid(); - - (tx, txid) - }; - - co.yield_(Action::RedeemBtc(signed_tx_redeem)).await; - - match select( - bitcoin_client.watch_for_raw_transaction(tx_redeem_txid), - poll_until_btc_has_expired, - ) - .await - { - Either::Left(_) => {} - Either::Right(_) => return Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)), - }; - - Ok(()) - } - .await; - - if let Err(ref err) = swap_result { - error!("swap failed: {:?}", err); - } - - if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result { - let refund_result: Result<(), RefundFailed> = async { - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); - let signed_tx_cancel = { - let sig_a = a.sign(tx_cancel.digest()); - let sig_b = tx_cancel_sig_bob.clone(); - - tx_cancel - .clone() - .add_signatures(&tx_lock, (a.public(), sig_a), (B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - - bitcoin_client - .watch_for_raw_transaction(tx_cancel.txid()) - .await; - - let tx_cancel_height = bitcoin_client - .transaction_block_height(tx_cancel.txid()) - .await; - let poll_until_bob_can_be_punished = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_cancel_height + punish_timelock, - ) - .shared(); - pin_mut!(poll_until_bob_can_be_punished); - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let tx_refund_published = match select( - bitcoin_client.watch_for_raw_transaction(tx_refund.txid()), - poll_until_bob_can_be_punished, - ) - .await - { - Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(RefundFailed::BtcPunishable), - }; - - let s_a = monero::PrivateKey { - scalar: s_a.into_ed25519(), - }; - - let tx_refund_sig = tx_refund - .extract_signature_by_key(tx_refund_published, a.public()) - .map_err(|_| RefundFailed::BtcRefundSignature)?; - let tx_refund_encsig = a.encsign(S_b_bitcoin, tx_refund.digest()); - - let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) - .map_err(|_| RefundFailed::SecretRecovery)?; - let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); - - co.yield_(Action::CreateMoneroWalletForOutput { - spend_key: s_a + s_b, - view_key: v, - }) - .await; - - Ok(()) - } - .await; - - if let Err(ref err) = refund_result { - error!("refund failed: {:?}", err); - } - - // LIMITATION: When approaching the punish scenario, Bob could theoretically - // wake up in between Alice's publication of tx cancel and beat Alice's punish - // transaction with his refund transaction. Alice would then need to carry on - // with the refund on Monero. Doing so may be too verbose with the current, - // linear approach. A different design may be required - if let Err(RefundFailed::BtcPunishable) = refund_result { - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); - let tx_punish = - bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock); - let tx_punish_txid = tx_punish.txid(); - let signed_tx_punish = { - let sig_a = a.sign(tx_punish.digest()); - let sig_b = tx_punish_sig_bob; - - tx_punish - .add_signatures(&tx_cancel, (a.public(), sig_a), (B, sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::PunishBtc(signed_tx_punish)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_punish_txid) - .await; - } - } - }) -} +use tracing::info; -// There are no guarantees that send_message and receive_massage do not block -// the flow of execution. Therefore they must be paired between Alice/Bob, one -// send to one receive in the correct order. -pub async fn next_state< - R: RngCore + CryptoRng, - B: WatchForRawTransaction + BroadcastSignedTransaction, - M: CreateWalletForOutput + Transfer, - T: SendMessage + ReceiveMessage, ->( - bitcoin_wallet: &B, - monero_wallet: &M, - transport: &mut T, - state: State, - rng: &mut R, -) -> Result { - match state { - State::State0(state0) => { - let alice_message0 = state0.next_message(rng).into(); - - let bob_message0 = transport.receive_message().await?.try_into()?; - transport.send_message(alice_message0).await?; - - let state1 = state0.receive(bob_message0)?; - Ok(state1.into()) - } - State::State1(state1) => { - let bob_message1 = transport.receive_message().await?.try_into()?; - let state2 = state1.receive(bob_message1); - let alice_message1 = state2.next_message(); - transport.send_message(alice_message1.into()).await?; - Ok(state2.into()) - } - State::State2(state2) => { - let bob_message2 = transport.receive_message().await?.try_into()?; - let state3 = state2.receive(bob_message2)?; - Ok(state3.into()) - } - State::State3(state3) => { - tracing::info!("alice is watching for locked btc"); - let state4 = state3.watch_for_lock_btc(bitcoin_wallet).await?; - Ok(state4.into()) - } - State::State4(state4) => { - let state5 = state4.lock_xmr(monero_wallet).await?; - tracing::info!("alice has locked xmr"); - Ok(state5.into()) - } - State::State5(state5) => { - transport.send_message(state5.next_message().into()).await?; - // todo: pass in state4b as a parameter somewhere in this call to prevent the - // user from waiting for a message that wont be sent - let message3 = transport.receive_message().await?.try_into()?; - let state6 = state5.receive(message3); - tracing::info!("alice has received bob message 3"); - tracing::info!("alice is redeeming btc"); - state6.redeem_btc(bitcoin_wallet).await?; - Ok(state6.into()) - } - State::State6(state6) => Ok((*state6).into()), - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub enum State { - State0(State0), - State1(State1), - State2(State2), - State3(State3), - State4(State4), - State5(State5), - State6(Box), -} - -impl_try_from_parent_enum!(State0, State); -impl_try_from_parent_enum!(State1, State); -impl_try_from_parent_enum!(State2, State); -impl_try_from_parent_enum!(State3, State); -impl_try_from_parent_enum!(State4, State); -impl_try_from_parent_enum!(State5, State); -impl_try_from_parent_enum_for_boxed!(State6, State); - -impl_from_child_enum!(State0, State); -impl_from_child_enum!(State1, State); -impl_from_child_enum!(State2, State); -impl_from_child_enum!(State3, State); -impl_from_child_enum!(State4, State); -impl_from_child_enum!(State5, State); -impl_from_child_enum_for_boxed!(State6, State); - -impl State { - pub fn new( - rng: &mut R, - btc: bitcoin::Amount, - xmr: monero::Amount, - cancel_timelock: Timelock, - punish_timelock: Timelock, - redeem_address: bitcoin::Address, - punish_address: bitcoin::Address, - ) -> Self { - let a = bitcoin::SecretKey::new_random(rng); - let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = monero::PrivateViewKey::new_random(rng); - - Self::State0(State0::new( - a, - s_a, - v_a, - btc, - xmr, - cancel_timelock, - punish_timelock, - redeem_address, - punish_address, - )) - } -} +use crate::{ + bitcoin, + bitcoin::{ + current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, GetBlockHeight, + TransactionBlockHeight, WatchForRawTransaction, + }, + monero, + monero::CreateWalletForOutput, + protocol::{alice, bob}, + ExpiredTimelocks, +}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct State0 { @@ -475,11 +61,11 @@ impl State0 { } } - pub fn next_message(&self, rng: &mut R) -> Message0 { + pub fn next_message(&self, rng: &mut R) -> alice::Message0 { info!("Producing first message"); let dleq_proof_s_a = cross_curve_dleq::Proof::new(rng, &self.s_a); - Message0 { + alice::Message0 { A: self.a.public(), S_a_monero: monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a.into_ed25519(), @@ -580,7 +166,7 @@ pub struct State2 { } impl State2 { - pub fn next_message(&self) -> Message1 { + pub fn next_message(&self) -> alice::Message1 { let tx_cancel = bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B); @@ -593,7 +179,7 @@ impl State2 { let tx_refund_encsig = self.a.encsign(self.S_b_bitcoin, tx_refund.digest()); let tx_cancel_sig = self.a.sign(tx_cancel.digest()); - Message1 { + alice::Message1 { tx_refund_encsig, tx_cancel_sig, } @@ -831,8 +417,8 @@ pub struct State5 { } impl State5 { - pub fn next_message(&self) -> Message2 { - Message2 { + pub fn next_message(&self) -> alice::Message2 { + alice::Message2 { tx_lock_proof: self.tx_lock_proof.clone(), } } diff --git a/swap/src/alice/steps.rs b/swap/src/protocol/alice/steps.rs similarity index 94% rename from swap/src/alice/steps.rs rename to swap/src/protocol/alice/steps.rs index ca861ae3..9afc6bb0 100644 --- a/swap/src/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -1,7 +1,3 @@ -use crate::{ - alice::event_loop::EventLoopHandle, bitcoin, monero, network::request_response::AliceToBob, - SwapAmounts, -}; use anyhow::{bail, Context, Result}; use ecdsa_fun::{adaptor::Adaptor, nonce::Deterministic}; use futures::{ @@ -14,25 +10,30 @@ use sha2::Sha256; use std::{sync::Arc, time::Duration}; use tokio::time::timeout; use tracing::{info, trace}; -use xmr_btc::{ - alice, - alice::State3, + +use crate::{ + bitcoin, bitcoin::{ - poll_until_block_height_is_gte, BlockHeight, BroadcastSignedTransaction, - EncryptedSignature, GetBlockHeight, GetRawTransaction, Timelock, TransactionBlockHeight, - TxCancel, TxLock, TxRefund, WaitForTransactionFinality, WatchForRawTransaction, + poll_until_block_height_is_gte, + timelocks::{BlockHeight, Timelock}, + BroadcastSignedTransaction, EncryptedSignature, GetBlockHeight, GetRawTransaction, + TransactionBlockHeight, TxCancel, TxLock, TxRefund, WaitForTransactionFinality, + WatchForRawTransaction, }, config::Config, - cross_curve_dleq, + monero, monero::Transfer, + network::request_response::AliceToBob, + protocol::{alice, alice::event_loop::EventLoopHandle}, + SwapAmounts, }; pub async fn negotiate( - state0: xmr_btc::alice::State0, + state0: alice::State0, amounts: SwapAmounts, event_loop_handle: &mut EventLoopHandle, config: Config, -) -> Result<(ResponseChannel, State3)> { +) -> Result<(ResponseChannel, alice::State3)> { trace!("Starting negotiate"); // todo: we can move this out, we dont need to timeout here @@ -115,7 +116,7 @@ where pub async fn lock_xmr( channel: ResponseChannel, amounts: SwapAmounts, - state3: State3, + state3: alice::State3, event_loop_handle: &mut EventLoopHandle, monero_wallet: Arc, ) -> Result<()> diff --git a/swap/src/alice/swap.rs b/swap/src/protocol/alice/swap.rs similarity index 98% rename from swap/src/alice/swap.rs rename to swap/src/protocol/alice/swap.rs index d77f2187..4058de51 100644 --- a/swap/src/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -1,20 +1,5 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. -use crate::{ - alice::{ - event_loop::EventLoopHandle, - steps::{ - build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, - extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction, - publish_bitcoin_redeem_transaction, publish_cancel_transaction, - wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, - }, - }, - bitcoin::EncryptedSignature, - database::{Database, Swap}, - network::request_response::AliceToBob, - SwapAmounts, -}; use anyhow::Result; use async_recursion::async_recursion; use futures::{ @@ -26,12 +11,28 @@ use rand::{CryptoRng, RngCore}; use std::{fmt, sync::Arc}; use tracing::info; use uuid::Uuid; -use xmr_btc::{ - alice::{State0, State3}, - bitcoin::{TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction}, + +use crate::{ + bitcoin, + bitcoin::{ + EncryptedSignature, TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction, + }, config::Config, + database::{Database, Swap}, + monero, monero::CreateWalletForOutput, - ExpiredTimelocks, + network::request_response::AliceToBob, + protocol::alice::{ + event_loop::EventLoopHandle, + state::{State0, State3}, + steps::{ + build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, + extract_monero_private_key, lock_xmr, negotiate, publish_bitcoin_punish_transaction, + publish_bitcoin_redeem_transaction, publish_cancel_transaction, + wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin, + }, + }, + ExpiredTimelocks, SwapAmounts, }; trait Rng: RngCore + CryptoRng + Send {} @@ -105,8 +106,8 @@ impl fmt::Display for AliceState { pub async fn swap( state: AliceState, event_loop_handle: EventLoopHandle, - bitcoin_wallet: Arc, - monero_wallet: Arc, + bitcoin_wallet: Arc, + monero_wallet: Arc, config: Config, swap_id: Uuid, db: Database, diff --git a/swap/src/bob.rs b/swap/src/protocol/bob.rs similarity index 90% rename from swap/src/bob.rs rename to swap/src/protocol/bob.rs index 33917b57..9ba62485 100644 --- a/swap/src/bob.rs +++ b/swap/src/protocol/bob.rs @@ -1,24 +1,22 @@ //! 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::{ - network::{ - peer_tracker::{self, PeerTracker}, - transport::SwapTransport, - TokioExecutor, - }, - SwapAmounts, -}; +pub use self::{amounts::*, message0::*, message1::*, message2::*, message3::*, state::*}; use anyhow::Result; use libp2p::{ core::{identity::Keypair, Multiaddr}, NetworkBehaviour, PeerId, }; use tracing::{debug, info}; -use xmr_btc::{ - alice, + +use crate::{ bitcoin::EncryptedSignature, - bob::{self}, + network::{ + peer_tracker::{self, PeerTracker}, + transport::SwapTransport, + TokioExecutor, + }, + protocol::{alice, bob}, + SwapAmounts, }; mod amounts; @@ -27,6 +25,7 @@ mod message0; mod message1; mod message2; mod message3; +pub mod state; pub mod swap; pub type Swarm = libp2p::Swarm; @@ -112,10 +111,10 @@ impl From for OutEvent { pub struct Behaviour { pt: PeerTracker, amounts: Amounts, - message0: Message0, - message1: Message1, - message2: Message2, - message3: Message3, + message0: Message0Behaviour, + message1: Message1Behaviour, + message2: Message2Behaviour, + message3: Message3Behaviour, #[behaviour(ignore)] identity: Keypair, } @@ -174,10 +173,10 @@ impl Default for Behaviour { Self { pt: PeerTracker::default(), amounts: Amounts::default(), - message0: Message0::default(), - message1: Message1::default(), - message2: Message2::default(), - message3: Message3::default(), + message0: Message0Behaviour::default(), + message1: Message1Behaviour::default(), + message2: Message2Behaviour::default(), + message3: Message3Behaviour::default(), identity, } } diff --git a/swap/src/bob/amounts.rs b/swap/src/protocol/bob/amounts.rs similarity index 100% rename from swap/src/bob/amounts.rs rename to swap/src/protocol/bob/amounts.rs diff --git a/swap/src/bob/event_loop.rs b/swap/src/protocol/bob/event_loop.rs similarity index 98% rename from swap/src/bob/event_loop.rs rename to swap/src/protocol/bob/event_loop.rs index 80a3fac9..b193a5f2 100644 --- a/swap/src/bob/event_loop.rs +++ b/swap/src/protocol/bob/event_loop.rs @@ -1,7 +1,3 @@ -use crate::{ - bob::{Behaviour, OutEvent}, - network::{transport::SwapTransport, TokioExecutor}, -}; use anyhow::{anyhow, Result}; use futures::FutureExt; use libp2p::{core::Multiaddr, PeerId}; @@ -10,7 +6,15 @@ use tokio::{ sync::mpsc::{Receiver, Sender}, }; use tracing::{debug, error, info}; -use xmr_btc::{alice, bitcoin::EncryptedSignature, bob}; + +use crate::{ + bitcoin::EncryptedSignature, + network::{transport::SwapTransport, TokioExecutor}, + protocol::{ + alice, + bob::{self, Behaviour, OutEvent}, + }, +}; #[derive(Debug)] pub struct Channels { diff --git a/swap/src/bob/message0.rs b/swap/src/protocol/bob/message0.rs similarity index 81% rename from swap/src/bob/message0.rs rename to swap/src/protocol/bob/message0.rs index 16ae9588..5bbfd3b2 100644 --- a/swap/src/bob/message0.rs +++ b/swap/src/protocol/bob/message0.rs @@ -6,6 +6,7 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, PeerId, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -13,8 +14,21 @@ use std::{ }; use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT}; -use xmr_btc::{alice, bob}; +use crate::{ + bitcoin, monero, + network::request_response::{AliceToBob, BobToAlice, Codec, Message0Protocol, TIMEOUT}, + protocol::{alice, bob}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message0 { + pub(crate) B: bitcoin::PublicKey, + pub(crate) S_b_monero: monero::PublicKey, + pub(crate) S_b_bitcoin: bitcoin::PublicKey, + pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, + pub(crate) v_b: monero::PrivateViewKey, + pub(crate) refund_address: bitcoin::Address, +} #[derive(Debug)] pub enum OutEvent { @@ -25,13 +39,13 @@ pub enum OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message0 { +pub struct Message0Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message0 { +impl Message0Behaviour { pub fn send(&mut self, alice: PeerId, msg: bob::Message0) { let msg = BobToAlice::Message0(Box::new(msg)); let _id = self.rr.send_request(&alice, msg); @@ -50,7 +64,7 @@ impl Message0 { } } -impl Default for Message0 { +impl Default for Message0Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -67,7 +81,9 @@ impl Default for Message0 { } } -impl NetworkBehaviourEventProcess> for Message0 { +impl NetworkBehaviourEventProcess> + for Message0Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/swap/src/bob/message1.rs b/swap/src/protocol/bob/message1.rs similarity index 85% rename from swap/src/bob/message1.rs rename to swap/src/protocol/bob/message1.rs index 62696180..1701d03b 100644 --- a/swap/src/bob/message1.rs +++ b/swap/src/protocol/bob/message1.rs @@ -6,15 +6,25 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, PeerId, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, time::Duration, }; + use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT}; -use xmr_btc::{alice, bob}; +use crate::{ + bitcoin, + network::request_response::{AliceToBob, BobToAlice, Codec, Message1Protocol, TIMEOUT}, + protocol::alice, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message1 { + pub(crate) tx_lock: bitcoin::TxLock, +} #[derive(Debug)] pub enum OutEvent { @@ -25,14 +35,14 @@ pub enum OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message1 { +pub struct Message1Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message1 { - pub fn send(&mut self, alice: PeerId, msg: bob::Message1) { +impl Message1Behaviour { + pub fn send(&mut self, alice: PeerId, msg: Message1) { let msg = BobToAlice::Message1(msg); let _id = self.rr.send_request(&alice, msg); } @@ -50,7 +60,7 @@ impl Message1 { } } -impl Default for Message1 { +impl Default for Message1Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -67,7 +77,9 @@ impl Default for Message1 { } } -impl NetworkBehaviourEventProcess> for Message1 { +impl NetworkBehaviourEventProcess> + for Message1Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/swap/src/bob/message2.rs b/swap/src/protocol/bob/message2.rs similarity index 84% rename from swap/src/bob/message2.rs rename to swap/src/protocol/bob/message2.rs index 17425227..11f0acb1 100644 --- a/swap/src/bob/message2.rs +++ b/swap/src/protocol/bob/message2.rs @@ -1,3 +1,4 @@ +use ecdsa_fun::Signature; use libp2p::{ request_response::{ handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig, @@ -6,15 +7,25 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, PeerId, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, time::Duration, }; + use tracing::{debug, error}; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT}; -use xmr_btc::{alice, bob}; +use crate::{ + network::request_response::{AliceToBob, BobToAlice, Codec, Message2Protocol, TIMEOUT}, + protocol::alice, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message2 { + pub(crate) tx_punish_sig: Signature, + pub(crate) tx_cancel_sig: Signature, +} #[derive(Debug)] pub enum OutEvent { @@ -25,14 +36,14 @@ pub enum OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message2 { +pub struct Message2Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message2 { - pub fn send(&mut self, alice: PeerId, msg: bob::Message2) { +impl Message2Behaviour { + pub fn send(&mut self, alice: PeerId, msg: Message2) { let msg = BobToAlice::Message2(msg); let _id = self.rr.send_request(&alice, msg); } @@ -50,7 +61,7 @@ impl Message2 { } } -impl Default for Message2 { +impl Default for Message2Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -67,7 +78,9 @@ impl Default for Message2 { } } -impl NetworkBehaviourEventProcess> for Message2 { +impl NetworkBehaviourEventProcess> + for Message2Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/swap/src/bob/message3.rs b/swap/src/protocol/bob/message3.rs similarity index 85% rename from swap/src/bob/message3.rs rename to swap/src/protocol/bob/message3.rs index e53c08e9..9c9ed4fd 100644 --- a/swap/src/bob/message3.rs +++ b/swap/src/protocol/bob/message3.rs @@ -6,6 +6,7 @@ use libp2p::{ swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters}, NetworkBehaviour, PeerId, }; +use serde::{Deserialize, Serialize}; use std::{ collections::VecDeque, task::{Context, Poll}, @@ -13,8 +14,15 @@ use std::{ }; use tracing::error; -use crate::network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT}; -use xmr_btc::bob; +use crate::{ + bitcoin::EncryptedSignature, + network::request_response::{AliceToBob, BobToAlice, Codec, Message3Protocol, TIMEOUT}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message3 { + pub tx_redeem_encsig: EncryptedSignature, +} #[derive(Debug, Copy, Clone)] pub enum OutEvent { @@ -25,14 +33,14 @@ pub enum OutEvent { #[derive(NetworkBehaviour)] #[behaviour(out_event = "OutEvent", poll_method = "poll")] #[allow(missing_debug_implementations)] -pub struct Message3 { +pub struct Message3Behaviour { rr: RequestResponse>, #[behaviour(ignore)] events: VecDeque, } -impl Message3 { - pub fn send(&mut self, alice: PeerId, msg: bob::Message3) { +impl Message3Behaviour { + pub fn send(&mut self, alice: PeerId, msg: Message3) { let msg = BobToAlice::Message3(msg); let _id = self.rr.send_request(&alice, msg); } @@ -50,7 +58,7 @@ impl Message3 { } } -impl Default for Message3 { +impl Default for Message3Behaviour { fn default() -> Self { let timeout = Duration::from_secs(TIMEOUT); let mut config = RequestResponseConfig::default(); @@ -67,7 +75,9 @@ impl Default for Message3 { } } -impl NetworkBehaviourEventProcess> for Message3 { +impl NetworkBehaviourEventProcess> + for Message3Behaviour +{ fn inject_event(&mut self, event: RequestResponseEvent) { match event { RequestResponseEvent::Message { diff --git a/xmr-btc/src/bob.rs b/swap/src/protocol/bob/state.rs similarity index 61% rename from xmr-btc/src/bob.rs rename to swap/src/protocol/bob/state.rs index fe8e1e18..7e01424c 100644 --- a/xmr-btc/src/bob.rs +++ b/swap/src/protocol/bob/state.rs @@ -1,352 +1,25 @@ -use crate::{ - alice, - bitcoin::{ - self, poll_until_block_height_is_gte, BroadcastSignedTransaction, BuildTxLockPsbt, - SignTxLock, TxCancel, WatchForRawTransaction, - }, - monero, - serde::monero_private_key, - transport::{ReceiveMessage, SendMessage}, - ExpiredTimelocks, -}; +use ::bitcoin::{Transaction, Txid}; use anyhow::{anyhow, Result}; -use async_trait::async_trait; use ecdsa_fun::{ adaptor::{Adaptor, EncryptedSignature}, nonce::Deterministic, Signature, }; -use futures::{ - future::{select, Either}, - pin_mut, FutureExt, -}; -use genawaiter::sync::{Gen, GenBoxed}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - time::Duration, -}; -use tokio::{sync::Mutex, time::timeout}; -use tracing::error; -pub mod message; use crate::{ bitcoin::{ - current_epoch, wait_for_cancel_timelock_to_expire, GetBlockHeight, GetRawTransaction, - Network, Timelock, TransactionBlockHeight, + self, current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, + BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, GetRawTransaction, Network, + TransactionBlockHeight, TxCancel, WatchForRawTransaction, }, - monero::{CreateWalletForOutput, WatchForTransfer}, + monero, + protocol::{alice, bob}, + serde::monero_private_key, + ExpiredTimelocks, }; -use ::bitcoin::{Transaction, Txid}; -pub use message::{Message, Message0, Message1, Message2, Message3}; - -#[derive(Debug)] -pub enum Action { - LockBtc(bitcoin::TxLock), - SendBtcRedeemEncsig(bitcoin::EncryptedSignature), - CreateXmrWalletForOutput { - spend_key: monero::PrivateKey, - view_key: monero::PrivateViewKey, - }, - CancelBtc(bitcoin::Transaction), - RefundBtc(bitcoin::Transaction), -} - -// TODO: This could be moved to the monero module -#[async_trait] -pub trait ReceiveTransferProof { - async fn receive_transfer_proof(&mut self) -> monero::TransferProof; -} - -/// Perform the on-chain protocol to swap monero and bitcoin as Bob. -/// -/// This is called post handshake, after all the keys, addresses and most of the -/// signatures have been exchanged. -/// -/// The argument `bitcoin_tx_lock_timeout` is used to determine how long we will -/// wait for Bob, the caller of this function, to lock up the bitcoin. -pub fn action_generator( - network: Arc>, - monero_client: Arc, - bitcoin_client: Arc, - // TODO: Replace this with a new, slimmer struct? - State2 { - A, - b, - s_b, - S_a_monero, - S_a_bitcoin, - v, - xmr, - cancel_timelock, - redeem_address, - refund_address, - tx_lock, - tx_cancel_sig_a, - tx_refund_encsig, - .. - }: State2, - bitcoin_tx_lock_timeout: u64, -) -> GenBoxed -where - N: ReceiveTransferProof + Send + 'static, - M: monero::WatchForTransfer + Send + Sync + 'static, - B: bitcoin::GetBlockHeight - + bitcoin::TransactionBlockHeight - + bitcoin::WatchForRawTransaction - + Send - + Sync - + 'static, -{ - #[derive(Debug)] - enum SwapFailed { - BeforeBtcLock(Reason), - AfterBtcLock(Reason), - AfterBtcRedeem(Reason), - } - - /// Reason why the swap has failed. - #[derive(Debug)] - enum Reason { - /// Bob was too slow to lock the bitcoin. - InactiveBob, - /// The refund timelock has been reached. - BtcExpired, - /// Alice did not lock up enough monero in the shared output. - InsufficientXmr(monero::InsufficientFunds), - /// Could not find Bob's signature on the redeem transaction witness - /// stack. - BtcRedeemSignature, - /// Could not recover secret `s_a` from Bob's redeem transaction - /// signature. - SecretRecovery, - } - - Gen::new_boxed(|co| async move { - let swap_result: Result<(), SwapFailed> = async { - co.yield_(Action::LockBtc(tx_lock.clone())).await; - - timeout( - Duration::from_secs(bitcoin_tx_lock_timeout), - bitcoin_client.watch_for_raw_transaction(tx_lock.txid()), - ) - .await - .map(|tx| tx.txid()) - .map_err(|_| SwapFailed::BeforeBtcLock(Reason::InactiveBob))?; - - let tx_lock_height = bitcoin_client - .transaction_block_height(tx_lock.txid()) - .await; - let poll_until_btc_has_expired = poll_until_block_height_is_gte( - bitcoin_client.as_ref(), - tx_lock_height + cancel_timelock, - ) - .shared(); - pin_mut!(poll_until_btc_has_expired); - - let transfer_proof = { - let mut guard = network.as_ref().lock().await; - let transfer_proof = match select( - guard.receive_transfer_proof(), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((proof, _)) => proof, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - tracing::debug!("select returned transfer proof from message"); - - transfer_proof - }; - - let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar( - s_b.into_ed25519(), - )); - let S = S_a_monero + S_b_monero; - - match select( - monero_client.watch_for_transfer(S, v.public(), transfer_proof, xmr, 0), - poll_until_btc_has_expired.clone(), - ) - .await - { - Either::Left((Err(e), _)) => { - return Err(SwapFailed::AfterBtcLock(Reason::InsufficientXmr(e))) - } - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - _ => {} - } - - let tx_redeem = bitcoin::TxRedeem::new(&tx_lock, &redeem_address); - let tx_redeem_encsig = b.encsign(S_a_bitcoin, tx_redeem.digest()); - - co.yield_(Action::SendBtcRedeemEncsig(tx_redeem_encsig.clone())) - .await; - - let tx_redeem_published = match select( - bitcoin_client.watch_for_raw_transaction(tx_redeem.txid()), - poll_until_btc_has_expired, - ) - .await - { - Either::Left((tx, _)) => tx, - Either::Right(_) => return Err(SwapFailed::AfterBtcLock(Reason::BtcExpired)), - }; - - let tx_redeem_sig = tx_redeem - .extract_signature_by_key(tx_redeem_published, b.public()) - .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::BtcRedeemSignature))?; - let s_a = bitcoin::recover(S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig) - .map_err(|_| SwapFailed::AfterBtcRedeem(Reason::SecretRecovery))?; - let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); - - let s_b = monero::PrivateKey { - scalar: s_b.into_ed25519(), - }; - - co.yield_(Action::CreateXmrWalletForOutput { - spend_key: s_a + s_b, - view_key: v, - }) - .await; - - Ok(()) - } - .await; - - if let Err(ref err) = swap_result { - error!("swap failed: {:?}", err); - } - - if let Err(SwapFailed::AfterBtcLock(_)) = swap_result { - let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, A, b.public()); - let tx_cancel_txid = tx_cancel.txid(); - let signed_tx_cancel = { - let sig_a = tx_cancel_sig_a.clone(); - let sig_b = b.sign(tx_cancel.digest()); - - tx_cancel - .clone() - .add_signatures(&tx_lock, (A, sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_cancel") - }; - - co.yield_(Action::CancelBtc(signed_tx_cancel)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_cancel_txid) - .await; - - let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &refund_address); - let tx_refund_txid = tx_refund.txid(); - let signed_tx_refund = { - let adaptor = Adaptor::>::default(); - - let sig_a = - adaptor.decrypt_signature(&s_b.into_secp256k1(), tx_refund_encsig.clone()); - let sig_b = b.sign(tx_refund.digest()); - - tx_refund - .add_signatures(&tx_cancel, (A, sig_a), (b.public(), sig_b)) - .expect("sig_{a,b} to be valid signatures for tx_refund") - }; - - co.yield_(Action::RefundBtc(signed_tx_refund)).await; - - let _ = bitcoin_client - .watch_for_raw_transaction(tx_refund_txid) - .await; - } - }) -} - -// There are no guarantees that send_message and receive_massage do not block -// the flow of execution. Therefore they must be paired between Alice/Bob, one -// send to one receive in the correct order. -pub async fn next_state< - R: RngCore + CryptoRng, - B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction + Network, - M: CreateWalletForOutput + WatchForTransfer, - T: SendMessage + ReceiveMessage, ->( - bitcoin_wallet: &B, - monero_wallet: &M, - transport: &mut T, - state: State, - rng: &mut R, -) -> Result { - match state { - State::State0(state0) => { - transport - .send_message(state0.next_message(rng).into()) - .await?; - let message0 = transport.receive_message().await?.try_into()?; - let state1 = state0.receive(bitcoin_wallet, message0).await?; - Ok(state1.into()) - } - State::State1(state1) => { - transport.send_message(state1.next_message().into()).await?; - - let message1 = transport.receive_message().await?.try_into()?; - let state2 = state1.receive(message1)?; - - let message2 = state2.next_message(); - transport.send_message(message2.into()).await?; - Ok(state2.into()) - } - State::State2(state2) => { - let state3 = state2.lock_btc(bitcoin_wallet).await?; - tracing::info!("bob has locked btc"); - - Ok(state3.into()) - } - State::State3(state3) => { - let message2 = transport.receive_message().await?.try_into()?; - let state4 = state3.watch_for_lock_xmr(monero_wallet, message2).await?; - tracing::info!("bob has seen that alice has locked xmr"); - Ok(state4.into()) - } - State::State4(state4) => { - transport.send_message(state4.next_message().into()).await?; - tracing::info!("bob is watching for redeem_btc"); - let state5 = state4.watch_for_redeem_btc(bitcoin_wallet).await?; - tracing::info!("bob has seen that alice has redeemed btc"); - state5.claim_xmr(monero_wallet).await?; - tracing::info!("bob has claimed xmr"); - Ok(state5.into()) - } - State::State5(state5) => Ok(state5.into()), - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub enum State { - State0(State0), - State1(State1), - State2(State2), - State3(State3), - State4(State4), - State5(State5), -} - -impl_try_from_parent_enum!(State0, State); -impl_try_from_parent_enum!(State1, State); -impl_try_from_parent_enum!(State2, State); -impl_try_from_parent_enum!(State3, State); -impl_try_from_parent_enum!(State4, State); -impl_try_from_parent_enum!(State5, State); - -impl_from_child_enum!(State0, State); -impl_from_child_enum!(State1, State); -impl_from_child_enum!(State2, State); -impl_from_child_enum!(State3, State); -impl_from_child_enum!(State4, State); -impl_from_child_enum!(State5, State); #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct State0 { @@ -390,10 +63,10 @@ impl State0 { } } - pub fn next_message(&self, rng: &mut R) -> Message0 { + pub fn next_message(&self, rng: &mut R) -> bob::Message0 { let dleq_proof_s_b = cross_curve_dleq::Proof::new(rng, &self.s_b); - Message0 { + bob::Message0 { B: self.b.public(), S_b_monero: monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_b.into_ed25519(), @@ -461,8 +134,8 @@ pub struct State1 { } impl State1 { - pub fn next_message(&self) -> Message1 { - Message1 { + pub fn next_message(&self) -> bob::Message1 { + bob::Message1 { tx_lock: self.tx_lock.clone(), } } @@ -524,14 +197,14 @@ pub struct State2 { } impl State2 { - pub fn next_message(&self) -> Message2 { + pub fn next_message(&self) -> bob::Message2 { let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public()); let tx_cancel_sig = self.b.sign(tx_cancel.digest()); let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &self.punish_address, self.punish_timelock); let tx_punish_sig = self.b.sign(tx_punish.digest()); - Message2 { + bob::Message2 { tx_punish_sig, tx_cancel_sig, } @@ -705,11 +378,11 @@ pub struct State4 { } impl State4 { - pub fn next_message(&self) -> Message3 { + pub fn next_message(&self) -> bob::Message3 { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address); let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()); - Message3 { tx_redeem_encsig } + bob::Message3 { tx_redeem_encsig } } pub fn tx_redeem_encsig(&self) -> EncryptedSignature { diff --git a/swap/src/bob/swap.rs b/swap/src/protocol/bob/swap.rs similarity index 90% rename from swap/src/bob/swap.rs rename to swap/src/protocol/bob/swap.rs index 223f29c1..b3bea27e 100644 --- a/swap/src/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,4 +1,3 @@ -use crate::{bob::event_loop::EventLoopHandle, database, database::Database, SwapAmounts}; use anyhow::{bail, Result}; use async_recursion::async_recursion; use rand::{CryptoRng, RngCore}; @@ -6,25 +5,28 @@ use std::{fmt, sync::Arc}; use tokio::select; use tracing::info; use uuid::Uuid; -use xmr_btc::{ - bob::{self, State2}, - ExpiredTimelocks, + +use crate::{ + config::Config, + database::{Database, Swap}, + protocol::bob::{self, event_loop::EventLoopHandle, state::*}, + ExpiredTimelocks, SwapAmounts, }; #[derive(Debug, Clone)] pub enum BobState { Started { - state0: bob::State0, + state0: State0, amounts: SwapAmounts, }, - Negotiated(bob::State2), - BtcLocked(bob::State3), - XmrLocked(bob::State4), - EncSigSent(bob::State4), - BtcRedeemed(bob::State5), - CancelTimelockExpired(bob::State4), - BtcCancelled(bob::State4), - BtcRefunded(bob::State4), + Negotiated(State2), + BtcLocked(State3), + XmrLocked(State4), + EncSigSent(State4), + BtcRedeemed(State5), + CancelTimelockExpired(State4), + BtcCancelled(State4), + BtcRefunded(State4), XmrRedeemed, BtcPunished, SafelyAborted, @@ -133,8 +135,7 @@ where let state = BobState::Negotiated(state2); let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -155,8 +156,7 @@ where let state = BobState::BtcLocked(state3); let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -209,8 +209,7 @@ where BobState::CancelTimelockExpired(state4) }; let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -251,8 +250,7 @@ where BobState::CancelTimelockExpired(state) }; let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -287,8 +285,7 @@ where }; let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -307,8 +304,7 @@ where let state = BobState::XmrRedeemed; let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -331,7 +327,7 @@ where } let state = BobState::BtcCancelled(state4); - db.insert_latest_state(swap_id, database::Swap::Bob(state.clone().into())) + db.insert_latest_state(swap_id, Swap::Bob(state.clone().into())) .await?; run_until( @@ -360,8 +356,7 @@ where }; let db_state = state.clone().into(); - db.insert_latest_state(swap_id, database::Swap::Bob(db_state)) - .await?; + db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?; run_until( state, is_target_state, @@ -383,12 +378,12 @@ where } pub async fn negotiate( - state0: xmr_btc::bob::State0, + state0: crate::protocol::bob::state::State0, amounts: SwapAmounts, swarm: &mut EventLoopHandle, mut rng: R, bitcoin_wallet: Arc, -) -> Result +) -> Result where R: RngCore + CryptoRng + Send, { diff --git a/xmr-btc/src/serde.rs b/swap/src/serde.rs similarity index 100% rename from xmr-btc/src/serde.rs rename to swap/src/serde.rs diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index 874378e1..899e1e41 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -6,11 +6,15 @@ use futures::{ use get_port::get_port; use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{alice, bob}; +use swap::{ + bitcoin, + config::Config, + monero, + protocol::{alice, bob}, +}; use testcontainers::clients::Cli; use testutils::init_tracing; use uuid::Uuid; -use xmr_btc::{bitcoin, config::Config}; pub mod testutils; @@ -35,9 +39,9 @@ async fn happy_path() { // this xmr value matches the logic of alice::calculate_amounts i.e. btc * // 10_000 * 100 - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); let xmr_alice = xmr_to_swap * 10; - let xmr_bob = xmr_btc::monero::Amount::ZERO; + let xmr_bob = monero::Amount::ZERO; let port = get_port().expect("Failed to find a free port"); let alice_multiaddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/{}", port) diff --git a/swap/tests/happy_path_restart_alice.rs b/swap/tests/happy_path_restart_alice.rs index bb7b2bab..5ac972ca 100644 --- a/swap/tests/happy_path_restart_alice.rs +++ b/swap/tests/happy_path_restart_alice.rs @@ -2,12 +2,17 @@ use crate::testutils::{init_alice, init_bob}; use get_port::get_port; use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{alice, alice::swap::AliceState, bitcoin, bob, database::Database}; +use swap::{ + bitcoin, + config::Config, + database::Database, + monero, + protocol::{alice, alice::swap::AliceState, bob}, +}; use tempfile::tempdir; use testcontainers::clients::Cli; use testutils::init_tracing; use uuid::Uuid; -use xmr_btc::config::Config; pub mod testutils; @@ -25,7 +30,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { ) = testutils::init_containers(&cli).await; let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); let bob_btc_starting_balance = btc_to_swap * 10; let alice_xmr_starting_balance = xmr_to_swap * 10; diff --git a/swap/tests/happy_path_restart_bob_after_comm.rs b/swap/tests/happy_path_restart_bob_after_comm.rs index 5b9c6007..80f34f52 100644 --- a/swap/tests/happy_path_restart_bob_after_comm.rs +++ b/swap/tests/happy_path_restart_bob_after_comm.rs @@ -2,12 +2,17 @@ use crate::testutils::{init_alice, init_bob}; use get_port::get_port; use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{alice, bitcoin, bob, bob::swap::BobState, database::Database}; +use swap::{ + bitcoin, + config::Config, + database::Database, + monero, + protocol::{alice, bob, bob::swap::BobState}, +}; use tempfile::tempdir; use testcontainers::clients::Cli; use testutils::init_tracing; use uuid::Uuid; -use xmr_btc::config::Config; pub mod testutils; @@ -25,7 +30,7 @@ async fn given_bob_restarts_after_encsig_is_sent_resume_swap() { ) = testutils::init_containers(&cli).await; let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); let bob_btc_starting_balance = btc_to_swap * 10; let alice_xmr_starting_balance = xmr_to_swap * 10; diff --git a/swap/tests/happy_path_restart_bob_before_comm.rs b/swap/tests/happy_path_restart_bob_before_comm.rs index c6d24634..2694d053 100644 --- a/swap/tests/happy_path_restart_bob_before_comm.rs +++ b/swap/tests/happy_path_restart_bob_before_comm.rs @@ -2,13 +2,18 @@ use crate::testutils::{init_alice, init_bob}; use get_port::get_port; use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database}; +use swap::{ + bitcoin, + config::Config, + database::Database, + monero, + protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState}, +}; use tempfile::tempdir; use testcontainers::clients::Cli; use testutils::init_tracing; use tokio::select; use uuid::Uuid; -use xmr_btc::config::Config; pub mod testutils; @@ -26,10 +31,10 @@ async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { ) = testutils::init_containers(&cli).await; let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); let bob_btc_starting_balance = btc_to_swap * 10; - let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0); + let bob_xmr_starting_balance = monero::Amount::from_piconero(0); let alice_btc_starting_balance = bitcoin::Amount::ZERO; let alice_xmr_starting_balance = xmr_to_swap * 10; diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index 16ed427e..0be3e032 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -6,11 +6,15 @@ use futures::{ use get_port::get_port; use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState}; +use swap::{ + bitcoin, + config::Config, + monero, + protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState}, +}; use testcontainers::clients::Cli; use testutils::init_tracing; use uuid::Uuid; -use xmr_btc::{bitcoin, config::Config}; pub mod testutils; @@ -30,7 +34,7 @@ async fn alice_punishes_if_bob_never_acts_after_fund() { ) = testutils::init_containers(&cli).await; let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); let bob_btc_starting_balance = btc_to_swap * 10; diff --git a/swap/tests/refund_restart_alice.rs b/swap/tests/refund_restart_alice.rs index 6eb03c18..29f9782a 100644 --- a/swap/tests/refund_restart_alice.rs +++ b/swap/tests/refund_restart_alice.rs @@ -3,13 +3,18 @@ use futures::future::try_join; use get_port::get_port; use libp2p::Multiaddr; use rand::rngs::OsRng; -use swap::{alice, alice::swap::AliceState, bob, bob::swap::BobState, database::Database}; +use swap::{ + bitcoin, + config::Config, + database::Database, + monero, + protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState}, +}; use tempfile::tempdir; use testcontainers::clients::Cli; use testutils::init_tracing; use tokio::select; use uuid::Uuid; -use xmr_btc::{bitcoin, config::Config}; pub mod testutils; @@ -29,10 +34,10 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { ) = testutils::init_containers(&cli).await; let btc_to_swap = bitcoin::Amount::from_sat(1_000_000); - let xmr_to_swap = xmr_btc::monero::Amount::from_piconero(1_000_000_000_000); + let xmr_to_swap = monero::Amount::from_piconero(1_000_000_000_000); let bob_btc_starting_balance = btc_to_swap * 10; - let bob_xmr_starting_balance = xmr_btc::monero::Amount::from_piconero(0); + let bob_xmr_starting_balance = monero::Amount::from_piconero(0); let alice_btc_starting_balance = bitcoin::Amount::ZERO; let alice_xmr_starting_balance = xmr_to_swap * 10; diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index bff01109..6c905442 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -4,14 +4,18 @@ use monero_harness::{image, Monero}; use rand::rngs::OsRng; use std::sync::Arc; use swap::{ - alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database, monero, - network::transport::build, SwapAmounts, + bitcoin, + config::Config, + database::Database, + monero, + network::transport::build, + protocol::{alice, alice::swap::AliceState, bob, bob::swap::BobState}, + SwapAmounts, }; use tempfile::tempdir; use testcontainers::{clients::Cli, Container}; use tracing_core::dispatcher::DefaultGuard; use tracing_log::LogTracer; -use xmr_btc::{alice::State0, config::Config, cross_curve_dleq}; pub async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); @@ -27,8 +31,8 @@ pub async fn init_wallets( name: &str, bitcoind: &Bitcoind<'_>, monero: &Monero, - btc_starting_balance: Option, - xmr_starting_balance: Option, + btc_starting_balance: Option<::bitcoin::Amount>, + xmr_starting_balance: Option, config: Config, ) -> (Arc, Arc) { match xmr_starting_balance { @@ -80,12 +84,12 @@ pub async fn init_alice_state( xmr: xmr_to_swap, }; - let a = crate::bitcoin::SecretKey::new_random(rng); + let a = bitcoin::SecretKey::new_random(rng); let s_a = cross_curve_dleq::Scalar::random(rng); - let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); + let v_a = monero::PrivateViewKey::new_random(rng); let redeem_address = alice_btc_wallet.as_ref().new_address().await.unwrap(); let punish_address = redeem_address.clone(); - let state0 = State0::new( + let state0 = alice::State0::new( a, s_a, v_a, @@ -118,7 +122,7 @@ pub async fn init_alice( monero: &Monero, btc_to_swap: bitcoin::Amount, xmr_to_swap: monero::Amount, - xmr_starting_balance: xmr_btc::monero::Amount, + xmr_starting_balance: monero::Amount, listen: Multiaddr, config: Config, ) -> ( @@ -159,7 +163,7 @@ pub async fn init_alice( pub async fn init_bob_state( btc_to_swap: bitcoin::Amount, - xmr_to_swap: xmr_btc::monero::Amount, + xmr_to_swap: monero::Amount, bob_btc_wallet: Arc, config: Config, ) -> BobState { @@ -169,7 +173,7 @@ pub async fn init_bob_state( }; let refund_address = bob_btc_wallet.new_address().await.unwrap(); - let state0 = xmr_btc::bob::State0::new( + let state0 = bob::State0::new( &mut OsRng, btc_to_swap, xmr_to_swap, @@ -200,7 +204,7 @@ pub async fn init_bob( monero: &Monero, btc_to_swap: bitcoin::Amount, btc_starting_balance: bitcoin::Amount, - xmr_to_swap: xmr_btc::monero::Amount, + xmr_to_swap: monero::Amount, config: Config, ) -> ( BobState, diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml deleted file mode 100644 index 57e134b5..00000000 --- a/xmr-btc/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "xmr-btc" -version = "0.1.0" -authors = ["CoBloX Team "] -edition = "2018" - -# TODO: Check for stale dependencies, this looks like its a bit of a mess. - -[dependencies] -anyhow = "1" -async-trait = "0.1" -bitcoin = { version = "0.25", features = ["rand", "serde"] } -conquer-once = "0.3" -cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] } -curve25519-dalek = "2" -ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] } -ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3 -futures = "0.3" -genawaiter = "0.99.1" -miniscript = { version = "4", features = ["serde"] } -monero = { version = "0.9", features = ["serde_support"] } -rand = "0.7" -rust_decimal = "1.8" -serde = { version = "1", features = ["derive"] } -sha2 = "0.9" -thiserror = "1" -tokio = { version = "0.2", default-features = false, features = ["time"] } -tracing = "0.1" - -[dev-dependencies] -serde_cbor = "0.11" diff --git a/xmr-btc/src/alice/message.rs b/xmr-btc/src/alice/message.rs deleted file mode 100644 index b82f6f9f..00000000 --- a/xmr-btc/src/alice/message.rs +++ /dev/null @@ -1,43 +0,0 @@ -use anyhow::Result; -use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{bitcoin, monero}; - -#[derive(Debug)] -pub enum Message { - Message0(Message0), - Message1(Message1), - Message2(Message2), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message0 { - pub(crate) A: bitcoin::PublicKey, - pub(crate) S_a_monero: monero::PublicKey, - pub(crate) S_a_bitcoin: bitcoin::PublicKey, - pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof, - pub(crate) v_a: monero::PrivateViewKey, - pub(crate) redeem_address: bitcoin::Address, - pub(crate) punish_address: bitcoin::Address, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message1 { - pub(crate) tx_cancel_sig: Signature, - pub(crate) tx_refund_encsig: EncryptedSignature, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message2 { - pub tx_lock_proof: monero::TransferProof, -} - -impl_try_from_parent_enum!(Message0, Message); -impl_try_from_parent_enum!(Message1, Message); -impl_try_from_parent_enum!(Message2, Message); - -impl_from_child_enum!(Message0, Message); -impl_from_child_enum!(Message1, Message); -impl_from_child_enum!(Message2, Message); diff --git a/xmr-btc/src/bob/message.rs b/xmr-btc/src/bob/message.rs deleted file mode 100644 index 178c0218..00000000 --- a/xmr-btc/src/bob/message.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{bitcoin, monero}; -use anyhow::Result; -use ecdsa_fun::{adaptor::EncryptedSignature, Signature}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -#[derive(Clone, Debug)] -pub enum Message { - Message0(Message0), - Message1(Message1), - Message2(Message2), - Message3(Message3), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message0 { - pub(crate) B: bitcoin::PublicKey, - pub(crate) S_b_monero: monero::PublicKey, - pub(crate) S_b_bitcoin: bitcoin::PublicKey, - pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof, - pub(crate) v_b: monero::PrivateViewKey, - pub(crate) refund_address: bitcoin::Address, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message1 { - pub(crate) tx_lock: bitcoin::TxLock, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message2 { - pub(crate) tx_punish_sig: Signature, - pub(crate) tx_cancel_sig: Signature, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message3 { - pub tx_redeem_encsig: EncryptedSignature, -} - -impl_try_from_parent_enum!(Message0, Message); -impl_try_from_parent_enum!(Message1, Message); -impl_try_from_parent_enum!(Message2, Message); -impl_try_from_parent_enum!(Message3, Message); - -impl_from_child_enum!(Message0, Message); -impl_from_child_enum!(Message1, Message); -impl_from_child_enum!(Message2, Message); -impl_from_child_enum!(Message3, Message); diff --git a/xmr-btc/src/lib.rs b/xmr-btc/src/lib.rs index 22fc8807..e69de29b 100644 --- a/xmr-btc/src/lib.rs +++ b/xmr-btc/src/lib.rs @@ -1,91 +0,0 @@ -#![warn( - unused_extern_crates, - missing_debug_implementations, - missing_copy_implementations, - rust_2018_idioms, - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - clippy::fallible_impl_from, - clippy::cast_precision_loss, - clippy::cast_possible_wrap, - clippy::dbg_macro -)] -#![cfg_attr(not(test), warn(clippy::unwrap_used))] -#![forbid(unsafe_code)] -#![allow(non_snake_case)] - -#[derive(Debug, Clone, Copy)] -pub enum ExpiredTimelocks { - None, - Cancel, - Punish, -} - -#[macro_use] -mod utils { - - macro_rules! impl_try_from_parent_enum { - ($type:ident, $parent:ident) => { - impl TryFrom<$parent> for $type { - type Error = anyhow::Error; - fn try_from(from: $parent) -> Result { - if let $parent::$type(inner) = from { - Ok(inner) - } else { - Err(anyhow::anyhow!( - "Failed to convert parent state to child state" - )) - } - } - } - }; - } - - macro_rules! impl_try_from_parent_enum_for_boxed { - ($type:ident, $parent:ident) => { - impl TryFrom<$parent> for $type { - type Error = anyhow::Error; - - fn try_from(from: $parent) -> Result { - if let $parent::$type(inner) = from { - Ok(*inner) - } else { - Err(anyhow::anyhow!( - "Failed to convert parent state to child state" - )) - } - } - } - }; - } - - macro_rules! impl_from_child_enum { - ($type:ident, $parent:ident) => { - impl From<$type> for $parent { - fn from(from: $type) -> Self { - $parent::$type(from) - } - } - }; - } - - macro_rules! impl_from_child_enum_for_boxed { - ($type:ident, $parent:ident) => { - impl From<$type> for $parent { - fn from(from: $type) -> Self { - $parent::$type(Box::new(from)) - } - } - }; - } -} - -pub mod alice; -pub mod bitcoin; -pub mod bob; -pub mod config; -pub mod monero; -pub mod serde; -pub mod transport; - -pub use cross_curve_dleq; diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index d7242292..e69de29b 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -1,274 +0,0 @@ -use crate::serde::monero_private_key; -use anyhow::Result; -use async_trait::async_trait; -use rand::{CryptoRng, RngCore}; -use serde::{Deserialize, Serialize}; -use std::ops::{Add, Mul, Sub}; - -use bitcoin::hashes::core::fmt::Formatter; -pub use curve25519_dalek::scalar::Scalar; -pub use monero::*; -use rust_decimal::{ - prelude::{FromPrimitive, ToPrimitive}, - Decimal, -}; -use std::{fmt::Display, str::FromStr}; - -pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; - -pub fn random_private_key(rng: &mut R) -> PrivateKey { - let scalar = Scalar::random(rng); - - PrivateKey::from_scalar(scalar) -} - -pub fn private_key_from_secp256k1_scalar(scalar: crate::bitcoin::Scalar) -> PrivateKey { - let mut bytes = scalar.to_bytes(); - - // we must reverse the bytes because a secp256k1 scalar is big endian, whereas a - // ed25519 scalar is little endian - bytes.reverse(); - - PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes)) -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] -pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey); - -impl PrivateViewKey { - pub fn new_random(rng: &mut R) -> Self { - let scalar = Scalar::random(rng); - let private_key = PrivateKey::from_scalar(scalar); - - Self(private_key) - } - - pub fn public(&self) -> PublicViewKey { - PublicViewKey(PublicKey::from_private_key(&self.0)) - } -} - -impl Add for PrivateViewKey { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl From for PrivateKey { - fn from(from: PrivateViewKey) -> Self { - from.0 - } -} - -impl From for PublicKey { - fn from(from: PublicViewKey) -> Self { - from.0 - } -} - -#[derive(Clone, Copy, Debug)] -pub struct PublicViewKey(PublicKey); - -#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)] -pub struct Amount(u64); - -impl Amount { - pub const ZERO: Self = Self(0); - /// Create an [Amount] with piconero precision and the given number of - /// piconeros. - /// - /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. - pub fn from_piconero(amount: u64) -> Self { - Amount(amount) - } - - pub fn as_piconero(&self) -> u64 { - self.0 - } - - pub fn parse_monero(amount: &str) -> Result { - let decimal = Decimal::from_str(amount)?; - let piconeros_dec = - decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); - let piconeros = piconeros_dec - .to_u64() - .ok_or_else(|| OverflowError(amount.to_owned()))?; - Ok(Amount(piconeros)) - } -} - -impl Add for Amount { - type Output = Amount; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Sub for Amount { - type Output = Amount; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl Mul for Amount { - type Output = Amount; - - fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * rhs) - } -} - -impl From for u64 { - fn from(from: Amount) -> u64 { - from.0 - } -} - -impl Display for Amount { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut decimal = Decimal::from(self.0); - decimal - .set_scale(12) - .expect("12 is smaller than max precision of 28"); - write!(f, "{} XMR", decimal) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TransferProof { - tx_hash: TxHash, - #[serde(with = "monero_private_key")] - tx_key: PrivateKey, -} - -impl TransferProof { - pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self { - Self { tx_hash, tx_key } - } - pub fn tx_hash(&self) -> TxHash { - self.tx_hash.clone() - } - pub fn tx_key(&self) -> PrivateKey { - self.tx_key - } -} - -// TODO: add constructor/ change String to fixed length byte array -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TxHash(pub String); - -impl From for String { - fn from(from: TxHash) -> Self { - from.0 - } -} - -#[async_trait] -pub trait Transfer { - async fn transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - amount: Amount, - ) -> anyhow::Result<(TransferProof, Amount)>; -} - -#[async_trait] -pub trait WatchForTransfer { - async fn watch_for_transfer( - &self, - public_spend_key: PublicKey, - public_view_key: PublicViewKey, - transfer_proof: TransferProof, - amount: Amount, - expected_confirmations: u32, - ) -> Result<(), InsufficientFunds>; -} - -#[derive(Debug, Clone, Copy, thiserror::Error)] -#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")] -pub struct InsufficientFunds { - pub expected: Amount, - pub actual: Amount, -} - -#[async_trait] -pub trait CreateWalletForOutput { - async fn create_and_load_wallet_for_output( - &self, - private_spend_key: PrivateKey, - private_view_key: PrivateViewKey, - ) -> anyhow::Result<()>; -} - -#[derive(thiserror::Error, Debug, Clone, PartialEq)] -#[error("Overflow, cannot convert {0} to u64")] -pub struct OverflowError(pub String); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn display_monero_min() { - let min_pics = 1; - let amount = Amount::from_piconero(min_pics); - let monero = amount.to_string(); - assert_eq!("0.000000000001 XMR", monero); - } - - #[test] - fn display_monero_one() { - let min_pics = 1000000000000; - let amount = Amount::from_piconero(min_pics); - let monero = amount.to_string(); - assert_eq!("1.000000000000 XMR", monero); - } - - #[test] - fn display_monero_max() { - let max_pics = 18_446_744_073_709_551_615; - let amount = Amount::from_piconero(max_pics); - let monero = amount.to_string(); - assert_eq!("18446744.073709551615 XMR", monero); - } - - #[test] - fn parse_monero_min() { - let monero_min = "0.000000000001"; - let amount = Amount::parse_monero(monero_min).unwrap(); - let pics = amount.0; - assert_eq!(1, pics); - } - - #[test] - fn parse_monero() { - let monero = "123"; - let amount = Amount::parse_monero(monero).unwrap(); - let pics = amount.0; - assert_eq!(123000000000000, pics); - } - - #[test] - fn parse_monero_max() { - let monero = "18446744.073709551615"; - let amount = Amount::parse_monero(monero).unwrap(); - let pics = amount.0; - assert_eq!(18446744073709551615, pics); - } - - #[test] - fn parse_monero_overflows() { - let overflow_pics = "18446744.073709551616"; - let error = Amount::parse_monero(overflow_pics).unwrap_err(); - assert_eq!( - error.downcast_ref::().unwrap(), - &OverflowError(overflow_pics.to_owned()) - ); - } -} diff --git a/xmr-btc/src/transport.rs b/xmr-btc/src/transport.rs deleted file mode 100644 index f71992fb..00000000 --- a/xmr-btc/src/transport.rs +++ /dev/null @@ -1,12 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; - -#[async_trait] -pub trait SendMessage { - async fn send_message(&mut self, message: SendMsg) -> Result<()>; -} - -#[async_trait] -pub trait ReceiveMessage { - async fn receive_message(&mut self) -> Result; -}