diff --git a/Cargo.lock b/Cargo.lock index 1bb4713..924569a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -955,6 +955,21 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "function_name" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ab577a896d09940b5fe12ec5ae71f9d8211fff62c919c03a3750a9901e98a7" +dependencies = [ + "function_name-proc-macro", +] + +[[package]] +name = "function_name-proc-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673464e1e314dd67a0fd9544abc99e8eb28d0c7e3b69b033bcff9b2d00b87333" + [[package]] name = "futures" version = "0.1.31" @@ -1737,6 +1752,31 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.0", + "bitflags 1.3.2", + "bytes 1.1.0", + "headers-core", + "http", + "httpdate 1.0.1", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.3" @@ -1897,6 +1937,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-proxy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" +dependencies = [ + "bytes 1.1.0", + "futures 0.3.17", + "headers", + "http", + "hyper 0.14.14", + "hyper-tls 0.5.0", + "native-tls", + "tokio 1.12.0", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.20.0" @@ -1956,6 +2014,19 @@ dependencies = [ "tokio-tls", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper 0.14.14", + "native-tls", + "tokio 1.12.0", + "tokio-native-tls", +] + [[package]] name = "i18n-config" version = "0.4.2" @@ -2478,6 +2549,7 @@ dependencies = [ "curve25519-dalek 2.1.3", "dirs", "ed25519-dalek", + "function_name", "futures 0.3.17", "grin_api 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", "grin_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", @@ -2493,6 +2565,7 @@ dependencies = [ "grin_wallet_util", "hmac 0.12.0", "hyper 0.14.14", + "hyper-proxy", "itertools", "jsonrpc-core 18.0.0", "jsonrpc-derive", @@ -3280,7 +3353,7 @@ dependencies = [ "http-body 0.3.1", "hyper 0.13.10", "hyper-rustls 0.21.0", - "hyper-tls", + "hyper-tls 0.4.3", "ipnet", "js-sys", "lazy_static", @@ -3634,6 +3707,17 @@ dependencies = [ "yaml-rust 0.4.5", ] +[[package]] +name = "sha1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cc229fb94bcb689ffc39bd4ded842f6ff76885efede7c6d1ffb62582878bea" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.1", +] + [[package]] name = "sha2" version = "0.8.2" @@ -4019,6 +4103,16 @@ dependencies = [ "syn 1.0.84", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.12.0", +] + [[package]] name = "tokio-rustls" version = "0.13.1" diff --git a/Cargo.toml b/Cargo.toml index 7078fb5..2e3e3f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,11 @@ clap = { version = "2.33", features = ["yaml"] } curve25519-dalek = "2.1" dirs = "2.0" ed25519-dalek = "1.0.1" +function_name = "0.3.0" futures = "0.3" hmac = { version = "0.12.0", features = ["std"]} hyper = { version = "0.14", features = ["full"] } +hyper-proxy = "0.9.1" itertools = { version = "0.10.3"} jsonrpc-core = "18.0" jsonrpc-derive = "18.0" diff --git a/README.md b/README.md index 3a2a1fc..e9efe52 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # MWixnet This is an implementation of @tromp's [CoinSwap Proposal](https://forum.grin.mw/t/mimblewimble-coinswap-proposal/8322) with some slight modifications. -A set of n CoinSwap servers (nodei with i=1...n) are agreed upon in advance. They each have a known public key. +A set of n CoinSwap servers (Ni with i=1...n) are agreed upon in advance. They each have a known public key. + +We refer to the first server (N1) as the "Swap Server." This is the server that wallets can submit their coinswaps too. + +We refer to the remaining servers (N2...Nn) as "Mixers." ### Setup #### init-config @@ -19,7 +23,7 @@ A grin-wallet account must be created for receiving extra mwixnet fees. The wall With your wallet and fully synced node both online and listening at the addresses configured, the mwixnet server can be started by running `mwixnet` and providing the server key password and wallet password when prompted. ### SWAP API -The first CoinSwap server (n1) provides the `swap` API, publicly available for use by GRIN wallets. +The Swap Server (N1) provides the `swap` API, which is publicly available for use by GRIN wallets. **jsonrpc:** `2.0` **method:** `swap` diff --git a/mwixnet.yml b/mwixnet.yml index bd1c5d8..b3b3091 100644 --- a/mwixnet.yml +++ b/mwixnet.yml @@ -38,6 +38,18 @@ args: help: Address to bind the rpc server to (e.g. 127.0.0.1:3000) long: bind_addr takes_value: true + - socks_addr: + help: Address to bind the SOCKS5 tor proxy to (e.g. 127.0.0.1:3001) + long: socks_addr + takes_value: true + - prev_server: + help: Hex public key of the previous swap/mix server + long: prev_server + takes_value: true + - next_server: + help: Hex public key of the next mix server + long: next_server + takes_value: true subcommands: - init-config: about: Writes a new configuration file \ No newline at end of file diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..d286a1b --- /dev/null +++ b/src/client.rs @@ -0,0 +1,171 @@ +use crate::config::ServerConfig; +use crate::crypto::dalek; +use crate::onion::Onion; +use crate::servers::mix_rpc::MixReq; +use crate::tx::TxComponents; +use crate::{tor, DalekPublicKey}; + +use grin_api::client; +use grin_api::json_rpc::build_request; +use grin_core::ser; +use grin_core::ser::ProtocolVersion; +use grin_wallet_util::OnionV3Address; +use hyper::client::HttpConnector; +use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use serde_json; +use thiserror::Error; + +/// Error types for interacting with nodes +#[derive(Error, Debug)] +pub enum ClientError { + #[error("Tor Error: {0:?}")] + Tor(tor::TorError), + #[error("API Error: {0:?}")] + API(grin_api::Error), + #[error("Dalek Error: {0:?}")] + Dalek(dalek::DalekError), + #[error("Error parsing response: {0:?}")] + ResponseParse(serde_json::Error), + #[error("Custom client error: {0:?}")] + Custom(String), +} + +/// A client for consuming a mix API +pub trait MixClient: Send + Sync { + /// Swaps the outputs provided and returns the final swapped outputs and kernels. + fn mix_outputs(&self, onions: &Vec) -> Result<(Vec, TxComponents), ClientError>; +} + +pub struct MixClientImpl { + config: ServerConfig, + addr: OnionV3Address, +} + +impl MixClientImpl { + pub fn new(config: ServerConfig, next_pubkey: DalekPublicKey) -> Self { + let addr = OnionV3Address::from_bytes(next_pubkey.as_ref().to_bytes()); + MixClientImpl { config, addr } + } + + fn send_json_request( + &self, + addr: &OnionV3Address, + method: &str, + params: &serde_json::Value, + ) -> Result { + let _tor = tor::init_tor_sender(&self.config).map_err(ClientError::Tor)?; + + let proxy = { + let proxy_uri = format!("http://{:?}", self.config.socks_proxy_addr) + .parse() + .unwrap(); + let proxy = Proxy::new(Intercept::All, proxy_uri); + //proxy.set_authorization(Authorization::basic("John Doe", "Agent1234")); + let connector = HttpConnector::new(); + let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); + proxy_connector + }; + + let url = format!("{}/v1", addr.to_http_str()); + let mut req = client::create_post_request(&url, None, &build_request(method, params)) + .map_err(ClientError::API)?; + + let uri = url.parse().unwrap(); + if let Some(headers) = proxy.http_headers(&uri) { + req.headers_mut().extend(headers.clone().into_iter()); + } + + let res = client::send_request(req).map_err(ClientError::API)?; + + serde_json::from_str(&res).map_err(ClientError::ResponseParse) + } +} + +impl MixClient for MixClientImpl { + fn mix_outputs(&self, onions: &Vec) -> Result<(Vec, TxComponents), ClientError> { + let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap(); + let sig = + dalek::sign(&self.config.key, serialized.as_slice()).map_err(ClientError::Dalek)?; + let mix = MixReq::new(onions.clone(), sig); + + let params = serde_json::json!(mix); + + self.send_json_request::<(Vec, TxComponents)>(&self.addr, "mix", ¶ms) + } +} + +#[cfg(test)] +pub mod mock { + use super::{ClientError, MixClient}; + use crate::onion::Onion; + use crate::tx::TxComponents; + + use std::collections::HashMap; + + pub struct MockMixClient { + results: HashMap, (Vec, TxComponents)>, + } + + impl MockMixClient { + pub fn new() -> MockMixClient { + MockMixClient { + results: HashMap::new(), + } + } + + pub fn set_response(&mut self, onions: &Vec, r: (Vec, TxComponents)) { + self.results.insert(onions.clone(), r); + } + } + + impl MixClient for MockMixClient { + fn mix_outputs( + &self, + onions: &Vec, + ) -> Result<(Vec, TxComponents), ClientError> { + self.results + .get(onions) + .map(|r| Ok(r.clone())) + .unwrap_or(Err(ClientError::Custom("No response set for input".into()))) + } + } +} + +#[cfg(test)] +pub mod test_util { + use super::{ClientError, MixClient}; + use crate::crypto::dalek; + use crate::crypto::secp::SecretKey; + use crate::onion::Onion; + use crate::servers::mix::MixServer; + use crate::tx::TxComponents; + use crate::DalekPublicKey; + use grin_core::ser; + use grin_core::ser::ProtocolVersion; + use std::sync::Arc; + + /// Implementation of the 'MixClient' trait that calls a mix server implementation directly. + /// No JSON-RPC serialization or socket communication occurs. + #[derive(Clone)] + pub struct DirectMixClient { + pub key: SecretKey, + pub mix_server: Arc, + } + + impl MixClient for DirectMixClient { + fn mix_outputs( + &self, + onions: &Vec, + ) -> Result<(Vec, TxComponents), ClientError> { + let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap(); + let sig = dalek::sign(&self.key, serialized.as_slice()).map_err(ClientError::Dalek)?; + + sig.verify( + &DalekPublicKey::from_secret(&self.key), + serialized.as_slice(), + ) + .unwrap(); + Ok(self.mix_server.mix_outputs(&onions, &sig).unwrap()) + } + } +} diff --git a/src/config.rs b/src/config.rs index d6928e8..1fab7dc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,10 @@ -use crate::secp::SecretKey; +use crate::crypto::dalek::DalekPublicKey; +use crate::crypto::secp::SecretKey; use core::num::NonZeroU32; use grin_core::global::ChainTypes; use grin_util::{file, ToHex, ZeroingString}; +use grin_wallet_util::OnionV3Address; use rand::{thread_rng, Rng}; use ring::{aead, pbkdf2}; use serde_derive::{Deserialize, Serialize}; @@ -26,6 +28,8 @@ pub struct ServerConfig { pub interval_s: u32, /// socket address the server listener should bind to pub addr: SocketAddr, + /// socket address the tor sender should bind to + pub socks_proxy_addr: SocketAddr, /// foreign api address of the grin node pub grin_node_url: SocketAddr, /// path to file containing api secret for the grin node @@ -34,9 +38,23 @@ pub struct ServerConfig { pub wallet_owner_url: SocketAddr, /// path to file containing secret for the grin wallet's owner api pub wallet_owner_secret_path: Option, + /// public key of the previous mix/swap server (e.g. N_1 if this is N_2) + #[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)] + pub prev_server: Option, + /// public key of the next mix server + #[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)] + pub next_server: Option, } impl ServerConfig { + pub fn onion_address(&self) -> OnionV3Address { + OnionV3Address::from_private(&self.key.0).unwrap() + } + + pub fn server_pubkey(&self) -> DalekPublicKey { + DalekPublicKey::from_secret(&self.key) + } + pub fn node_api_secret(&self) -> Option { file::get_first_line(self.grin_node_secret_path.clone()) } @@ -163,10 +181,15 @@ struct RawConfig { nonce: String, interval_s: u32, addr: SocketAddr, + socks_proxy_addr: SocketAddr, grin_node_url: SocketAddr, grin_node_secret_path: Option, wallet_owner_url: SocketAddr, wallet_owner_secret_path: Option, + #[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)] + prev_server: Option, + #[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)] + next_server: Option, } /// Writes the server config to the config_path given, encrypting the server_key first. @@ -183,10 +206,13 @@ pub fn write_config( nonce: encrypted.nonce, interval_s: server_config.interval_s, addr: server_config.addr, + socks_proxy_addr: server_config.socks_proxy_addr, grin_node_url: server_config.grin_node_url, grin_node_secret_path: server_config.grin_node_secret_path.clone(), wallet_owner_url: server_config.wallet_owner_url, wallet_owner_secret_path: server_config.wallet_owner_secret_path.clone(), + prev_server: server_config.prev_server.clone(), + next_server: server_config.next_server.clone(), }; let encoded: String = toml::to_string(&raw_config).map_err(|e| ConfigError::EncodingError(e))?; @@ -203,10 +229,8 @@ pub fn load_config( config_path: &PathBuf, password: &ZeroingString, ) -> Result { - let contents = - std::fs::read_to_string(config_path).map_err(|e| ConfigError::ReadConfigError(e))?; - let raw_config: RawConfig = - toml::from_str(&contents).map_err(|e| ConfigError::DecodingError(e))?; + let contents = std::fs::read_to_string(config_path).map_err(ConfigError::ReadConfigError)?; + let raw_config: RawConfig = toml::from_str(&contents).map_err(ConfigError::DecodingError)?; let encrypted_key = EncryptedServerKey { encrypted_key: raw_config.encrypted_key, @@ -219,10 +243,13 @@ pub fn load_config( key: secret_key, interval_s: raw_config.interval_s, addr: raw_config.addr, + socks_proxy_addr: raw_config.socks_proxy_addr, grin_node_url: raw_config.grin_node_url, grin_node_secret_path: raw_config.grin_node_secret_path, wallet_owner_url: raw_config.wallet_owner_url, wallet_owner_secret_path: raw_config.wallet_owner_secret_path, + prev_server: raw_config.prev_server, + next_server: raw_config.next_server, }) } @@ -260,10 +287,37 @@ pub fn wallet_owner_url(_chain_type: &ChainTypes) -> SocketAddr { "127.0.0.1:3420".parse().unwrap() } +#[cfg(test)] +pub mod test_util { + use crate::{DalekPublicKey, ServerConfig}; + use secp256k1zkp::SecretKey; + use std::net::TcpListener; + + pub fn local_config( + server_key: &SecretKey, + prev_server: &Option, + next_server: &Option, + ) -> Result> { + let config = ServerConfig { + key: server_key.clone(), + interval_s: 1, + addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, + socks_proxy_addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, + grin_node_url: "127.0.0.1:3413".parse()?, + grin_node_secret_path: None, + wallet_owner_url: "127.0.0.1:3420".parse()?, + wallet_owner_secret_path: None, + prev_server: prev_server.clone(), + next_server: next_server.clone(), + }; + Ok(config) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::secp; + use crate::crypto::secp; #[test] fn server_key_encrypt() { diff --git a/src/secp.rs b/src/crypto/comsig.rs similarity index 61% rename from src/secp.rs rename to src/crypto/comsig.rs index 72fd14d..55fc627 100644 --- a/src/secp.rs +++ b/src/crypto/comsig.rs @@ -1,275 +1,189 @@ -pub use secp256k1zkp::aggsig; -pub use secp256k1zkp::constants::{ - AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE, - SECRET_KEY_SIZE, -}; -pub use secp256k1zkp::ecdh::SharedSecret; -pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY}; -pub use secp256k1zkp::pedersen::{Commitment, RangeProof}; -pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature}; - -use blake2::blake2b::Blake2b; -use byteorder::{BigEndian, ByteOrder}; -use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; -use secp256k1zkp::rand::thread_rng; -use thiserror::Error; - -/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys -#[derive(Clone)] -pub struct ComSignature { - pub_nonce: Commitment, - s: SecretKey, - t: SecretKey, -} - -/// Error types for Commitment Signatures -#[derive(Error, Debug)] -pub enum ComSigError { - #[error("Commitment signature is invalid")] - InvalidSig, - #[error("Secp256k1zkp error: {0:?}")] - Secp256k1zkp(secp256k1zkp::Error), -} - -impl From for ComSigError { - fn from(err: secp256k1zkp::Error) -> ComSigError { - ComSigError::Secp256k1zkp(err) - } -} - -impl ComSignature { - pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { - ComSignature { - pub_nonce: pub_nonce.to_owned(), - s: s.to_owned(), - t: t.to_owned(), - } - } - - #[allow(dead_code)] - pub fn sign( - amount: u64, - blind: &SecretKey, - msg: &Vec, - ) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - - let mut amt_bytes = [0; 32]; - BigEndian::write_u64(&mut amt_bytes[24..32], amount); - let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; - - let k_1 = SecretKey::new(&secp, &mut thread_rng()); - let k_2 = SecretKey::new(&secp, &mut thread_rng()); - - let commitment = secp.commit(amount, blind.clone())?; - let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?; - - let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?; - - // s = k_1 + (e * amount) - let mut s = k_amt.clone(); - s.mul_assign(&secp, &e)?; - s.add_assign(&secp, &k_1)?; - - // t = k_2 + (e * blind) - let mut t = blind.clone(); - t.mul_assign(&secp, &e)?; - t.add_assign(&secp, &k_2)?; - - Ok(ComSignature::new(&nonce_commitment, &s, &t)) - } - - #[allow(non_snake_case)] - pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<(), ComSigError> { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - - let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?; - - let mut Ce = commit.to_pubkey(&secp)?; - let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?; - Ce.mul_assign(&secp, &e)?; - - let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()]; - let S2 = secp.commit_sum(commits, Vec::new())?; - - if S1 != S2 { - return Err(ComSigError::InvalidSig); - } - - Ok(()) - } - - fn calc_challenge( - secp: &Secp256k1, - commit: &Commitment, - nonce_commit: &Commitment, - msg: &Vec, - ) -> Result { - let mut challenge_hasher = Blake2b::new(32); - challenge_hasher.update(&commit.0); - challenge_hasher.update(&nonce_commit.0); - challenge_hasher.update(msg); - - let mut challenge = [0; 32]; - challenge.copy_from_slice(challenge_hasher.finalize().as_bytes()); - - Ok(SecretKey::from_slice(&secp, &challenge)?) - } -} - -/// Serializes a ComSignature to and from hex -pub mod comsig_serde { - use super::ComSignature; - use grin_core::ser::{self, ProtocolVersion}; - use grin_util::ToHex; - use serde::{Deserialize, Serializer}; - - /// Serializes a ComSignature as a hex string - pub fn serialize(comsig: &ComSignature, serializer: S) -> Result - where - S: Serializer, - { - use serde::ser::Error; - let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; - serializer.serialize_str(&bytes.to_hex()) - } - - /// Creates a ComSignature from a hex string - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - let bytes = String::deserialize(deserializer) - .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; - let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?; - Ok(sig) - } -} - -#[allow(non_snake_case)] -impl Readable for ComSignature { - fn read(reader: &mut R) -> Result { - let R = Commitment::read(reader)?; - let s = read_secret_key(reader)?; - let t = read_secret_key(reader)?; - Ok(ComSignature::new(&R, &s, &t)) - } -} - -impl Writeable for ComSignature { - fn write(&self, writer: &mut W) -> Result<(), ser::Error> { - writer.write_fixed_bytes(self.pub_nonce.0)?; - writer.write_fixed_bytes(self.s.0)?; - writer.write_fixed_bytes(self.t.0)?; - Ok(()) - } -} - -/// Generate a random SecretKey. -pub fn random_secret() -> SecretKey { - let secp = Secp256k1::new(); - SecretKey::new(&secp, &mut thread_rng()) -} - -/// Deserialize a SecretKey from a Reader -pub fn read_secret_key(reader: &mut R) -> Result { - let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; - let secp = Secp256k1::with_caps(ContextFlag::None); - let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; - Ok(pk) -} - -/// Build a Pedersen Commitment using the provided value and blinding factor -pub fn commit(value: u64, blind: &SecretKey) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let commit = secp.commit(value, blind.clone())?; - Ok(commit) -} - -/// Add a blinding factor to an existing Commitment -pub fn add_excess( - commitment: &Commitment, - excess: &SecretKey, -) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let excess_commit: Commitment = secp.commit(0, excess.clone())?; - - let commits = vec![commitment.clone(), excess_commit.clone()]; - let sum = secp.commit_sum(commits, Vec::new())?; - Ok(sum) -} - -/// Subtracts a value (v*H) from an existing commitment -pub fn sub_value(commitment: &Commitment, value: u64) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?; - let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; - Ok(sum) -} - -/// Signs the message with the provided SecretKey -pub fn sign(sk: &SecretKey, msg: &Message) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Full); - let pubkey = PublicKey::from_secret_key(&secp, &sk)?; - let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?; - Ok(sig) -} - -#[cfg(test)] -pub mod test_util { - use crate::secp::{self, Commitment, RangeProof, Secp256k1}; - use grin_core::core::hash::Hash; - use grin_util::ToHex; - use rand::RngCore; - - pub fn rand_commit() -> Commitment { - secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap() - } - - pub fn rand_hash() -> Hash { - Hash::from_hex(secp::random_secret().to_hex().as_str()).unwrap() - } - - pub fn rand_proof() -> RangeProof { - let secp = Secp256k1::new(); - secp.bullet_proof( - rand::thread_rng().next_u64(), - secp::random_secret(), - secp::random_secret(), - secp::random_secret(), - None, - None, - ) - } -} - -#[cfg(test)] -mod tests { - use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; - - use rand::Rng; - use secp256k1zkp::rand::{thread_rng, RngCore}; - - /// Test signing and verification of ComSignatures - #[test] - fn verify_comsig() -> Result<(), ComSigError> { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - - let amount = thread_rng().next_u64(); - let blind = SecretKey::new(&secp, &mut thread_rng()); - let msg: [u8; 16] = rand::thread_rng().gen(); - let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?; - - let commit = secp.commit(amount, blind.clone())?; - assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); - - let wrong_msg: [u8; 16] = rand::thread_rng().gen(); - assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); - - let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; - assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); - - Ok(()) - } -} +use crate::crypto::secp::{self, Commitment, ContextFlag, Secp256k1, SecretKey}; + +use blake2::blake2b::Blake2b; +use byteorder::{BigEndian, ByteOrder}; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use secp256k1zkp::rand::thread_rng; +use thiserror::Error; + +/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys +#[derive(Clone)] +pub struct ComSignature { + pub_nonce: Commitment, + s: SecretKey, + t: SecretKey, +} + +/// Error types for Commitment Signatures +#[derive(Error, Debug)] +pub enum ComSigError { + #[error("Commitment signature is invalid")] + InvalidSig, + #[error("Secp256k1zkp error: {0:?}")] + Secp256k1zkp(secp256k1zkp::Error), +} + +impl From for ComSigError { + fn from(err: secp256k1zkp::Error) -> ComSigError { + ComSigError::Secp256k1zkp(err) + } +} + +impl ComSignature { + pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { + ComSignature { + pub_nonce: pub_nonce.to_owned(), + s: s.to_owned(), + t: t.to_owned(), + } + } + + #[allow(dead_code)] + pub fn sign( + amount: u64, + blind: &SecretKey, + msg: &Vec, + ) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let mut amt_bytes = [0; 32]; + BigEndian::write_u64(&mut amt_bytes[24..32], amount); + let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; + + let k_1 = SecretKey::new(&secp, &mut thread_rng()); + let k_2 = SecretKey::new(&secp, &mut thread_rng()); + + let commitment = secp.commit(amount, blind.clone())?; + let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?; + + let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?; + + // s = k_1 + (e * amount) + let mut s = k_amt.clone(); + s.mul_assign(&secp, &e)?; + s.add_assign(&secp, &k_1)?; + + // t = k_2 + (e * blind) + let mut t = blind.clone(); + t.mul_assign(&secp, &e)?; + t.add_assign(&secp, &k_2)?; + + Ok(ComSignature::new(&nonce_commitment, &s, &t)) + } + + #[allow(non_snake_case)] + pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<(), ComSigError> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?; + + let mut Ce = commit.to_pubkey(&secp)?; + let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?; + Ce.mul_assign(&secp, &e)?; + + let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()]; + let S2 = secp.commit_sum(commits, Vec::new())?; + + if S1 != S2 { + return Err(ComSigError::InvalidSig); + } + + Ok(()) + } + + fn calc_challenge( + secp: &Secp256k1, + commit: &Commitment, + nonce_commit: &Commitment, + msg: &Vec, + ) -> Result { + let mut challenge_hasher = Blake2b::new(32); + challenge_hasher.update(&commit.0); + challenge_hasher.update(&nonce_commit.0); + challenge_hasher.update(msg); + + let mut challenge = [0; 32]; + challenge.copy_from_slice(challenge_hasher.finalize().as_bytes()); + + Ok(SecretKey::from_slice(&secp, &challenge)?) + } +} + +/// Serializes a ComSignature to and from hex +pub mod comsig_serde { + use super::ComSignature; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use serde::{Deserialize, Serializer}; + + /// Serializes a ComSignature as a hex string + pub fn serialize(comsig: &ComSignature, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::Error; + let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; + serializer.serialize_str(&bytes.to_hex()) + } + + /// Creates a ComSignature from a hex string + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let bytes = String::deserialize(deserializer) + .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; + let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?; + Ok(sig) + } +} + +#[allow(non_snake_case)] +impl Readable for ComSignature { + fn read(reader: &mut R) -> Result { + let R = Commitment::read(reader)?; + let s = secp::read_secret_key(reader)?; + let t = secp::read_secret_key(reader)?; + Ok(ComSignature::new(&R, &s, &t)) + } +} + +impl Writeable for ComSignature { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.pub_nonce.0)?; + writer.write_fixed_bytes(self.s.0)?; + writer.write_fixed_bytes(self.t.0)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; + + use rand::Rng; + use secp256k1zkp::rand::{thread_rng, RngCore}; + + /// Test signing and verification of ComSignatures + #[test] + fn verify_comsig() -> Result<(), ComSigError> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let amount = thread_rng().next_u64(); + let blind = SecretKey::new(&secp, &mut thread_rng()); + let msg: [u8; 16] = rand::thread_rng().gen(); + let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?; + + let commit = secp.commit(amount, blind.clone())?; + assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); + + let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); + + let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; + assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); + + Ok(()) + } +} diff --git a/src/crypto/dalek.rs b/src/crypto/dalek.rs new file mode 100644 index 0000000..45474b3 --- /dev/null +++ b/src/crypto/dalek.rs @@ -0,0 +1,285 @@ +use crate::crypto::secp::SecretKey; + +use ed25519_dalek::{Keypair, PublicKey, Signature, Signer, Verifier}; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use grin_util::ToHex; +use thiserror::Error; + +/// Error types for Dalek structures and logic +#[derive(Clone, Error, Debug, PartialEq)] +pub enum DalekError { + #[error("Hex error {0:?}")] + HexError(String), + #[error("Failed to parse secret key")] + KeyParseError, + #[error("Failed to verify signature")] + SigVerifyFailed, +} + +/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization +#[derive(Clone, Debug, PartialEq)] +pub struct DalekPublicKey(PublicKey); + +impl DalekPublicKey { + /// Convert DalekPublicKey to hex string + pub fn to_hex(&self) -> String { + self.0.to_hex() + } + + /// Convert hex string to DalekPublicKey. + pub fn from_hex(hex: &str) -> Result { + let bytes = grin_util::from_hex(hex) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let pk = PublicKey::from_bytes(bytes.as_ref()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + Ok(DalekPublicKey(pk)) + } + + /// Compute DalekPublicKey from a SecretKey + pub fn from_secret(key: &SecretKey) -> Self { + let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap(); + let pk: PublicKey = (&secret).into(); + DalekPublicKey(pk) + } +} + +impl AsRef for DalekPublicKey { + fn as_ref(&self) -> &PublicKey { + &self.0 + } +} + +/// Serializes an Option to and from hex +pub mod option_dalek_pubkey_serde { + use super::DalekPublicKey; + use grin_util::ToHex; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(pk: &Option, serializer: S) -> Result + where + S: Serializer, + { + match pk { + Some(pk) => serializer.serialize_str(&pk.0.to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => DalekPublicKey::from_hex(&string) + .map_err(|e| Error::custom(e.to_string())) + .and_then(|pk: DalekPublicKey| Ok(Some(pk))), + None => Ok(None), + }) + } +} + +impl Readable for DalekPublicKey { + fn read(reader: &mut R) -> Result { + let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?) + .map_err(|_| ser::Error::CorruptedData)?; + Ok(DalekPublicKey(pk)) + } +} + +impl Writeable for DalekPublicKey { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.0.to_bytes())?; + Ok(()) + } +} + +/// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization +#[derive(Clone, Debug, PartialEq)] +pub struct DalekSignature(Signature); + +impl DalekSignature { + /// Convert hex string to DalekSignature. + pub fn from_hex(hex: &str) -> Result { + let bytes = grin_util::from_hex(hex) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let sig = Signature::from_bytes(bytes.as_ref()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + Ok(DalekSignature(sig)) + } + + /// Verifies DalekSignature + pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> { + pk.as_ref() + .verify(&msg, &self.0) + .map_err(|_| DalekError::SigVerifyFailed) + } +} + +impl AsRef for DalekSignature { + fn as_ref(&self) -> &Signature { + &self.0 + } +} + +/// Serializes a DalekSignature to and from hex +pub mod dalek_sig_serde { + use super::DalekSignature; + use grin_util::ToHex; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(sig: &DalekSignature, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&sig.0.to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let str = String::deserialize(deserializer)?; + let sig = DalekSignature::from_hex(&str).map_err(|e| Error::custom(e.to_string()))?; + Ok(sig) + } +} + +pub fn sign(sk: &SecretKey, message: &[u8]) -> Result { + let secret = + ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?; + let public: PublicKey = (&secret).into(); + let keypair = Keypair { secret, public }; + let sig = keypair.sign(&message); + Ok(DalekSignature(sig)) +} + +#[cfg(test)] +pub mod test_util { + use super::*; + use crate::crypto::secp::random_secret; + + pub fn rand_keypair() -> (SecretKey, DalekPublicKey) { + let sk = random_secret(); + let pk = DalekPublicKey::from_secret(&sk); + (sk, pk) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::dalek::test_util; + use crate::crypto::dalek::test_util::rand_keypair; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use rand::Rng; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestPubKeySerde { + #[serde(with = "option_dalek_pubkey_serde", default)] + pk: Option, + } + + #[test] + fn pubkey_test() -> Result<(), Box> { + // Test from_hex + let rand_pk = test_util::rand_keypair().1; + let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap(); + assert_eq!(rand_pk.0, pk_from_hex.0); + + // Test ser (de-)serialization + let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap(); + assert_eq!(bytes.len(), 32); + let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap(); + assert_eq!(rand_pk.0, pk_from_deser.0); + + // Test serde with Some(rand_pk) + let some = TestPubKeySerde { + pk: Some(rand_pk.clone()), + }; + let val = serde_json::to_value(some.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("pk").unwrap() { + assert_eq!(s, &rand_pk.0.to_hex()); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(some, serde_json::from_value(val).unwrap()); + + // Test serde with empty pk field + let none = TestPubKeySerde { pk: None }; + let val = serde_json::to_value(none.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::Null = o.get("pk").unwrap() { + // ok + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(none, serde_json::from_value(val).unwrap()); + + // Test serde with no pk field + let none2 = serde_json::from_str::("{}").unwrap(); + assert_eq!(none, none2); + + Ok(()) + } + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestSigSerde { + #[serde(with = "dalek_sig_serde")] + sig: DalekSignature, + } + + #[test] + fn sig_test() -> Result<(), Box> { + // Sign a message + let (sk, pk) = rand_keypair(); + let msg: [u8; 16] = rand::thread_rng().gen(); + let sig = sign(&sk, &msg).unwrap(); + + // Verify signature + assert!(sig.verify(&pk, &msg).is_ok()); + + // Wrong message + let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + assert!(sig.verify(&pk, &wrong_msg).is_err()); + + // Wrong pubkey + let wrong_pk = rand_keypair().1; + assert!(sig.verify(&wrong_pk, &msg).is_err()); + + // Test from_hex + let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap(); + assert_eq!(sig.0, sig_from_hex.0); + + // Test serde (de-)serialization + let serde_test = TestSigSerde { sig: sig.clone() }; + let val = serde_json::to_value(serde_test.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("sig").unwrap() { + assert_eq!(s, &sig.0.to_hex()); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(serde_test, serde_json::from_value(val).unwrap()); + + Ok(()) + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 0000000..2fdcdb2 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,3 @@ +pub mod comsig; +pub mod dalek; +pub mod secp; diff --git a/src/crypto/secp.rs b/src/crypto/secp.rs new file mode 100644 index 0000000..4956ece --- /dev/null +++ b/src/crypto/secp.rs @@ -0,0 +1,117 @@ +pub use secp256k1zkp::aggsig; +pub use secp256k1zkp::constants::{ + AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE, + SECRET_KEY_SIZE, +}; +pub use secp256k1zkp::ecdh::SharedSecret; +pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY}; +pub use secp256k1zkp::pedersen::{Commitment, RangeProof}; +pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature}; + +use grin_core::ser::{self, Reader}; +use secp256k1zkp::rand::thread_rng; + +/// Generate a random SecretKey. +pub fn random_secret() -> SecretKey { + let secp = Secp256k1::new(); + SecretKey::new(&secp, &mut thread_rng()) +} + +/// Deserialize a SecretKey from a Reader +pub fn read_secret_key(reader: &mut R) -> Result { + let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; + let secp = Secp256k1::with_caps(ContextFlag::None); + let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; + Ok(pk) +} + +/// Build a Pedersen Commitment using the provided value and blinding factor +pub fn commit(value: u64, blind: &SecretKey) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let commit = secp.commit(value, blind.clone())?; + Ok(commit) +} + +/// Add a blinding factor to an existing Commitment +pub fn add_excess( + commitment: &Commitment, + excess: &SecretKey, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let excess_commit: Commitment = secp.commit(0, excess.clone())?; + + let commits = vec![commitment.clone(), excess_commit.clone()]; + let sum = secp.commit_sum(commits, Vec::new())?; + Ok(sum) +} + +/// Subtracts a value (v*H) from an existing commitment +pub fn sub_value(commitment: &Commitment, value: u64) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?; + let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; + Ok(sum) +} + +/// Signs the message with the provided SecretKey +pub fn sign(sk: &SecretKey, msg: &Message) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Full); + let pubkey = PublicKey::from_secret_key(&secp, &sk)?; + let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?; + Ok(sig) +} + +#[cfg(test)] +pub mod test_util { + use crate::crypto::secp::{self, Commitment, RangeProof, Secp256k1, SecretKey}; + use grin_core::core::hash::Hash; + use grin_util::ToHex; + use rand::RngCore; + + pub fn rand_commit() -> Commitment { + secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap() + } + + pub fn rand_hash() -> Hash { + Hash::from_hex(secp::random_secret().to_hex().as_str()).unwrap() + } + + pub fn rand_proof() -> RangeProof { + let secp = Secp256k1::new(); + secp.bullet_proof( + rand::thread_rng().next_u64(), + secp::random_secret(), + secp::random_secret(), + secp::random_secret(), + None, + None, + ) + } + + pub fn proof( + value: u64, + fee: u64, + input_blind: &SecretKey, + hop_excesses: &Vec<&SecretKey>, + ) -> (Commitment, RangeProof) { + let secp = Secp256k1::new(); + + let mut blind = input_blind.clone(); + for hop_excess in hop_excesses { + blind.add_assign(&secp, &hop_excess).unwrap(); + } + + let out_value = value - fee; + + let rp = secp.bullet_proof( + out_value, + blind.clone(), + secp::random_secret(), + secp::random_secret(), + None, + None, + ); + + (secp::commit(out_value, &blind).unwrap(), rp) + } +} diff --git a/src/main.rs b/src/main.rs index 19e5348..d5a705b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,16 @@ use config::ServerConfig; use node::HttpGrinNode; -use std::collections::HashMap; use store::SwapStore; use wallet::HttpWallet; +use crate::client::{MixClient, MixClientImpl}; +use crate::crypto::dalek::DalekPublicKey; use crate::node::GrinNode; use crate::store::StoreError; use clap::App; use grin_core::global; use grin_core::global::ChainTypes; use grin_util::{StopState, ZeroingString}; -use grin_wallet_impls::tor::config as tor_config; -use grin_wallet_impls::tor::process as tor_process; -use grin_wallet_util::OnionV3Address; use rpassword; use std::path::PathBuf; use std::sync::Arc; @@ -21,20 +19,22 @@ use tokio::runtime::Runtime; #[macro_use] extern crate clap; +mod client; mod config; +mod crypto; mod node; mod onion; -mod rpc; -mod secp; -mod server; +mod servers; mod store; +mod tor; +mod tx; mod types; mod wallet; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; -fn main() { - real_main().unwrap(); +fn main() -> Result<(), Box> { + real_main()?; std::process::exit(0); } @@ -61,10 +61,17 @@ fn real_main() -> Result<(), Box> { .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() { @@ -76,9 +83,10 @@ fn real_main() -> Result<(), Box> { } let server_config = ServerConfig { - key: secp::random_secret(), + 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), @@ -99,6 +107,8 @@ fn real_main() -> Result<(), Box> { .to_str() .map(|p| p.to_owned()), }, + prev_server, + next_server, }; let password = prompt_password_confirm(); @@ -113,11 +123,6 @@ fn real_main() -> Result<(), Box> { let password = prompt_password(); let mut server_config = config::load_config(&config_path, &password)?; - // Override bind_addr, if supplied - if let Some(bind_addr) = bind_addr { - server_config.addr = bind_addr.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()?; @@ -138,6 +143,26 @@ fn real_main() -> Result<(), Box> { 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 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 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, @@ -165,17 +190,7 @@ fn real_main() -> Result<(), Box> { } }; - // Open SwapStore - let store = SwapStore::new( - config::get_grin_path(&chain_type) // todo: load from config - .join("db") - .to_str() - .ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr( - "db_root path error".to_string(), - )))?, - )?; - - let mut tor_process = init_tor_listener(&server_config)?; + let mut tor_process = tor::init_tor_listener(&server_config)?; let stop_state = Arc::new(StopState::new()); let stop_state_clone = stop_state.clone(); @@ -187,14 +202,52 @@ fn real_main() -> Result<(), Box> { stop_state_clone.stop(); }); - // Start the mwixnet JSON-RPC HTTP server - rpc::listen( - server_config, - Arc::new(wallet), - Arc::new(node), - store, - stop_state, - ) + 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() + ); + + servers::mix_rpc::listen( + server_config, + next_mixer, + Arc::new(wallet), + Arc::new(node), + stop_state, + ) + } 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(), + )))?, + )?; + + // Start the mwixnet JSON-RPC HTTP 'swap' server + servers::swap_rpc::listen( + server_config, + next_mixer, + Arc::new(wallet), + Arc::new(node), + store, + stop_state, + ) + } } #[cfg(unix)] @@ -245,39 +298,3 @@ fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString { } } } - -fn init_tor_listener( - server_config: &ServerConfig, -) -> Result> { - let mut tor_dir = config::get_grin_path(&global::get_chain_type()); - tor_dir.push("tor/listener"); - - let mut torrc_dir = tor_dir.clone(); - torrc_dir.push("torrc"); - - tor_config::output_tor_listener_config( - tor_dir.to_str().unwrap(), - server_config.addr.to_string().as_str(), - &vec![server_config.key.clone()], - HashMap::new(), - HashMap::new(), - ) - .unwrap(); - - // Start TOR process - let mut process = tor_process::TorProcess::new(); - process - .torrc_path(torrc_dir.to_str().unwrap()) - .working_dir(tor_dir.to_str().unwrap()) - .timeout(20) - .completion_percent(100) - .launch() - .unwrap(); - - let onion_address = OnionV3Address::from_private(&server_config.key.0).unwrap(); - println!( - "Server listening at http://{}.onion", - onion_address.to_ov3_str() - ); - Ok(process) -} diff --git a/src/node.rs b/src/node.rs index d37cde0..b8dc236 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,4 +1,4 @@ -use crate::secp::Commitment; +use crate::crypto::secp::Commitment; use grin_api::client; use grin_api::json_rpc::{build_request, Request, Response}; @@ -43,10 +43,10 @@ pub fn is_unspent(node: &Arc, commit: &Commitment) -> Result, - output_commit: &Commitment, + commit: &Commitment, next_block_height: u64, ) -> Result { - let output = node.get_utxo(&output_commit)?; + let output = node.get_utxo(&commit)?; if let Some(out) = output { let is_coinbase = match out.output_type { OutputType::Coinbase => true, @@ -166,7 +166,7 @@ impl GrinNode for HttpGrinNode { #[cfg(test)] pub mod mock { use super::{GrinNode, NodeError}; - use crate::secp::Commitment; + use crate::crypto::secp::Commitment; use grin_api::{OutputPrintable, OutputType}; use grin_core::core::Transaction; @@ -181,13 +181,24 @@ pub mod mock { } impl MockGrinNode { - pub fn new() -> MockGrinNode { + pub fn new() -> Self { MockGrinNode { utxos: HashMap::new(), txns_posted: RwLock::new(Vec::new()), } } + pub fn new_with_utxos(utxos: &Vec<&Commitment>) -> Self { + let mut node = MockGrinNode { + utxos: HashMap::new(), + txns_posted: RwLock::new(Vec::new()), + }; + for utxo in utxos { + node.add_default_utxo(utxo); + } + node + } + pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) { self.utxos.insert(output_commit.clone(), utxo.clone()); } diff --git a/src/onion.rs b/src/onion.rs index ff61971..792dff5 100644 --- a/src/onion.rs +++ b/src/onion.rs @@ -1,4 +1,4 @@ -use crate::secp::{self, Commitment, SecretKey}; +use crate::crypto::secp::{self, Commitment, SecretKey}; use crate::types::Payload; use crate::onion::OnionError::{InvalidKeyLength, SerializationError}; @@ -57,6 +57,15 @@ fn vec_to_32_byte_arr(v: Vec) -> Result<[u8; 32], OnionError> { v.try_into().map_err(|_| InvalidKeyLength) } +/// An onion with a layer decrypted +#[derive(Clone, Debug)] +pub struct PeeledOnion { + /// The payload from the peeled layer + pub payload: Payload, + /// The onion remaining after a layer was peeled + pub onion: Onion, +} + impl Onion { pub fn serialize(&self) -> Result, ser::Error> { let mut vec = vec![]; @@ -65,7 +74,7 @@ impl Onion { } /// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload - pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion), OnionError> { + pub fn peel_layer(&self, secret_key: &SecretKey) -> Result { let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey); let mut cipher = new_stream_cipher(&shared_secret)?; @@ -106,7 +115,10 @@ impl Onion { commit: commitment.clone(), enc_payloads, }; - Ok((decrypted_payload, peeled_onion)) + Ok(PeeledOnion { + payload: decrypted_payload, + onion: peeled_onion, + }) } } @@ -292,13 +304,14 @@ impl From for OnionError { #[cfg(test)] pub mod test_util { use super::{Onion, OnionError, RawBytes}; - use crate::secp::test_util::{rand_commit, rand_proof}; - use crate::secp::{random_secret, Commitment, SecretKey}; + use crate::crypto::secp::test_util::{rand_commit, rand_proof}; + use crate::crypto::secp::{random_secret, Commitment, SecretKey}; use crate::types::Payload; use chacha20::cipher::StreamCipher; use grin_core::core::FeeFields; use rand::{thread_rng, RngCore}; + use secp256k1zkp::pedersen::RangeProof; use x25519_dalek::PublicKey as xPublicKey; use x25519_dalek::{SharedSecret, StaticSecret}; @@ -307,6 +320,23 @@ pub mod test_util { pub pubkey: xPublicKey, pub payload: Payload, } + + pub fn new_hop( + server_key: &SecretKey, + hop_excess: &SecretKey, + fee: u64, + proof: Option, + ) -> 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, + }, + } + } + /* Choose random xi for each node ni and create a Payload (Pi) for each containing xi Build a rangeproof for Cn=Cin+(Σx1...n)*G and include it in payload Pn @@ -391,7 +421,7 @@ pub mod test_util { #[cfg(test)] pub mod tests { use super::test_util::{self, Hop}; - use crate::secp; + use crate::crypto::secp; use crate::types::Payload; use grin_core::core::FeeFields; @@ -454,8 +484,8 @@ pub mod tests { }; for i in 0..5 { let peeled = onion_packet.peel_layer(&keys[i]).unwrap(); - payload = peeled.0; - onion_packet = peeled.1; + payload = peeled.payload; + onion_packet = peeled.onion; } assert!(payload.rangeproof.is_some()); diff --git a/src/servers/mix.rs b/src/servers/mix.rs new file mode 100644 index 0000000..1391bd5 --- /dev/null +++ b/src/servers/mix.rs @@ -0,0 +1,409 @@ +use crate::config::ServerConfig; +use crate::crypto::dalek::{self, DalekSignature}; +use crate::onion::{Onion, OnionError, PeeledOnion}; +use crate::wallet::Wallet; +use crate::{node, tx, GrinNode}; +use std::collections::{HashMap, HashSet}; + +use crate::client::MixClient; +use crate::tx::TxComponents; +use grin_core::core::{Output, OutputFeatures, TransactionBody}; +use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; +use grin_core::ser; +use grin_core::ser::ProtocolVersion; +use itertools::Itertools; +use secp256k1zkp::key::ZERO_KEY; +use secp256k1zkp::Secp256k1; +use std::sync::Arc; +use thiserror::Error; + +/// Mixer error types +#[derive(Error, Debug)] +pub enum MixError { + #[error("Invalid number of payloads provided")] + InvalidPayloadLength, + #[error("Signature is invalid")] + InvalidSignature, + #[error("Rangeproof is invalid")] + InvalidRangeproof, + #[error("Rangeproof is required but was not supplied")] + MissingRangeproof, + #[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("None of the outputs could be mixed")] + NoValidOutputs, + #[error("Dalek error: {0:?}")] + Dalek(dalek::DalekError), + #[error("Secp error: {0:?}")] + Secp(grin_util::secp::Error), + #[error("Error building transaction: {0:?}")] + TxError(tx::TxError), + #[error("Wallet error: {0:?}")] + WalletError(crate::wallet::WalletError), + #[error("Client comm error: {0:?}")] + Client(crate::client::ClientError), +} + +/// An internal MWixnet server - a "Mixer" +pub trait MixServer: Send + Sync { + /// Swaps the outputs provided and returns the final swapped outputs and kernels. + fn mix_outputs( + &self, + onions: &Vec, + sig: &DalekSignature, + ) -> Result<(Vec, TxComponents), MixError>; +} + +/// The standard MWixnet "Mixer" implementation +#[derive(Clone)] +pub struct MixServerImpl { + secp: Secp256k1, + server_config: ServerConfig, + mix_client: Option>, + wallet: Arc, + node: Arc, +} + +impl MixServerImpl { + /// Create a new 'Mix' server + pub fn new( + server_config: ServerConfig, + mix_client: Option>, + wallet: Arc, + node: Arc, + ) -> Self { + MixServerImpl { + secp: Secp256k1::new(), + server_config, + mix_client, + wallet, + node, + } + } + + /// 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 mix. + /// Requires enough fee for the mixer's kernel. + fn get_minimum_mix_fee(&self) -> u64 { + TransactionBody::weight_by_iok(0, 0, 1) * self.get_fee_base() + } + + fn peel_onion(&self, onion: &Onion) -> Result { + // 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(MixError::InvalidPayloadLength); + } + + // Peel the top layer + let peeled = onion + .peel_layer(&self.server_config.key) + .map_err(|e| MixError::PeelOnionFailure(e))?; + + // Verify the fee meets the minimum + let fee: u64 = peeled.payload.fee.into(); + if fee < self.get_minimum_mix_fee() { + return Err(MixError::FeeTooLow { + minimum_fee: self.get_minimum_mix_fee(), + actual_fee: fee, + }); + } + + if let Some(r) = peeled.payload.rangeproof { + // Verify the bullet proof + self.secp + .verify_bullet_proof(peeled.onion.commit, r, None) + .map_err(|_| MixError::InvalidRangeproof)?; + } else if peeled.onion.enc_payloads.is_empty() { + // A rangeproof is required in the last payload + return Err(MixError::MissingRangeproof); + } + + Ok(peeled) + } + + fn build_final_outputs( + &self, + peeled: &Vec<(usize, PeeledOnion)>, + ) -> Result<(Vec, TxComponents), MixError> { + // Filter out commitments that already exist in the UTXO set + let filtered: Vec<&(usize, PeeledOnion)> = peeled + .iter() + .filter(|(_, p)| !node::is_unspent(&self.node, &p.onion.commit).unwrap_or(true)) + .collect(); + + // Build plain outputs for each mix entry + let outputs: Vec = filtered + .iter() + .map(|(_, p)| { + Output::new( + OutputFeatures::Plain, + p.onion.commit, + p.payload.rangeproof.unwrap(), + ) + }) + .collect(); + + let fees_paid = filtered.iter().map(|(_, p)| p.payload.fee.fee()).sum(); + let output_excesses = filtered + .iter() + .map(|(_, p)| p.payload.excess.clone()) + .collect(); + + let components = tx::assemble_components( + &self.wallet, + &TxComponents { + offset: ZERO_KEY, + kernels: Vec::new(), + outputs, + }, + &output_excesses, + self.get_fee_base(), + fees_paid, + ) + .map_err(MixError::TxError)?; + + let indices = filtered.iter().map(|(i, _)| *i).collect(); + + Ok((indices, components)) + } + + fn call_next_mixer( + &self, + peeled: &Vec<(usize, PeeledOnion)>, + ) -> Result<(Vec, TxComponents), MixError> { + // Sort by commitment + let mut onions_with_index = peeled.clone(); + onions_with_index + .sort_by(|(_, a), (_, b)| a.onion.commit.partial_cmp(&b.onion.commit).unwrap()); + + // Create map of prev indices to next indices + let map_indices: HashMap = + HashMap::from_iter(onions_with_index.iter().enumerate().map(|(i, j)| (j.0, i))); + + // Call next server + let onions = peeled.iter().map(|(_, p)| p.onion.clone()).collect(); + let (mixed_indices, mixed_components) = self + .mix_client + .as_ref() + .unwrap() + .mix_outputs(&onions) + .map_err(MixError::Client)?; + + // Remove filtered entries + let kept_next_indices = HashSet::<_>::from_iter(mixed_indices.clone()); + let filtered_onions: Vec<&(usize, PeeledOnion)> = onions_with_index + .iter() + .filter(|(i, _)| { + map_indices.contains_key(i) + && kept_next_indices.contains(map_indices.get(i).unwrap()) + }) + .collect(); + + // Calculate excess of entries kept + let excesses = filtered_onions + .iter() + .map(|(_, p)| p.payload.excess.clone()) + .collect(); + + // Calculate total fee of entries kept + let fees_paid = filtered_onions + .iter() + .fold(0, |f, (_, p)| f + p.payload.fee.fee()); + + let indices = kept_next_indices.into_iter().sorted().collect(); + + let components = tx::assemble_components( + &self.wallet, + &mixed_components, + &excesses, + self.get_fee_base(), + fees_paid, + ) + .map_err(MixError::TxError)?; + + Ok((indices, components)) + } +} + +impl MixServer for MixServerImpl { + fn mix_outputs( + &self, + onions: &Vec, + sig: &DalekSignature, + ) -> Result<(Vec, TxComponents), MixError> { + // Verify Signature + let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap(); + sig.verify( + self.server_config.prev_server.as_ref().unwrap(), + serialized.as_slice(), + ) + .map_err(|_| MixError::InvalidSignature)?; + + // Peel onions and filter + let mut peeled: Vec<(usize, PeeledOnion)> = onions + .iter() + .enumerate() + .filter_map(|(i, o)| { + if let Some(p) = self.peel_onion(&o).ok() { + Some((i, p)) + } else { + None + } + }) + .collect(); + + // Remove duplicate commitments + peeled.sort_by_key(|(_, o)| o.onion.commit); + peeled.dedup_by_key(|(_, o)| o.onion.commit); + peeled.sort_by_key(|(i, _)| *i); + + if peeled.is_empty() { + return Err(MixError::NoValidOutputs); + } + + if self.server_config.next_server.is_some() { + self.call_next_mixer(&peeled) + } else { + self.build_final_outputs(&peeled) + } + } +} + +#[cfg(test)] +mod test_util { + use crate::client::test_util::DirectMixClient; + use crate::node::mock::MockGrinNode; + use crate::wallet::mock::MockWallet; + use crate::{config, DalekPublicKey, MixClient}; + + use crate::servers::mix::MixServerImpl; + use secp256k1zkp::SecretKey; + use std::sync::Arc; + + pub fn new_mixer( + server_key: &SecretKey, + prev_server: (&SecretKey, &DalekPublicKey), + next_server: &Option<(DalekPublicKey, Arc)>, + node: &Arc, + ) -> (Arc, Arc) { + let config = config::test_util::local_config( + &server_key, + &Some(prev_server.1.clone()), + &next_server.as_ref().map(|(k, _)| k.clone()), + ) + .unwrap(); + + let wallet = Arc::new(MockWallet::new()); + let mix_server = Arc::new(MixServerImpl::new( + config, + next_server.as_ref().map(|(_, c)| c.clone()), + wallet.clone(), + node.clone(), + )); + let client = Arc::new(DirectMixClient { + key: prev_server.0.clone(), + mix_server: mix_server.clone(), + }); + + (client, wallet) + } +} + +#[cfg(test)] +mod tests { + use crate::crypto::dalek; + use crate::crypto::secp::{self, Commitment}; + use crate::node::mock::MockGrinNode; + use crate::onion::test_util; + use crate::MixClient; + + use ::function_name::named; + use std::collections::HashSet; + use std::sync::Arc; + + macro_rules! init_test { + () => {{ + grin_core::global::set_local_chain_type( + grin_core::global::ChainTypes::AutomatedTesting, + ); + let db_root = concat!("./target/tmp/.", function_name!()); + let _ = std::fs::remove_dir_all(db_root); + () + }}; + } + + #[test] + #[named] + fn mix_lifecycle() -> Result<(), Box> { + init_test!(); + + 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 node = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); + + let (swap_sk, swap_pk) = dalek::test_util::rand_keypair(); + let (mix1_sk, mix1_pk) = dalek::test_util::rand_keypair(); + let (mix2_sk, mix2_pk) = dalek::test_util::rand_keypair(); + + let swap_hop_excess = secp::random_secret(); + let swap_hop = test_util::new_hop(&swap_sk, &swap_hop_excess, fee, None); + + let mix1_hop_excess = secp::random_secret(); + let mix1_hop = test_util::new_hop(&mix1_sk, &mix1_hop_excess, fee, None); + + let mix2_hop_excess = secp::random_secret(); + let (out_commit, proof) = secp::test_util::proof( + value, + fee * 3, + &blind, + &vec![&swap_hop_excess, &mix1_hop_excess, &mix2_hop_excess], + ); + let mix2_hop = test_util::new_hop(&mix2_sk, &mix2_hop_excess, fee, Some(proof)); + + let onion = test_util::create_onion(&input_commit, &vec![swap_hop, mix1_hop, mix2_hop])?; + + let (mixer2_client, mixer2_wallet) = + super::test_util::new_mixer(&mix2_sk, (&mix1_sk, &mix1_pk), &None, &node); + + let (mixer1_client, mixer1_wallet) = super::test_util::new_mixer( + &mix1_sk, + (&swap_sk, &swap_pk), + &Some((mix2_pk.clone(), mixer2_client.clone())), + &node, + ); + + // Emulate the swap server peeling the onion and then calling mix1 + let mix1_onion = onion.peel_layer(&swap_sk)?; + let (mixed_indices, mixed_components) = + mixer1_client.mix_outputs(&vec![mix1_onion.onion.clone()])?; + + // Verify 3 outputs are returned: mixed output, mixer1's output, and mixer2's output + assert_eq!(mixed_indices, vec![0 as usize]); + assert_eq!(mixed_components.outputs.len(), 3); + let output_commits: HashSet = mixed_components + .outputs + .iter() + .map(|o| o.identifier.commit.clone()) + .collect(); + assert!(output_commits.contains(&out_commit)); + + assert_eq!(mixer1_wallet.built_outputs().len(), 1); + assert!(output_commits.contains(mixer1_wallet.built_outputs().get(0).unwrap())); + + assert_eq!(mixer2_wallet.built_outputs().len(), 1); + assert!(output_commits.contains(mixer2_wallet.built_outputs().get(0).unwrap())); + + Ok(()) + } +} diff --git a/src/servers/mix_rpc.rs b/src/servers/mix_rpc.rs new file mode 100644 index 0000000..033639b --- /dev/null +++ b/src/servers/mix_rpc.rs @@ -0,0 +1,116 @@ +use crate::config::ServerConfig; +use crate::crypto::dalek::{self, DalekSignature}; +use crate::node::GrinNode; +use crate::onion::Onion; +use crate::servers::mix::{MixError, MixServer, MixServerImpl}; +use crate::wallet::Wallet; + +use crate::client::MixClient; +use grin_util::StopState; +use jsonrpc_derive::rpc; +use jsonrpc_http_server::jsonrpc_core::{self as jsonrpc, IoHandler}; +use jsonrpc_http_server::{DomainsValidation, ServerBuilder}; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; +use std::thread::{sleep, spawn}; +use std::time::Duration; + +#[derive(Serialize, Deserialize)] +pub struct MixReq { + onions: Vec, + #[serde(with = "dalek::dalek_sig_serde")] + sig: DalekSignature, +} + +impl MixReq { + pub fn new(onions: Vec, sig: DalekSignature) -> Self { + MixReq { onions, sig } + } +} + +#[rpc(server)] +pub trait MixAPI { + #[rpc(name = "mix")] + fn mix(&self, mix: MixReq) -> jsonrpc::Result; +} + +#[derive(Clone)] +struct RPCMixServer { + server_config: ServerConfig, + server: Arc>, +} + +impl RPCMixServer { + /// Spin up an instance of the JSON-RPC HTTP server. + fn start_http(&self) -> jsonrpc_http_server::Server { + let mut io = IoHandler::new(); + io.extend_with(RPCMixServer::to_delegate(self.clone())); + + ServerBuilder::new(io) + .cors(DomainsValidation::Disabled) + .request_middleware(|request: hyper::Request| { + if request.uri() == "/v1" { + request.into() + } else { + jsonrpc_http_server::Response::bad_request("Only v1 supported").into() + } + }) + .start_http(&self.server_config.addr) + .expect("Unable to start RPC server") + } +} + +impl From for jsonrpc::Error { + fn from(e: MixError) -> Self { + jsonrpc::Error::invalid_params(e.to_string()) + } +} + +impl MixAPI for RPCMixServer { + fn mix(&self, mix: MixReq) -> jsonrpc::Result { + self.server + .lock() + .unwrap() + .mix_outputs(&mix.onions, &mix.sig)?; + Ok(jsonrpc::Value::String("success".into())) + } +} + +/// Spin up the JSON-RPC web server +pub fn listen( + server_config: ServerConfig, + next_server: Option>, + wallet: Arc, + node: Arc, + stop_state: Arc, +) -> Result<(), Box> { + let server = MixServerImpl::new( + server_config.clone(), + next_server, + wallet.clone(), + node.clone(), + ); + let server = Arc::new(Mutex::new(server)); + + let rpc_server = RPCMixServer { + server_config: server_config.clone(), + server: server.clone(), + }; + + let http_server = rpc_server.start_http(); + + 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)); + }); + + http_server.wait(); + round_handle.join().unwrap(); + + Ok(()) +} diff --git a/src/servers/mod.rs b/src/servers/mod.rs new file mode 100644 index 0000000..acd569c --- /dev/null +++ b/src/servers/mod.rs @@ -0,0 +1,4 @@ +pub mod mix; +pub mod mix_rpc; +pub mod swap; +pub mod swap_rpc; diff --git a/src/server.rs b/src/servers/swap.rs similarity index 55% rename from src/server.rs rename to src/servers/swap.rs index 3b388d3..66e8021 100644 --- a/src/server.rs +++ b/src/servers/swap.rs @@ -1,687 +1,806 @@ -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, Box>; -} - -/// The standard MWixnet server implementation -#[derive(Clone)] -pub struct ServerImpl { - server_config: ServerConfig, - wallet: Arc, - node: Arc, - store: Arc>, -} - -impl ServerImpl { - /// Create a new MWixnet server - pub fn new( - server_config: ServerConfig, - wallet: Arc, - node: Arc, - 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, Box> { - let locked_store = self.store.lock().unwrap(); - let next_block_height = self.node.get_chain_height()? + 1; - - let spendable: Vec = 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 = spendable.iter().enumerate().map(|(_, s)| s.input).collect(); - - let outputs: Vec = spendable - .iter() - .enumerate() - .map(|(_, s)| { - Output::new( - OutputFeatures::Plain, - s.output_commit, - s.rangeproof.unwrap(), - ) - }) - .collect(); - - let excesses: Vec = 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, - } - - 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, Box> { - 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) { - 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, - ) -> 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> { - 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> { - 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 = 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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(()) - } -} +use crate::config::ServerConfig; +use crate::crypto::comsig::ComSignature; +use crate::crypto::secp::{Commitment, Secp256k1, SecretKey}; +use crate::node::{self, GrinNode}; +use crate::onion::{Onion, OnionError}; +use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; +use crate::tx; +use crate::wallet::Wallet; +use std::collections::HashSet; + +use crate::client::MixClient; +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 secp256k1zkp::key::ZERO_KEY; +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")] + 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("Client communication error: {0:?}")] + ClientError(String), + #[error("{0}")] + UnknownError(String), +} + +/// A public MWixnet server - the "Swap Server" +pub trait SwapServer: 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. + fn execute_round(&self) -> Result, Box>; +} + +/// 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(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() + } +} + +impl SwapServer for SwapServerImpl { + 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::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(), + })?; + + // 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().unwrap(); + + 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(()) + } + + fn execute_round(&self) -> Result, Box> { + let locked_store = self.store.lock().unwrap(); + let next_block_height = self.node.get_chain_height()? + 1; + + let spendable: Vec = 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)) + .sorted_by(|a, b| a.output_commit.partial_cmp(&b.output_commit).unwrap()) + .collect(); + + if spendable.len() == 0 { + return Ok(None); + } + + 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 (indices, mixed) = client + .mix_outputs(&onions) + .map_err(|e| SwapError::ClientError(e.to_string()))?; + + // Filter out failed entries + let kept_indices = HashSet::<_>::from_iter(indices.clone()); + let filtered = spendable + .iter() + .enumerate() + .filter(|(i, _)| kept_indices.contains(i)) + .map(|(_, j)| j.clone()) + .collect(); + + let failed = spendable + .iter() + .enumerate() + .filter(|(i, _)| !kept_indices.contains(i)) + .map(|(_, j)| j.clone()) + .collect(); + + (filtered, failed, mixed.offset, mixed.outputs, mixed.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::assemble_tx( + &self.wallet, + &inputs, + &outputs, + &kernels, + self.get_fee_base(), + fees_paid, + &offset, + &output_excesses, + )?; + + self.node.post_tx(&tx)?; + + // Update status to in process + let kernel_hash = tx.kernels().first().unwrap().hash(); + for mut swap in filtered { + swap.status = SwapStatus::InProcess { kernel_hash }; + 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(tx)) + } +} + +#[cfg(test)] +pub mod mock { + use super::{SwapError, SwapServer}; + use crate::crypto::comsig::ComSignature; + use crate::onion::Onion; + + use grin_core::core::Transaction; + use std::collections::HashMap; + + 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); + } + } + + impl SwapServer for MockSwapServer { + 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, Box> { + Ok(None) + } + } +} + +#[cfg(test)] +pub mod test_util { + use crate::crypto::dalek::DalekPublicKey; + use crate::crypto::secp::SecretKey; + use crate::servers::swap::SwapServerImpl; + use crate::wallet::mock::MockWallet; + use crate::{config, GrinNode, MixClient, SwapStore}; + 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(); + + 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 crate::crypto::comsig::ComSignature; + use crate::crypto::dalek; + use crate::crypto::secp; + use crate::node::mock::MockGrinNode; + use crate::onion::test_util::{self, Hop}; + use crate::onion::Onion; + use crate::servers::swap::{SwapError, SwapServer}; + use crate::store::{SwapData, SwapStatus}; + use crate::tx::TxComponents; + use crate::{client, tx, MixClient}; + + use ::function_name::named; + use grin_core::core::hash::Hashed; + use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting}; + use secp256k1zkp::key::ZERO_KEY; + use std::sync::Arc; + + 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. + #[test] + #[named] + fn swap_standalone() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (output_commit, proof) = secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::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 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)?; + + // 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, + 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(()) + } + + /// Multi-server test to verify proper MixClient communication. + #[test] + #[named] + 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: u64 = 50_000_000; + let (swap_sk, _swap_pk) = dalek::test_util::rand_keypair(); + let swap_hop_excess = secp::random_secret(); + let swap_hop = test_util::new_hop(&swap_sk, &swap_hop_excess, swap_fee, None); + + // Mixer data + let mixer_fee: u64 = 30_000_000; + let (mixer_sk, mixer_pk) = dalek::test_util::rand_keypair(); + let mixer_hop_excess = secp::random_secret(); + let (output_commit, proof) = secp::test_util::proof( + value, + swap_fee + mixer_fee, + &blind, + &vec![&swap_hop_excess, &mixer_hop_excess], + ); + let mixer_hop = test_util::new_hop(&mixer_sk, &mixer_hop_excess, mixer_fee, Some(proof)); + + // Create onion + let onion = test_util::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)?], + }; + mock_mixer.set_response( + &vec![mixer_onion.clone()], + (vec![0 as usize], 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)?; + + let tx = swapper.execute_round()?; + 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. + #[test] + #[named] + fn swap_too_many_payloads() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::new_hop(&server_key, &hop_excess, fee, Some(proof)); + + let hops: Vec = 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 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); + assert_eq!(Err(SwapError::InvalidPayloadLength), 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] + #[named] + fn swap_invalid_com_signature() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::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 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); + 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] + #[named] + fn swap_invalid_rangeproof() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(wrong_value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::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 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); + 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] + #[named] + fn swap_missing_rangeproof() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 = test_util::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 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); + 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] + #[named] + fn swap_utxo_missing() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::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 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); + 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] + #[named] + fn swap_already_swapped() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::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 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)?; + + // 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] + #[named] + fn swap_peel_onion_failure() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + + let wrong_server_key = secp::random_secret(); + let hop = test_util::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 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); + + assert!(result.is_err()); + assert_error_type!(result, SwapError::PeelOnionFailure(_)); + + Ok(()) + } + + /// Returns FeeTooLow when the minimum fee is not met. + #[test] + #[named] + fn swap_fee_too_low() -> Result<(), Box> { + let test_dir = init_test!(); + + 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 (_output_commit, proof) = + secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = test_util::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 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); + assert_eq!( + Err(SwapError::FeeTooLow { + minimum_fee: 12_500_000, + actual_fee: fee + }), + result + ); + + Ok(()) + } +} diff --git a/src/rpc.rs b/src/servers/swap_rpc.rs similarity index 80% rename from src/rpc.rs rename to src/servers/swap_rpc.rs index c4e2c32..d79006d 100644 --- a/src/rpc.rs +++ b/src/servers/swap_rpc.rs @@ -1,11 +1,12 @@ use crate::config::ServerConfig; +use crate::crypto::comsig::{self, ComSignature}; use crate::node::GrinNode; use crate::onion::Onion; -use crate::secp::{self, ComSignature}; -use crate::server::{Server, ServerImpl, SwapError}; +use crate::servers::swap::{SwapError, SwapServer, SwapServerImpl}; use crate::store::SwapStore; use crate::wallet::Wallet; +use crate::client::MixClient; use grin_util::StopState; use jsonrpc_core::Value; use jsonrpc_derive::rpc; @@ -19,31 +20,27 @@ use std::time::Duration; #[derive(Serialize, Deserialize)] pub struct SwapReq { onion: Onion, - #[serde(with = "secp::comsig_serde")] + #[serde(with = "comsig::comsig_serde")] comsig: ComSignature, } #[rpc(server)] -pub trait API { +pub trait SwapAPI { #[rpc(name = "swap")] fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result; - - // milestone 3: Used by mwixnet coinswap servers to communicate with each other - // fn derive_outputs(&self, entries: Vec) -> jsonrpc_core::Result; - // fn derive_kernel(&self, tx: Tx) -> jsonrpc_core::Result; } #[derive(Clone)] -struct RPCServer { +struct RPCSwapServer { server_config: ServerConfig, - server: Arc>, + server: Arc>, } -impl RPCServer { +impl RPCSwapServer { /// Spin up an instance of the JSON-RPC HTTP server. fn start_http(&self) -> jsonrpc_http_server::Server { let mut io = IoHandler::new(); - io.extend_with(RPCServer::to_delegate(self.clone())); + io.extend_with(RPCSwapServer::to_delegate(self.clone())); ServerBuilder::new(io) .cors(DomainsValidation::Disabled) @@ -72,8 +69,7 @@ impl From for Error { } } -impl API for RPCServer { - /// Implements the 'swap' API +impl SwapAPI for RPCSwapServer { fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result { self.server .lock() @@ -86,15 +82,22 @@ impl API for RPCServer { /// Spin up the JSON-RPC web server pub fn listen( server_config: ServerConfig, + next_server: Option>, wallet: Arc, node: Arc, store: SwapStore, stop_state: Arc, ) -> std::result::Result<(), Box> { - let server = ServerImpl::new(server_config.clone(), wallet.clone(), node.clone(), store); + let server = SwapServerImpl::new( + server_config.clone(), + next_server, + wallet.clone(), + node.clone(), + store, + ); let server = Arc::new(Mutex::new(server)); - let rpc_server = RPCServer { + let rpc_server = RPCSwapServer { server_config: server_config.clone(), server: server.clone(), }; @@ -128,11 +131,12 @@ pub fn listen( #[cfg(test)] mod tests { use crate::config::ServerConfig; + use crate::crypto::comsig::ComSignature; + use crate::crypto::secp; use crate::onion::test_util; - use crate::rpc::{RPCServer, SwapReq}; - use crate::secp::{self, ComSignature}; - use crate::server::mock::MockServer; - use crate::server::{Server, SwapError}; + use crate::servers::swap::mock::MockSwapServer; + use crate::servers::swap::{SwapError, SwapServer}; + use crate::servers::swap_rpc::{RPCSwapServer, SwapReq}; use std::net::TcpListener; use std::sync::{Arc, Mutex}; @@ -147,20 +151,23 @@ mod tests { /// Spin up a temporary web service, query the API, then cleanup and return response fn make_request( - server: Arc>, + server: Arc>, req: String, ) -> Result> { let server_config = ServerConfig { key: secp::random_secret(), interval_s: 1, addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, + socks_proxy_addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, grin_node_url: "127.0.0.1:3413".parse()?, grin_node_secret_path: None, wallet_owner_url: "127.0.0.1:3420".parse()?, wallet_owner_secret_path: None, + prev_server: None, + next_server: None, }; - let rpc_server = RPCServer { + let rpc_server = RPCSwapServer { server_config: server_config.clone(), server: server.clone(), }; @@ -208,7 +215,7 @@ mod tests { comsig, }; - let server: Arc> = Arc::new(Mutex::new(MockServer::new())); + let server: Arc> = Arc::new(Mutex::new(MockSwapServer::new())); let req = format!( "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", @@ -224,7 +231,7 @@ mod tests { #[test] fn swap_bad_request() -> Result<(), Box> { - let server: Arc> = Arc::new(Mutex::new(MockServer::new())); + let server: Arc> = Arc::new(Mutex::new(MockSwapServer::new())); let params = "{ \"param\": \"Not a valid Swap request\" }"; let req = format!( @@ -248,14 +255,14 @@ mod tests { comsig, }; - let mut server = MockServer::new(); + let mut server = MockSwapServer::new(); server.set_response( &onion, SwapError::CoinNotFound { commit: commitment.clone(), }, ); - let server: Arc> = Arc::new(Mutex::new(server)); + let server: Arc> = Arc::new(Mutex::new(server)); let req = format!( "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", @@ -263,9 +270,9 @@ mod tests { ); let response = make_request(server, req)?; let expected = format!( - "{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n", - commitment - ); + "{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n", + commitment + ); assert_eq!(response, expected); Ok(()) } diff --git a/src/store.rs b/src/store.rs index 76b4c7a..d1b21c5 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,5 +1,5 @@ +use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey}; use crate::onion::Onion; -use crate::secp::{self, Commitment, RangeProof, SecretKey}; use crate::types::{read_optional, write_optional}; use grin_core::core::hash::Hash; @@ -23,6 +23,7 @@ pub enum SwapStatus { Unprocessed, InProcess { kernel_hash: Hash }, Completed { kernel_hash: Hash, block_hash: Hash }, + Failed, } impl Writeable for SwapStatus { @@ -43,6 +44,9 @@ impl Writeable for SwapStatus { kernel_hash.write(writer)?; block_hash.write(writer)?; } + SwapStatus::Failed => { + writer.write_u8(3)?; + } }; Ok(()) @@ -65,6 +69,7 @@ impl Readable for SwapStatus { block_hash, } } + 3 => SwapStatus::Failed, _ => { return Err(ser::Error::CorruptedData); } @@ -239,10 +244,11 @@ impl SwapStore { #[cfg(test)] mod tests { + use crate::crypto::secp; + use crate::crypto::secp::test_util::{rand_commit, rand_hash, rand_proof}; use crate::onion::test_util::rand_onion; - use crate::secp::test_util::{rand_commit, rand_hash, rand_proof}; use crate::store::{SwapData, SwapStatus, SwapStore}; - use crate::{secp, StoreError}; + use crate::StoreError; use grin_core::core::{Input, OutputFeatures}; use grin_core::global::{self, ChainTypes}; use rand::RngCore; diff --git a/src/tor.rs b/src/tor.rs new file mode 100644 index 0000000..dfbcb72 --- /dev/null +++ b/src/tor.rs @@ -0,0 +1,83 @@ +use crate::config::{self, ServerConfig}; + +use grin_core::global; +use grin_wallet_impls::tor::config as tor_config; +use grin_wallet_impls::tor::process::TorProcess; +use std::collections::HashMap; +use thiserror::Error; + +/// Tor error types +#[derive(Error, Debug)] +pub enum TorError { + #[error("Error generating config: {0:?}")] + ConfigError(String), + #[error("Error starting process: {0:?}")] + ProcessError(grin_wallet_impls::tor::process::Error), +} + +pub fn init_tor_listener(server_config: &ServerConfig) -> Result { + println!("Initializing tor listener"); + + let mut tor_dir = config::get_grin_path(&global::get_chain_type()); + tor_dir.push("tor/listener"); + + let mut torrc_dir = tor_dir.clone(); + torrc_dir.push("torrc"); + + tor_config::output_tor_listener_config( + tor_dir.to_str().unwrap(), + server_config.addr.to_string().as_str(), + &vec![server_config.key.clone()], + HashMap::new(), + HashMap::new(), + ) + .map_err(|e| TorError::ConfigError(e.to_string()))?; + + // Start TOR process + let mut process = TorProcess::new(); + process + .torrc_path(torrc_dir.to_str().unwrap()) + .working_dir(tor_dir.to_str().unwrap()) + .timeout(20) + .completion_percent(100) + .launch() + .map_err(TorError::ProcessError)?; + + println!( + "Server listening at http://{}.onion", + server_config.onion_address().to_ov3_str() + ); + Ok(process) +} + +pub fn init_tor_sender(server_config: &ServerConfig) -> Result { + println!( + "Starting TOR Process for send at {:?}", + server_config.socks_proxy_addr + ); + + let mut tor_dir = config::get_grin_path(&global::get_chain_type()); + tor_dir.push("tor/sender"); + + let mut torrc_dir = tor_dir.clone(); + torrc_dir.push("torrc"); + + tor_config::output_tor_sender_config( + tor_dir.to_str().unwrap(), + &server_config.socks_proxy_addr.to_string(), + HashMap::new(), + HashMap::new(), + ) + .map_err(|e| TorError::ConfigError(e.to_string()))?; + + // Start TOR process + let mut tor_process = TorProcess::new(); + tor_process + .torrc_path(torrc_dir.to_str().unwrap()) + .working_dir(tor_dir.to_str().unwrap()) + .timeout(20) + .completion_percent(100) + .launch() + .map_err(TorError::ProcessError)?; + Ok(tor_process) +} diff --git a/src/tx.rs b/src/tx.rs new file mode 100644 index 0000000..7cd47a9 --- /dev/null +++ b/src/tx.rs @@ -0,0 +1,182 @@ +use crate::crypto::secp; +use crate::wallet::Wallet; + +use grin_core::core::{ + FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel, +}; +use grin_keychain::BlindingFactor; +use secp256k1zkp::{ContextFlag, Secp256k1, SecretKey}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use thiserror::Error; + +/// Error types for interacting with wallets +#[derive(Error, Debug)] +pub enum TxError { + #[error("Error computing transactions's offset: {0:?}")] + OffsetError(secp256k1zkp::Error), + #[error("Error building kernel's fee fields: {0:?}")] + KernelFeeError(grin_core::core::transaction::Error), + #[error("Error computing kernel's excess: {0:?}")] + KernelExcessError(secp256k1zkp::Error), + #[error("Error computing kernel's signature message: {0:?}")] + KernelSigMessageError(grin_core::core::transaction::Error), + #[error("Error signing kernel: {0:?}")] + KernelSigError(secp256k1zkp::Error), + #[error("Built kernel failed to verify: {0:?}")] + KernelVerifyError(grin_core::core::transaction::Error), + #[error("Output blinding factor is invalid: {0:?}")] + OutputBlindError(secp256k1zkp::Error), + #[error("Wallet error: {0:?}")] + WalletError(crate::wallet::WalletError), +} + +/// A collection of transaction components +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TxComponents { + /// Transaction offset + pub offset: SecretKey, + /// Transaction kernels + pub kernels: Vec, + /// Transaction outputs + pub outputs: Vec, +} + +/// Builds and verifies the finalized swap 'Transaction' using the provided components. +pub fn assemble_tx( + wallet: &Arc, + inputs: &Vec, + outputs: &Vec, + kernels: &Vec, + fee_base: u64, + fees_paid: u64, + prev_offset: &SecretKey, + output_excesses: &Vec, +) -> Result { + // calculate minimum fee required for the kernel + let min_kernel_fee = + TransactionBody::weight_by_iok(inputs.len() as u64, outputs.len() as u64, 1) * fee_base; + + let components = add_kernel_and_collect_fees( + &wallet, + &outputs, + &kernels, + fee_base, + min_kernel_fee, + fees_paid, + &prev_offset, + &output_excesses, + )?; + + // assemble the transaction + let tx = Transaction::new( + Inputs::from(inputs.as_slice()), + &components.outputs, + &components.kernels, + ) + .with_offset(BlindingFactor::from_secret_key(components.offset)); + Ok(tx) +} + +/// Adds a kernel and output to a collection of transaction components to consume fees and offset excesses. +pub fn assemble_components( + wallet: &Arc, + components: &TxComponents, + output_excesses: &Vec, + fee_base: u64, + fees_paid: u64, +) -> Result { + // calculate minimum fee required for the kernel + let min_kernel_fee = TransactionBody::weight_by_iok(0, 0, 1) * fee_base; + + add_kernel_and_collect_fees( + &wallet, + &components.outputs, + &components.kernels, + fee_base, + min_kernel_fee, + fees_paid, + &components.offset, + &output_excesses, + ) +} + +fn add_kernel_and_collect_fees( + wallet: &Arc, + outputs: &Vec, + kernels: &Vec, + fee_base: u64, + min_kernel_fee: u64, + fees_paid: u64, + prev_offset: &SecretKey, + output_excesses: &Vec, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let mut txn_outputs = outputs.clone(); + let mut txn_excesses = output_excesses.clone(); + let mut txn_kernels = kernels.clone(); + let mut kernel_fee = fees_paid; + + // calculate fee required if we add our own output + let fee_to_collect = TransactionBody::weight_by_iok(0, 1, 0) * fee_base; + + // calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it + let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base; + + // collect any leftover fees + if fees_paid > min_kernel_fee + fee_to_collect + fee_to_spend { + let amount = fees_paid - (min_kernel_fee + fee_to_collect); + kernel_fee -= amount; + + let wallet_output = wallet.build_output(amount).map_err(TxError::WalletError)?; + txn_outputs.push(wallet_output.1); + + let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref()) + .map_err(TxError::OutputBlindError)?; + txn_excesses.push(output_excess); + } + + // generate random transaction offset + let our_offset = secp::random_secret(); + let txn_offset = secp + .blind_sum(vec![prev_offset.clone(), our_offset.clone()], Vec::new()) + .map_err(TxError::OffsetError)?; + + // calculate kernel excess + let kern_excess = secp + .blind_sum(txn_excesses, vec![our_offset.clone()]) + .map_err(TxError::KernelExcessError)?; + + // build and verify kernel + let kernel = build_kernel(&kern_excess, kernel_fee)?; + txn_kernels.push(kernel); + + // Sort outputs & kernels by commitment + txn_kernels.sort_by(|a, b| a.excess.partial_cmp(&b.excess).unwrap()); + txn_outputs.sort_by(|a, b| { + a.identifier + .commit + .partial_cmp(&b.identifier.commit) + .unwrap() + }); + + Ok(TxComponents { + offset: txn_offset, + kernels: txn_kernels, + outputs: txn_outputs, + }) +} + +pub fn build_kernel(excess: &SecretKey, fee: u64) -> Result { + let mut kernel = TxKernel::with_features(KernelFeatures::Plain { + fee: FeeFields::new(0, fee).map_err(TxError::KernelFeeError)?, + }); + let msg = kernel + .msg_to_sign() + .map_err(TxError::KernelSigMessageError)?; + kernel.excess = secp::commit(0, &excess).map_err(TxError::KernelExcessError)?; + kernel.excess_sig = secp::sign(&excess, &msg).map_err(TxError::KernelSigError)?; + kernel.verify().map_err(TxError::KernelVerifyError)?; + + Ok(kernel) +} diff --git a/src/types.rs b/src/types.rs index 9fe73b4..ec15988 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,4 @@ -use crate::secp::{self, RangeProof, SecretKey}; +use crate::crypto::secp::{self, RangeProof, SecretKey}; use grin_core::core::FeeFields; use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; diff --git a/src/wallet.rs b/src/wallet.rs index 4b68121..74395c1 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,19 +1,16 @@ -use crate::secp; +use crate::crypto::secp; use grin_api::client; use grin_api::json_rpc::{build_request, Request, Response}; -use grin_core::core::{ - FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel, -}; +use grin_core::core::Output; use grin_core::libtx::secp_ser; use grin_keychain::BlindingFactor; use grin_util::{ToHex, ZeroingString}; use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token}; -use secp256k1zkp::{ContextFlag, PublicKey, Secp256k1, SecretKey}; +use secp256k1zkp::{PublicKey, Secp256k1, SecretKey}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::net::SocketAddr; -use std::sync::Arc; use thiserror::Error; pub trait Wallet: Send + Sync { @@ -24,18 +21,6 @@ pub trait Wallet: Send + Sync { /// Error types for interacting with wallets #[derive(Error, Debug)] pub enum WalletError { - #[error("Error building kernel's fee fields: {0:?}")] - KernelFeeError(grin_core::core::transaction::Error), - #[error("Error computing kernel's excess: {0:?}")] - KernelExcessError(secp256k1zkp::Error), - #[error("Error computing kernel's signature message: {0:?}")] - KernelSigMessageError(grin_core::core::transaction::Error), - #[error("Error signing kernel: {0:?}")] - KernelSigError(secp256k1zkp::Error), - #[error("Built kernel failed to verify: {0:?}")] - KernelVerifyError(grin_core::core::transaction::Error), - #[error("Output blinding factor is invalid: {0:?}")] - OutputBlindError(secp256k1zkp::Error), #[error("Error encrypting request: {0:?}")] EncryptRequestError(grin_wallet_libwallet::Error), #[error("Error decrypting response: {0:?}")] @@ -48,67 +33,6 @@ pub enum WalletError { ResponseParseError(grin_api::json_rpc::Error), } -/// Builds and verifies a 'Transaction' using the provided components. -pub fn assemble_tx( - wallet: &Arc, - inputs: &Vec, - outputs: &Vec, - fee_base: u64, - total_fee: u64, - excesses: &Vec, -) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let txn_inputs = Inputs::from(inputs.as_slice()); - let mut txn_outputs = outputs.clone(); - let mut txn_excesses = excesses.clone(); - let mut kernel_fee = total_fee; - - // calculate fee required if we add our own output - let fee_required = - TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1) - * fee_base; - - // calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it - let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base; - - // collect any leftover fees - if total_fee > fee_required + fee_to_spend { - let amount = total_fee - fee_required; - kernel_fee -= amount; - - let wallet_output = wallet.build_output(amount)?; - txn_outputs.push(wallet_output.1); - - let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref()) - .map_err(WalletError::OutputBlindError)?; - txn_excesses.push(output_excess); - } - - // generate random transaction offset - let offset = secp::random_secret(); - - // calculate kernel excess - let kern_excess = secp - .blind_sum(txn_excesses, vec![offset.clone()]) - .map_err(WalletError::KernelExcessError)?; - - // build and verify kernel - let mut kernel = TxKernel::with_features(KernelFeatures::Plain { - fee: FeeFields::new(0, kernel_fee).map_err(WalletError::KernelFeeError)?, - }); - let msg = kernel - .msg_to_sign() - .map_err(WalletError::KernelSigMessageError)?; - kernel.excess = secp::commit(0, &kern_excess).map_err(WalletError::KernelExcessError)?; - kernel.excess_sig = secp::sign(&kern_excess, &msg).map_err(WalletError::KernelSigError)?; - kernel.verify().map_err(WalletError::KernelVerifyError)?; - - // assemble the transaction - let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel]) - .with_offset(BlindingFactor::from_secret_key(offset)); - Ok(tx) -} - /// HTTP (JSONRPC) implementation of the 'Wallet' trait. #[derive(Clone)] pub struct HttpWallet { @@ -272,15 +196,34 @@ impl Wallet for HttpWallet { #[cfg(test)] pub mod mock { use super::{Wallet, WalletError}; - use crate::secp; + use crate::crypto::secp; + use std::borrow::BorrowMut; use grin_core::core::{Output, OutputFeatures}; use grin_keychain::BlindingFactor; + use secp256k1zkp::pedersen::Commitment; use secp256k1zkp::Secp256k1; + use std::sync::{Arc, Mutex}; - /// HTTP (JSONRPC) implementation of the 'Wallet' trait. + /// Mock implementation of the 'Wallet' trait for unit-tests. #[derive(Clone)] - pub struct MockWallet {} + pub struct MockWallet { + built_outputs: Arc>>, + } + + impl MockWallet { + /// Creates a new, empty MockWallet. + pub fn new() -> Self { + MockWallet { + built_outputs: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Returns the commitments of all outputs built for the wallet. + pub fn built_outputs(&self) -> Vec { + self.built_outputs.lock().unwrap().clone() + } + } impl Wallet for MockWallet { /// Builds an 'Output' for the wallet using the 'build_output' RPC API. @@ -297,6 +240,10 @@ pub mod mock { None, ); let output = Output::new(OutputFeatures::Plain, commit.clone(), proof); + + let mut locked = self.built_outputs.lock().unwrap(); + locked.borrow_mut().push(output.commitment().clone()); + Ok((BlindingFactor::from_secret_key(blind), output)) } }