From d1ae6863f8d1a95fa0fcb6224a2cfb8cecc3c984 Mon Sep 17 00:00:00 2001 From: scilio Date: Tue, 2 Apr 2024 17:27:08 -0400 Subject: [PATCH] basic reorg protection (cherry picked from commit e3696ed73c2012f20205b98099f397ad799fcff4) --- src/bin/mwixnet.rs | 535 ++++++++------- src/node.rs | 33 +- src/servers/swap.rs | 1529 ++++++++++++++++++++++--------------------- src/store.rs | 597 +++++++++-------- 4 files changed, 1422 insertions(+), 1272 deletions(-) diff --git a/src/bin/mwixnet.rs b/src/bin/mwixnet.rs index a02aa1c..101af2f 100644 --- a/src/bin/mwixnet.rs +++ b/src/bin/mwixnet.rs @@ -16,9 +16,10 @@ use mwixnet::node::GrinNode; use mwixnet::store::StoreError; use rpassword; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::thread::{sleep, spawn}; use std::time::Duration; +use grin_core::core::Transaction; #[macro_use] extern crate clap; @@ -26,311 +27,339 @@ extern crate clap; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; fn main() -> Result<(), Box> { - real_main()?; - std::process::exit(0); + real_main()?; + std::process::exit(0); } fn real_main() -> Result<(), Box> { - let yml = load_yaml!("mwixnet.yml"); - let args = App::from_yaml(yml).get_matches(); - let chain_type = if args.is_present("testnet") { - ChainTypes::Testnet - } else { - ChainTypes::Mainnet - }; - global::set_local_chain_type(chain_type); + let yml = load_yaml!("mwixnet.yml"); + let args = App::from_yaml(yml).get_matches(); + let chain_type = if args.is_present("testnet") { + ChainTypes::Testnet + } else { + ChainTypes::Mainnet + }; + global::set_local_chain_type(chain_type); - let config_path = match args.value_of("config_file") { - Some(path) => PathBuf::from(path), - None => { - let mut grin_path = config::get_grin_path(&chain_type); - grin_path.push("mwixnet-config.toml"); - grin_path - } - }; + let config_path = match args.value_of("config_file") { + Some(path) => PathBuf::from(path), + None => { + let mut grin_path = config::get_grin_path(&chain_type); + grin_path.push("mwixnet-config.toml"); + grin_path + } + }; - let round_time = args - .value_of("round_time") - .map(|t| t.parse::().unwrap()); - let bind_addr = args.value_of("bind_addr"); - let socks_addr = args.value_of("socks_addr"); - let grin_node_url = args.value_of("grin_node_url"); - let grin_node_secret_path = args.value_of("grin_node_secret_path"); - let wallet_owner_url = args.value_of("wallet_owner_url"); - let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path"); - let prev_server = args - .value_of("prev_server") - .map(|p| DalekPublicKey::from_hex(&p).unwrap()); - let next_server = args - .value_of("next_server") - .map(|p| DalekPublicKey::from_hex(&p).unwrap()); + let round_time = args + .value_of("round_time") + .map(|t| t.parse::().unwrap()); + let bind_addr = args.value_of("bind_addr"); + let socks_addr = args.value_of("socks_addr"); + let grin_node_url = args.value_of("grin_node_url"); + let grin_node_secret_path = args.value_of("grin_node_secret_path"); + let wallet_owner_url = args.value_of("wallet_owner_url"); + let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path"); + let prev_server = args + .value_of("prev_server") + .map(|p| DalekPublicKey::from_hex(&p).unwrap()); + let next_server = args + .value_of("next_server") + .map(|p| DalekPublicKey::from_hex(&p).unwrap()); - // Write a new config file if init-config command is supplied - if let ("init-config", Some(_)) = args.subcommand() { - if config_path.exists() { - panic!( - "Config file already exists at {}", - config_path.to_string_lossy() - ); - } + // Write a new config file if init-config command is supplied + if let ("init-config", Some(_)) = args.subcommand() { + if config_path.exists() { + panic!( + "Config file already exists at {}", + config_path.to_string_lossy() + ); + } - let server_config = ServerConfig { - key: crypto::secp::random_secret(), - interval_s: round_time.unwrap_or(DEFAULT_INTERVAL), - addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?, - socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?, - grin_node_url: match grin_node_url { - Some(u) => u.parse()?, - None => config::grin_node_url(&chain_type), - }, - grin_node_secret_path: match grin_node_secret_path { - Some(p) => Some(p.to_owned()), - None => config::node_secret_path(&chain_type) - .to_str() - .map(|p| p.to_owned()), - }, - wallet_owner_url: match wallet_owner_url { - Some(u) => u.parse()?, - None => config::wallet_owner_url(&chain_type), - }, - wallet_owner_secret_path: match wallet_owner_secret_path { - Some(p) => Some(p.to_owned()), - None => config::wallet_owner_secret_path(&chain_type) - .to_str() - .map(|p| p.to_owned()), - }, - prev_server, - next_server, - }; + let server_config = ServerConfig { + key: crypto::secp::random_secret(), + interval_s: round_time.unwrap_or(DEFAULT_INTERVAL), + addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?, + socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?, + grin_node_url: match grin_node_url { + Some(u) => u.parse()?, + None => config::grin_node_url(&chain_type), + }, + grin_node_secret_path: match grin_node_secret_path { + Some(p) => Some(p.to_owned()), + None => config::node_secret_path(&chain_type) + .to_str() + .map(|p| p.to_owned()), + }, + wallet_owner_url: match wallet_owner_url { + Some(u) => u.parse()?, + None => config::wallet_owner_url(&chain_type), + }, + wallet_owner_secret_path: match wallet_owner_secret_path { + Some(p) => Some(p.to_owned()), + None => config::wallet_owner_secret_path(&chain_type) + .to_str() + .map(|p| p.to_owned()), + }, + prev_server, + next_server, + }; - let password = prompt_password_confirm(); - config::write_config(&config_path, &server_config, &password)?; - println!( - "Config file written to {:?}. Please back this file up in a safe place.", - config_path - ); - return Ok(()); - } + let password = prompt_password_confirm(); + config::write_config(&config_path, &server_config, &password)?; + println!( + "Config file written to {:?}. Please back this file up in a safe place.", + config_path + ); + return Ok(()); + } - let password = prompt_password(); - let mut server_config = config::load_config(&config_path, &password)?; + let password = prompt_password(); + let mut server_config = config::load_config(&config_path, &password)?; - // Override grin_node_url, if supplied - if let Some(grin_node_url) = grin_node_url { - server_config.grin_node_url = grin_node_url.parse()?; - } + // Override grin_node_url, if supplied + if let Some(grin_node_url) = grin_node_url { + server_config.grin_node_url = grin_node_url.parse()?; + } - // Override grin_node_secret_path, if supplied - if let Some(grin_node_secret_path) = grin_node_secret_path { - server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned()); - } + // Override grin_node_secret_path, if supplied + if let Some(grin_node_secret_path) = grin_node_secret_path { + server_config.grin_node_secret_path = Some(grin_node_secret_path.to_owned()); + } - // Override wallet_owner_url, if supplied - if let Some(wallet_owner_url) = wallet_owner_url { - server_config.wallet_owner_url = wallet_owner_url.parse()?; - } + // Override wallet_owner_url, if supplied + if let Some(wallet_owner_url) = wallet_owner_url { + server_config.wallet_owner_url = wallet_owner_url.parse()?; + } - // Override wallet_owner_secret_path, if supplied - if let Some(wallet_owner_secret_path) = wallet_owner_secret_path { - server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned()); - } + // Override wallet_owner_secret_path, if supplied + if let Some(wallet_owner_secret_path) = wallet_owner_secret_path { + server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned()); + } - // Override bind_addr, if supplied - if let Some(bind_addr) = bind_addr { - server_config.addr = bind_addr.parse()?; - } + // Override bind_addr, if supplied + if let Some(bind_addr) = bind_addr { + server_config.addr = bind_addr.parse()?; + } - // Override socks_addr, if supplied - if let Some(socks_addr) = socks_addr { - server_config.socks_proxy_addr = socks_addr.parse()?; - } + // Override socks_addr, if supplied + if let Some(socks_addr) = socks_addr { + server_config.socks_proxy_addr = socks_addr.parse()?; + } - // Override prev_server, if supplied - if let Some(prev_server) = prev_server { - server_config.prev_server = Some(prev_server); - } + // Override prev_server, if supplied + if let Some(prev_server) = prev_server { + server_config.prev_server = Some(prev_server); + } - // Override next_server, if supplied - if let Some(next_server) = next_server { - server_config.next_server = Some(next_server); - } + // Override next_server, if supplied + if let Some(next_server) = next_server { + server_config.next_server = Some(next_server); + } - // Create GrinNode - let node = HttpGrinNode::new( - &server_config.grin_node_url, - &server_config.node_api_secret(), - ); + // Create GrinNode + let node = HttpGrinNode::new( + &server_config.grin_node_url, + &server_config.node_api_secret(), + ); - // Node API health check - let mut rt = tokio::runtime::Builder::new() - .threaded_scheduler() - .enable_all() - .build()?; - if let Err(e) = rt.block_on(node.async_get_chain_height()) { - eprintln!("Node communication failure. Is node listening?"); - return Err(e.into()); - }; + // Node API health check + let mut rt = tokio::runtime::Builder::new() + .threaded_scheduler() + .enable_all() + .build()?; + if let Err(e) = rt.block_on(node.async_get_chain_tip()) { + eprintln!("Node communication failure. Is node listening?"); + return Err(e.into()); + }; - // Open wallet - let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass")); - let wallet = rt.block_on(HttpWallet::async_open_wallet( - &server_config.wallet_owner_url, - &server_config.wallet_owner_api_secret(), - &wallet_pass, - )); - let wallet = match wallet { - Ok(w) => w, - Err(e) => { - eprintln!("Wallet communication failure. Is wallet listening?"); - return Err(e.into()); - } - }; + // Open wallet + let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass")); + let wallet = rt.block_on(HttpWallet::async_open_wallet( + &server_config.wallet_owner_url, + &server_config.wallet_owner_api_secret(), + &wallet_pass, + )); + let wallet = match wallet { + Ok(w) => w, + Err(e) => { + eprintln!("Wallet communication failure. Is wallet listening?"); + return Err(e.into()); + } + }; - let mut tor_process = tor::init_tor_listener( - &config::get_grin_path(&chain_type).to_str().unwrap(), - &server_config, - )?; + let mut tor_process = tor::init_tor_listener( + &config::get_grin_path(&chain_type).to_str().unwrap(), + &server_config, + )?; - let stop_state = Arc::new(StopState::new()); - let stop_state_clone = stop_state.clone(); + let stop_state = Arc::new(StopState::new()); + let stop_state_clone = stop_state.clone(); - rt.spawn(async move { - futures::executor::block_on(build_signals_fut()); - let _ = tor_process.kill(); - stop_state_clone.stop(); - }); + rt.spawn(async move { + futures::executor::block_on(build_signals_fut()); + let _ = tor_process.kill(); + stop_state_clone.stop(); + }); - let next_mixer: Option> = server_config.next_server.clone().map(|pk| { - let client: Arc = - Arc::new(MixClientImpl::new(server_config.clone(), pk.clone())); - client - }); + let next_mixer: Option> = server_config.next_server.clone().map(|pk| { + let client: Arc = + Arc::new(MixClientImpl::new(server_config.clone(), pk.clone())); + client + }); - if server_config.prev_server.is_some() { - // Start the JSON-RPC HTTP 'mix' server - println!( - "Starting MIX server with public key {:?}", - server_config.server_pubkey().to_hex() - ); + if server_config.prev_server.is_some() { + // Start the JSON-RPC HTTP 'mix' server + println!( + "Starting MIX server with public key {:?}", + server_config.server_pubkey().to_hex() + ); - let (_, http_server) = servers::mix_rpc::listen( - rt.handle(), - server_config, - next_mixer, - Arc::new(wallet), - Arc::new(node), - )?; + let (_, http_server) = servers::mix_rpc::listen( + rt.handle(), + server_config, + next_mixer, + Arc::new(wallet), + Arc::new(node), + )?; - let close_handle = http_server.close_handle(); - let round_handle = spawn(move || loop { - if stop_state.is_stopped() { - close_handle.close(); - break; - } + let close_handle = http_server.close_handle(); + let round_handle = spawn(move || loop { + if stop_state.is_stopped() { + close_handle.close(); + break; + } - sleep(Duration::from_millis(100)); - }); + sleep(Duration::from_millis(100)); + }); - http_server.wait(); - round_handle.join().unwrap(); - } else { - println!( - "Starting SWAP server with public key {:?}", - server_config.server_pubkey().to_hex() - ); + http_server.wait(); + round_handle.join().unwrap(); + } else { + println!( + "Starting SWAP server with public key {:?}", + server_config.server_pubkey().to_hex() + ); - // Open SwapStore - let store = SwapStore::new( - config::get_grin_path(&chain_type) - .join("db") - .to_str() - .ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr( - "db_root path error".to_string(), - )))?, - )?; + // Open SwapStore + let store = SwapStore::new( + config::get_grin_path(&chain_type) + .join("db") + .to_str() + .ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr( + "db_root path error".to_string(), + )))?, + )?; - // Start the mwixnet JSON-RPC HTTP 'swap' server - let (swap_server, http_server) = servers::swap_rpc::listen( - rt.handle(), - &server_config, - next_mixer, - Arc::new(wallet), - Arc::new(node), - store, - )?; + // Start the mwixnet JSON-RPC HTTP 'swap' server + let (swap_server, http_server) = servers::swap_rpc::listen( + rt.handle(), + &server_config, + next_mixer, + Arc::new(wallet), + Arc::new(node), + store, + )?; - let close_handle = http_server.close_handle(); - let round_handle = spawn(move || { - let mut secs = 0; - loop { - if stop_state.is_stopped() { - close_handle.close(); - break; - } + let close_handle = http_server.close_handle(); + let round_handle = spawn(move || { + let mut secs = 0; + let prev_tx = Arc::new(Mutex::new(None)); + let server = swap_server.clone(); - sleep(Duration::from_secs(1)); - secs = (secs + 1) % server_config.interval_s; + loop { + if stop_state.is_stopped() { + close_handle.close(); + break; + } - if secs == 0 { - let server = swap_server.clone(); - rt.spawn(async move { server.lock().await.execute_round().await }); - //let _ = swap_server.lock().unwrap().execute_round(); - } - } - }); + sleep(Duration::from_secs(1)); + secs = (secs + 1) % server_config.interval_s; - http_server.wait(); - round_handle.join().unwrap(); - } + if secs == 0 { + let prev_tx_clone = prev_tx.clone(); + let server_clone = server.clone(); + rt.spawn(async move { + let result = server_clone.lock().await.execute_round().await; + let mut prev_tx_lock = prev_tx_clone.lock().unwrap(); + *prev_tx_lock = match result { + Ok(Some(tx)) => Some(tx), + _ => None, + }; + }); + } else if secs % 30 == 0 { + let prev_tx_clone = prev_tx.clone(); + let server_clone = server.clone(); + rt.spawn(async move { + let tx_option = { + let prev_tx_lock = prev_tx_clone.lock().unwrap(); + prev_tx_lock.clone() + }; // Lock is dropped here - Ok(()) + if let Some(tx) = tx_option { + let result = server_clone.lock().await.check_reorg(tx).await; + let mut prev_tx_lock = prev_tx_clone.lock().unwrap(); + *prev_tx_lock = match result { + Ok(Some(tx)) => Some(tx), + _ => None, + }; + } + }); + } + } + }); + + http_server.wait(); + round_handle.join().unwrap(); + } + + Ok(()) } #[cfg(unix)] async fn build_signals_fut() { - use tokio::signal::unix::{signal, SignalKind}; + use tokio::signal::unix::{signal, SignalKind}; - // Listen for SIGINT, SIGQUIT, and SIGTERM - let mut terminate_signal = - signal(SignalKind::terminate()).expect("failed to create terminate signal"); - let mut quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal"); - let mut interrupt_signal = - signal(SignalKind::interrupt()).expect("failed to create interrupt signal"); + // Listen for SIGINT, SIGQUIT, and SIGTERM + let mut terminate_signal = + signal(SignalKind::terminate()).expect("failed to create terminate signal"); + let mut quit_signal = signal(SignalKind::quit()).expect("failed to create quit signal"); + let mut interrupt_signal = + signal(SignalKind::interrupt()).expect("failed to create interrupt signal"); - futures::future::select_all(vec![ - Box::pin(terminate_signal.recv()), - Box::pin(quit_signal.recv()), - Box::pin(interrupt_signal.recv()), - ]) - .await; + futures::future::select_all(vec![ + Box::pin(terminate_signal.recv()), + Box::pin(quit_signal.recv()), + Box::pin(interrupt_signal.recv()), + ]) + .await; } #[cfg(not(unix))] async fn build_signals_fut() { - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C signal handler"); + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C signal handler"); } fn prompt_password() -> ZeroingString { - ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap()) + ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap()) } fn prompt_password_confirm() -> ZeroingString { - let mut first = "first".to_string(); - let mut second = "second".to_string(); - while first != second { - first = rpassword::prompt_password_stdout("Server password: ").unwrap(); - second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap(); - } - ZeroingString::from(first) + let mut first = "first".to_string(); + let mut second = "second".to_string(); + while first != second { + first = rpassword::prompt_password_stdout("Server password: ").unwrap(); + second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap(); + } + ZeroingString::from(first) } fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString { - match *wallet_pass { - Some(wallet_pass) => ZeroingString::from(wallet_pass), - None => { - ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap()) - } - } + match *wallet_pass { + Some(wallet_pass) => ZeroingString::from(wallet_pass), + None => { + ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap()) + } + } } diff --git a/src/node.rs b/src/node.rs index 0220bc9..d8d2dd9 100644 --- a/src/node.rs +++ b/src/node.rs @@ -4,13 +4,14 @@ use grin_api::json_rpc::{build_request, Request, Response}; use grin_api::{client, LocatedTxKernel}; use grin_api::{OutputPrintable, OutputType, Tip}; use grin_core::consensus::COINBASE_MATURITY; -use grin_core::core::{Input, OutputFeatures, Transaction}; +use grin_core::core::{Committed, Input, OutputFeatures, Transaction}; use grin_util::ToHex; use async_trait::async_trait; use serde_json::json; use std::net::SocketAddr; use std::sync::Arc; +use grin_core::core::hash::Hash; use thiserror::Error; #[async_trait] @@ -21,8 +22,8 @@ pub trait GrinNode: Send + Sync { output_commit: &Commitment, ) -> Result, NodeError>; - /// Gets the height of the chain tip - async fn async_get_chain_height(&self) -> Result; + /// Gets the height and hash of the chain tip + async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError>; /// Posts a transaction to the grin node async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError>; @@ -108,6 +109,23 @@ pub async fn async_build_input( Ok(None) } +pub async fn async_is_tx_valid(node: &Arc, tx: &Transaction) -> Result { + let next_block_height = node.async_get_chain_tip().await?.0 + 1; + for input_commit in &tx.inputs_committed() { + if !async_is_spendable(&node, &input_commit, next_block_height).await? { + return Ok(false); + } + } + + for output_commit in &tx.outputs_committed() { + if async_is_unspent(&node, &output_commit).await? { + return Ok(false); + } + } + + Ok(true) +} + /// HTTP (JSON-RPC) implementation of the 'GrinNode' trait #[derive(Clone)] pub struct HttpGrinNode { @@ -176,7 +194,7 @@ impl GrinNode for HttpGrinNode { Ok(Some(outputs[0].clone())) } - async fn async_get_chain_height(&self) -> Result { + async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> { let params = json!([]); let tip_json = self .async_send_request::("get_tip", ¶ms) @@ -184,7 +202,7 @@ impl GrinNode for HttpGrinNode { let tip = serde_json::from_value::(tip_json).map_err(NodeError::DecodeResponseError)?; - Ok(tip.height) + Ok((tip.height, Hash::from_hex(tip.last_block_pushed.as_str()).unwrap())) } async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> { @@ -226,6 +244,7 @@ pub mod mock { use grin_onion::crypto::secp::Commitment; use std::collections::HashMap; use std::sync::RwLock; + use grin_core::core::hash::Hash; /// Implementation of 'GrinNode' trait that mocks a grin node instance. /// Use only for testing purposes. @@ -299,8 +318,8 @@ pub mod mock { Ok(None) } - async fn async_get_chain_height(&self) -> Result { - Ok(100) + async fn async_get_chain_tip(&self) -> Result<(u64, Hash), NodeError> { + Ok((100, Hash::default())) } async fn async_post_tx(&self, tx: &Transaction) -> Result<(), NodeError> { diff --git a/src/servers/swap.rs b/src/servers/swap.rs index b811bea..ce0602b 100644 --- a/src/servers/swap.rs +++ b/src/servers/swap.rs @@ -1,12 +1,12 @@ use crate::client::MixClient; use crate::config::ServerConfig; use crate::node::{self, GrinNode}; -use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; +use crate::store::{StoreError, SwapData, SwapStatus, SwapStore, SwapTx}; use crate::tx; use crate::wallet::Wallet; use async_trait::async_trait; -use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody}; +use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, TransactionBody}; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use grin_onion::crypto::comsig::ComSignature; use grin_onion::crypto::secp::{Commitment, Secp256k1, SecretKey}; @@ -21,400 +21,449 @@ use thiserror::Error; /// Swap error types #[derive(Clone, Error, Debug, PartialEq)] pub enum SwapError { - #[error("Invalid number of payloads provided")] - InvalidPayloadLength, - #[error("Commitment Signature is invalid")] - InvalidComSignature, - #[error("Rangeproof is invalid")] - InvalidRangeproof, - #[error("Rangeproof is required but was not supplied")] - MissingRangeproof, - #[error("Output {commit:?} does not exist, or is already spent.")] - CoinNotFound { commit: Commitment }, - #[error("Output {commit:?} is already in the swap list.")] - AlreadySwapped { commit: Commitment }, - #[error("Failed to peel onion layer: {0:?}")] - PeelOnionFailure(OnionError), - #[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")] - FeeTooLow { minimum_fee: u64, actual_fee: u64 }, - #[error("Error saving swap to data store: {0}")] - StoreError(StoreError), - #[error("Error building transaction: {0}")] - TxError(String), - #[error("Node communication error: {0}")] - NodeError(String), - #[error("Client communication error: {0:?}")] - ClientError(String), - #[error("{0}")] - UnknownError(String), + #[error("Invalid number of payloads provided")] + InvalidPayloadLength, + #[error("Commitment Signature is invalid")] + InvalidComSignature, + #[error("Rangeproof is invalid")] + InvalidRangeproof, + #[error("Rangeproof is required but was not supplied")] + MissingRangeproof, + #[error("Output {commit:?} does not exist, or is already spent.")] + CoinNotFound { commit: Commitment }, + #[error("Output {commit:?} is already in the swap list.")] + AlreadySwapped { commit: Commitment }, + #[error("Failed to peel onion layer: {0:?}")] + PeelOnionFailure(OnionError), + #[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")] + FeeTooLow { minimum_fee: u64, actual_fee: u64 }, + #[error("Error saving swap to data store: {0}")] + StoreError(StoreError), + #[error("Error building transaction: {0}")] + TxError(String), + #[error("Node communication error: {0}")] + NodeError(String), + #[error("Client communication error: {0:?}")] + ClientError(String), + #[error("{0}")] + UnknownError(String), } impl From for SwapError { - fn from(e: StoreError) -> SwapError { - SwapError::StoreError(e) - } + fn from(e: StoreError) -> SwapError { + SwapError::StoreError(e) + } } impl From for SwapError { - fn from(e: tx::TxError) -> SwapError { - SwapError::TxError(e.to_string()) - } + fn from(e: tx::TxError) -> SwapError { + SwapError::TxError(e.to_string()) + } } impl From for SwapError { - fn from(e: node::NodeError) -> SwapError { - SwapError::NodeError(e.to_string()) - } + fn from(e: node::NodeError) -> SwapError { + SwapError::NodeError(e.to_string()) + } } /// A public MWixnet server - the "Swap Server" #[async_trait] pub trait SwapServer: Send + Sync { - /// Submit a new output to be swapped. - async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>; + /// Submit a new output to be swapped. + async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>; - /// Iterate through all saved submissions, filter out any inputs that are no longer spendable, - /// and assemble the coinswap transaction, posting the transaction to the configured node. - async fn execute_round(&self) -> Result>, SwapError>; + /// Iterate through all saved submissions, filter out any inputs that are no longer spendable, + /// and assemble the coinswap transaction, posting the transaction to the configured node. + async fn execute_round(&self) -> Result>, SwapError>; + + /// Verify the previous swap transaction is in the active chain or mempool. + /// If it's not, rebroacast the transaction if it's still valid. + /// If the transaction is no longer valid, perform the swap again. + async fn check_reorg(&self, tx: Arc) -> Result>, SwapError>; } /// The standard MWixnet server implementation #[derive(Clone)] pub struct SwapServerImpl { - server_config: ServerConfig, - next_server: Option>, - wallet: Arc, - node: Arc, - store: Arc>, + server_config: ServerConfig, + next_server: Option>, + wallet: Arc, + node: Arc, + store: Arc>, } impl SwapServerImpl { - /// Create a new MWixnet server - pub fn new( - server_config: ServerConfig, - next_server: Option>, - wallet: Arc, - node: Arc, - store: SwapStore, - ) -> Self { - SwapServerImpl { - server_config, - next_server, - wallet, - node, - store: Arc::new(tokio::sync::Mutex::new(store)), - } - } + /// Create a new MWixnet server + pub fn new( + server_config: ServerConfig, + next_server: Option>, + wallet: Arc, + node: Arc, + store: SwapStore, + ) -> Self { + SwapServerImpl { + server_config, + next_server, + wallet, + node, + store: Arc::new(tokio::sync::Mutex::new(store)), + } + } - /// The fee base to use. For now, just using the default. - fn get_fee_base(&self) -> u64 { - DEFAULT_ACCEPT_FEE_BASE - } + /// The fee base to use. For now, just using the default. + fn get_fee_base(&self) -> u64 { + DEFAULT_ACCEPT_FEE_BASE + } - /// Minimum fee to perform a swap. - /// Requires enough fee for the swap server's kernel, 1 input and its output to swap. - fn get_minimum_swap_fee(&self) -> u64 { - TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base() - } + /// Minimum fee to perform a swap. + /// Requires enough fee for the swap server's kernel, 1 input and its output to swap. + fn get_minimum_swap_fee(&self) -> u64 { + TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base() + } - async fn async_is_spendable(&self, next_block_height: u64, swap: &SwapData) -> bool { - if let SwapStatus::Unprocessed = swap.status { - if node::async_is_spendable(&self.node, &swap.input.commit, next_block_height) - .await - .unwrap_or(false) - { - if !node::async_is_unspent(&self.node, &swap.output_commit) - .await - .unwrap_or(true) - { - return true; - } - } - } + async fn async_is_spendable(&self, next_block_height: u64, swap: &SwapData) -> bool { + if let SwapStatus::Unprocessed = swap.status { + if node::async_is_spendable(&self.node, &swap.input.commit, next_block_height) + .await + .unwrap_or(false) + { + if !node::async_is_unspent(&self.node, &swap.output_commit) + .await + .unwrap_or(true) + { + return true; + } + } + } - false - } + false + } + + async fn async_execute_round(&self, store: &SwapStore, mut swaps: Vec) -> Result>, SwapError> { + swaps.sort_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap()); + + if swaps.len() == 0 { + return Ok(None); + } + + let (filtered, failed, offset, outputs, kernels) = if let Some(client) = &self.next_server { + // Call next mix server + let onions = swaps.iter().map(|s| s.onion.clone()).collect(); + let mixed = client + .mix_outputs(&onions) + .await + .map_err(|e| SwapError::ClientError(e.to_string()))?; + + // Filter out failed entries + let kept_indices = HashSet::<_>::from_iter(mixed.indices.clone()); + let filtered = swaps + .iter() + .enumerate() + .filter(|(i, _)| kept_indices.contains(i)) + .map(|(_, j)| j.clone()) + .collect(); + + let failed = swaps + .iter() + .enumerate() + .filter(|(i, _)| !kept_indices.contains(i)) + .map(|(_, j)| j.clone()) + .collect(); + + ( + filtered, + failed, + mixed.components.offset, + mixed.components.outputs, + mixed.components.kernels, + ) + } else { + // Build plain outputs for each swap entry + let outputs: Vec = swaps + .iter() + .map(|s| { + Output::new( + OutputFeatures::Plain, + s.output_commit, + s.rangeproof.unwrap(), + ) + }) + .collect(); + + (swaps, Vec::new(), ZERO_KEY, outputs, Vec::new()) + }; + + let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum(); + let inputs: Vec = filtered.iter().map(|s| s.input).collect(); + let output_excesses: Vec = filtered.iter().map(|s| s.excess.clone()).collect(); + + let tx = tx::async_assemble_tx( + &self.wallet, + &inputs, + &outputs, + &kernels, + self.get_fee_base(), + fees_paid, + &offset, + &output_excesses, + ) + .await?; + + let chain_tip = self.node.async_get_chain_tip().await?; + self.node.async_post_tx(&tx).await?; + + store.save_swap_tx(&SwapTx { tx: tx.clone(), chain_tip })?; + + // Update status to in process + let kernel_commit = tx.kernels().first().unwrap().excess; + for mut swap in filtered { + swap.status = SwapStatus::InProcess { kernel_commit }; + store.save_swap(&swap, true)?; + } + + // Update status of failed swaps + for mut swap in failed { + swap.status = SwapStatus::Failed; + store.save_swap(&swap, true)?; + } + + Ok(Some(Arc::new(tx))) + } } #[async_trait] impl SwapServer for SwapServerImpl { - async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> { - // Verify that more than 1 payload exists when there's a next server, - // or that exactly 1 payload exists when this is the final server - if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1 - || self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1 - { - return Err(SwapError::InvalidPayloadLength); - } + async fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> { + // Verify that more than 1 payload exists when there's a next server, + // or that exactly 1 payload exists when this is the final server + if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1 + || self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1 + { + return Err(SwapError::InvalidPayloadLength); + } - // Verify commitment signature to ensure caller owns the output - let serialized_onion = onion - .serialize() - .map_err(|e| SwapError::UnknownError(e.to_string()))?; - let _ = comsig - .verify(&onion.commit, &serialized_onion) - .map_err(|_| SwapError::InvalidComSignature)?; + // Verify commitment signature to ensure caller owns the output + let serialized_onion = onion + .serialize() + .map_err(|e| SwapError::UnknownError(e.to_string()))?; + let _ = comsig + .verify(&onion.commit, &serialized_onion) + .map_err(|_| SwapError::InvalidComSignature)?; - // Verify that commitment is unspent - let input = node::async_build_input(&self.node, &onion.commit) - .await - .map_err(|e| SwapError::UnknownError(e.to_string()))?; - let input = input.ok_or(SwapError::CoinNotFound { - commit: onion.commit.clone(), - })?; + // Verify that commitment is unspent + let input = node::async_build_input(&self.node, &onion.commit) + .await + .map_err(|e| SwapError::UnknownError(e.to_string()))?; + let input = input.ok_or(SwapError::CoinNotFound { + commit: onion.commit.clone(), + })?; - // Peel off top layer of encryption - let peeled = onion - .peel_layer(&self.server_config.key) - .map_err(|e| SwapError::PeelOnionFailure(e))?; + // Peel off top layer of encryption + let peeled = onion + .peel_layer(&self.server_config.key) + .map_err(|e| SwapError::PeelOnionFailure(e))?; - // Verify the fee meets the minimum - let fee: u64 = peeled.payload.fee.into(); - if fee < self.get_minimum_swap_fee() { - return Err(SwapError::FeeTooLow { - minimum_fee: self.get_minimum_swap_fee(), - actual_fee: fee, - }); - } + // Verify the fee meets the minimum + let fee: u64 = peeled.payload.fee.into(); + if fee < self.get_minimum_swap_fee() { + return Err(SwapError::FeeTooLow { + minimum_fee: self.get_minimum_swap_fee(), + actual_fee: fee, + }); + } - // Verify the rangeproof - if let Some(r) = peeled.payload.rangeproof { - let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); - secp.verify_bullet_proof(peeled.onion.commit, r, None) - .map_err(|_| SwapError::InvalidRangeproof)?; - } else if peeled.onion.enc_payloads.is_empty() { - // A rangeproof is required in the last payload - return Err(SwapError::MissingRangeproof); - } + // Verify the rangeproof + if let Some(r) = peeled.payload.rangeproof { + let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); + secp.verify_bullet_proof(peeled.onion.commit, r, None) + .map_err(|_| SwapError::InvalidRangeproof)?; + } else if peeled.onion.enc_payloads.is_empty() { + // A rangeproof is required in the last payload + return Err(SwapError::MissingRangeproof); + } - let locked = self.store.lock().await; + let locked = self.store.lock().await; - locked - .save_swap( - &SwapData { - excess: peeled.payload.excess, - output_commit: peeled.onion.commit, - rangeproof: peeled.payload.rangeproof, - input, - fee: fee as u64, - onion: peeled.onion, - status: SwapStatus::Unprocessed, - }, - false, - ) - .map_err(|e| match e { - StoreError::AlreadyExists(_) => SwapError::AlreadySwapped { - commit: onion.commit.clone(), - }, - _ => SwapError::StoreError(e), - })?; - Ok(()) - } + locked + .save_swap( + &SwapData { + excess: peeled.payload.excess, + output_commit: peeled.onion.commit, + rangeproof: peeled.payload.rangeproof, + input, + fee: fee as u64, + onion: peeled.onion, + status: SwapStatus::Unprocessed, + }, + false, + ) + .map_err(|e| match e { + StoreError::AlreadyExists(_) => SwapError::AlreadySwapped { + commit: onion.commit.clone(), + }, + _ => SwapError::StoreError(e), + })?; + Ok(()) + } - async fn execute_round(&self) -> Result>, SwapError> { - let next_block_height = self.node.async_get_chain_height().await? + 1; + async fn execute_round(&self) -> Result>, SwapError> { + let next_block_height = self.node.async_get_chain_tip().await?.0 + 1; - let locked_store = self.store.lock().await; - let swaps: Vec = locked_store - .swaps_iter()? - .unique_by(|s| s.output_commit) - .collect(); - let mut spendable: Vec = vec![]; - for swap in &swaps { - if self.async_is_spendable(next_block_height, &swap).await { - spendable.push(swap.clone()); - } - } + let locked_store = self.store.lock().await; + let swaps: Vec = locked_store + .swaps_iter()? + .unique_by(|s| s.output_commit) + .collect(); + let mut spendable: Vec = vec![]; + for swap in &swaps { + if self.async_is_spendable(next_block_height, &swap).await { + spendable.push(swap.clone()); + } + } - spendable.sort_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap()); + self.async_execute_round(&locked_store, swaps).await + } - if spendable.len() == 0 { - return Ok(None); - } + async fn check_reorg(&self, tx: Arc) -> Result>, SwapError> { + let excess = tx.kernels().first().unwrap().excess; + if let Ok(swap_tx) = self.store.lock().await.get_swap_tx(&excess) { + // If kernel is in active chain, return tx + if self.node.async_get_kernel(&excess, Some(swap_tx.chain_tip.0), None).await?.is_some() { + return Ok(Some(tx)); + } - let (filtered, failed, offset, outputs, kernels) = if let Some(client) = &self.next_server { - // Call next mix server - let onions = spendable.iter().map(|s| s.onion.clone()).collect(); - let mixed = client - .mix_outputs(&onions) - .await - .map_err(|e| SwapError::ClientError(e.to_string()))?; + // If transaction is still valid, rebroadcast and return tx + if node::async_is_tx_valid(&self.node, &tx).await? { + self.node.async_post_tx(&tx).await?; + return Ok(Some(tx)); + } - // Filter out failed entries - let kept_indices = HashSet::<_>::from_iter(mixed.indices.clone()); - let filtered = spendable - .iter() - .enumerate() - .filter(|(i, _)| kept_indices.contains(i)) - .map(|(_, j)| j.clone()) - .collect(); + // Collect all swaps based on tx's inputs, and execute_round with those swaps + let next_block_height = self.node.async_get_chain_tip().await?.0 + 1; + let locked_store = self.store.lock().await; + let mut swaps = Vec::new(); + for input_commit in &tx.inputs_committed() { + if let Ok(swap) = locked_store.get_swap(&input_commit) { + if self.async_is_spendable(next_block_height, &swap).await { + swaps.push(swap); + } + } + } - let failed = spendable - .iter() - .enumerate() - .filter(|(i, _)| !kept_indices.contains(i)) - .map(|(_, j)| j.clone()) - .collect(); - - ( - filtered, - failed, - mixed.components.offset, - mixed.components.outputs, - mixed.components.kernels, - ) - } else { - // Build plain outputs for each swap entry - let outputs: Vec = spendable - .iter() - .map(|s| { - Output::new( - OutputFeatures::Plain, - s.output_commit, - s.rangeproof.unwrap(), - ) - }) - .collect(); - - (spendable, Vec::new(), ZERO_KEY, outputs, Vec::new()) - }; - - let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum(); - let inputs: Vec = filtered.iter().map(|s| s.input).collect(); - let output_excesses: Vec = filtered.iter().map(|s| s.excess.clone()).collect(); - - let tx = tx::async_assemble_tx( - &self.wallet, - &inputs, - &outputs, - &kernels, - self.get_fee_base(), - fees_paid, - &offset, - &output_excesses, - ) - .await?; - - self.node.async_post_tx(&tx).await?; - - // Update status to in process - let kernel_commit = tx.kernels().first().unwrap().excess; - for mut swap in filtered { - swap.status = SwapStatus::InProcess { kernel_commit }; - locked_store.save_swap(&swap, true)?; - } - - // Update status of failed swaps - for mut swap in failed { - swap.status = SwapStatus::Failed; - locked_store.save_swap(&swap, true)?; - } - - Ok(Some(Arc::new(tx))) - } + self.async_execute_round(&locked_store, swaps).await + } else { + Err(SwapError::UnknownError("Swap transaction not found".to_string())) // TODO: Create SwapError enum value + } + } } #[cfg(test)] pub mod mock { - use super::{SwapError, SwapServer}; + use super::{SwapError, SwapServer}; - use async_trait::async_trait; - use grin_core::core::Transaction; - use grin_onion::crypto::comsig::ComSignature; - use grin_onion::onion::Onion; - use std::collections::HashMap; + use async_trait::async_trait; + use grin_core::core::Transaction; + use grin_onion::crypto::comsig::ComSignature; + use grin_onion::onion::Onion; + use std::collections::HashMap; + use std::sync::Arc; - pub struct MockSwapServer { - errors: HashMap, - } + pub struct MockSwapServer { + errors: HashMap, + } - impl MockSwapServer { - pub fn new() -> MockSwapServer { - MockSwapServer { - errors: HashMap::new(), - } - } + impl MockSwapServer { + pub fn new() -> MockSwapServer { + MockSwapServer { + errors: HashMap::new(), + } + } - pub fn set_response(&mut self, onion: &Onion, e: SwapError) { - self.errors.insert(onion.clone(), e); - } - } + pub fn set_response(&mut self, onion: &Onion, e: SwapError) { + self.errors.insert(onion.clone(), e); + } + } - #[async_trait] - impl SwapServer for MockSwapServer { - async fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> { - if let Some(e) = self.errors.get(&onion) { - return Err(e.clone()); - } + #[async_trait] + impl SwapServer for MockSwapServer { + async fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> { + if let Some(e) = self.errors.get(&onion) { + return Err(e.clone()); + } - Ok(()) - } + Ok(()) + } - async fn execute_round(&self) -> Result>, SwapError> { - Ok(None) - } - } + async fn execute_round(&self) -> Result>, SwapError> { + Ok(None) + } + + async fn check_reorg(&self, tx: Arc) -> Result>, SwapError> { + Ok(Some(tx)) + } + } } #[cfg(test)] pub mod test_util { - use crate::client::MixClient; - use crate::config; - use crate::node::GrinNode; - use crate::servers::swap::SwapServerImpl; - use crate::store::SwapStore; - use crate::wallet::mock::MockWallet; + use crate::client::MixClient; + use crate::config; + use crate::node::GrinNode; + use crate::servers::swap::SwapServerImpl; + use crate::store::SwapStore; + use crate::wallet::mock::MockWallet; - use grin_onion::crypto::dalek::DalekPublicKey; - use grin_onion::crypto::secp::SecretKey; - use std::sync::Arc; + use grin_onion::crypto::dalek::DalekPublicKey; + use grin_onion::crypto::secp::SecretKey; + use std::sync::Arc; - pub fn new_swapper( - test_dir: &str, - server_key: &SecretKey, - next_server: Option<(&DalekPublicKey, &Arc)>, - node: Arc, - ) -> (Arc, Arc) { - let config = - config::test_util::local_config(&server_key, &None, &next_server.map(|n| n.0.clone())) - .unwrap(); + pub fn new_swapper( + test_dir: &str, + server_key: &SecretKey, + next_server: Option<(&DalekPublicKey, &Arc)>, + node: Arc, + ) -> (Arc, Arc) { + let config = + config::test_util::local_config(&server_key, &None, &next_server.map(|n| n.0.clone())) + .unwrap(); - let wallet = Arc::new(MockWallet::new()); - let store = SwapStore::new(test_dir).unwrap(); - let swap_server = Arc::new(SwapServerImpl::new( - config, - next_server.map(|n| n.1.clone()), - wallet.clone(), - node, - store, - )); + let wallet = Arc::new(MockWallet::new()); + let store = SwapStore::new(test_dir).unwrap(); + let swap_server = Arc::new(SwapServerImpl::new( + config, + next_server.map(|n| n.1.clone()), + wallet.clone(), + node, + store, + )); - (swap_server, wallet) - } + (swap_server, wallet) + } } #[cfg(test)] mod tests { - use crate::client::{self, MixClient}; - use crate::node::mock::MockGrinNode; - use crate::servers::mix_rpc::MixResp; - use crate::servers::swap::{SwapError, SwapServer}; - use crate::store::{SwapData, SwapStatus}; - use crate::tx; - use crate::tx::TxComponents; + use crate::client::{self, MixClient}; + use crate::node::mock::MockGrinNode; + use crate::servers::mix_rpc::MixResp; + use crate::servers::swap::{SwapError, SwapServer}; + use crate::store::{SwapData, SwapStatus}; + use crate::tx; + use crate::tx::TxComponents; - use ::function_name::named; - use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting}; - use grin_onion::crypto::comsig::ComSignature; - use grin_onion::crypto::secp; - use grin_onion::onion::Onion; - use grin_onion::test_util as onion_test_util; - use grin_onion::{create_onion, new_hop, Hop}; - use secp256k1zkp::key::ZERO_KEY; - use std::sync::Arc; - use x25519_dalek::PublicKey as xPublicKey; + use ::function_name::named; + use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting}; + use grin_onion::crypto::comsig::ComSignature; + use grin_onion::crypto::secp; + use grin_onion::onion::Onion; + use grin_onion::test_util as onion_test_util; + use grin_onion::{create_onion, new_hop, Hop}; + use secp256k1zkp::key::ZERO_KEY; + use std::sync::Arc; + use x25519_dalek::PublicKey as xPublicKey; - macro_rules! assert_error_type { + macro_rules! assert_error_type { ($result:expr, $error_type:pat) => { assert!($result.is_err()); assert!(if let $error_type = $result.unwrap_err() { @@ -425,7 +474,7 @@ mod tests { }; } - macro_rules! init_test { + macro_rules! init_test { () => {{ grin_core::global::set_local_chain_type( grin_core::global::ChainTypes::AutomatedTesting, @@ -436,417 +485,417 @@ mod tests { }}; } - /// Standalone swap server to demonstrate request validation and onion unwrapping. - #[tokio::test] - #[named] - async fn swap_standalone() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (output_commit, proof) = onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop.clone()])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - server.swap(&onion, &comsig).await?; - - // Make sure entry is added to server. - let expected = SwapData { - excess: hop_excess.clone(), - output_commit: output_commit.clone(), - rangeproof: Some(proof), - input: Input::new(OutputFeatures::Plain, input_commit.clone()), - fee: fee as u64, - onion: Onion { - ephemeral_pubkey: xPublicKey::from([0u8; 32]), - commit: output_commit.clone(), - enc_payloads: vec![], - }, - status: SwapStatus::Unprocessed, - }; - - { - let store = server.store.lock().await; - assert_eq!(1, store.swaps_iter().unwrap().count()); - assert!(store.swap_exists(&input_commit).unwrap()); - assert_eq!(expected, store.get_swap(&input_commit).unwrap()); - } - - let tx = server.execute_round().await?; - assert!(tx.is_some()); - - { - // check that status was updated - let store = server.store.lock().await; - assert!(match store.get_swap(&input_commit)?.status { - SwapStatus::InProcess { kernel_commit } => - kernel_commit == tx.unwrap().kernels().first().unwrap().excess, - _ => false, - }); - } - - // check that the transaction was posted - let posted_txns = node.get_posted_txns(); - assert_eq!(posted_txns.len(), 1); - let posted_txn: Transaction = posted_txns.into_iter().next().unwrap(); - assert!(posted_txn.inputs_committed().contains(&input_commit)); - assert!(posted_txn.outputs_committed().contains(&output_commit)); - // todo: check that outputs also contain the commitment generated by our wallet - - posted_txn.validate(Weighting::AsTransaction)?; - - Ok(()) - } - - /// Multi-server test to verify proper MixClient communication. - #[tokio::test] - #[named] - async fn swap_multiserver() -> Result<(), Box> { - let test_dir = init_test!(); - - // Setup input - let value: u64 = 200_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - - // Swapper data - let swap_fee: u32 = 50_000_000; - let (swap_sk, _swap_pk) = onion_test_util::rand_keypair(); - let swap_hop_excess = secp::random_secret(); - let swap_hop = new_hop(&swap_sk, &swap_hop_excess, swap_fee, None); - - // Mixer data - let mixer_fee: u32 = 30_000_000; - let (mixer_sk, mixer_pk) = onion_test_util::rand_keypair(); - let mixer_hop_excess = secp::random_secret(); - let (output_commit, proof) = onion_test_util::proof( - value, - swap_fee + mixer_fee, - &blind, - &vec![&swap_hop_excess, &mixer_hop_excess], - ); - let mixer_hop = new_hop(&mixer_sk, &mixer_hop_excess, mixer_fee, Some(proof)); - - // Create onion - let onion = create_onion(&input_commit, &vec![swap_hop, mixer_hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - // Mock mixer - let mixer_onion = onion.peel_layer(&swap_sk)?.onion; - let mut mock_mixer = client::mock::MockMixClient::new(); - let mixer_response = TxComponents { - offset: ZERO_KEY, - outputs: vec![Output::new( - OutputFeatures::Plain, - output_commit.clone(), - proof.clone(), - )], - kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee as u64)?], - }; - mock_mixer.set_response( - &vec![mixer_onion.clone()], - MixResp { - indices: vec![0 as usize], - components: mixer_response, - }, - ); - - let mixer: Arc = Arc::new(mock_mixer); - let (swapper, _) = super::test_util::new_swapper( - &test_dir, - &swap_sk, - Some((&mixer_pk, &mixer)), - node.clone(), - ); - swapper.swap(&onion, &comsig).await?; - - let tx = swapper.execute_round().await?; - assert!(tx.is_some()); - - // check that the transaction was posted - let posted_txns = node.get_posted_txns(); - assert_eq!(posted_txns.len(), 1); - let posted_txn: Transaction = posted_txns.into_iter().next().unwrap(); - assert!(posted_txn.inputs_committed().contains(&input_commit)); - assert!(posted_txn.outputs_committed().contains(&output_commit)); - // todo: check that outputs also contain the commitment generated by our wallet - - posted_txn.validate(Weighting::AsTransaction)?; - - Ok(()) - } - - /// Returns InvalidPayloadLength when too many payloads are provided. - #[tokio::test] - #[named] - async fn swap_too_many_payloads() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (_output_commit, proof) = - onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let hops: Vec = vec![hop.clone(), hop.clone()]; // Multiple payloads - let onion = create_onion(&input_commit, &hops)?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - assert_eq!(Err(SwapError::InvalidPayloadLength), result); - - // Make sure no entry is added to the store - assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); - - Ok(()) - } - - /// Returns InvalidComSignature when ComSignature fails to verify. - #[tokio::test] - #[named] - async fn swap_invalid_com_signature() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (_output_commit, proof) = - onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop])?; - - let wrong_blind = secp::random_secret(); - let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - assert_eq!(Err(SwapError::InvalidComSignature), result); - - // Make sure no entry is added to the store - assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); - - Ok(()) - } - - /// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment. - #[tokio::test] - #[named] - async fn swap_invalid_rangeproof() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let wrong_value = value + 10_000_000; - let (_output_commit, proof) = - onion_test_util::proof(wrong_value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - assert_eq!(Err(SwapError::InvalidRangeproof), result); - - // Make sure no entry is added to the store - assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); - - Ok(()) - } - - /// Returns MissingRangeproof when no rangeproof is provided. - #[tokio::test] - #[named] - async fn swap_missing_rangeproof() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let hop = new_hop(&server_key, &hop_excess, fee, None); - - let onion = create_onion(&input_commit, &vec![hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - assert_eq!(Err(SwapError::MissingRangeproof), result); - - // Make sure no entry is added to the store - assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); - - Ok(()) - } - - /// Returns CoinNotFound when there's no matching output in the UTXO set. - #[tokio::test] - #[named] - async fn swap_utxo_missing() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (_output_commit, proof) = - onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new()); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - assert_eq!( - Err(SwapError::CoinNotFound { - commit: input_commit.clone() - }), - result - ); - - // Make sure no entry is added to the store - assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); - - Ok(()) - } - - /// Returns AlreadySwapped when trying to swap the same commitment multiple times. - #[tokio::test] - #[named] - async fn swap_already_swapped() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (_output_commit, proof) = - onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - server.swap(&onion, &comsig).await?; - - // Call swap a second time - let result = server.swap(&onion, &comsig).await; - assert_eq!( - Err(SwapError::AlreadySwapped { - commit: input_commit.clone() - }), - result - ); - - Ok(()) - } - - /// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload. - #[tokio::test] - #[named] - async fn swap_peel_onion_failure() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 50_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (_output_commit, proof) = - onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - - let wrong_server_key = secp::random_secret(); - let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - - assert!(result.is_err()); - assert_error_type!(result, SwapError::PeelOnionFailure(_)); - - Ok(()) - } - - /// Returns FeeTooLow when the minimum fee is not met. - #[tokio::test] - #[named] - async fn swap_fee_too_low() -> Result<(), Box> { - let test_dir = init_test!(); - - let value: u64 = 200_000_000; - let fee: u32 = 1_000_000; - let blind = secp::random_secret(); - let input_commit = secp::commit(value, &blind)?; - - let server_key = secp::random_secret(); - let hop_excess = secp::random_secret(); - let (_output_commit, proof) = - onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); - let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - - let onion = create_onion(&input_commit, &vec![hop])?; - let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; - - let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); - let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); - let result = server.swap(&onion, &comsig).await; - assert_eq!( - Err(SwapError::FeeTooLow { - minimum_fee: 12_500_000, - actual_fee: fee as u64 - }), - result - ); - - Ok(()) - } + /// Standalone swap server to demonstrate request validation and onion unwrapping. + #[tokio::test] + #[named] + async fn swap_standalone() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (output_commit, proof) = onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop.clone()])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + server.swap(&onion, &comsig).await?; + + // Make sure entry is added to server. + let expected = SwapData { + excess: hop_excess.clone(), + output_commit: output_commit.clone(), + rangeproof: Some(proof), + input: Input::new(OutputFeatures::Plain, input_commit.clone()), + fee: fee as u64, + onion: Onion { + ephemeral_pubkey: xPublicKey::from([0u8; 32]), + commit: output_commit.clone(), + enc_payloads: vec![], + }, + status: SwapStatus::Unprocessed, + }; + + { + let store = server.store.lock().await; + assert_eq!(1, store.swaps_iter().unwrap().count()); + assert!(store.swap_exists(&input_commit).unwrap()); + assert_eq!(expected, store.get_swap(&input_commit).unwrap()); + } + + let tx = server.execute_round().await?; + assert!(tx.is_some()); + + { + // check that status was updated + let store = server.store.lock().await; + assert!(match store.get_swap(&input_commit)?.status { + SwapStatus::InProcess { kernel_commit } => + kernel_commit == tx.unwrap().kernels().first().unwrap().excess, + _ => false, + }); + } + + // check that the transaction was posted + let posted_txns = node.get_posted_txns(); + assert_eq!(posted_txns.len(), 1); + let posted_txn: Transaction = posted_txns.into_iter().next().unwrap(); + assert!(posted_txn.inputs_committed().contains(&input_commit)); + assert!(posted_txn.outputs_committed().contains(&output_commit)); + // todo: check that outputs also contain the commitment generated by our wallet + + posted_txn.validate(Weighting::AsTransaction)?; + + Ok(()) + } + + /// Multi-server test to verify proper MixClient communication. + #[tokio::test] + #[named] + async fn swap_multiserver() -> Result<(), Box> { + let test_dir = init_test!(); + + // Setup input + let value: u64 = 200_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + + // Swapper data + let swap_fee: u32 = 50_000_000; + let (swap_sk, _swap_pk) = onion_test_util::rand_keypair(); + let swap_hop_excess = secp::random_secret(); + let swap_hop = new_hop(&swap_sk, &swap_hop_excess, swap_fee, None); + + // Mixer data + let mixer_fee: u32 = 30_000_000; + let (mixer_sk, mixer_pk) = onion_test_util::rand_keypair(); + let mixer_hop_excess = secp::random_secret(); + let (output_commit, proof) = onion_test_util::proof( + value, + swap_fee + mixer_fee, + &blind, + &vec![&swap_hop_excess, &mixer_hop_excess], + ); + let mixer_hop = new_hop(&mixer_sk, &mixer_hop_excess, mixer_fee, Some(proof)); + + // Create onion + let onion = create_onion(&input_commit, &vec![swap_hop, mixer_hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + // Mock mixer + let mixer_onion = onion.peel_layer(&swap_sk)?.onion; + let mut mock_mixer = client::mock::MockMixClient::new(); + let mixer_response = TxComponents { + offset: ZERO_KEY, + outputs: vec![Output::new( + OutputFeatures::Plain, + output_commit.clone(), + proof.clone(), + )], + kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee as u64)?], + }; + mock_mixer.set_response( + &vec![mixer_onion.clone()], + MixResp { + indices: vec![0 as usize], + components: mixer_response, + }, + ); + + let mixer: Arc = Arc::new(mock_mixer); + let (swapper, _) = super::test_util::new_swapper( + &test_dir, + &swap_sk, + Some((&mixer_pk, &mixer)), + node.clone(), + ); + swapper.swap(&onion, &comsig).await?; + + let tx = swapper.execute_round().await?; + assert!(tx.is_some()); + + // check that the transaction was posted + let posted_txns = node.get_posted_txns(); + assert_eq!(posted_txns.len(), 1); + let posted_txn: Transaction = posted_txns.into_iter().next().unwrap(); + assert!(posted_txn.inputs_committed().contains(&input_commit)); + assert!(posted_txn.outputs_committed().contains(&output_commit)); + // todo: check that outputs also contain the commitment generated by our wallet + + posted_txn.validate(Weighting::AsTransaction)?; + + Ok(()) + } + + /// Returns InvalidPayloadLength when too many payloads are provided. + #[tokio::test] + #[named] + async fn swap_too_many_payloads() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (_output_commit, proof) = + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let hops: Vec = vec![hop.clone(), hop.clone()]; // Multiple payloads + let onion = create_onion(&input_commit, &hops)?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + assert_eq!(Err(SwapError::InvalidPayloadLength), result); + + // Make sure no entry is added to the store + assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); + + Ok(()) + } + + /// Returns InvalidComSignature when ComSignature fails to verify. + #[tokio::test] + #[named] + async fn swap_invalid_com_signature() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (_output_commit, proof) = + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop])?; + + let wrong_blind = secp::random_secret(); + let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + assert_eq!(Err(SwapError::InvalidComSignature), result); + + // Make sure no entry is added to the store + assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); + + Ok(()) + } + + /// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment. + #[tokio::test] + #[named] + async fn swap_invalid_rangeproof() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let wrong_value = value + 10_000_000; + let (_output_commit, proof) = + onion_test_util::proof(wrong_value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + assert_eq!(Err(SwapError::InvalidRangeproof), result); + + // Make sure no entry is added to the store + assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); + + Ok(()) + } + + /// Returns MissingRangeproof when no rangeproof is provided. + #[tokio::test] + #[named] + async fn swap_missing_rangeproof() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let hop = new_hop(&server_key, &hop_excess, fee, None); + + let onion = create_onion(&input_commit, &vec![hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + assert_eq!(Err(SwapError::MissingRangeproof), result); + + // Make sure no entry is added to the store + assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); + + Ok(()) + } + + /// Returns CoinNotFound when there's no matching output in the UTXO set. + #[tokio::test] + #[named] + async fn swap_utxo_missing() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (_output_commit, proof) = + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new()); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + assert_eq!( + Err(SwapError::CoinNotFound { + commit: input_commit.clone() + }), + result + ); + + // Make sure no entry is added to the store + assert_eq!(0, server.store.lock().await.swaps_iter().unwrap().count()); + + Ok(()) + } + + /// Returns AlreadySwapped when trying to swap the same commitment multiple times. + #[tokio::test] + #[named] + async fn swap_already_swapped() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (_output_commit, proof) = + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + server.swap(&onion, &comsig).await?; + + // Call swap a second time + let result = server.swap(&onion, &comsig).await; + assert_eq!( + Err(SwapError::AlreadySwapped { + commit: input_commit.clone() + }), + result + ); + + Ok(()) + } + + /// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload. + #[tokio::test] + #[named] + async fn swap_peel_onion_failure() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 50_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (_output_commit, proof) = + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + + let wrong_server_key = secp::random_secret(); + let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + + assert!(result.is_err()); + assert_error_type!(result, SwapError::PeelOnionFailure(_)); + + Ok(()) + } + + /// Returns FeeTooLow when the minimum fee is not met. + #[tokio::test] + #[named] + async fn swap_fee_too_low() -> Result<(), Box> { + let test_dir = init_test!(); + + let value: u64 = 200_000_000; + let fee: u32 = 1_000_000; + let blind = secp::random_secret(); + let input_commit = secp::commit(value, &blind)?; + + let server_key = secp::random_secret(); + let hop_excess = secp::random_secret(); + let (_output_commit, proof) = + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let onion = create_onion(&input_commit, &vec![hop])?; + let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; + + let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); + let result = server.swap(&onion, &comsig).await; + assert_eq!( + Err(SwapError::FeeTooLow { + minimum_fee: 12_500_000, + actual_fee: fee as u64, + }), + result + ); + + Ok(()) + } } diff --git a/src/store.rs b/src/store.rs index 39cbfba..61d128e 100644 --- a/src/store.rs +++ b/src/store.rs @@ -3,9 +3,9 @@ use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey}; use grin_onion::onion::Onion; use grin_onion::util::{read_optional, write_optional}; -use grin_core::core::Input; +use grin_core::core::{Input, Transaction}; use grin_core::ser::{ - self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer, + self, DeserializationMode, ProtocolVersion, Readable, Reader, Writeable, Writer, }; use grin_store::{self as store, Store}; use grin_util::ToHex; @@ -14,337 +14,390 @@ use thiserror::Error; const DB_NAME: &str = "swap"; const STORE_SUBPATH: &str = "swaps"; -const CURRENT_VERSION: u8 = 0; +const CURRENT_SWAP_VERSION: u8 = 0; const SWAP_PREFIX: u8 = b'S'; +const CURRENT_TX_VERSION: u8 = 0; +const TX_PREFIX: u8 = b'T'; + /// Swap statuses #[derive(Clone, Debug, PartialEq)] pub enum SwapStatus { - Unprocessed, - InProcess { - kernel_commit: Commitment, - }, - Completed { - kernel_commit: Commitment, - block_hash: Hash, - }, - Failed, + Unprocessed, + InProcess { + kernel_commit: Commitment, + }, + Completed { + kernel_commit: Commitment, + block_hash: Hash, + }, + Failed, } impl Writeable for SwapStatus { - fn write(&self, writer: &mut W) -> Result<(), ser::Error> { - match self { - SwapStatus::Unprocessed => { - writer.write_u8(0)?; - } - SwapStatus::InProcess { kernel_commit } => { - writer.write_u8(1)?; - kernel_commit.write(writer)?; - } - SwapStatus::Completed { - kernel_commit, - block_hash, - } => { - writer.write_u8(2)?; - kernel_commit.write(writer)?; - block_hash.write(writer)?; - } - SwapStatus::Failed => { - writer.write_u8(3)?; - } - }; + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + match self { + SwapStatus::Unprocessed => { + writer.write_u8(0)?; + } + SwapStatus::InProcess { kernel_commit } => { + writer.write_u8(1)?; + kernel_commit.write(writer)?; + } + SwapStatus::Completed { + kernel_commit, + block_hash, + } => { + writer.write_u8(2)?; + kernel_commit.write(writer)?; + block_hash.write(writer)?; + } + SwapStatus::Failed => { + writer.write_u8(3)?; + } + }; - Ok(()) - } + Ok(()) + } } impl Readable for SwapStatus { - fn read(reader: &mut R) -> Result { - let status = match reader.read_u8()? { - 0 => SwapStatus::Unprocessed, - 1 => { - let kernel_commit = Commitment::read(reader)?; - SwapStatus::InProcess { kernel_commit } - } - 2 => { - let kernel_commit = Commitment::read(reader)?; - let block_hash = Hash::read(reader)?; - SwapStatus::Completed { - kernel_commit, - block_hash, - } - } - 3 => SwapStatus::Failed, - _ => { - return Err(ser::Error::CorruptedData); - } - }; - Ok(status) - } + fn read(reader: &mut R) -> Result { + let status = match reader.read_u8()? { + 0 => SwapStatus::Unprocessed, + 1 => { + let kernel_commit = Commitment::read(reader)?; + SwapStatus::InProcess { kernel_commit } + } + 2 => { + let kernel_commit = Commitment::read(reader)?; + let block_hash = Hash::read(reader)?; + SwapStatus::Completed { + kernel_commit, + block_hash, + } + } + 3 => SwapStatus::Failed, + _ => { + return Err(ser::Error::CorruptedData); + } + }; + Ok(status) + } } /// Data needed to swap a single output. #[derive(Clone, Debug, PartialEq)] pub struct SwapData { - /// The total excess for the output commitment - pub excess: SecretKey, - /// The derived output commitment after applying excess and fee - pub output_commit: Commitment, - /// The rangeproof, included only for the final hop (node N) - pub rangeproof: Option, - /// Transaction input being spent - pub input: Input, - /// Transaction fee - pub fee: u64, - /// The remaining onion after peeling off our layer - pub onion: Onion, - /// The status of the swap - pub status: SwapStatus, + /// The total excess for the output commitment + pub excess: SecretKey, + /// The derived output commitment after applying excess and fee + pub output_commit: Commitment, + /// The rangeproof, included only for the final hop (node N) + pub rangeproof: Option, + /// Transaction input being spent + pub input: Input, + /// Transaction fee + pub fee: u64, + /// The remaining onion after peeling off our layer + pub onion: Onion, + /// The status of the swap + pub status: SwapStatus, } impl Writeable for SwapData { - fn write(&self, writer: &mut W) -> Result<(), ser::Error> { - writer.write_u8(CURRENT_VERSION)?; - writer.write_fixed_bytes(&self.excess)?; - writer.write_fixed_bytes(&self.output_commit)?; - write_optional(writer, &self.rangeproof)?; - self.input.write(writer)?; - writer.write_u64(self.fee.into())?; - self.onion.write(writer)?; - self.status.write(writer)?; + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(CURRENT_SWAP_VERSION)?; + writer.write_fixed_bytes(&self.excess)?; + writer.write_fixed_bytes(&self.output_commit)?; + write_optional(writer, &self.rangeproof)?; + self.input.write(writer)?; + writer.write_u64(self.fee.into())?; + self.onion.write(writer)?; + self.status.write(writer)?; - Ok(()) - } + Ok(()) + } } impl Readable for SwapData { - fn read(reader: &mut R) -> Result { - let version = reader.read_u8()?; - if version != CURRENT_VERSION { - return Err(ser::Error::UnsupportedProtocolVersion); - } + fn read(reader: &mut R) -> Result { + let version = reader.read_u8()?; + if version != CURRENT_SWAP_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } - let excess = secp::read_secret_key(reader)?; - let output_commit = Commitment::read(reader)?; - let rangeproof = read_optional(reader)?; - let input = Input::read(reader)?; - let fee = reader.read_u64()?; - let onion = Onion::read(reader)?; - let status = SwapStatus::read(reader)?; - Ok(SwapData { - excess, - output_commit, - rangeproof, - input, - fee, - onion, - status, - }) - } + let excess = secp::read_secret_key(reader)?; + let output_commit = Commitment::read(reader)?; + let rangeproof = read_optional(reader)?; + let input = Input::read(reader)?; + let fee = reader.read_u64()?; + let onion = Onion::read(reader)?; + let status = SwapStatus::read(reader)?; + Ok(SwapData { + excess, + output_commit, + rangeproof, + input, + fee, + onion, + status, + }) + } +} + +/// A transaction created as part of a swap round. +#[derive(Clone, Debug, PartialEq)] +pub struct SwapTx { + pub tx: Transaction, + pub chain_tip: (u64, Hash), + // TODO: Include status +} + +impl Writeable for SwapTx { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(CURRENT_TX_VERSION)?; + self.tx.write(writer)?; + writer.write_u64(self.chain_tip.0)?; + self.chain_tip.1.write(writer)?; + Ok(()) + } +} + +impl Readable for SwapTx { + fn read(reader: &mut R) -> Result { + let version = reader.read_u8()?; + if version != CURRENT_TX_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } + + let tx = Transaction::read(reader)?; + let height = reader.read_u64()?; + let block_hash = Hash::read(reader)?; + Ok(SwapTx { + tx, + chain_tip: (height, block_hash), + }) + } } /// Storage facility for swap data. pub struct SwapStore { - db: Store, + db: Store, } /// Store error types #[derive(Clone, Error, Debug, PartialEq)] pub enum StoreError { - #[error("Swap entry already exists for '{0:?}'")] - AlreadyExists(Commitment), - #[error("Error occurred while attempting to open db: {0}")] - OpenError(store::lmdb::Error), - #[error("Serialization error occurred: {0}")] - SerializationError(ser::Error), - #[error("Error occurred while attempting to read from db: {0}")] - ReadError(store::lmdb::Error), - #[error("Error occurred while attempting to write to db: {0}")] - WriteError(store::lmdb::Error), + #[error("Swap entry already exists for '{0:?}'")] + AlreadyExists(Commitment), + #[error("Error occurred while attempting to open db: {0}")] + OpenError(store::lmdb::Error), + #[error("Serialization error occurred: {0}")] + SerializationError(ser::Error), + #[error("Error occurred while attempting to read from db: {0}")] + ReadError(store::lmdb::Error), + #[error("Error occurred while attempting to write to db: {0}")] + WriteError(store::lmdb::Error), } impl From for StoreError { - fn from(e: ser::Error) -> StoreError { - StoreError::SerializationError(e) - } + fn from(e: ser::Error) -> StoreError { + StoreError::SerializationError(e) + } } impl SwapStore { - /// Create new chain store - pub fn new(db_root: &str) -> Result { - let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None) - .map_err(StoreError::OpenError)?; - Ok(SwapStore { db }) - } + /// Create new chain store + pub fn new(db_root: &str) -> Result { + let db = Store::new(db_root, Some(DB_NAME), Some(STORE_SUBPATH), None) + .map_err(StoreError::OpenError)?; + Ok(SwapStore { db }) + } - /// Writes a single key-value pair to the database - fn write>( - &self, - prefix: u8, - k: K, - value: &Vec, - overwrite: bool, - ) -> Result { - let batch = self.db.batch()?; - let key = store::to_key(prefix, k); - if !overwrite && batch.exists(&key[..])? { - Ok(false) - } else { - batch.put(&key[..], &value[..])?; - batch.commit()?; - Ok(true) - } - } + /// Writes a single key-value pair to the database + fn write>( + &self, + prefix: u8, + k: K, + value: &Vec, + overwrite: bool, + ) -> Result { + let batch = self.db.batch()?; + let key = store::to_key(prefix, k); + if !overwrite && batch.exists(&key[..])? { + Ok(false) + } else { + batch.put(&key[..], &value[..])?; + batch.commit()?; + Ok(true) + } + } - /// Reads a single value by key - fn read + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result { - store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k)[..], None), || { - format!("{}:{}", prefix, k.to_hex()) - }) - .map_err(StoreError::ReadError) - } + /// Reads a single value by key + fn read + Copy, V: Readable>(&self, prefix: u8, k: K) -> Result { + store::option_to_not_found(self.db.get_ser(&store::to_key(prefix, k)[..], None), || { + format!("{}:{}", prefix, k.to_hex()) + }) + .map_err(StoreError::ReadError) + } - /// Saves a swap to the database - pub fn save_swap(&self, s: &SwapData, overwrite: bool) -> Result<(), StoreError> { - let data = ser::ser_vec(&s, ProtocolVersion::local())?; - let saved = self - .write(SWAP_PREFIX, &s.input.commit, &data, overwrite) - .map_err(StoreError::WriteError)?; - if !saved { - Err(StoreError::AlreadyExists(s.input.commit.clone())) - } else { - Ok(()) - } - } + /// Saves a swap to the database + pub fn save_swap(&self, s: &SwapData, overwrite: bool) -> Result<(), StoreError> { + let data = ser::ser_vec(&s, ProtocolVersion::local())?; + let saved = self + .write(SWAP_PREFIX, &s.input.commit, &data, overwrite) + .map_err(StoreError::WriteError)?; + if !saved { + Err(StoreError::AlreadyExists(s.input.commit.clone())) + } else { + Ok(()) + } + } - /// Iterator over all swaps. - pub fn swaps_iter(&self) -> Result, StoreError> { - let key = store::to_key(SWAP_PREFIX, ""); - let protocol_version = self.db.protocol_version(); - self.db - .iter(&key[..], move |_, mut v| { - ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) - .map_err(From::from) - }) - .map_err(|e| StoreError::ReadError(e)) - } + /// Iterator over all swaps. + pub fn swaps_iter(&self) -> Result, StoreError> { + let key = store::to_key(SWAP_PREFIX, ""); + let protocol_version = self.db.protocol_version(); + self.db + .iter(&key[..], move |_, mut v| { + ser::deserialize(&mut v, protocol_version, DeserializationMode::default()) + .map_err(From::from) + }) + .map_err(|e| StoreError::ReadError(e)) + } - /// Checks if a matching swap exists in the database - #[allow(dead_code)] - pub fn swap_exists(&self, input_commit: &Commitment) -> Result { - let key = store::to_key(SWAP_PREFIX, input_commit); - self.db - .batch() - .map_err(StoreError::ReadError)? - .exists(&key[..]) - .map_err(StoreError::ReadError) - } + /// Checks if a matching swap exists in the database + #[allow(dead_code)] + pub fn swap_exists(&self, input_commit: &Commitment) -> Result { + let key = store::to_key(SWAP_PREFIX, input_commit); + self.db + .batch() + .map_err(StoreError::ReadError)? + .exists(&key[..]) + .map_err(StoreError::ReadError) + } - /// Reads a swap from the database - #[allow(dead_code)] - pub fn get_swap(&self, input_commit: &Commitment) -> Result { - self.read(SWAP_PREFIX, input_commit) - } + /// Reads a swap from the database + #[allow(dead_code)] + pub fn get_swap(&self, input_commit: &Commitment) -> Result { + self.read(SWAP_PREFIX, input_commit) + } + + /// Saves a swap transaction to the database + pub fn save_swap_tx(&self, s: &SwapTx) -> Result<(), StoreError> { + let data = ser::ser_vec(&s, ProtocolVersion::local())?; + self + .write(TX_PREFIX, &s.tx.kernels().first().unwrap().excess, &data, true) + .map_err(StoreError::WriteError)?; + + Ok(()) + } + + /// Reads a swap tx from the database + pub fn get_swap_tx(&self, kernel_excess: &Commitment) -> Result { + self.read(TX_PREFIX, kernel_excess) + } } #[cfg(test)] mod tests { - use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; - use grin_core::core::{Input, OutputFeatures}; - use grin_core::global::{self, ChainTypes}; - use grin_onion::crypto::secp; - use grin_onion::test_util as onion_test_util; - use rand::RngCore; - use std::cmp::Ordering; + use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; + use grin_core::core::{Input, OutputFeatures}; + use grin_core::global::{self, ChainTypes}; + use grin_onion::crypto::secp; + use grin_onion::test_util as onion_test_util; + use rand::RngCore; + use std::cmp::Ordering; - fn new_store(test_name: &str) -> SwapStore { - global::set_local_chain_type(ChainTypes::AutomatedTesting); - let db_root = format!("./target/tmp/.{}", test_name); - let _ = std::fs::remove_dir_all(db_root.as_str()); - SwapStore::new(db_root.as_str()).unwrap() - } + fn new_store(test_name: &str) -> SwapStore { + global::set_local_chain_type(ChainTypes::AutomatedTesting); + let db_root = format!("./target/tmp/.{}", test_name); + let _ = std::fs::remove_dir_all(db_root.as_str()); + SwapStore::new(db_root.as_str()).unwrap() + } - fn rand_swap_with_status(status: SwapStatus) -> SwapData { - SwapData { - excess: secp::random_secret(), - output_commit: onion_test_util::rand_commit(), - rangeproof: Some(onion_test_util::rand_proof()), - input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()), - fee: rand::thread_rng().next_u64(), - onion: onion_test_util::rand_onion(), - status, - } - } + fn rand_swap_with_status(status: SwapStatus) -> SwapData { + SwapData { + excess: secp::random_secret(), + output_commit: onion_test_util::rand_commit(), + rangeproof: Some(onion_test_util::rand_proof()), + input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()), + fee: rand::thread_rng().next_u64(), + onion: onion_test_util::rand_onion(), + status, + } + } - fn rand_swap() -> SwapData { - let s = rand::thread_rng().next_u64() % 3; - let status = if s == 0 { - SwapStatus::Unprocessed - } else if s == 1 { - SwapStatus::InProcess { - kernel_commit: onion_test_util::rand_commit(), - } - } else { - SwapStatus::Completed { - kernel_commit: onion_test_util::rand_commit(), - block_hash: onion_test_util::rand_hash(), - } - }; - rand_swap_with_status(status) - } + fn rand_swap() -> SwapData { + let s = rand::thread_rng().next_u64() % 3; + let status = if s == 0 { + SwapStatus::Unprocessed + } else if s == 1 { + SwapStatus::InProcess { + kernel_commit: onion_test_util::rand_commit(), + } + } else { + SwapStatus::Completed { + kernel_commit: onion_test_util::rand_commit(), + block_hash: onion_test_util::rand_hash(), + } + }; + rand_swap_with_status(status) + } - #[test] - fn swap_iter() -> Result<(), Box> { - let store = new_store("swap_iter"); - let mut swaps: Vec = Vec::new(); - for _ in 0..5 { - let swap = rand_swap(); - store.save_swap(&swap, false)?; - swaps.push(swap); - } + #[test] + fn swap_iter() -> Result<(), Box> { + let store = new_store("swap_iter"); + let mut swaps: Vec = Vec::new(); + for _ in 0..5 { + let swap = rand_swap(); + store.save_swap(&swap, false)?; + swaps.push(swap); + } - swaps.sort_by(|a, b| { - if a.input.commit < b.input.commit { - Ordering::Less - } else if a.input.commit == b.input.commit { - Ordering::Equal - } else { - Ordering::Greater - } - }); + swaps.sort_by(|a, b| { + if a.input.commit < b.input.commit { + Ordering::Less + } else if a.input.commit == b.input.commit { + Ordering::Equal + } else { + Ordering::Greater + } + }); - let mut i: usize = 0; - for swap in store.swaps_iter()? { - assert_eq!(swap, *swaps.get(i).unwrap()); - i += 1; - } + let mut i: usize = 0; + for swap in store.swaps_iter()? { + assert_eq!(swap, *swaps.get(i).unwrap()); + i += 1; + } - Ok(()) - } + Ok(()) + } - #[test] - fn save_swap() -> Result<(), Box> { - let store = new_store("save_swap"); + #[test] + fn save_swap() -> Result<(), Box> { + let store = new_store("save_swap"); - let mut swap = rand_swap_with_status(SwapStatus::Unprocessed); - assert!(!store.swap_exists(&swap.input.commit)?); + let mut swap = rand_swap_with_status(SwapStatus::Unprocessed); + assert!(!store.swap_exists(&swap.input.commit)?); - store.save_swap(&swap, false)?; - assert_eq!(swap, store.get_swap(&swap.input.commit)?); - assert!(store.swap_exists(&swap.input.commit)?); + store.save_swap(&swap, false)?; + assert_eq!(swap, store.get_swap(&swap.input.commit)?); + assert!(store.swap_exists(&swap.input.commit)?); - swap.status = SwapStatus::InProcess { - kernel_commit: onion_test_util::rand_commit(), - }; - let result = store.save_swap(&swap, false); - assert_eq!( - Err(StoreError::AlreadyExists(swap.input.commit.clone())), - result - ); + swap.status = SwapStatus::InProcess { + kernel_commit: onion_test_util::rand_commit(), + }; + let result = store.save_swap(&swap, false); + assert_eq!( + Err(StoreError::AlreadyExists(swap.input.commit.clone())), + result + ); - store.save_swap(&swap, true)?; - assert_eq!(swap, store.get_swap(&swap.input.commit)?); + store.save_swap(&swap, true)?; + assert_eq!(swap, store.get_swap(&swap.input.commit)?); - Ok(()) - } + Ok(()) + } }