mirror of
https://github.com/jwinterm/mwixnet.git
synced 2026-10-18 22:58:32 +02:00
688 lines
21 KiB
Rust
688 lines
21 KiB
Rust
use crate::config::ServerConfig;
|
|
use crate::node::{self, GrinNode};
|
|
use crate::onion::{Onion, OnionError};
|
|
use crate::secp::{ComSignature, Commitment, Secp256k1, SecretKey};
|
|
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
|
|
use crate::wallet::{self, Wallet};
|
|
|
|
use grin_core::core::hash::Hashed;
|
|
use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody};
|
|
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
|
use itertools::Itertools;
|
|
use std::result::Result;
|
|
use std::sync::{Arc, Mutex};
|
|
use thiserror::Error;
|
|
|
|
/// Swap error types
|
|
#[derive(Clone, Error, Debug, PartialEq)]
|
|
pub enum SwapError {
|
|
#[error("Invalid number of payloads provided (expected {expected:?}, found {found:?})")]
|
|
InvalidPayloadLength { expected: usize, found: usize },
|
|
#[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("{0}")]
|
|
UnknownError(String),
|
|
}
|
|
|
|
/// A MWixnet server
|
|
pub trait Server: Send + Sync {
|
|
/// Submit a new output to be swapped.
|
|
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.
|
|
///
|
|
/// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes.
|
|
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>>;
|
|
}
|
|
|
|
/// The standard MWixnet server implementation
|
|
#[derive(Clone)]
|
|
pub struct ServerImpl {
|
|
server_config: ServerConfig,
|
|
wallet: Arc<dyn Wallet>,
|
|
node: Arc<dyn GrinNode>,
|
|
store: Arc<Mutex<SwapStore>>,
|
|
}
|
|
|
|
impl ServerImpl {
|
|
/// Create a new MWixnet server
|
|
pub fn new(
|
|
server_config: ServerConfig,
|
|
wallet: Arc<dyn Wallet>,
|
|
node: Arc<dyn GrinNode>,
|
|
store: SwapStore,
|
|
) -> Self {
|
|
ServerImpl {
|
|
server_config,
|
|
wallet,
|
|
node,
|
|
store: Arc::new(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 mwixnet 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()
|
|
}
|
|
}
|
|
|
|
impl Server for ServerImpl {
|
|
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
|
|
// milestone 3: check that enc_payloads length matches number of configured servers
|
|
if onion.enc_payloads.len() != 1 {
|
|
return Err(SwapError::InvalidPayloadLength {
|
|
expected: 1,
|
|
found: onion.enc_payloads.len(),
|
|
});
|
|
}
|
|
|
|
// 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::build_input(&self.node, &onion.commit)
|
|
.map_err(|e| SwapError::UnknownError(e.to_string()))?;
|
|
let input = input.ok_or(SwapError::CoinNotFound {
|
|
commit: onion.commit.clone(),
|
|
})?;
|
|
|
|
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.0.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 bullet proof and build the final output
|
|
if let Some(r) = peeled.0.rangeproof {
|
|
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
|
secp.verify_bullet_proof(peeled.1.commit, r, None)
|
|
.map_err(|_| SwapError::InvalidRangeproof)?;
|
|
} else {
|
|
// milestone 3: only the last hop will have a rangeproof
|
|
return Err(SwapError::MissingRangeproof);
|
|
}
|
|
|
|
let locked = self.store.lock().unwrap();
|
|
|
|
locked
|
|
.save_swap(
|
|
&SwapData {
|
|
excess: peeled.0.excess,
|
|
output_commit: peeled.1.commit,
|
|
rangeproof: peeled.0.rangeproof,
|
|
input,
|
|
fee,
|
|
onion: peeled.1,
|
|
status: SwapStatus::Unprocessed,
|
|
},
|
|
false,
|
|
)
|
|
.map_err(|e| match e {
|
|
StoreError::AlreadyExists(_) => SwapError::AlreadySwapped {
|
|
commit: onion.commit.clone(),
|
|
},
|
|
_ => SwapError::StoreError(e),
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>> {
|
|
let locked_store = self.store.lock().unwrap();
|
|
let next_block_height = self.node.get_chain_height()? + 1;
|
|
|
|
let spendable: Vec<SwapData> = locked_store
|
|
.swaps_iter()?
|
|
.unique_by(|s| s.output_commit)
|
|
.filter(|s| match s.status {
|
|
SwapStatus::Unprocessed => true,
|
|
_ => false,
|
|
})
|
|
.filter(|s| {
|
|
node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)
|
|
})
|
|
.filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true))
|
|
.collect();
|
|
|
|
if spendable.len() == 0 {
|
|
return Ok(None);
|
|
}
|
|
|
|
let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum();
|
|
|
|
let inputs: Vec<Input> = spendable.iter().enumerate().map(|(_, s)| s.input).collect();
|
|
|
|
let outputs: Vec<Output> = spendable
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(_, s)| {
|
|
Output::new(
|
|
OutputFeatures::Plain,
|
|
s.output_commit,
|
|
s.rangeproof.unwrap(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let excesses: Vec<SecretKey> = spendable
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(_, s)| s.excess.clone())
|
|
.collect();
|
|
|
|
let tx = wallet::assemble_tx(
|
|
&self.wallet,
|
|
&inputs,
|
|
&outputs,
|
|
self.get_fee_base(),
|
|
total_fee,
|
|
&excesses,
|
|
)?;
|
|
|
|
self.node.post_tx(&tx)?;
|
|
|
|
// Update status to in process
|
|
let kernel_hash = tx.kernels().first().unwrap().hash();
|
|
for mut swap in spendable {
|
|
swap.status = SwapStatus::InProcess { kernel_hash };
|
|
locked_store.save_swap(&swap, true)?;
|
|
}
|
|
|
|
Ok(Some(tx))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod mock {
|
|
use super::{Server, SwapError};
|
|
use crate::onion::Onion;
|
|
use crate::secp::ComSignature;
|
|
|
|
use grin_core::core::Transaction;
|
|
use std::collections::HashMap;
|
|
|
|
pub struct MockServer {
|
|
errors: HashMap<Onion, SwapError>,
|
|
}
|
|
|
|
impl MockServer {
|
|
pub fn new() -> MockServer {
|
|
MockServer {
|
|
errors: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn set_response(&mut self, onion: &Onion, e: SwapError) {
|
|
self.errors.insert(onion.clone(), e);
|
|
}
|
|
}
|
|
|
|
impl Server for MockServer {
|
|
fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> {
|
|
if let Some(e) = self.errors.get(&onion) {
|
|
return Err(e.clone());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>> {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::config::ServerConfig;
|
|
use crate::node::mock::MockGrinNode;
|
|
use crate::onion::test_util::{self, Hop};
|
|
use crate::onion::Onion;
|
|
use crate::secp::{self, ComSignature, Commitment, RangeProof, Secp256k1, SecretKey};
|
|
use crate::server::{Server, ServerImpl, SwapError};
|
|
use crate::store::{SwapData, SwapStatus, SwapStore};
|
|
use crate::types::Payload;
|
|
use crate::wallet::mock::MockWallet;
|
|
|
|
use grin_core::core::hash::Hashed;
|
|
use grin_core::core::{Committed, FeeFields, Input, OutputFeatures, Transaction, Weighting};
|
|
use grin_core::global::{self, ChainTypes};
|
|
use std::net::TcpListener;
|
|
use std::sync::Arc;
|
|
use x25519_dalek::PublicKey as xPublicKey;
|
|
use x25519_dalek::StaticSecret;
|
|
|
|
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
|
|
});
|
|
};
|
|
}
|
|
|
|
fn new_server(
|
|
test_name: &str,
|
|
server_key: &SecretKey,
|
|
utxos: &Vec<&Commitment>,
|
|
) -> (ServerImpl, Arc<MockGrinNode>) {
|
|
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());
|
|
|
|
let config = ServerConfig {
|
|
key: server_key.clone(),
|
|
interval_s: 1,
|
|
addr: TcpListener::bind("127.0.0.1:0")
|
|
.unwrap()
|
|
.local_addr()
|
|
.unwrap(),
|
|
grin_node_url: "127.0.0.1:3413".parse().unwrap(),
|
|
grin_node_secret_path: None,
|
|
wallet_owner_url: "127.0.0.1:3420".parse().unwrap(),
|
|
wallet_owner_secret_path: None,
|
|
};
|
|
let wallet = Arc::new(MockWallet {});
|
|
let mut mut_node = MockGrinNode::new();
|
|
for utxo in utxos {
|
|
mut_node.add_default_utxo(&utxo);
|
|
}
|
|
let node = Arc::new(mut_node);
|
|
let store = SwapStore::new(db_root.as_str()).unwrap();
|
|
|
|
let server = ServerImpl::new(config, wallet.clone(), node.clone(), store);
|
|
(server, node)
|
|
}
|
|
|
|
fn proof(value: u64, fee: u64, input_blind: &SecretKey, hop_excess: &SecretKey) -> RangeProof {
|
|
let secp = Secp256k1::new();
|
|
let nonce = secp::random_secret();
|
|
|
|
let mut blind = input_blind.clone();
|
|
blind.add_assign(&secp, &hop_excess).unwrap();
|
|
|
|
secp.bullet_proof(
|
|
value - fee,
|
|
blind.clone(),
|
|
nonce.clone(),
|
|
nonce.clone(),
|
|
None,
|
|
None,
|
|
)
|
|
}
|
|
|
|
fn new_hop(
|
|
server_key: &SecretKey,
|
|
hop_excess: &SecretKey,
|
|
fee: u64,
|
|
proof: Option<RangeProof>,
|
|
) -> Hop {
|
|
Hop {
|
|
pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
|
|
payload: Payload {
|
|
excess: hop_excess.clone(),
|
|
fee: FeeFields::from(fee as u32),
|
|
rangeproof: proof,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Single hop to demonstrate request validation and onion unwrapping.
|
|
#[test]
|
|
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, node) = new_server("swap_lifecycle", &server_key, &vec![&input_commit]);
|
|
server.swap(&onion, &comsig)?;
|
|
|
|
// Make sure entry is added to server.
|
|
let output_commit = secp::add_excess(&input_commit, &hop_excess)?;
|
|
let output_commit = secp::sub_value(&output_commit, fee)?;
|
|
|
|
let expected = SwapData {
|
|
excess: hop_excess.clone(),
|
|
output_commit: output_commit.clone(),
|
|
rangeproof: Some(proof),
|
|
input: Input::new(OutputFeatures::Plain, input_commit.clone()),
|
|
fee,
|
|
onion: Onion {
|
|
ephemeral_pubkey: test_util::next_ephemeral_pubkey(&onion, &server_key)?,
|
|
commit: output_commit.clone(),
|
|
enc_payloads: vec![],
|
|
},
|
|
status: SwapStatus::Unprocessed,
|
|
};
|
|
|
|
{
|
|
let store = server.store.lock().unwrap();
|
|
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()?;
|
|
assert!(tx.is_some());
|
|
|
|
{
|
|
// check that status was updated
|
|
let store = server.store.lock().unwrap();
|
|
assert!(match store.get_swap(&input_commit)?.status {
|
|
SwapStatus::InProcess { kernel_hash } =>
|
|
kernel_hash == tx.unwrap().kernels().first().unwrap().hash(),
|
|
_ => 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(())
|
|
}
|
|
|
|
/// Returns InvalidPayloadLength when too many payloads are provided.
|
|
#[test]
|
|
fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let hops: Vec<Hop> = vec![hop.clone(), hop.clone()]; // Multiple payloads
|
|
let onion = test_util::create_onion(&input_commit, &hops)?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) =
|
|
new_server("swap_too_many_payloads", &server_key, &vec![&input_commit]);
|
|
let result = server.swap(&onion, &comsig);
|
|
assert_eq!(
|
|
Err(SwapError::InvalidPayloadLength {
|
|
expected: 1,
|
|
found: 2
|
|
}),
|
|
result
|
|
);
|
|
|
|
// Make sure no entry is added to the store
|
|
assert_eq!(
|
|
0,
|
|
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns InvalidComSignature when ComSignature fails to verify.
|
|
#[test]
|
|
fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
|
|
let wrong_blind = secp::random_secret();
|
|
let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) = new_server(
|
|
"swap_invalid_com_signature",
|
|
&server_key,
|
|
&vec![&input_commit],
|
|
);
|
|
let result = server.swap(&onion, &comsig);
|
|
assert_eq!(Err(SwapError::InvalidComSignature), result);
|
|
|
|
// Make sure no entry is added to the store
|
|
assert_eq!(
|
|
0,
|
|
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment.
|
|
#[test]
|
|
fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(wrong_value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) =
|
|
new_server("swap_invalid_rangeproof", &server_key, &vec![&input_commit]);
|
|
let result = server.swap(&onion, &comsig);
|
|
assert_eq!(Err(SwapError::InvalidRangeproof), result);
|
|
|
|
// Make sure no entry is added to the store
|
|
assert_eq!(
|
|
0,
|
|
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns MissingRangeproof when no rangeproof is provided.
|
|
#[test]
|
|
fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) =
|
|
new_server("swap_missing_rangeproof", &server_key, &vec![&input_commit]);
|
|
let result = server.swap(&onion, &comsig);
|
|
assert_eq!(Err(SwapError::MissingRangeproof), result);
|
|
|
|
// Make sure no entry is added to the store
|
|
assert_eq!(
|
|
0,
|
|
server.store.lock().unwrap().swaps_iter().unwrap().count()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns CoinNotFound when there's no matching output in the UTXO set.
|
|
#[test]
|
|
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) = new_server("swap_utxo_missing", &server_key, &vec![]);
|
|
let result = server.swap(&onion, &comsig);
|
|
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().unwrap().swaps_iter().unwrap().count()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns AlreadySwapped when trying to swap the same commitment multiple times.
|
|
#[test]
|
|
fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) = new_server("swap_already_swapped", &server_key, &vec![&input_commit]);
|
|
server.swap(&onion, &comsig)?;
|
|
|
|
// Call swap a second time
|
|
let result = server.swap(&onion, &comsig);
|
|
assert_eq!(
|
|
Err(SwapError::AlreadySwapped {
|
|
commit: input_commit.clone()
|
|
}),
|
|
result
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload.
|
|
#[test]
|
|
fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
|
|
let wrong_server_key = secp::random_secret();
|
|
let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) =
|
|
new_server("swap_peel_onion_failure", &server_key, &vec![&input_commit]);
|
|
let result = server.swap(&onion, &comsig);
|
|
|
|
assert!(result.is_err());
|
|
assert_error_type!(result, SwapError::PeelOnionFailure(_));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns FeeTooLow when the minimum fee is not met.
|
|
#[test]
|
|
fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> {
|
|
let value: u64 = 200_000_000;
|
|
let fee: u64 = 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 proof = proof(value, fee, &blind, &hop_excess);
|
|
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof));
|
|
|
|
let onion = test_util::create_onion(&input_commit, &vec![hop])?;
|
|
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
|
|
|
|
let (server, _node) = new_server("swap_fee_too_low", &server_key, &vec![&input_commit]);
|
|
let result = server.swap(&onion, &comsig);
|
|
assert_eq!(
|
|
Err(SwapError::FeeTooLow {
|
|
minimum_fee: 12_500_000,
|
|
actual_fee: fee
|
|
}),
|
|
result
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|