mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-25 15:39:25 -05:00
Merge pull request #125 from comit-network/reorganise-modules-2
Merge xmr_btc crate
This commit is contained in:
commit
64ba8d6a87
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
|
105
Cargo.lock
generated
105
Cargo.lock
generated
@ -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,45 +2349,19 @@ 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-macro-error-attr",
|
||||
"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-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn-mid",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
@ -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"
|
||||
|
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = ["monero-harness", "xmr-btc", "swap"]
|
||||
members = ["monero-harness", "swap"]
|
||||
|
@ -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"
|
||||
|
@ -1,202 +1,295 @@
|
||||
use anyhow::{Context, 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,
|
||||
use ::bitcoin::{
|
||||
hashes::{hex::ToHex, Hash},
|
||||
secp256k1,
|
||||
util::psbt::PartiallySignedTransaction,
|
||||
SigHash,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
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;
|
||||
|
||||
pub use ::bitcoin::{Address, Transaction};
|
||||
pub use xmr_btc::bitcoin::*;
|
||||
use crate::{bitcoin::timelocks::BlockHeight, config::Config, ExpiredTimelocks};
|
||||
|
||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||
pub use crate::bitcoin::{
|
||||
timelocks::Timelock,
|
||||
transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund},
|
||||
};
|
||||
pub use ::bitcoin::{util::amount::Amount, Address, Network, Transaction, Txid};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||
pub use wallet::Wallet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: bitcoin_harness::Wallet,
|
||||
pub network: bitcoin::Network,
|
||||
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 Wallet {
|
||||
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
|
||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
||||
impl SecretKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
Ok(Self {
|
||||
inner: wallet,
|
||||
network,
|
||||
})
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self.inner.balance().await?;
|
||||
Ok(balance)
|
||||
pub fn public(&self) -> PublicKey {
|
||||
PublicKey(self.public)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
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<Amount> {
|
||||
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")?;
|
||||
pub fn sign(&self, digest: SigHash) -> Signature {
|
||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||
|
||||
Ok(fee)
|
||||
ecdsa.sign(&self.inner, &digest.into_inner())
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really
|
||||
// subtraction, it's recover)
|
||||
|
||||
// self = a, Y = S_b, digest = tx_refund
|
||||
pub fn encsign(&self, Y: PublicKey, digest: SigHash) -> EncryptedSignature {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PublicKey(Point);
|
||||
|
||||
impl From<PublicKey> for Point {
|
||||
fn from(from: PublicKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for SecretKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKey> for Scalar {
|
||||
fn from(sk: SecretKey) -> Self {
|
||||
sk.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for PublicKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
PublicKey(ecdsa.verification_key_for(&scalar))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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::<Sha256, Deterministic<Sha256>>::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<bitcoin::PublicKey> {
|
||||
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::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||
.expect("a valid miniscript");
|
||||
|
||||
Descriptor::Wsh(miniscript)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BuildTxLockPsbt for Wallet {
|
||||
pub trait BuildTxLockPsbt {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
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)
|
||||
}
|
||||
) -> Result<PartiallySignedTransaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SignTxLock for Wallet {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
||||
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)
|
||||
}
|
||||
pub trait SignTxLock {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BroadcastSignedTransaction for Wallet {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
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")
|
||||
}
|
||||
pub trait BroadcastSignedTransaction {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetRawTransaction for Wallet {
|
||||
// todo: potentially replace with option
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
Ok(self.inner.get_raw_transaction(txid).await?)
|
||||
}
|
||||
pub trait WatchForRawTransaction {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
|
||||
}
|
||||
|
||||
#[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 WaitForTransactionFinality {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
|
||||
}
|
||||
|
||||
#[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<Error>>::Ok(block_height)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
pub trait GetBlockHeight {
|
||||
async fn get_block_height(&self) -> 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
|
||||
pub trait TransactionBlockHeight {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
|
||||
}
|
||||
|
||||
// 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);
|
||||
#[async_trait]
|
||||
pub trait WaitForBlockHeight {
|
||||
async fn wait_for_block_height(&self, height: BlockHeight);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
#[async_trait]
|
||||
pub trait GetRawTransaction {
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[async_trait]
|
||||
pub trait GetNetwork {
|
||||
fn get_network(&self) -> Network;
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::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<B>(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<W>(
|
||||
bitcoin_wallet: &W,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> anyhow::Result<ExpiredTimelocks>
|
||||
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<W>(
|
||||
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(())
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
use crate::bitcoin::{
|
||||
build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, Network, OutPoint, PublicKey,
|
||||
Timelock, Txid, TX_FEE,
|
||||
use ::bitcoin::{
|
||||
util::{bip143::SigHashCache, psbt::PartiallySignedTransaction},
|
||||
OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin::{
|
||||
util::{bip143::SigHashCache, psbt::PartiallySignedTransaction},
|
||||
Address, Amount, SigHash, SigHashType, Transaction, TxIn, TxOut,
|
||||
};
|
||||
use ecdsa_fun::Signature;
|
||||
use miniscript::{Descriptor, NullCtx};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::bitcoin::{
|
||||
build_shared_output_descriptor, timelocks::Timelock, verify_sig, Address, Amount,
|
||||
BuildTxLockPsbt, GetNetwork, PublicKey, Transaction, TX_FEE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TxLock {
|
||||
inner: Transaction,
|
||||
@ -21,7 +22,7 @@ pub struct TxLock {
|
||||
impl TxLock {
|
||||
pub async fn new<W>(wallet: &W, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self>
|
||||
where
|
||||
W: BuildTxLockPsbt + Network,
|
||||
W: BuildTxLockPsbt + GetNetwork,
|
||||
{
|
||||
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||
let address = lock_output_descriptor
|
201
swap/src/bitcoin/wallet.rs
Normal file
201
swap/src/bitcoin/wallet.rs
Normal file
@ -0,0 +1,201 @@
|
||||
use ::bitcoin::{util::psbt::PartiallySignedTransaction, 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, Address, Amount, BroadcastSignedTransaction, BuildTxLockPsbt,
|
||||
GetBlockHeight, GetNetwork, GetRawTransaction, SignTxLock, Transaction,
|
||||
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<Self> {
|
||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self.inner.balance().await?;
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
self.inner.new_address().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
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<PartiallySignedTransaction> {
|
||||
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<Transaction> {
|
||||
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<Txid> {
|
||||
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<Transaction> {
|
||||
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<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 GetNetwork for Wallet {
|
||||
fn get_network(&self) -> bitcoin::Network {
|
||||
self.network
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ use libp2p::{core::Multiaddr, PeerId};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{bitcoin, 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,7 +34,7 @@ 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,
|
||||
@ -60,7 +62,7 @@ pub enum Command {
|
||||
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<bitcoin::Amount> {
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
fn parse_xmr(str: &str) -> anyhow::Result<xmr_btc::monero::Amount> {
|
||||
let amount = xmr_btc::monero::Amount::parse_monero(str)?;
|
||||
fn parse_xmr(str: &str) -> anyhow::Result<monero::Amount> {
|
||||
let amount = monero::Amount::parse_monero(str)?;
|
||||
Ok(amount)
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ pub struct Config {
|
||||
pub monero_finality_confirmations: u32,
|
||||
pub bitcoin_cancel_timelock: Timelock,
|
||||
pub bitcoin_punish_timelock: Timelock,
|
||||
pub bitcoin_network: ::bitcoin::Network,
|
||||
pub monero_network: ::monero::Network,
|
||||
pub bitcoin_network: bitcoin::Network,
|
||||
pub monero_network: monero::Network,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -28,8 +28,8 @@ impl Config {
|
||||
monero_finality_confirmations: mainnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_cancel_timelock: mainnet::BITCOIN_CANCEL_TIMELOCK,
|
||||
bitcoin_punish_timelock: mainnet::BITCOIN_PUNISH_TIMELOCK,
|
||||
bitcoin_network: ::bitcoin::Network::Bitcoin,
|
||||
monero_network: ::monero::Network::Mainnet,
|
||||
bitcoin_network: bitcoin::Network::Bitcoin,
|
||||
monero_network: monero::Network::Mainnet,
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,8 +45,8 @@ impl Config {
|
||||
monero_finality_confirmations: testnet::MONERO_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_cancel_timelock: testnet::BITCOIN_CANCEL_TIMELOCK,
|
||||
bitcoin_punish_timelock: testnet::BITCOIN_PUNISH_TIMELOCK,
|
||||
bitcoin_network: ::bitcoin::Network::Testnet,
|
||||
monero_network: ::monero::Network::Stagenet,
|
||||
bitcoin_network: bitcoin::Network::Testnet,
|
||||
monero_network: monero::Network::Stagenet,
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,8 +62,8 @@ impl Config {
|
||||
monero_finality_confirmations: regtest::MONERO_FINALITY_CONFIRMATIONS,
|
||||
bitcoin_cancel_timelock: regtest::BITCOIN_CANCEL_TIMELOCK,
|
||||
bitcoin_punish_timelock: regtest::BITCOIN_PUNISH_TIMELOCK,
|
||||
bitcoin_network: ::bitcoin::Network::Regtest,
|
||||
monero_network: ::monero::Network::default(),
|
||||
bitcoin_network: bitcoin::Network::Regtest,
|
||||
monero_network: monero::Network::default(),
|
||||
}
|
||||
}
|
||||
}
|
@ -6,10 +6,36 @@ use uuid::Uuid;
|
||||
mod alice;
|
||||
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<Alice> for Swap {
|
||||
fn from(from: Alice) -> Self {
|
||||
Swap::Alice(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bob> 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<Alice> for Swap {
|
||||
fn from(from: Alice) -> Self {
|
||||
Swap::Alice(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bob> 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() {
|
||||
|
@ -1,14 +1,15 @@
|
||||
use crate::{alice::swap::AliceState, SwapAmounts};
|
||||
use bitcoin::hashes::core::fmt::Display;
|
||||
use ::bitcoin::hashes::core::fmt::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xmr_btc::{
|
||||
alice,
|
||||
|
||||
use crate::{
|
||||
bitcoin::{EncryptedSignature, TxCancel, TxRefund},
|
||||
monero,
|
||||
serde::monero_private_key,
|
||||
monero::monero_private_key,
|
||||
protocol::{alice, alice::AliceState},
|
||||
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)]
|
||||
|
@ -1,7 +1,10 @@
|
||||
use crate::{bob::swap::BobState, SwapAmounts};
|
||||
use bitcoin::hashes::core::fmt::Display;
|
||||
use ::bitcoin::hashes::core::fmt::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use xmr_btc::bob;
|
||||
|
||||
use crate::{
|
||||
protocol::{bob, bob::BobState},
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub enum Bob {
|
||||
|
@ -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,24 @@
|
||||
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 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 trace;
|
||||
|
||||
pub type Never = std::convert::Infallible;
|
||||
@ -48,7 +51,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 = "monero::monero_amount")]
|
||||
pub xmr: monero::Amount,
|
||||
}
|
||||
|
||||
@ -63,3 +66,10 @@ impl Display for SwapAmounts {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ExpiredTimelocks {
|
||||
None,
|
||||
Cancel,
|
||||
Punish,
|
||||
}
|
||||
|
@ -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::AliceState, bob, bob::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<bitcoin::Wallet>, Arc<monero::Wallet>)> {
|
||||
) -> Result<(Arc<swap::bitcoin::Wallet>, Arc<swap::monero::Wallet>)> {
|
||||
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<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<swap::monero::Wallet>,
|
||||
config: Config,
|
||||
db: Database,
|
||||
) -> Result<AliceState> {
|
||||
@ -306,8 +305,8 @@ async fn alice_swap(
|
||||
async fn bob_swap(
|
||||
swap_id: Uuid,
|
||||
state: BobState,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
bitcoin_wallet: Arc<swap::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<swap::monero::Wallet>,
|
||||
db: Database,
|
||||
alice_peer_id: PeerId,
|
||||
alice_addr: Multiaddr,
|
||||
|
@ -1,143 +1,377 @@
|
||||
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,
|
||||
};
|
||||
|
||||
pub use xmr_btc::monero::*;
|
||||
use crate::bitcoin;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: wallet::Client,
|
||||
pub network: Network,
|
||||
pub use ::monero::{Network, PrivateKey, PublicKey};
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
||||
|
||||
pub fn random_private_key<R: RngCore + CryptoRng>(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<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
let private_key = PrivateKey::from_scalar(scalar);
|
||||
|
||||
Self(private_key)
|
||||
}
|
||||
|
||||
/// Get the balance of the primary account.
|
||||
pub async fn get_balance(&self) -> Result<Amount> {
|
||||
let amount = self.inner.get_balance(0).await?;
|
||||
pub fn public(&self) -> PublicViewKey {
|
||||
PublicViewKey(PublicKey::from_private_key(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Amount::from_piconero(amount))
|
||||
impl Add for PrivateViewKey {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> 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<Self> {
|
||||
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<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> 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<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transfer for Wallet {
|
||||
pub trait Transfer {
|
||||
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))
|
||||
}
|
||||
) -> anyhow::Result<(TransferProof, Amount)>;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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 },
|
||||
) -> 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);
|
||||
|
||||
pub mod monero_private_key {
|
||||
use monero::{
|
||||
consensus::{Decodable, Encodable},
|
||||
PrivateKey,
|
||||
};
|
||||
use serde::{de, de::Visitor, ser::Error, Deserializer, Serializer};
|
||||
use std::{fmt, io::Cursor};
|
||||
|
||||
struct BytesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for BytesVisitor {
|
||||
type Value = PrivateKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "a byte array representing a Monero private key")
|
||||
}
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let mut s = s;
|
||||
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
|
||||
}
|
||||
}
|
||||
|
||||
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))?;
|
||||
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut bytes = Cursor::new(vec![]);
|
||||
x.consensus_encode(&mut bytes)
|
||||
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
|
||||
s.serialize_bytes(bytes.into_inner().as_ref())
|
||||
}
|
||||
|
||||
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(())
|
||||
pub fn deserialize<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<PrivateKey, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let key = deserializer.deserialize_bytes(BytesVisitor)?;
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod monero_amount {
|
||||
use crate::monero::Amount;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_u64(x.as_piconero())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let picos = u64::deserialize(deserializer)?;
|
||||
let amount = Amount::from_piconero(picos);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[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::<OverflowError>().unwrap(),
|
||||
&OverflowError(overflow_pics.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
||||
crate::monero::Scalar::random(&mut OsRng),
|
||||
));
|
||||
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_amount() {
|
||||
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
||||
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(amount, decoded);
|
||||
}
|
||||
}
|
||||
|
147
swap/src/monero/wallet.rs
Normal file
147
swap/src/monero/wallet.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use ::monero::{Address, Network, PrivateKey, PublicKey};
|
||||
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 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<Amount> {
|
||||
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(())
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
2
swap/src/protocol.rs
Normal file
2
swap/src/protocol.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod alice;
|
||||
pub mod bob;
|
@ -1,15 +1,5 @@
|
||||
//! Run an XMR/BTC swap in the role of Alice.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use self::{amounts::*, message0::*, message1::*, message2::*, message3::*};
|
||||
use crate::{
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
request_response::AliceToBob,
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
SwapAmounts,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use libp2p::{
|
||||
core::{identity::Keypair, Multiaddr},
|
||||
@ -17,7 +7,19 @@ use libp2p::{
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
use xmr_btc::bob;
|
||||
|
||||
use crate::{
|
||||
network::{
|
||||
peer_tracker::{self, PeerTracker},
|
||||
request_response::AliceToBob,
|
||||
transport::SwapTransport,
|
||||
TokioExecutor,
|
||||
},
|
||||
protocol::bob,
|
||||
SwapAmounts,
|
||||
};
|
||||
|
||||
pub use self::{amounts::*, message0::Message0, message1::Message1, message2::Message2, state::*};
|
||||
|
||||
mod amounts;
|
||||
pub mod event_loop;
|
||||
@ -25,6 +27,7 @@ mod message0;
|
||||
mod message1;
|
||||
mod message2;
|
||||
mod message3;
|
||||
pub mod state;
|
||||
mod steps;
|
||||
pub mod swap;
|
||||
|
||||
@ -133,10 +136,10 @@ impl From<message3::OutEvent> for OutEvent {
|
||||
pub struct Behaviour {
|
||||
pt: PeerTracker,
|
||||
amounts: Amounts,
|
||||
message0: Message0,
|
||||
message1: Message1,
|
||||
message2: Message2,
|
||||
message3: Message3,
|
||||
message0: message0::Behaviour,
|
||||
message1: message1::Behaviour,
|
||||
message2: message2::Behaviour,
|
||||
message3: message3::Behaviour,
|
||||
#[behaviour(ignore)]
|
||||
identity: Keypair,
|
||||
}
|
||||
@ -158,31 +161,19 @@ impl Behaviour {
|
||||
}
|
||||
|
||||
/// Send Message0 to Bob in response to receiving his Message0.
|
||||
pub fn send_message0(
|
||||
&mut self,
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
msg: xmr_btc::alice::Message0,
|
||||
) {
|
||||
pub fn send_message0(&mut self, channel: ResponseChannel<AliceToBob>, 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<AliceToBob>,
|
||||
msg: xmr_btc::alice::Message1,
|
||||
) {
|
||||
pub fn send_message1(&mut self, channel: ResponseChannel<AliceToBob>, 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<AliceToBob>,
|
||||
msg: xmr_btc::alice::Message2,
|
||||
) {
|
||||
pub fn send_message2(&mut self, channel: ResponseChannel<AliceToBob>, msg: Message2) {
|
||||
self.message2.send(channel, msg);
|
||||
debug!("Sent Message2");
|
||||
}
|
||||
@ -195,10 +186,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: message0::Behaviour::default(),
|
||||
message1: message1::Behaviour::default(),
|
||||
message2: message2::Behaviour::default(),
|
||||
message3: message3::Behaviour::default(),
|
||||
identity,
|
||||
}
|
||||
}
|
@ -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)]
|
@ -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<T> {
|
||||
@ -36,7 +40,7 @@ pub struct EventLoopHandle {
|
||||
msg1: Receiver<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
||||
msg2: Receiver<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
||||
msg3: Receiver<bob::Message3>,
|
||||
request: Receiver<crate::alice::amounts::OutEvent>,
|
||||
request: Receiver<crate::protocol::alice::amounts::OutEvent>,
|
||||
conn_established: Receiver<PeerId>,
|
||||
send_amounts: Sender<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
||||
send_msg0: Sender<(ResponseChannel<AliceToBob>, 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<crate::alice::amounts::OutEvent> {
|
||||
pub async fn recv_request(&mut self) -> Result<crate::protocol::alice::amounts::OutEvent> {
|
||||
self.request
|
||||
.recv()
|
||||
.await
|
||||
@ -131,7 +135,7 @@ pub struct EventLoop {
|
||||
msg1: Sender<(bob::Message1, ResponseChannel<AliceToBob>)>,
|
||||
msg2: Sender<(bob::Message2, ResponseChannel<AliceToBob>)>,
|
||||
msg3: Sender<bob::Message3>,
|
||||
request: Sender<crate::alice::amounts::OutEvent>,
|
||||
request: Sender<crate::protocol::alice::amounts::OutEvent>,
|
||||
conn_established: Sender<PeerId>,
|
||||
send_amounts: Receiver<(ResponseChannel<AliceToBob>, SwapAmounts)>,
|
||||
send_msg0: Receiver<(ResponseChannel<AliceToBob>, alice::Message0)>,
|
@ -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 Behaviour {
|
||||
rr: RequestResponse<Codec<Message0Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message0 {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message0) {
|
||||
impl Behaviour {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, 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 Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -70,7 +84,7 @@ impl Default for Message0 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -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 Behaviour {
|
||||
rr: RequestResponse<Codec<Message1Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message1 {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message1) {
|
||||
impl Behaviour {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, 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 Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -72,7 +82,7 @@ impl Default for Message1 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -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 Behaviour {
|
||||
rr: RequestResponse<Codec<Message2Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message2 {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, msg: xmr_btc::alice::Message2) {
|
||||
impl Behaviour {
|
||||
pub fn send(&mut self, channel: ResponseChannel<AliceToBob>, 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 Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -72,7 +81,7 @@ impl Default for Message2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -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 Behaviour {
|
||||
rr: RequestResponse<Codec<Message3Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message3 {
|
||||
impl Behaviour {
|
||||
fn poll(
|
||||
&mut self,
|
||||
_: &mut Context<'_>,
|
||||
@ -45,7 +47,7 @@ impl Message3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Message3 {
|
||||
impl Default for Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -62,7 +64,7 @@ impl Default for Message3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -1,437 +1,89 @@
|
||||
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 libp2p::request_response::ResponseChannel;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
use std::fmt;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
bitcoin,
|
||||
bitcoin::{
|
||||
current_epoch, timelocks::Timelock, wait_for_cancel_timelock_to_expire, GetBlockHeight,
|
||||
TransactionBlockHeight, TxCancel, TxRefund, WatchForRawTransaction,
|
||||
},
|
||||
monero,
|
||||
monero::CreateWalletForOutput,
|
||||
network::request_response::AliceToBob,
|
||||
protocol::{alice, bob},
|
||||
ExpiredTimelocks, SwapAmounts,
|
||||
};
|
||||
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,
|
||||
pub enum AliceState {
|
||||
Started {
|
||||
amounts: SwapAmounts,
|
||||
state0: State0,
|
||||
},
|
||||
RedeemBtc(bitcoin::Transaction),
|
||||
CreateMoneroWalletForOutput {
|
||||
Negotiated {
|
||||
channel: Option<ResponseChannel<AliceToBob>>,
|
||||
amounts: SwapAmounts,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcLocked {
|
||||
channel: Option<ResponseChannel<AliceToBob>>,
|
||||
amounts: SwapAmounts,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
XmrLocked {
|
||||
state3: Box<State3>,
|
||||
},
|
||||
EncSigLearned {
|
||||
encrypted_signature: EncryptedSignature,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcRedeemed,
|
||||
BtcCancelled {
|
||||
tx_cancel: TxCancel,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcRefunded {
|
||||
spend_key: monero::PrivateKey,
|
||||
view_key: monero::PrivateViewKey,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
CancelBtc(bitcoin::Transaction),
|
||||
PunishBtc(bitcoin::Transaction),
|
||||
BtcPunishable {
|
||||
tx_refund: TxRefund,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
XmrRefunded,
|
||||
CancelTimelockExpired {
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcPunished,
|
||||
SafelyAborted,
|
||||
}
|
||||
|
||||
// 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<N, B>(
|
||||
network: Arc<Mutex<N>>,
|
||||
bitcoin_client: Arc<B>,
|
||||
// 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<Action, (), ()>
|
||||
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::<Sha256, Deterministic<Sha256>>::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(())
|
||||
impl fmt::Display for AliceState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AliceState::Started { .. } => write!(f, "started"),
|
||||
AliceState::Negotiated { .. } => write!(f, "negotiated"),
|
||||
AliceState::BtcLocked { .. } => write!(f, "btc is locked"),
|
||||
AliceState::XmrLocked { .. } => write!(f, "xmr is locked"),
|
||||
AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"),
|
||||
AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
|
||||
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
|
||||
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
|
||||
AliceState::BtcPunished => write!(f, "btc is punished"),
|
||||
AliceState::SafelyAborted => write!(f, "safely aborted"),
|
||||
AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"),
|
||||
AliceState::XmrRefunded => write!(f, "xmr is refunded"),
|
||||
AliceState::CancelTimelockExpired { .. } => write!(f, "cancel timelock is expired"),
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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<Message> + ReceiveMessage<bob::Message>,
|
||||
>(
|
||||
bitcoin_wallet: &B,
|
||||
monero_wallet: &M,
|
||||
transport: &mut T,
|
||||
state: State,
|
||||
rng: &mut R,
|
||||
) -> Result<State> {
|
||||
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<State6>),
|
||||
}
|
||||
|
||||
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<R: RngCore + CryptoRng>(
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -475,11 +127,11 @@ impl State0 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
|
||||
pub fn next_message<R: RngCore + CryptoRng>(&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 +232,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 +245,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 +483,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(),
|
||||
}
|
||||
}
|
@ -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<AliceToBob>, State3)> {
|
||||
) -> Result<(ResponseChannel<AliceToBob>, 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<W>(
|
||||
channel: ResponseChannel<AliceToBob>,
|
||||
amounts: SwapAmounts,
|
||||
state3: State3,
|
||||
state3: alice::State3,
|
||||
event_loop_handle: &mut EventLoopHandle,
|
||||
monero_wallet: Arc<W>,
|
||||
) -> Result<()>
|
@ -1,7 +1,24 @@
|
||||
//! Run an XMR/BTC swap in the role of Alice.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use futures::{
|
||||
future::{select, Either},
|
||||
pin_mut,
|
||||
};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
alice::{
|
||||
bitcoin,
|
||||
bitcoin::{TransactionBlockHeight, WatchForRawTransaction},
|
||||
config::Config,
|
||||
database::{Database, Swap},
|
||||
monero,
|
||||
monero::CreateWalletForOutput,
|
||||
protocol::alice::{
|
||||
event_loop::EventLoopHandle,
|
||||
steps::{
|
||||
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction,
|
||||
@ -9,28 +26,8 @@ use crate::{
|
||||
publish_bitcoin_redeem_transaction, publish_cancel_transaction,
|
||||
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
||||
},
|
||||
AliceState,
|
||||
},
|
||||
bitcoin::EncryptedSignature,
|
||||
database::{Database, Swap},
|
||||
network::request_response::AliceToBob,
|
||||
SwapAmounts,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use futures::{
|
||||
future::{select, Either},
|
||||
pin_mut,
|
||||
};
|
||||
use libp2p::request_response::ResponseChannel;
|
||||
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},
|
||||
config::Config,
|
||||
monero::CreateWalletForOutput,
|
||||
ExpiredTimelocks,
|
||||
};
|
||||
|
||||
@ -38,75 +35,11 @@ trait Rng: RngCore + CryptoRng + Send {}
|
||||
|
||||
impl<T> Rng for T where T: RngCore + CryptoRng + Send {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AliceState {
|
||||
Started {
|
||||
amounts: SwapAmounts,
|
||||
state0: State0,
|
||||
},
|
||||
Negotiated {
|
||||
channel: Option<ResponseChannel<AliceToBob>>,
|
||||
amounts: SwapAmounts,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcLocked {
|
||||
channel: Option<ResponseChannel<AliceToBob>>,
|
||||
amounts: SwapAmounts,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
XmrLocked {
|
||||
state3: Box<State3>,
|
||||
},
|
||||
EncSigLearned {
|
||||
encrypted_signature: EncryptedSignature,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcRedeemed,
|
||||
BtcCancelled {
|
||||
tx_cancel: TxCancel,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcRefunded {
|
||||
spend_key: monero::PrivateKey,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcPunishable {
|
||||
tx_refund: TxRefund,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
XmrRefunded,
|
||||
CancelTimelockExpired {
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcPunished,
|
||||
SafelyAborted,
|
||||
}
|
||||
|
||||
impl fmt::Display for AliceState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AliceState::Started { .. } => write!(f, "started"),
|
||||
AliceState::Negotiated { .. } => write!(f, "negotiated"),
|
||||
AliceState::BtcLocked { .. } => write!(f, "btc is locked"),
|
||||
AliceState::XmrLocked { .. } => write!(f, "xmr is locked"),
|
||||
AliceState::EncSigLearned { .. } => write!(f, "encrypted signature is learned"),
|
||||
AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
|
||||
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
|
||||
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
|
||||
AliceState::BtcPunished => write!(f, "btc is punished"),
|
||||
AliceState::SafelyAborted => write!(f, "safely aborted"),
|
||||
AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"),
|
||||
AliceState::XmrRefunded => write!(f, "xmr is refunded"),
|
||||
AliceState::CancelTimelockExpired { .. } => write!(f, "cancel timelock is expired"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn swap(
|
||||
state: AliceState,
|
||||
event_loop_handle: EventLoopHandle,
|
||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||
monero_wallet: Arc<crate::monero::Wallet>,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallet>,
|
||||
config: Config,
|
||||
swap_id: Uuid,
|
||||
db: Database,
|
@ -1,24 +1,26 @@
|
||||
//! 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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
amounts::*, message0::Message0, message1::Message1, message2::Message2, message3::Message3,
|
||||
state::*,
|
||||
};
|
||||
|
||||
mod amounts;
|
||||
@ -27,6 +29,7 @@ mod message0;
|
||||
mod message1;
|
||||
mod message2;
|
||||
mod message3;
|
||||
pub mod state;
|
||||
pub mod swap;
|
||||
|
||||
pub type Swarm = libp2p::Swarm<Behaviour>;
|
||||
@ -112,10 +115,10 @@ impl From<message3::OutEvent> for OutEvent {
|
||||
pub struct Behaviour {
|
||||
pt: PeerTracker,
|
||||
amounts: Amounts,
|
||||
message0: Message0,
|
||||
message1: Message1,
|
||||
message2: Message2,
|
||||
message3: Message3,
|
||||
message0: message0::Behaviour,
|
||||
message1: message1::Behaviour,
|
||||
message2: message2::Behaviour,
|
||||
message3: message3::Behaviour,
|
||||
#[behaviour(ignore)]
|
||||
identity: Keypair,
|
||||
}
|
||||
@ -174,10 +177,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: message0::Behaviour::default(),
|
||||
message1: message1::Behaviour::default(),
|
||||
message2: message2::Behaviour::default(),
|
||||
message3: message3::Behaviour::default(),
|
||||
identity,
|
||||
}
|
||||
}
|
@ -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<T> {
|
@ -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 Behaviour {
|
||||
rr: RequestResponse<Codec<Message0Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message0 {
|
||||
impl Behaviour {
|
||||
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 Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -67,7 +81,7 @@ impl Default for Message0 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message0 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -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,16 @@ use std::{
|
||||
};
|
||||
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 +34,14 @@ pub enum OutEvent {
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message1 {
|
||||
pub struct Behaviour {
|
||||
rr: RequestResponse<Codec<Message1Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message1 {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message1) {
|
||||
impl Behaviour {
|
||||
pub fn send(&mut self, alice: PeerId, msg: Message1) {
|
||||
let msg = BobToAlice::Message1(msg);
|
||||
let _id = self.rr.send_request(&alice, msg);
|
||||
}
|
||||
@ -50,7 +59,7 @@ impl Message1 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Message1 {
|
||||
impl Default for Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -67,7 +76,7 @@ impl Default for Message1 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message1 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -1,3 +1,4 @@
|
||||
use ecdsa_fun::Signature;
|
||||
use libp2p::{
|
||||
request_response::{
|
||||
handler::RequestProtocol, ProtocolSupport, RequestResponse, RequestResponseConfig,
|
||||
@ -6,6 +7,7 @@ use libp2p::{
|
||||
swarm::{NetworkBehaviourAction, NetworkBehaviourEventProcess, PollParameters},
|
||||
NetworkBehaviour, PeerId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
task::{Context, Poll},
|
||||
@ -13,8 +15,16 @@ use std::{
|
||||
};
|
||||
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 +35,14 @@ pub enum OutEvent {
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "OutEvent", poll_method = "poll")]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Message2 {
|
||||
pub struct Behaviour {
|
||||
rr: RequestResponse<Codec<Message2Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message2 {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message2) {
|
||||
impl Behaviour {
|
||||
pub fn send(&mut self, alice: PeerId, msg: Message2) {
|
||||
let msg = BobToAlice::Message2(msg);
|
||||
let _id = self.rr.send_request(&alice, msg);
|
||||
}
|
||||
@ -50,7 +60,7 @@ impl Message2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Message2 {
|
||||
impl Default for Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -67,7 +77,7 @@ impl Default for Message2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message2 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -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 Behaviour {
|
||||
rr: RequestResponse<Codec<Message3Protocol>>,
|
||||
#[behaviour(ignore)]
|
||||
events: VecDeque<OutEvent>,
|
||||
}
|
||||
|
||||
impl Message3 {
|
||||
pub fn send(&mut self, alice: PeerId, msg: bob::Message3) {
|
||||
impl Behaviour {
|
||||
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 Behaviour {
|
||||
fn default() -> Self {
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let mut config = RequestResponseConfig::default();
|
||||
@ -67,7 +75,7 @@ impl Default for Message3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Message3 {
|
||||
impl NetworkBehaviourEventProcess<RequestResponseEvent<BobToAlice, AliceToBob>> for Behaviour {
|
||||
fn inject_event(&mut self, event: RequestResponseEvent<BobToAlice, AliceToBob>) {
|
||||
match event {
|
||||
RequestResponseEvent::Message {
|
@ -1,353 +1,64 @@
|
||||
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 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;
|
||||
use std::fmt;
|
||||
|
||||
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, GetNetwork, GetRawTransaction,
|
||||
Transaction, TransactionBlockHeight, TxCancel, Txid, WatchForRawTransaction,
|
||||
},
|
||||
monero::{CreateWalletForOutput, WatchForTransfer},
|
||||
monero,
|
||||
monero::monero_private_key,
|
||||
protocol::{alice, bob},
|
||||
ExpiredTimelocks, SwapAmounts,
|
||||
};
|
||||
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,
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BobState {
|
||||
Started {
|
||||
state0: State0,
|
||||
amounts: SwapAmounts,
|
||||
},
|
||||
CancelBtc(bitcoin::Transaction),
|
||||
RefundBtc(bitcoin::Transaction),
|
||||
Negotiated(State2),
|
||||
BtcLocked(State3),
|
||||
XmrLocked(State4),
|
||||
EncSigSent(State4),
|
||||
BtcRedeemed(State5),
|
||||
CancelTimelockExpired(State4),
|
||||
BtcCancelled(State4),
|
||||
BtcRefunded(State4),
|
||||
XmrRedeemed,
|
||||
BtcPunished,
|
||||
SafelyAborted,
|
||||
}
|
||||
|
||||
// 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<N, M, B>(
|
||||
network: Arc<Mutex<N>>,
|
||||
monero_client: Arc<M>,
|
||||
bitcoin_client: Arc<B>,
|
||||
// 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<Action, (), ()>
|
||||
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(())
|
||||
impl fmt::Display for BobState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BobState::Started { .. } => write!(f, "started"),
|
||||
BobState::Negotiated(..) => write!(f, "negotiated"),
|
||||
BobState::BtcLocked(..) => write!(f, "btc is locked"),
|
||||
BobState::XmrLocked(..) => write!(f, "xmr is locked"),
|
||||
BobState::EncSigSent(..) => write!(f, "encrypted signature is sent"),
|
||||
BobState::BtcRedeemed(..) => write!(f, "btc is redeemed"),
|
||||
BobState::CancelTimelockExpired(..) => write!(f, "cancel timelock is expired"),
|
||||
BobState::BtcCancelled(..) => write!(f, "btc is cancelled"),
|
||||
BobState::BtcRefunded(..) => write!(f, "btc is refunded"),
|
||||
BobState::XmrRedeemed => write!(f, "xmr is redeemed"),
|
||||
BobState::BtcPunished => write!(f, "btc is punished"),
|
||||
BobState::SafelyAborted => write!(f, "safely aborted"),
|
||||
}
|
||||
.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::<Sha256, Deterministic<Sha256>>::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<Message> + ReceiveMessage<alice::Message>,
|
||||
>(
|
||||
bitcoin_wallet: &B,
|
||||
monero_wallet: &M,
|
||||
transport: &mut T,
|
||||
state: State,
|
||||
rng: &mut R,
|
||||
) -> Result<State> {
|
||||
match state {
|
||||
State::State0(state0) => {
|
||||
transport
|
||||
.send_message(state0.next_message(rng).into())
|
||||
.await?;
|
||||
let message0 = 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 {
|
||||
b: bitcoin::SecretKey,
|
||||
@ -390,10 +101,10 @@ impl State0 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_message<R: RngCore + CryptoRng>(&self, rng: &mut R) -> Message0 {
|
||||
pub fn next_message<R: RngCore + CryptoRng>(&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(),
|
||||
@ -407,7 +118,7 @@ impl State0 {
|
||||
|
||||
pub async fn receive<W>(self, wallet: &W, msg: alice::Message0) -> anyhow::Result<State1>
|
||||
where
|
||||
W: BuildTxLockPsbt + Network,
|
||||
W: BuildTxLockPsbt + GetNetwork,
|
||||
{
|
||||
msg.dleq_proof_s_a.verify(
|
||||
msg.S_a_bitcoin.clone().into(),
|
||||
@ -461,8 +172,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 +235,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 +416,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 {
|
@ -1,54 +1,17 @@
|
||||
use crate::{bob::event_loop::EventLoopHandle, database, database::Database, SwapAmounts};
|
||||
use anyhow::{bail, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::{fmt, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use tokio::select;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
use xmr_btc::{
|
||||
bob::{self, State2},
|
||||
ExpiredTimelocks,
|
||||
|
||||
use crate::{
|
||||
database::{Database, Swap},
|
||||
protocol::bob::{self, event_loop::EventLoopHandle, state::*},
|
||||
ExpiredTimelocks, SwapAmounts,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BobState {
|
||||
Started {
|
||||
state0: bob::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),
|
||||
XmrRedeemed,
|
||||
BtcPunished,
|
||||
SafelyAborted,
|
||||
}
|
||||
|
||||
impl fmt::Display for BobState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BobState::Started { .. } => write!(f, "started"),
|
||||
BobState::Negotiated(..) => write!(f, "negotiated"),
|
||||
BobState::BtcLocked(..) => write!(f, "btc is locked"),
|
||||
BobState::XmrLocked(..) => write!(f, "xmr is locked"),
|
||||
BobState::EncSigSent(..) => write!(f, "encrypted signature is sent"),
|
||||
BobState::BtcRedeemed(..) => write!(f, "btc is redeemed"),
|
||||
BobState::CancelTimelockExpired(..) => write!(f, "cancel timelock is expired"),
|
||||
BobState::BtcCancelled(..) => write!(f, "btc is cancelled"),
|
||||
BobState::BtcRefunded(..) => write!(f, "btc is refunded"),
|
||||
BobState::XmrRedeemed => write!(f, "xmr is redeemed"),
|
||||
BobState::BtcPunished => write!(f, "btc is punished"),
|
||||
BobState::SafelyAborted => write!(f, "safely aborted"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(Franck): Make this a method on a struct
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn swap<R>(
|
||||
@ -133,8 +96,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 +117,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 +170,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 +211,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 +246,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 +265,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 +288,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 +317,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 +339,12 @@ where
|
||||
}
|
||||
|
||||
pub async fn negotiate<R>(
|
||||
state0: xmr_btc::bob::State0,
|
||||
state0: crate::protocol::bob::state::State0,
|
||||
amounts: SwapAmounts,
|
||||
swarm: &mut EventLoopHandle,
|
||||
mut rng: R,
|
||||
bitcoin_wallet: Arc<crate::bitcoin::Wallet>,
|
||||
) -> Result<State2>
|
||||
) -> Result<bob::state::State2>
|
||||
where
|
||||
R: RngCore + CryptoRng + Send,
|
||||
{
|
@ -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)
|
||||
|
@ -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::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;
|
||||
|
@ -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::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;
|
||||
|
@ -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::AliceState, bob, bob::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;
|
||||
|
@ -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::AliceState, bob, bob::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;
|
||||
|
||||
|
@ -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::AliceState, bob, bob::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;
|
||||
|
@ -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::AliceState, bob, bob::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_btc::bitcoin::Amount>,
|
||||
xmr_starting_balance: Option<xmr_btc::monero::Amount>,
|
||||
btc_starting_balance: Option<::bitcoin::Amount>,
|
||||
xmr_starting_balance: Option<monero::Amount>,
|
||||
config: Config,
|
||||
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
||||
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<bitcoin::Wallet>,
|
||||
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,
|
||||
|
@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "xmr-btc"
|
||||
version = "0.1.0"
|
||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||
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"
|
@ -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);
|
@ -1,285 +0,0 @@
|
||||
mod timelocks;
|
||||
pub mod transactions;
|
||||
|
||||
use crate::{config::Config, ExpiredTimelocks};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
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;
|
||||
|
||||
pub use bitcoin::{util::psbt::PartiallySignedTransaction, *};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||
pub use timelocks::*;
|
||||
pub use transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
||||
|
||||
// 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<R: RngCore + CryptoRng>(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 fn public(&self) -> PublicKey {
|
||||
PublicKey(self.public)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
pub fn sign(&self, digest: SigHash) -> Signature {
|
||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||
|
||||
ecdsa.sign(&self.inner, &digest.into_inner())
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really
|
||||
// subtraction, it's recover)
|
||||
|
||||
// self = a, Y = S_b, digest = tx_refund
|
||||
pub fn encsign(&self, Y: PublicKey, digest: SigHash) -> EncryptedSignature {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PublicKey(Point);
|
||||
|
||||
impl From<PublicKey> for Point {
|
||||
fn from(from: PublicKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for SecretKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKey> for Scalar {
|
||||
fn from(sk: SecretKey) -> Self {
|
||||
sk.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Scalar> for PublicKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
PublicKey(ecdsa.verification_key_for(&scalar))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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::<Sha256, Deterministic<Sha256>>::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<bitcoin::PublicKey> {
|
||||
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::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||
.expect("a valid miniscript");
|
||||
|
||||
Descriptor::Wsh(miniscript)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait BuildTxLockPsbt {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait SignTxLock {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait BroadcastSignedTransaction {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait WatchForRawTransaction {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait WaitForTransactionFinality {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait GetBlockHeight {
|
||||
async fn get_block_height(&self) -> BlockHeight;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TransactionBlockHeight {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait WaitForBlockHeight {
|
||||
async fn wait_for_block_height(&self, height: BlockHeight);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait GetRawTransaction {
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Network {
|
||||
fn get_network(&self) -> bitcoin::Network;
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::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<B>(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;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn current_epoch<W>(
|
||||
bitcoin_wallet: &W,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> anyhow::Result<ExpiredTimelocks>
|
||||
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<W>(
|
||||
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(())
|
||||
}
|
@ -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);
|
@ -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<Self> {
|
||||
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<Self> {
|
||||
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;
|
@ -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<R: RngCore + CryptoRng>(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<R: RngCore + CryptoRng>(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<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> 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<Self> {
|
||||
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<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> 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<TxHash> 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::<OverflowError>().unwrap(),
|
||||
&OverflowError(overflow_pics.to_owned())
|
||||
);
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
pub mod monero_private_key {
|
||||
use monero::{
|
||||
consensus::{Decodable, Encodable},
|
||||
PrivateKey,
|
||||
};
|
||||
use serde::{de, de::Visitor, ser::Error, Deserializer, Serializer};
|
||||
use std::{fmt, io::Cursor};
|
||||
|
||||
struct BytesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for BytesVisitor {
|
||||
type Value = PrivateKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "a byte array representing a Monero private key")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let mut s = s;
|
||||
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut bytes = Cursor::new(vec![]);
|
||||
x.consensus_encode(&mut bytes)
|
||||
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
|
||||
s.serialize_bytes(bytes.into_inner().as_ref())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<PrivateKey, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let key = deserializer.deserialize_bytes(BytesVisitor)?;
|
||||
Ok(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod monero_amount {
|
||||
use crate::monero::Amount;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_u64(x.as_piconero())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let picos = u64::deserialize(deserializer)?;
|
||||
let amount = Amount::from_piconero(picos);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
||||
|
||||
#[test]
|
||||
fn serde_monero_private_key() {
|
||||
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
||||
crate::monero::Scalar::random(&mut OsRng),
|
||||
));
|
||||
let encoded = serde_cbor::to_vec(&key).unwrap();
|
||||
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(key, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_monero_amount() {
|
||||
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
||||
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
||||
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
||||
assert_eq!(amount, decoded);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SendMessage<SendMsg> {
|
||||
async fn send_message(&mut self, message: SendMsg) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ReceiveMessage<RecvMsg> {
|
||||
async fn receive_message(&mut self) -> Result<RecvMsg>;
|
||||
}
|
Loading…
Reference in New Issue
Block a user