use std::collections::HashSet; use std::result::Result; use std::sync::Arc; use async_trait::async_trait; use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, TransactionBody}; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use itertools::Itertools; use secp256k1zkp::key::ZERO_KEY; use thiserror::Error; use grin_onion::crypto::comsig::ComSignature; use grin_onion::crypto::secp::{Commitment, Secp256k1, SecretKey}; use grin_onion::onion::{Onion, OnionError}; use crate::config::ServerConfig; use crate::mix_client::MixClient; use crate::node::{self, GrinNode}; use crate::store::{StoreError, SwapData, SwapStatus, SwapStore, SwapTx}; use crate::tx; use crate::wallet::Wallet; /// 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("Swap transaction not found: {0:?}")] SwapTxNotFound(Commitment), #[error("{0}")] UnknownError(String), } impl From for SwapError { fn from(e: StoreError) -> SwapError { SwapError::StoreError(e) } } impl From for SwapError { 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()) } } /// 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>; /// 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>, } 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)), } } /// 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() } 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 } 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); } // 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(), })?; // 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 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; locked .save_swap( &SwapData { excess: peeled.payload.excess, output_commit: peeled.onion.commit, rangeproof: peeled.payload.rangeproof, input, fee, 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_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()); } } self.async_execute_round(&locked_store, swaps).await } async fn check_reorg( &self, tx: &Arc, ) -> Result>, SwapError> { let excess = tx.kernels().first().unwrap().excess; let locked_store = self.store.lock().await; if let Ok(swap_tx) = locked_store.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.clone())); } // 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.clone())); } // 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 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); } } } self.async_execute_round(&locked_store, swaps).await } else { Err(SwapError::SwapTxNotFound(excess)) } } } #[cfg(test)] pub mod mock { use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; use grin_core::core::Transaction; use grin_onion::crypto::comsig::ComSignature; use grin_onion::onion::Onion; use super::{SwapError, SwapServer}; pub struct MockSwapServer { errors: HashMap, } 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); } } #[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(()) } async fn execute_round(&self) -> Result>, SwapError> { Ok(None) } async fn check_reorg( &self, tx: &Arc, ) -> Result>, SwapError> { Ok(Some(tx.clone())) } } } #[cfg(test)] pub mod test_util { use std::sync::Arc; use grin_onion::crypto::dalek::DalekPublicKey; use grin_onion::crypto::secp::SecretKey; use crate::config; use crate::mix_client::MixClient; use crate::node::GrinNode; use crate::servers::swap::SwapServerImpl; use crate::store::SwapStore; use crate::wallet::mock::MockWallet; 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, )); (swap_server, wallet) } } #[cfg(test)] mod tests { use std::sync::Arc; use ::function_name::named; use grin_core::core::{ Committed, Input, Inputs, Output, OutputFeatures, Transaction, Weighting, }; use secp256k1zkp::key::ZERO_KEY; use x25519_dalek::PublicKey as xPublicKey; 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 crate::mix_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; macro_rules! assert_error_type { ($result:expr, $error_type:pat) => { assert!($result.is_err()); assert!(if let $error_type = $result.unwrap_err() { true } else { false }); }; } macro_rules! init_test { () => {{ grin_core::global::set_local_chain_type( grin_core::global::ChainTypes::AutomatedTesting, ); let test_dir = concat!("./target/tmp/.", function_name!()); let _ = std::fs::remove_dir_all(test_dir); test_dir }}; } /// 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 = mix_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 SwapTxNotFound when trying to check_reorg with a transaction not found in the store. #[tokio::test] #[named] async fn swap_tx_not_found() -> Result<(), Box> { let test_dir = init_test!(); let server_key = secp::random_secret(); let node: Arc = Arc::new(MockGrinNode::new()); let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone()); let kern = tx::build_kernel(&secp::random_secret(), 1000u64)?; let tx: Arc = Arc::new(Transaction::new(Inputs::default(), &[], &[kern.clone()])); let result = server.check_reorg(&tx).await; assert_eq!(Err(SwapError::SwapTxNotFound(kern.excess())), 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(()) } }