From 76532c899e22ff5721eddd1452116cb875afa5e3 Mon Sep 17 00:00:00 2001 From: scilio Date: Sat, 7 Jan 2023 16:09:04 -0500 Subject: [PATCH 1/3] inter-server communication --- Cargo.lock | 96 +- Cargo.toml | 2 + README.md | 8 +- mwixnet.yml | 12 + src/client.rs | 171 +++ src/config.rs | 66 +- src/{secp.rs => crypto/comsig.rs} | 464 ++++----- src/crypto/dalek.rs | 285 +++++ src/crypto/mod.rs | 3 + src/crypto/secp.rs | 117 +++ src/main.rs | 157 +-- src/node.rs | 21 +- src/onion.rs | 46 +- src/servers/mix.rs | 409 ++++++++ src/servers/mix_rpc.rs | 116 +++ src/servers/mod.rs | 4 + src/{server.rs => servers/swap.rs} | 1493 +++++++++++++++------------ src/{rpc.rs => servers/swap_rpc.rs} | 65 +- src/store.rs | 12 +- src/tor.rs | 83 ++ src/tx.rs | 182 ++++ src/types.rs | 2 +- src/wallet.rs | 111 +- 23 files changed, 2756 insertions(+), 1169 deletions(-) create mode 100644 src/client.rs rename src/{secp.rs => crypto/comsig.rs} (61%) create mode 100644 src/crypto/dalek.rs create mode 100644 src/crypto/mod.rs create mode 100644 src/crypto/secp.rs create mode 100644 src/servers/mix.rs create mode 100644 src/servers/mix_rpc.rs create mode 100644 src/servers/mod.rs rename src/{server.rs => servers/swap.rs} (55%) rename src/{rpc.rs => servers/swap_rpc.rs} (80%) create mode 100644 src/tor.rs create mode 100644 src/tx.rs 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)) } } From d99aa6ec7c5062689ab111a2c9ca1f9669c8d752 Mon Sep 17 00:00:00 2001 From: scilio Date: Mon, 22 May 2023 20:13:30 -0400 Subject: [PATCH 2/3] generate random pubkey for each payload (https://github.com/mimblewimble/mwixnet/issues/19) --- doc/onion.md | 202 ++++++++++++++++++++++++++++++++ doc/store.md | 27 +++++ doc/swap_api.md | 72 ++++++++++++ src/crypto/comsig.rs | 2 +- src/crypto/dalek.rs | 3 +- src/crypto/secp.rs | 4 +- src/main.rs | 2 +- src/onion.rs | 249 +++++++++++++++++++++------------------- src/servers/mix.rs | 112 ++++++++++++------ src/servers/swap.rs | 35 +++--- src/servers/swap_rpc.rs | 1 - src/store.rs | 2 +- src/types.rs | 82 ------------- src/util.rs | 164 ++++++++++++++++++++++++++ 14 files changed, 695 insertions(+), 262 deletions(-) create mode 100644 doc/onion.md create mode 100644 doc/store.md create mode 100644 doc/swap_api.md delete mode 100644 src/types.rs create mode 100644 src/util.rs diff --git a/doc/onion.md b/doc/onion.md new file mode 100644 index 0000000..f14251f --- /dev/null +++ b/doc/onion.md @@ -0,0 +1,202 @@ +# Onion Routing + +## Overview + +The Onion Routing scheme in this context is a method of encrypting and routing transactions in a privacy-preserving manner. At each step, a server peels off a layer of encryption, revealing information intended for it, and passes on the rest of the data to the next server. + +The main component in this encryption scheme is an `Onion`, which contains encrypted payloads, an ephemeral public key and an output commitment. + +## Data Structures + +### `Hop` structure + +Each `Hop` represents a step in the routing process, which has its own unique encryption parameters. A `Hop` consists of: + +- `server_pubkey`: The public key of the server for this hop. +- `excess`: An additional blinding factor to add to the commitment. +- `fee`: The transaction fee for this hop. +- `rangeproof`: An optional rangeproof, included only for the final hop. + +### `Onion` structure + +An `Onion` represents a complete route at a particular stage. It contains: + +- `ephemeral_pubkey`: The ephemeral public key for the server that is next in line to peel off a layer of the onion. +- `commit`: The modified commitment at this stage in the routing process, which will be the original commitment for the first server in the chain, and then will be a recalculated commitment at each following stage. +- `enc_payloads`: The list of encrypted payloads for each remaining hop in the route. + +The `commit` in an `Onion` will be the original unspent output's commitment for the very first `Onion` object sent to the swap server, but then for each peeled layer (i.e., after each hop), a new `Onion` object will be created with a recalculated commitment. This new commitment reflects the additional blinding factor and subtracted fee at each stage. The `Onion` passed from one server to the next then contains this adjusted commitment, not the original one. + +### `Payload` structure + +Each encrypted payload contains the information needed by a server to process the hop. This includes: + +- `next_ephemeral_pk`: The ephemeral public key for the next hop. +- `excess`: The additional blinding factor for the commitment at this hop. +- `fee`: The transaction fee for this hop. +- `rangeproof`: A rangeproof if the payload is for the final hop. + Absolutely, let's go into more detail on the cryptographic methods utilized during the creation and peeling of the Onion. + +### Creating an Onion + +The creation of the Onion involves both symmetric and asymmetric encryption techniques: + +1. **Ephemeral keys:** For each hop (server) in the network, an ephemeral secret key is randomly generated. These ephemeral keys are used to create shared secrets with the server's public key through the Diffie-Hellman key exchange. The first ephemeral public key is included in the Onion, and each subsequent ephemeral public key is encrypted and included in the payload for the previous server. + +2. **Shared secrets:** A shared secret is generated between the sender (the client) and each server (hop) in the path. This is done using the Elliptic Curve Diffie-Hellman (ECDH) method. The shared secret for each server is calculated from the server's public key and the client's ephemeral secret key. + +3. **Payload encryption:** Detailed in the next section. + +### Payload Encryption with ChaCha20 Cipher + +After the shared secrets are created, they are used to derive keys for symmetric encryption with the ChaCha20 cipher. Here is the process: + +1. **Key derivation:** An HMAC-SHA-256 is used as a key derivation function (KDF). The shared secret is fed into this HMAC function with a constant key of "MWIXNET". The HMAC is used here as a pseudo-random function (PRF) to derive a 256-bit key from the shared secret. The purpose of using HMAC in this manner is to ensure that the output key is indistinguishable from random data, assuming the shared secret is also indistinguishable from random data. The output of the HMAC function is a 256-bit key. + +2. **Nonce:** A nonce is a random or pseudo-random value that is meant to prevent replay attacks. For the ChaCha20 cipher, a 12-byte nonce is used. In this case, a static nonce of "NONCE1234567" is used. This means that the security of the cipher relies solely on the never-reusing any key more than once, since the nonce is not being used as an input of randomness. + +3. **ChaCha20 Initialization**: The derived key and static nonce are used to initialize the ChaCha20 cipher. + +4. **Payload Encryption** Each server's payload is encrypted with all the shared secrets of that server and all previous servers, in reverse order. This means the payload for the first server is encrypted once, the second server's payload is encrypted twice, and so on, creating the layered "onion" encryption. + +```rust +fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { + let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; + mu_hmac.update(shared_secret.as_bytes()); + let mukey = mu_hmac.finalize().into_bytes(); + + let key = Key::from_slice(&mukey[0..32]); + let nonce = Nonce::from_slice(b"NONCE1234567"); + + Ok(ChaCha20::new(&key, &nonce)) +} +``` + +### Peeling an Onion + +The peeling of the Onion is basically the decryption process that happens at each server: + +1. **Shared secret:** The server creates a shared secret using its own private key and the client's ephemeral public key. + +2. **Decryption:** The server uses the shared secret to derive a key for the ChaCha20 cipher. It then uses this to decrypt the payloads. Because of the layered encryption, each server can only decrypt the first remaining payload in the Onion, which reveals the payload intended for that server, while leaving the other payloads still encrypted. + +3. **Payload extraction:** After decryption, the server extracts its fee, excess, and the next server's ephemeral public key from its payload. If the server is the last one in the chain, it also receives a rangeproof. + +4. **Commitment recalculation:** Using the excess and fee from the decrypted payload, the server recalculates the commitment. The new commitment is `C' = (C - fee*H + excess*G)`, where `C` is the previous commitment, `H` is the hash of a known value and `G` is the generator of the elliptic curve group. + +5. **New Onion creation:** The server creates a new Onion with the recalculated commitment, the next server's ephemeral public key, and the remaining still-encrypted payloads (minus the server's own decrypted payload). + +This process repeats until the Onion reaches the final server in the path. The final server, after peeling its layer, should find the commitment matches the provided rangeproof, thus ensuring the integrity of the transaction and the anonymity of the involved parties. + +### 2-Hop Example + +Initial output: + +```json +{ + "value": 1000, + "blind": "c2df4d2331659e8e9c780d27309dba453e34ef48f6e38aab1be50545a0431f95", + "commit": "0899dadc2b75d66d738b7dbfcba4a37460622dcedaf222e688a2a84826eaa1cff1" +} + +``` + +Server keys: + +```json +[ + { + "sk": "a129111d283b13bf93957c06bf6605c3417b4b89db4b5cb2e7dab2c15e36e0a4", + "pk": "96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b" + }, + { + "sk": "2231414c56488b3596bb56b555ce1b4f8f6ed6b128914760ff89cd42c3d38ad6", + "pk": "a2fa3c7043e5080429bdcfb48fb6a8502bca77139d88c6603c9a75234fd6c718" + } +] +``` + +For the first hop, the user provides: +```json +{ + "server_pubkey": "96ced236bdf1aca722ef68b818445755e6ed4bacf23e19d7b71c43efc5f0077b", + "excess": "a9f15dc4760a1a280f68c6fc16d8aeada415fd66d5da805ff05cac6857a09db4", + "fee": 5, + "rangeproof": "None" +} +``` +The ephemeral key is randomly generated as +```json +{ + "sk": "e8debf70567d3240f5d8e7743e3d986962de4efdd8e638e9989a3afbbafaa85f", + "pk": "808ed260a56fe8910444dce931e2d67be0d2c6518134643450d2b9db9dfe7c26" +} +``` + +For the second hop, the user provides: +```json +{ + "server_pubkey": "a2fa3c7043e5080429bdcfb48fb6a8502bca77139d88c6603c9a75234fd6c718", + "excess": "d777cf064daf8929e66d2dfc6898fd0cf0774d8546bccb40699c8c47da215663", + "fee": 5, + "rangeproof": "b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa" +} +``` + +The generated onion becomes: +```json +{ + "commit": "0899dadc2b75d66d738b7dbfcba4a37460622dcedaf222e688a2a84826eaa1cff1", + "data": [ + "d19f914e7a7b5ba1c0ab36d982bd0bc59aa651458a6ee86c2015b57d96424da4a41559069be5ba59e0d2b688f95f1dfb20648e21f9b01ac400cb4879f2ba434c8eb0337d3f58e9e643aa", + "a5663659b17f48fba8a335e774f9968e7039db4ce67f3a9d11419e3cf24d96b1c1338d574ba8c6ac36e74b08aef0e61f0dacb5bb769aa76227f894cc49e5ac0f55800130f2a49509123963734ddf21db0fa4f662a3d37c7a0850f8c0c0dd31b26be9cce1e264d60b2240e52f465c7c2b14b11b77fbe3b2fa9bbe78e71df4083d554aa80bcfb1266bd2132ea5c5b356d54a166c16e38c66a58c19c85bb72d11e2097ab2cb13efb4f1de62b3fdf5ee90093de04e8e31e5ef036e155c3db4d7912240abd8807d26f6e9e116bfe90958a8f4347377ed774b1408adb217847efc9dc1bd0067d283a180757f5d23bcdb9e2a87dba903191ecf13ec9ff2e9bed2e774f68fa06f6f51b8f90895058ad3c699d44b9917d2b4b56096bd7885ca8d44d4b635bd975db07780006000faa40b59280aef2bf99088677f0d24efdddd670b7d0b713a9bec34f3c47cc74f594675174508061c7957fcbcf4a5f27ae9fc92f1e9d56f64a3cbcb1492c6845fd08ea04990234ea3cebf62c17f79d3a93fc6ab076fb02563579c903673759b89cfaffef68bf86daf7cb42939ee7fbc8a92e832fb0d7f8071d46323c95676d7e7821c40595d2db8dd7e29bf988eaca8f4ef6ed71a4084a01beea45497d0a5e06476c7092d7774fb4c6f9c52a0ab8bd4cc0d1569696f58deb521c2a11b774f4934fb171f3c2cf0cbfadb02c32a93a70895c5388f04824c486b5075cadf143594f46cb792145932d6a67845b5b744451517728df77f194fd5cbfda7dab160c329d7d340a2cbe3cd2accdecbd32494f75aed1892d65248124aaf9c82951edf49e46de1d80fa465ee70552d76b4f5e5f68d8bbac534f98454adc396050c9eaa7a782c3a27d6f2116a831e75cd1726b8b543738a084d7c1ee592aad80798461eb7a88ba5ea3ecf1a3329ffb7bdc882644efea1d97ceb10206356678e05aa555dd090a695da43e193ecaa239116ba1df350a86a508feb4e57696ec66f17864c394c06de614fe35c7417d51be837dfd2a1eabe135bb985be8d11847990dff17ba3f7b74a68cdfad6d83fec0700fc5fd5" + ], + "pubkey": "808ed260a56fe8910444dce931e2d67be0d2c6518134643450d2b9db9dfe7c26" +} +``` + +This is provided to the swap (first) server, which uses its server secret key to peel one layer, resulting in: +```json +{ + "payload": { + "next_ephemeral_pk": "5353ed848b8b2514aa08c8d9a5109ca4ddafe575c07a2a7cb2f19defa58d8442", + "excess": "a9f15dc4760a1a280f68c6fc16d8aeada415fd66d5da805ff05cac6857a09db4", + "fee": 5, + "proof": "None" + }, + "onion": { + "commit":"08b045d9f160fd2528feb50e134a0873ae91a5ab7c44eb2a73ae246eee426bdbde", + "data":[ + "1df9a17573e657575b3fe17a458adc83907a9c643f1121fc5e93ca06467adc1c26fab89974d4b906683625e78ea8b778d6f48628220515699e921e8ec8059f09f4bc81cf84ace0e01fe542b9181e3cb79e4242845cf2b8c0d9a23654bede0cd149e92a6be92fa039602f5e81be2bdaaf7580e1102dd7dfec3df9dbfd4cd6977321bb7212b60810c337c1f83cce1fe9d8a1d8780cbc650dd77082b427e21cae914745f3563557f21315ed16332d09bdeee1cd5b1981433449533e515bfa223202fe8343989f57b9dc783e99b03750be23fa3ba87d973b907b173fb8d8c0790e3db3689f560eaf95c7073e9b71c453261c2e5598cdfed2503200b527724cb1a7dec033bf48e220f1a46b8cf3dc6c961d0173d10b487154fefd850c4e04923ce924a743a8ff403699ca319756e09106d5af5005e214bb02d23d9df99d6ee01fc578ecd82334d3bfdb18eeb3592d98d66a232bc1eecee972cbdf7e2f9dbc60b8fed1767afbb94220efed6a7f7ad51bfd10bc81089e650ca175c0ff0c4b0f5d592fa3166a2689871a89665c17d94a2ce9a4d25f4d174befc0a4b66e72ae6b559ae5250c23806d002a51713800190c25e310aae57167803de7413783f607b8d6cfbb3071dcb6fec6a2bfacd7b0656e8e24060c1a20c9b201ab5d5875455098770d0c4d48ceac73c9d5d6d357fb8fe24d9de27f9bd461e7076c7a28dd1e961d1e373e6890b8d4cf697a8bc11c5b252370ce2be403306390c1bf0d2aaeb6ef5f62064fb87e7fccca5e8a503c8d35651a4fbcfce89e44bd8595dac54e45d11861ca075af49cfdd1dd0bc56085548c8605c6b1706cdbcdfca0d37a77732039cfb9f28b4e216d3cc996d0e69b184d33c54d162d63efa0d7d2738dbcd09690d99277be25ce758d3a90880565d3a03e7c6308a8eb0fbbb450259bf916e1802c72f1226ccd1444503a8ce95a4e296eec4ffbba47e6a41d94b5672499b98f77e72cbed7660e2a0d66598ccc81de1055130393158d4a04805797444b0d8cc713184120a554a130ed4179e51f98db094fab5b1e27accd4b3d2351ebad62" + ], + "pubkey":"5353ed848b8b2514aa08c8d9a5109ca4ddafe575c07a2a7cb2f19defa58d8442" + } +} +``` + +This is passed to the second server, which uses its server secret key to peel the last layer, resulting in: +```json +{ + "payload": { + "next_ephemeral_pk": "0000000000000000000000000000000000000000000000000000000000000000", + "excess": "d777cf064daf8929e66d2dfc6898fd0cf0774d8546bccb40699c8c47da215663", + "fee": 5, + "proof": "b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa" + }, + "onion": { + "commit": "0996a01db5f4d43b7c185491db087fa0c01dd8e3517a0751787f244ef6c0a0a7f0", + "data": [], + "pubkey": "0000000000000000000000000000000000000000000000000000000000000000" + } +} +``` + +The final commitment is returned as part of the last onion packet: `0996a01db5f4d43b7c185491db087fa0c01dd8e3517a0751787f244ef6c0a0a7f0` + +The final payload contains a valid rangeproof for it: `b58e8833cf94a470423b3787193f6f3bd4e9c769eec7fb0030e6ec9747b1a796c6e84f21d88714c2d2e790c6f08d4599ecc766666be95b65c46b1c22ca98ba8206a92fe720ed2e7548e794cc41d2b38d409e19625936902e8a3d64905c08927abade0ed923158abd74b52ae0c302c4a53b58e42ccedc49f6a37133edd43a3fa544a704bf7fff2bd5bcd931e4176e30349b5e687486c8cefdc0418ba5e6d389934b78d619301a0a2ba911e57c88c26002da471c8a4827b0a80330fcc28b550f987329c534189d41869dd12ca5d04e7d283bf2d37bb5fe180cfd8f8fc76fd81a9c929f6c9688e8acc7ec7fb8d0633b362e2e080643b384f1fcad09894bc523bbe4539d76aae858a6dc822187f7e2cae3c41fe26ce4441f2a29b2d874689247c6b08e5c25b512bced45467592a092811b3dafb83b49857ddfddeced32d62dfa807f85a9c9262445e985a1e5f6c5cb723de7e4d8ffe1d9e546b27a7d3e0a30604f0cbce500d0122e5312cf46c09621c60b75a0ca33ad1f193cfb2289784a0ec65d22eaf0721faf556536723e6bc0c4127b86562db4921cadb384bd6f2a9262f3125ed7c90f4c7339cdeaf07d4b8f515306428142d81c27a7440a7dfaf7c79cdd9f2a75a3dfad995ec403dbf7a1cf0011cf1acf97c5f3b550dc2582633bf22cb743bb05565eb67c1d9229a644362f46f3b6fcc5283e765f34273770c0123ebc0463b123df7afa547257d9bbe2fce7d44bac396f8872dfbcb6eea357359a2f618b2a3e0e1cdf27316b5130bd9e36e2eb9c28f6b878f2f9802e4ab4950b3e0d158f596120144a76c4db95ee951146ffb15b3e0104897082c8bf4831d7b7a35a77c1729376ce0c46183ccd2957c9c0869b75dd4d90395ea3da024e0d5f490920ad1b18c68d9ac6cc874e782b7406ceffa48b218abe00ca9aa0c517b0c2dc49f1dc2bdfb4592dfa` + +## Security Considerations + +The security of this scheme comes from the use of ephemeral keys and the double encryption of payloads. Each server only has the keys to decrypt its own layer, and cannot derive the keys for any other layers. + +This means that a server can only see the data intended for it, and has no information about the rest of the route or the details of any previous hops. This provides strong privacy guarantees for the sender of the transaction. \ No newline at end of file diff --git a/doc/store.md b/doc/store.md new file mode 100644 index 0000000..121354d --- /dev/null +++ b/doc/store.md @@ -0,0 +1,27 @@ +# SwapStore + +## Overview + +The `SwapStore` is an lmdb database, responsible for storing unprocessed and in-process `SwapData` entries. + +The `SwapStore` is used to hold onto new `SwapData` entries until the next swap round, when the mixing process actually occurs. At that time, they will be marked as `InProcess` until the swap is in a confirmed transaction, at which time they will be marked `Completed` and eventually erased. + +## Data Model + +`SwapData` entries are keyed with prefix 'S' followed by the commitment of the output being swapped. Entries are all unique by key. + +### `SwapData` + +The `SwapData` structure contains information needed to swap a single output. It has the following fields: + +- `excess`: The total excess for the output commitment. +- `output_commit`: The derived output commitment after applying excess and fee. +- `rangeproof`: The rangeproof, included only for the final hop (node N). +- `input`: The transaction input being spent. +- `fee`: The transaction fee. +- `onion`: The remaining onion after peeling off our layer. +- `status`: The status of the swap, represented by the `SwapStatus` enum, which can be one of the following: + - `Unprocessed`: The swap has been received but not yet processed. + - `InProcess { kernel_hash: Hash }`: The swap is currently being processed, and is expected to be a transaction with the kernel matching the given `kernel_hash`. + - `Completed { kernel_hash: Hash, block_hash: Hash }`: The swap has been successfully processed and included in the block matching the given `block_hash`. + - `Failed`: The swap has failed, potentially due to expiration or because the output is no longer in the UTXO set. \ No newline at end of file diff --git a/doc/swap_api.md b/doc/swap_api.md new file mode 100644 index 0000000..a779d63 --- /dev/null +++ b/doc/swap_api.md @@ -0,0 +1,72 @@ +# Swap Server API + +## Overview + +The Swap Server provides a single JSON-RPC API with the method `swap`. This API is used by clients to initiate the mixing process for their outputs, obscuring their coin history in a transaction with other users. + +## SWAP + +### Request +The `swap` method accepts a single JSON object containing the following fields: + +- `onion`: an `Onion` data structure, which is the encrypted onion packet containing the key information necessary to transform the user's output. +- `comsig`: a Commitment Signature that proves the client knows the secret key and value of the output's commitment. + +#### `Onion` data structure + +The `Onion` data structure consists of the following fields: + +- `pubkey`: an ephemeral pubkey to as the onion originator's portion of the shared secret, represented as an `x25519_dalek::PublicKey`. +- `commit`: the Pedersen commitment before adjusting the excess and subtracting the fee, represented as a 33-byte `secp256k1` Pedersen commitment. +- `data`: a vector of encrypted payloads, each representing a layer of the onion. When completely decrypted, these are serialized `Payload` objects. + +Each entry in the `enc_payloads` vector corresponds to a server in the system, in order, with the first entry containing the payload for the swap server, and the last entry containing the payload for the final mix server. + +#### `Payload` data structure + +A `Payload` represents a single, decrypted/peeled layer of an Onion. It consists of the following fields: + +- `next_ephemeral_pk`: an `xPublicKey` representing the public key for the next layer. +- `excess`: a `SecretKey` representing the excess value. +- `fee`: a `FeeFields` value representing the transaction fee. +- `rangeproof`: an optional `RangeProof` value. + +### Response + +A successful call to the 'swap' API will result in an empty JSON-RPC response with no error. + +In case of errors, the API will return a `SwapError` type with one of the following variants: + +- `InvalidPayloadLength`: The provided number of payloads is invalid. +- `InvalidComSignature`: The Commitment Signature is invalid. +- `InvalidRangeproof`: The provided rangeproof is invalid. +- `MissingRangeproof`: A rangeproof is required but was not supplied. +- `CoinNotFound`: The output does not exist, or it is already spent. +- `AlreadySwapped`: The output is already in the swap list. +- `PeelOnionFailure`: Failed to peel onion layer due to an `OnionError`. +- `FeeTooLow`: The provided fee is too low. +- `StoreError`: An error occurred when saving swap to the data store. +- `ClientError`: An error occurred during client communication. +- `UnknownError`: An unknown error occurred. + +### Example + +Here is an example of how to call the 'swap' API: +```json +{ + "jsonrpc": "2.0", + "method": "swap", + "params": { + "comsig": "09ca34db2ac772a9a0e954b4ae2180ba936d8f96219824fe7ec1f5439bef3a0afe7e18867db3d391f37260285feea38ff740b0b49196a4b0a7910c1a72ceca1c5a3e4a53d6e06ffb0536f0dad78812a72ef14e6ff83df8d0dd2aa71615fb00fbe2", + "onion": { + "commit": "0962da257e8c663d1a35128cf87363657ae6ec4a3c78fda4742a77e9c4f17e1a20", + "data": [ + "fd06dd3e506b1c1e76fd6546beec1e88bb13e7e13be7c02a7e525cd22c43d5dc7a906c77e5c07b08d7a5eeb7e7983b87376b02a33f7582ffc1bf2adac498fefbc2dba840d76d4c8e945f", + "ecead273b9b707d101aae71c2c7cb8ce3e7c95347aa730015af206baaf37302df48e5e635ecc94ddf3eee12b314e276f23e29e7dde9f30f712b14ea227801719ecdd1a53999f854a7f4878b905c94905d5f1bfbb4ad9bcf01afeb55070ebcc665d29b0a85093b4d134a52adc76293ad9e963a9f7156dcfc95c1c600a31b919495bf6d3b7ec75eeffcc70aef15b98c43c41468f34b1a96c49b9e20328849a3b12c84d97893145a65d820c37dae51eba62121d681543d060d600167ede3a8c6e807a5765c5ebb2d568366c89bba2b08590a4615822ca64fb848e54267b18fc35fb0f9f6834f1524d7e0da89163e5385de65613e09fed6fec8d9cc60354baa86131b80aa1c8cd5be916a3d757cd8e8253c17158555539a2f8e4d9d1a4b996b218b1af3e7b28bdf9e0f3db2ea9f4d5e11d798d9b7698d037e69df3ca89c2165760963a4d80207917a70a4986d7df83b463547f4d704d28b1eec2e5a93aa70b5b7c73559120e23cd4cfbf76e4d2b21ef215d4c0210001c17318eba633a3c177c18ef88b6c1718e11c552cc77b297dab5c1020557915853434b8ca5698685b3a66bba73164e83d2440473ebb0591df593e0264b605dc3b35055a7de0d40c5c7cc7542dcbe5ade436098dd41e1ac395d2d0baf5c82fdd5932b2e182f8f11a67bccc90e6e63ec8928bd7f0306c6949122fadf12493a7de17f7bfad72501f4f792fca388b3614d6eb3165d948d7c9efe168b5273b132fa27ea6e8df63d70d8b099a9220903b02898b5cc925010ebfab78ccceb19a9f2f6d6e0392c4837977bf0e3e014913e154913c0204913514684f64d7166b3a7203cbab9dddd96ed7db35b4a17fec50abd752348cdf53181ddd6954bc1fb907ed86206dcf05c04efb432cb6ba6db25082b4ce0bf520e3c508163b44c82efaa44b2ec904ddd938a0b99044666941bc72be58e22122027c2fcbc4299e52bc29916eb51206c41e618bce1a5c0d859d116807217282d0883fdabe6f9250cda63082f71fbf921b65ab17cd9bfb0561c4cabe1369c7d6a85c51c0e4f43f51622e70ab4eb0e3fab5" + ], + "pubkey": "500b161d3bbd9249161d9760ba038d9805be86c0e5273782303a67cda50edb5a" + } + }, + "id": "1" +} +``` \ No newline at end of file diff --git a/src/crypto/comsig.rs b/src/crypto/comsig.rs index 55fc627..dd8a0c5 100644 --- a/src/crypto/comsig.rs +++ b/src/crypto/comsig.rs @@ -7,7 +7,7 @@ use secp256k1zkp::rand::thread_rng; use thiserror::Error; /// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ComSignature { pub_nonce: Commitment, s: SecretKey, diff --git a/src/crypto/dalek.rs b/src/crypto/dalek.rs index 45474b3..9af9eaf 100644 --- a/src/crypto/dalek.rs +++ b/src/crypto/dalek.rs @@ -174,7 +174,6 @@ pub mod test_util { #[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; @@ -191,7 +190,7 @@ mod tests { #[test] fn pubkey_test() -> Result<(), Box> { // Test from_hex - let rand_pk = test_util::rand_keypair().1; + let rand_pk = 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); diff --git a/src/crypto/secp.rs b/src/crypto/secp.rs index 4956ece..6a682d2 100644 --- a/src/crypto/secp.rs +++ b/src/crypto/secp.rs @@ -90,7 +90,7 @@ pub mod test_util { pub fn proof( value: u64, - fee: u64, + fee: u32, input_blind: &SecretKey, hop_excesses: &Vec<&SecretKey>, ) -> (Commitment, RangeProof) { @@ -101,7 +101,7 @@ pub mod test_util { blind.add_assign(&secp, &hop_excess).unwrap(); } - let out_value = value - fee; + let out_value = value - (fee as u64); let rp = secp.bullet_proof( out_value, diff --git a/src/main.rs b/src/main.rs index d5a705b..0f4b0e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ mod servers; mod store; mod tor; mod tx; -mod types; +mod util; mod wallet; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; diff --git a/src/onion.rs b/src/onion.rs index 792dff5..7153028 100644 --- a/src/onion.rs +++ b/src/onion.rs @@ -1,17 +1,17 @@ -use crate::crypto::secp::{self, Commitment, SecretKey}; -use crate::types::Payload; - +use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey}; use crate::onion::OnionError::{InvalidKeyLength, SerializationError}; +use crate::util::{read_optional, vec_to_array, write_optional}; + use chacha20::cipher::{NewCipher, StreamCipher}; use chacha20::{ChaCha20, Key, Nonce}; +use grin_core::core::FeeFields; use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; use grin_util::{self, ToHex}; use hmac::digest::InvalidLength; use hmac::{Hmac, Mac}; use serde::ser::SerializeStruct; use serde::Deserialize; -use sha2::{Digest, Sha256}; -use std::convert::TryInto; +use sha2::Sha256; use std::fmt; use std::hash::{Hash, Hasher}; use std::result::Result; @@ -21,6 +21,8 @@ use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret}; type HmacSha256 = Hmac; type RawBytes = Vec; +const CURRENT_ONION_VERSION: u8 = 0; + /// A data packet with layers of encryption #[derive(Clone, Debug)] pub struct Onion { @@ -53,8 +55,59 @@ impl Hash for Onion { } } -fn vec_to_32_byte_arr(v: Vec) -> Result<[u8; 32], OnionError> { - v.try_into().map_err(|_| InvalidKeyLength) +/// A single, decrypted/peeled layer of an Onion. +#[derive(Debug, Clone)] +pub struct Payload { + pub next_ephemeral_pk: xPublicKey, + pub excess: SecretKey, + pub fee: FeeFields, + pub rangeproof: Option, +} + +impl Payload { + pub fn deserialize(bytes: &Vec) -> Result { + let payload: Payload = ser::deserialize_default(&mut &bytes[..])?; + Ok(payload) + } + + #[cfg(test)] + pub fn serialize(&self) -> Result, ser::Error> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } +} + +impl Readable for Payload { + fn read(reader: &mut R) -> Result { + let version = reader.read_u8()?; + if version != CURRENT_ONION_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } + + let next_ephemeral_pk = + xPublicKey::from(vec_to_array::<32>(&reader.read_fixed_bytes(32)?)?); + let excess = secp::read_secret_key(reader)?; + let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?; + let rangeproof = read_optional(reader)?; + Ok(Payload { + next_ephemeral_pk, + excess, + fee, + rangeproof, + }) + } +} + +impl Writeable for Payload { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(CURRENT_ONION_VERSION)?; + writer.write_fixed_bytes(&self.next_ephemeral_pk.as_bytes())?; + writer.write_fixed_bytes(&self.excess)?; + writer.write_u64(self.fee.into())?; + write_optional(writer, &self.rangeproof)?; + Ok(()) + } } /// An onion with a layer decrypted @@ -74,8 +127,8 @@ 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 { - let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey); + pub fn peel_layer(&self, server_key: &SecretKey) -> Result { + let shared_secret = StaticSecret::from(server_key.0).diffie_hellman(&self.ephemeral_pubkey); let mut cipher = new_stream_cipher(&shared_secret)?; let mut decrypted_bytes = self.enc_payloads[0].clone(); @@ -95,15 +148,6 @@ impl Onion { }) .collect(); - let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?; - - let ephemeral_key = StaticSecret::from( - *blinding_factor - .diffie_hellman(&self.ephemeral_pubkey) - .as_bytes(), - ); - let ephemeral_pubkey = xPublicKey::from(&ephemeral_key); - let mut commitment = self.commit.clone(); commitment = secp::add_excess(&commitment, &decrypted_payload.excess) .map_err(|e| OnionError::CalcCommitError(e))?; @@ -111,7 +155,7 @@ impl Onion { .map_err(|e| OnionError::CalcCommitError(e))?; let peeled_onion = Onion { - ephemeral_pubkey, + ephemeral_pubkey: decrypted_payload.next_ephemeral_pk, commit: commitment.clone(), enc_payloads, }; @@ -122,22 +166,6 @@ impl Onion { } } -fn calc_blinding_factor( - shared_secret: &SharedSecret, - ephemeral_pubkey: &xPublicKey, -) -> Result { - let mut hasher = Sha256::default(); - hasher.update(ephemeral_pubkey.as_bytes()); - hasher.update(shared_secret.as_bytes()); - let hashed: [u8; 32] = hasher - .finalize() - .as_slice() - .try_into() - .map_err(|_| InvalidKeyLength)?; - - Ok(StaticSecret::from(hashed)) -} - fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; mu_hmac.update(shared_secret.as_bytes()); @@ -164,8 +192,7 @@ impl Writeable for Onion { impl Readable for Onion { fn read(reader: &mut R) -> Result { - let pubkey_bytes: [u8; 32] = - vec_to_32_byte_arr(reader.read_fixed_bytes(32)?).map_err(|_| ser::Error::CountError)?; + let pubkey_bytes: [u8; 32] = vec_to_array(&reader.read_fixed_bytes(32)?)?; let ephemeral_pubkey = xPublicKey::from(pubkey_bytes); let commit = Commitment::read(reader)?; let mut enc_payloads: Vec = Vec::new(); @@ -236,9 +263,9 @@ impl<'de> serde::de::Deserialize<'de> for Onion { let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; pubkey = - Some(xPublicKey::from(vec_to_32_byte_arr(vec).map_err(|_| { - serde::de::Error::custom("Invalid length pubkey") - })?)); + Some(xPublicKey::from(vec_to_array::<32>(&vec).map_err( + |_| serde::de::Error::custom("Invalid length pubkey"), + )?)); } Field::Commit => { let val: String = map.next_value()?; @@ -303,10 +330,9 @@ impl From for OnionError { #[cfg(test)] pub mod test_util { - use super::{Onion, OnionError, RawBytes}; + use super::{Onion, OnionError, Payload, RawBytes}; 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; @@ -317,54 +343,59 @@ pub mod test_util { #[derive(Clone)] pub struct Hop { - pub pubkey: xPublicKey, - pub payload: Payload, + pub server_pubkey: xPublicKey, + pub excess: SecretKey, + pub fee: FeeFields, + pub rangeproof: Option, } pub fn new_hop( server_key: &SecretKey, hop_excess: &SecretKey, - fee: u64, + fee: u32, 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, - }, + server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())), + 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 - Choose random initial ephemeral keypair (r1, R1) - Derive remaining ephemeral keypairs such that ri+1=ri*Sha256(Ri||si) where si=ECDH(Ri, Ki) - For each node ni, use ChaCha20 stream cipher with key=HmacSha256("MWIXNET"||si) and nonce "NONCE1234567" to encrypt payloads Pi...n - */ - /// Create an Onion for the Commitment, encrypting the payload for each hop pub fn create_onion(commitment: &Commitment, hops: &Vec) -> Result { - let initial_key = StaticSecret::new(&mut thread_rng()); - let mut ephemeral_key = initial_key.clone(); + if hops.is_empty() { + return Ok(Onion { + ephemeral_pubkey: xPublicKey::from([0u8; 32]), + commit: commitment.clone(), + enc_payloads: vec![], + }); + } let mut shared_secrets: Vec = Vec::new(); let mut enc_payloads: Vec = Vec::new(); - for hop in hops { - let shared_secret = ephemeral_key.diffie_hellman(&hop.pubkey); - - let ephemeral_pubkey = xPublicKey::from(&ephemeral_key); - let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?; - + let mut ephemeral_sk = StaticSecret::from(random_secret().0); + let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk); + for i in 0..hops.len() { + let hop = &hops[i]; + let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey); shared_secrets.push(shared_secret); - enc_payloads.push(hop.payload.serialize()?); - ephemeral_key = StaticSecret::from( - *ephemeral_key - .diffie_hellman(&xPublicKey::from(&blinding_factor)) - .as_bytes(), - ); + + ephemeral_sk = StaticSecret::from(random_secret().0); + let next_ephemeral_pk = if i < (hops.len() - 1) { + xPublicKey::from(&ephemeral_sk) + } else { + xPublicKey::from([0u8; 32]) + }; + + let payload = Payload { + next_ephemeral_pk, + excess: hop.excess.clone(), + fee: hop.fee.clone(), + rangeproof: hop.rangeproof.clone(), + }; + enc_payloads.push(payload.serialize()?); } for i in (0..shared_secrets.len()).rev() { @@ -375,7 +406,7 @@ pub mod test_util { } let onion = Onion { - ephemeral_pubkey: xPublicKey::from(&initial_key), + ephemeral_pubkey: onion_ephemeral_pk, commit: commitment.clone(), enc_payloads, }; @@ -387,70 +418,56 @@ pub mod test_util { let mut hops = Vec::new(); let k = (thread_rng().next_u64() % 5) + 1; for i in 0..k { - let hop = Hop { - pubkey: xPublicKey::from(random_secret().0), - payload: Payload { - excess: random_secret(), - fee: FeeFields::from(thread_rng().next_u32()), - rangeproof: if i == (k - 1) { - Some(rand_proof()) - } else { - None - }, - }, + let rangeproof = if i == (k - 1) { + Some(rand_proof()) + } else { + None }; + let hop = new_hop( + &random_secret(), + &random_secret(), + thread_rng().next_u32(), + rangeproof, + ); hops.push(hop); } create_onion(&commit, &hops).unwrap() } - - /// Calculates the expected next ephemeral pubkey after peeling a layer off of the Onion. - pub fn next_ephemeral_pubkey( - onion: &Onion, - server_key: &SecretKey, - ) -> Result { - let shared_secret = - StaticSecret::from(server_key.0.clone()).diffie_hellman(&onion.ephemeral_pubkey); - let blinding_factor = super::calc_blinding_factor(&shared_secret, &onion.ephemeral_pubkey)?; - let mul = blinding_factor.diffie_hellman(&onion.ephemeral_pubkey); - Ok(xPublicKey::from(&StaticSecret::from(*mul.as_bytes()))) - } } #[cfg(test)] pub mod tests { - use super::test_util::{self, Hop}; - use crate::crypto::secp; - use crate::types::Payload; + use super::test_util::{new_hop, Hop}; + use super::*; + use crate::crypto::secp::random_secret; use grin_core::core::FeeFields; - use x25519_dalek::{PublicKey as xPublicKey, StaticSecret}; /// Test end-to-end Onion creation and unwrapping logic. #[test] fn onion() { let total_fee: u64 = 10; - let fee_per_hop: u64 = 2; + let fee_per_hop: u32 = 2; let in_value: u64 = 1000; let out_value: u64 = in_value - total_fee; - let blind = secp::random_secret(); + let blind = random_secret(); let commitment = secp::commit(in_value, &blind).unwrap(); let mut hops: Vec = Vec::new(); - let mut keys: Vec = Vec::new(); + let mut keys: Vec = Vec::new(); let mut final_commit = secp::commit(out_value, &blind).unwrap(); let mut final_blind = blind.clone(); for i in 0..5 { - keys.push(secp::random_secret()); + keys.push(random_secret()); - let excess = secp::random_secret(); + let excess = random_secret(); let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); final_blind.add_assign(&secp, &excess).unwrap(); final_commit = secp::add_excess(&final_commit, &excess).unwrap(); let proof = if i == 4 { - let n1 = secp::random_secret(); + let n1 = random_secret(); let rp = secp.bullet_proof( out_value, final_blind.clone(), @@ -465,20 +482,15 @@ pub mod tests { None }; - hops.push(Hop { - pubkey: xPublicKey::from(&StaticSecret::from(keys[i].0.clone())), - payload: Payload { - excess, - fee: FeeFields::from(fee_per_hop as u32), - rangeproof: proof, - }, - }); + let hop = new_hop(&keys[i], &excess, fee_per_hop, proof); + hops.push(hop); } let mut onion_packet = test_util::create_onion(&commitment, &hops).unwrap(); let mut payload = Payload { - excess: secp::random_secret(), + next_ephemeral_pk: onion_packet.ephemeral_pubkey.clone(), + excess: random_secret(), fee: FeeFields::from(fee_per_hop as u32), rangeproof: None, }; @@ -489,10 +501,7 @@ pub mod tests { } assert!(payload.rangeproof.is_some()); - assert_eq!( - payload.rangeproof.unwrap(), - hops[4].payload.rangeproof.unwrap() - ); + assert_eq!(payload.rangeproof.unwrap(), hops[4].rangeproof.unwrap()); assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit); assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32)); } diff --git a/src/servers/mix.rs b/src/servers/mix.rs index 1391bd5..a65430e 100644 --- a/src/servers/mix.rs +++ b/src/servers/mix.rs @@ -324,9 +324,12 @@ mod tests { use crate::crypto::secp::{self, Commitment}; use crate::node::mock::MockGrinNode; use crate::onion::test_util; - use crate::MixClient; + use crate::{DalekPublicKey, MixClient}; + use crate::onion::test_util::Hop; use ::function_name::named; + use secp256k1zkp::pedersen::RangeProof; + use secp256k1zkp::SecretKey; use std::collections::HashSet; use std::sync::Arc; @@ -341,50 +344,89 @@ mod tests { }}; } + struct ServerVars { + fee: u32, + sk: SecretKey, + pk: DalekPublicKey, + excess: SecretKey, + } + + impl ServerVars { + fn new(fee: u32) -> Self { + let (sk, pk) = dalek::test_util::rand_keypair(); + let excess = secp::random_secret(); + ServerVars { + fee, + sk, + pk, + excess, + } + } + + fn build_hop(&self, proof: Option) -> Hop { + test_util::new_hop(&self.sk, &self.excess, self.fee, proof) + } + } + + /// Tests the happy path for a 3 server setup. + /// + /// Servers: + /// * Swap Server - Simulated by test + /// * Mixer 1 - Internal MixServerImpl directly called by test + /// * Mixer 2 - Final MixServerImpl called by Mixer 1 #[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])); + // Setup Input(s) + let input1_value: u64 = 200_000_000; + let input1_blind = secp::random_secret(); + let input1_commit = secp::commit(input1_value, &input1_blind)?; + let input_commits = vec![&input1_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], + // Setup Servers + let (swap_vars, mix1_vars, mix2_vars) = ( + ServerVars::new(50_000_000), + ServerVars::new(50_000_000), + ServerVars::new(50_000_000), ); - 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())), + let node = Arc::new(MockGrinNode::new_with_utxos(&input_commits)); + let (mixer2_client, mixer2_wallet) = super::test_util::new_mixer( + &mix2_vars.sk, + (&mix1_vars.sk, &mix1_vars.pk), + &None, &node, ); - // Emulate the swap server peeling the onion and then calling mix1 - let mix1_onion = onion.peel_layer(&swap_sk)?; + let (mixer1_client, mixer1_wallet) = super::test_util::new_mixer( + &mix1_vars.sk, + (&swap_vars.sk, &swap_vars.pk), + &Some((mix2_vars.pk.clone(), mixer2_client.clone())), + &node, + ); + + // Build rangeproof + let (output_commit, proof) = secp::test_util::proof( + input1_value, + swap_vars.fee + mix1_vars.fee + mix2_vars.fee, + &input1_blind, + &vec![&swap_vars.excess, &mix1_vars.excess, &mix2_vars.excess], + ); + + // Create Onion + let onion = test_util::create_onion( + &input1_commit, + &vec![ + swap_vars.build_hop(None), + mix1_vars.build_hop(None), + mix2_vars.build_hop(Some(proof)), + ], + )?; + + // Simulate the swap server peeling the onion and then calling mix1 + let mix1_onion = onion.peel_layer(&swap_vars.sk)?; let (mixed_indices, mixed_components) = mixer1_client.mix_outputs(&vec![mix1_onion.onion.clone()])?; @@ -396,7 +438,7 @@ mod tests { .iter() .map(|o| o.identifier.commit.clone()) .collect(); - assert!(output_commits.contains(&out_commit)); + assert!(output_commits.contains(&output_commit)); assert_eq!(mixer1_wallet.built_outputs().len(), 1); assert!(output_commits.contains(mixer1_wallet.built_outputs().get(0).unwrap())); diff --git a/src/servers/swap.rs b/src/servers/swap.rs index 66e8021..e918d08 100644 --- a/src/servers/swap.rs +++ b/src/servers/swap.rs @@ -153,7 +153,7 @@ impl SwapServer for SwapServerImpl { output_commit: peeled.onion.commit, rangeproof: peeled.payload.rangeproof, input, - fee, + fee: fee as u64, onion: peeled.onion, status: SwapStatus::Unprocessed, }, @@ -355,6 +355,7 @@ mod tests { use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting}; use secp256k1zkp::key::ZERO_KEY; use std::sync::Arc; + use x25519_dalek::PublicKey as xPublicKey; macro_rules! assert_error_type { ($result:expr, $error_type:pat) => { @@ -385,7 +386,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -394,7 +395,7 @@ mod tests { 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 onion = test_util::create_onion(&input_commit, &vec![hop.clone()])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -407,9 +408,9 @@ mod tests { output_commit: output_commit.clone(), rangeproof: Some(proof), input: Input::new(OutputFeatures::Plain, input_commit.clone()), - fee, + fee: fee as u64, onion: Onion { - ephemeral_pubkey: test_util::next_ephemeral_pubkey(&onion, &server_key)?, + ephemeral_pubkey: xPublicKey::from([0u8; 32]), commit: output_commit.clone(), enc_payloads: vec![], }, @@ -462,13 +463,13 @@ mod tests { let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); // Swapper data - let swap_fee: u64 = 50_000_000; + let swap_fee: u32 = 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_fee: u32 = 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( @@ -493,7 +494,7 @@ mod tests { output_commit.clone(), proof.clone(), )], - kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee)?], + kernels: vec![tx::build_kernel(&mixer_hop_excess, mixer_fee as u64)?], }; mock_mixer.set_response( &vec![mixer_onion.clone()], @@ -532,7 +533,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -567,7 +568,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -603,7 +604,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -638,7 +639,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -670,7 +671,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -709,7 +710,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -745,7 +746,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 50_000_000; + let fee: u32 = 50_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -777,7 +778,7 @@ mod tests { let test_dir = init_test!(); let value: u64 = 200_000_000; - let fee: u64 = 1_000_000; + let fee: u32 = 1_000_000; let blind = secp::random_secret(); let input_commit = secp::commit(value, &blind)?; @@ -796,7 +797,7 @@ mod tests { assert_eq!( Err(SwapError::FeeTooLow { minimum_fee: 12_500_000, - actual_fee: fee + actual_fee: fee as u64 }), result ); diff --git a/src/servers/swap_rpc.rs b/src/servers/swap_rpc.rs index d79006d..313b450 100644 --- a/src/servers/swap_rpc.rs +++ b/src/servers/swap_rpc.rs @@ -221,7 +221,6 @@ mod tests { "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", serde_json::json!(swap) ); - println!("Request: {}", req); let response = make_request(server, req)?; let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n"; assert_eq!(response, expected); diff --git a/src/store.rs b/src/store.rs index d1b21c5..cf5b689 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,6 +1,6 @@ use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey}; use crate::onion::Onion; -use crate::types::{read_optional, write_optional}; +use crate::util::{read_optional, write_optional}; use grin_core::core::hash::Hash; use grin_core::core::Input; diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index ec15988..0000000 --- a/src/types.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::crypto::secp::{self, RangeProof, SecretKey}; - -use grin_core::core::FeeFields; -use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; -use serde::{Deserialize, Serialize}; - -const CURRENT_VERSION: u8 = 0; - -/// Writes an optional value as '1' + value if Some, or '0' if None -pub fn write_optional( - writer: &mut W, - o: &Option, -) -> Result<(), ser::Error> { - match &o { - Some(o) => { - writer.write_u8(1)?; - o.write(writer)?; - } - None => writer.write_u8(0)?, - }; - Ok(()) -} - -/// Reads an optional value as '1' + value if Some, or '0' if None -pub fn read_optional(reader: &mut R) -> Result, ser::Error> { - let o = if reader.read_u8()? == 0 { - None - } else { - Some(O::read(reader)?) - }; - Ok(o) -} - -// todo: Belongs in Onion -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Payload { - pub excess: SecretKey, - pub fee: FeeFields, - pub rangeproof: Option, -} - -impl Payload { - pub fn deserialize(bytes: &Vec) -> Result { - let payload: Payload = ser::deserialize_default(&mut &bytes[..])?; - Ok(payload) - } - - #[cfg(test)] - pub fn serialize(&self) -> Result, ser::Error> { - let mut vec = vec![]; - ser::serialize_default(&mut vec, &self)?; - Ok(vec) - } -} - -impl Readable for Payload { - fn read(reader: &mut R) -> Result { - let version = reader.read_u8()?; - if version != CURRENT_VERSION { - return Err(ser::Error::UnsupportedProtocolVersion); - } - - let excess = secp::read_secret_key(reader)?; - let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?; - let rangeproof = read_optional(reader)?; - Ok(Payload { - excess, - fee, - rangeproof, - }) - } -} - -impl Writeable for Payload { - fn write(&self, writer: &mut W) -> Result<(), ser::Error> { - writer.write_u8(CURRENT_VERSION)?; - writer.write_fixed_bytes(&self.excess)?; - writer.write_u64(self.fee.into())?; - write_optional(writer, &self.rangeproof)?; - Ok(()) - } -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..99ae709 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,164 @@ +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; + +/// Writes an optional value as '1' + value if Some, or '0' if None +/// +/// This function is used to serialize an optional value into a Writer. If the option +/// contains Some value, it writes '1' followed by the serialized value. If the option +/// is None, it just writes '0'. +/// +/// # Arguments +/// +/// * `writer` - A Writer instance where the data will be written. +/// * `o` - The Optional value that will be written. +/// +/// # Returns +/// +/// * If successful, returns Ok with nothing. +/// * If an error occurs during writing, returns Err wrapping the error. +/// +/// # Example +/// +/// ``` +/// let mut writer = vec![]; +/// let optional_value: Option = Some(10); +/// write_optional(&mut writer, &optional_value); +/// assert_eq!(buf, &[1, 0, 0, 0, 10]); +/// ``` +pub fn write_optional( + writer: &mut W, + o: &Option, +) -> Result<(), ser::Error> { + match &o { + Some(o) => { + writer.write_u8(1)?; + o.write(writer)?; + } + None => writer.write_u8(0)?, + }; + Ok(()) +} + +/// Reads an optional value as '1' + value if Some, or '0' if None +/// +/// This function is used to deserialize an optional value from a Reader. If the first byte +/// read is '0', it returns None. If the first byte is '1', it reads the next value and +/// returns Some(value). +/// +/// # Arguments +/// +/// * `reader` - A Reader instance from where the data will be read. +/// +/// # Returns +/// +/// * If successful, returns Ok wrapping an optional value. If the first byte read was '0', +/// returns None. If it was '1', returns Some(value). +/// * If an error occurs during reading, returns Err wrapping the error. +/// +/// # Example +/// +/// ``` +/// let mut buf: &[u8] = &[1, 0, 0, 0, 10]; +/// let mut reader = BinReader::new(&mut buf, ProtocolVersion::local(), DeserializationMode::default()); +/// let optional_value: Option = read_optional(&mut reader).unwrap(); +/// assert_eq!(optional_value, Some(10)); +/// ``` +pub fn read_optional(reader: &mut R) -> Result, ser::Error> { + let o = if reader.read_u8()? == 0 { + None + } else { + Some(O::read(reader)?) + }; + Ok(o) +} + +/// Convert a vector to an array of size `S`. +/// +/// # Arguments +/// +/// * `vec` - The input vector. +/// +/// # Returns +/// +/// * If successful, returns an `Ok` wrapping an array of size `S` containing +/// the first `S` bytes of `vec`. +/// * If `vec` is smaller than `S`, returns an `Err` indicating a count error. +/// +/// # Example +/// +/// ``` +/// let v = vec![0, 1, 2, 3, 4, 5]; +/// let a = vec_to_array::<4>(&v).unwrap(); +/// assert_eq!(a, [0, 1, 2, 3]); +/// ``` +pub fn vec_to_array(vec: &Vec) -> Result<[u8; S], ser::Error> { + if vec.len() < S { + return Err(ser::Error::CountError); + } + let arr: [u8; S] = vec[0..S].try_into().unwrap(); + Ok(arr) +} + +#[cfg(test)] +mod tests { + use super::*; + use grin_core::ser::{BinReader, BinWriter, DeserializationMode, ProtocolVersion}; + + #[test] + fn test_write_optional() { + // Test with Some value + let mut buf: Vec = vec![]; + let val: Option = Some(10); + write_optional(&mut BinWriter::default(&mut buf), &val).unwrap(); + assert_eq!(buf, &[1, 0, 0, 0, 10]); // 1 for Some, then 10 as a little-endian u32 + + // Test with None value + buf.clear(); + let val: Option = None; + write_optional(&mut BinWriter::default(&mut buf), &val).unwrap(); + assert_eq!(buf, &[0]); // 0 for None + } + + #[test] + fn test_read_optional() { + // Test with Some value + let mut buf: &[u8] = &[1, 0, 0, 0, 10]; // 1 for Some, then 10 as a little-endian u32 + let val: Option = read_optional(&mut BinReader::new( + &mut buf, + ProtocolVersion::local(), + DeserializationMode::default(), + )) + .unwrap(); + assert_eq!(val, Some(10)); + + // Test with None value + buf = &[0]; // 0 for None + let val: Option = read_optional(&mut BinReader::new( + &mut buf, + ProtocolVersion::local(), + DeserializationMode::default(), + )) + .unwrap(); + assert_eq!(val, None); + } + + #[test] + fn test_vec_to_array_success() { + let v = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let a = vec_to_array::<4>(&v).unwrap(); + assert_eq!(a, [1, 2, 3, 4]); + } + + #[test] + fn test_vec_to_array_too_small() { + let v = vec![1, 2, 3]; + let res = vec_to_array::<4>(&v); + assert!(res.is_err()); + } + + #[test] + fn test_vec_to_array_empty() { + let v = vec![]; + let res = vec_to_array::<4>(&v); + assert!(res.is_err()); + } +} From dc3f99ebfa6143b3c3818e8f14bbb65f50ec9ec7 Mon Sep 17 00:00:00 2001 From: scilio Date: Thu, 14 Sep 2023 11:49:04 -0400 Subject: [PATCH 3/3] move onion creation to a separate library --- Cargo.lock | 37 ++++++ Cargo.toml | 4 + onion/Cargo.toml | 38 +++++++ {src => onion/src}/crypto/comsig.rs | 0 {src => onion/src}/crypto/dalek.rs | 12 -- {src => onion/src}/crypto/mod.rs | 0 {src => onion/src}/crypto/secp.rs | 55 --------- onion/src/lib.rs | 170 ++++++++++++++++++++++++++++ {src => onion/src}/onion.rs | 120 +------------------- {src => onion/src}/util.rs | 0 src/client.rs | 6 +- src/main.rs | 6 +- src/servers/mix.rs | 25 ++-- src/servers/mix_rpc.rs | 4 +- src/servers/swap.rs | 82 +++++++------- src/servers/swap_rpc.rs | 12 +- src/store.rs | 27 +++-- src/tx.rs | 41 +++++++ 18 files changed, 374 insertions(+), 265 deletions(-) create mode 100644 onion/Cargo.toml rename {src => onion/src}/crypto/comsig.rs (100%) rename {src => onion/src}/crypto/dalek.rs (97%) rename {src => onion/src}/crypto/mod.rs (100%) rename {src => onion/src}/crypto/secp.rs (64%) create mode 100644 onion/src/lib.rs rename {src => onion/src}/onion.rs (75%) rename {src => onion/src}/util.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 924569a..b671a68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1354,6 +1354,42 @@ dependencies = [ "zeroize", ] +[[package]] +name = "grin_onion" +version = "0.1.0" +dependencies = [ + "blake2-rfc", + "byteorder", + "bytes 0.5.6", + "chacha20", + "curve25519-dalek 2.1.3", + "ed25519-dalek", + "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)", + "grin_core 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", + "grin_keychain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", + "grin_secp256k1zkp", + "grin_servers", + "grin_store 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 5.1.1", + "grin_wallet_api", + "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", + "hmac 0.12.0", + "itertools", + "lazy_static", + "rand 0.7.3", + "rpassword", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.0", + "thiserror", + "toml", + "x25519-dalek 0.6.0", +] + [[package]] name = "grin_p2p" version = "5.2.0-alpha.1" @@ -2555,6 +2591,7 @@ dependencies = [ "grin_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", "grin_core 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", "grin_keychain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", + "grin_onion", "grin_secp256k1zkp", "grin_servers", "grin_store 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", diff --git a/Cargo.toml b/Cargo.toml index 2e3e3f6..fbd4750 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = ["onion"] + [dependencies] blake2 = { package = "blake2-rfc", version = "0.2"} byteorder = "1" @@ -36,6 +39,7 @@ thiserror = "1.0.31" tokio = { version = "1", features = ["full"] } toml = "0.5" x25519-dalek = "0.6.0" +grin_onion = { path = "./onion" } grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]} grin_util = "5" grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } diff --git a/onion/Cargo.toml b/onion/Cargo.toml new file mode 100644 index 0000000..43a9e4b --- /dev/null +++ b/onion/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "grin_onion" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +blake2 = { package = "blake2-rfc", version = "0.2"} +byteorder = "1" +bytes = "0.5.6" +chacha20 = "0.8.1" +curve25519-dalek = "2.1" +ed25519-dalek = "1.0.1" +hmac = { version = "0.12.0", features = ["std"]} +itertools = { version = "0.10.3"} +lazy_static = "1" +rand = "0.7.3" +rpassword = "4.0" +serde = { version = "1", features= ["derive"]} +serde_derive = "1" +serde_json = "1" +sha2 = "0.10.0" +thiserror = "1.0.31" +toml = "0.5" +x25519-dalek = "0.6.0" +grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]} +grin_util = "5" +grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_core = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_chain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_keychain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_servers = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_store = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" } +grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } \ No newline at end of file diff --git a/src/crypto/comsig.rs b/onion/src/crypto/comsig.rs similarity index 100% rename from src/crypto/comsig.rs rename to onion/src/crypto/comsig.rs diff --git a/src/crypto/dalek.rs b/onion/src/crypto/dalek.rs similarity index 97% rename from src/crypto/dalek.rs rename to onion/src/crypto/dalek.rs index 9af9eaf..70c7654 100644 --- a/src/crypto/dalek.rs +++ b/onion/src/crypto/dalek.rs @@ -159,18 +159,6 @@ pub fn sign(sk: &SecretKey, message: &[u8]) -> Result (SecretKey, DalekPublicKey) { - let sk = random_secret(); - let pk = DalekPublicKey::from_secret(&sk); - (sk, pk) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/crypto/mod.rs b/onion/src/crypto/mod.rs similarity index 100% rename from src/crypto/mod.rs rename to onion/src/crypto/mod.rs diff --git a/src/crypto/secp.rs b/onion/src/crypto/secp.rs similarity index 64% rename from src/crypto/secp.rs rename to onion/src/crypto/secp.rs index 6a682d2..89923b9 100644 --- a/src/crypto/secp.rs +++ b/onion/src/crypto/secp.rs @@ -60,58 +60,3 @@ pub fn sign(sk: &SecretKey, msg: &Message) -> Result 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: u32, - 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 as u64); - - 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/onion/src/lib.rs b/onion/src/lib.rs new file mode 100644 index 0000000..d08860d --- /dev/null +++ b/onion/src/lib.rs @@ -0,0 +1,170 @@ +pub mod crypto; +pub mod onion; +pub mod util; + +use crate::crypto::secp::{random_secret, Commitment, SecretKey}; +use crate::onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes}; + +use chacha20::cipher::StreamCipher; +use grin_core::core::FeeFields; +use secp256k1zkp::pedersen::RangeProof; +use x25519_dalek::PublicKey as xPublicKey; +use x25519_dalek::{SharedSecret, StaticSecret}; + +#[derive(Clone)] +pub struct Hop { + pub server_pubkey: xPublicKey, + pub excess: SecretKey, + pub fee: FeeFields, + pub rangeproof: Option, +} + +pub fn new_hop( + server_key: &SecretKey, + hop_excess: &SecretKey, + fee: u32, + proof: Option, +) -> Hop { + Hop { + server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())), + excess: hop_excess.clone(), + fee: FeeFields::from(fee as u32), + rangeproof: proof, + } +} + +/// Create an Onion for the Commitment, encrypting the payload for each hop +pub fn create_onion(commitment: &Commitment, hops: &Vec) -> Result { + if hops.is_empty() { + return Ok(Onion { + ephemeral_pubkey: xPublicKey::from([0u8; 32]), + commit: commitment.clone(), + enc_payloads: vec![], + }); + } + + let mut shared_secrets: Vec = Vec::new(); + let mut enc_payloads: Vec = Vec::new(); + let mut ephemeral_sk = StaticSecret::from(random_secret().0); + let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk); + for i in 0..hops.len() { + let hop = &hops[i]; + let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey); + shared_secrets.push(shared_secret); + + ephemeral_sk = StaticSecret::from(random_secret().0); + let next_ephemeral_pk = if i < (hops.len() - 1) { + xPublicKey::from(&ephemeral_sk) + } else { + xPublicKey::from([0u8; 32]) + }; + + let payload = Payload { + next_ephemeral_pk, + excess: hop.excess.clone(), + fee: hop.fee.clone(), + rangeproof: hop.rangeproof.clone(), + }; + enc_payloads.push(payload.serialize()?); + } + + for i in (0..shared_secrets.len()).rev() { + let mut cipher = new_stream_cipher(&shared_secrets[i])?; + for j in i..shared_secrets.len() { + cipher.apply_keystream(&mut enc_payloads[j]); + } + } + + let onion = Onion { + ephemeral_pubkey: onion_ephemeral_pk, + commit: commitment.clone(), + enc_payloads, + }; + Ok(onion) +} + +pub mod test_util { + use super::*; + use crate::crypto::dalek::DalekPublicKey; + use crate::crypto::secp; + + use grin_core::core::hash::Hash; + use grin_util::ToHex; + use rand::{thread_rng, RngCore}; + use secp256k1zkp::Secp256k1; + + pub fn rand_onion() -> Onion { + let commit = rand_commit(); + let mut hops = Vec::new(); + let k = (thread_rng().next_u64() % 5) + 1; + for i in 0..k { + let rangeproof = if i == (k - 1) { + Some(rand_proof()) + } else { + None + }; + let hop = new_hop( + &random_secret(), + &random_secret(), + thread_rng().next_u32(), + rangeproof, + ); + hops.push(hop); + } + + create_onion(&commit, &hops).unwrap() + } + + 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: u32, + 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 as u64); + + let rp = secp.bullet_proof( + out_value, + blind.clone(), + secp::random_secret(), + secp::random_secret(), + None, + None, + ); + + (secp::commit(out_value, &blind).unwrap(), rp) + } + + pub fn rand_keypair() -> (SecretKey, DalekPublicKey) { + let sk = random_secret(); + let pk = DalekPublicKey::from_secret(&sk); + (sk, pk) + } +} diff --git a/src/onion.rs b/onion/src/onion.rs similarity index 75% rename from src/onion.rs rename to onion/src/onion.rs index 7153028..974b743 100644 --- a/src/onion.rs +++ b/onion/src/onion.rs @@ -1,5 +1,4 @@ use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey}; -use crate::onion::OnionError::{InvalidKeyLength, SerializationError}; use crate::util::{read_optional, vec_to_array, write_optional}; use chacha20::cipher::{NewCipher, StreamCipher}; @@ -19,7 +18,7 @@ use thiserror::Error; use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret}; type HmacSha256 = Hmac; -type RawBytes = Vec; +pub type RawBytes = Vec; const CURRENT_ONION_VERSION: u8 = 0; @@ -70,7 +69,6 @@ impl Payload { Ok(payload) } - #[cfg(test)] pub fn serialize(&self) -> Result, ser::Error> { let mut vec = vec![]; ser::serialize_default(&mut vec, &self)?; @@ -166,7 +164,7 @@ impl Onion { } } -fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { +pub fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; mu_hmac.update(shared_secret.as_bytes()); let mukey = mu_hmac.finalize().into_bytes(); @@ -318,129 +316,21 @@ pub enum OnionError { impl From for OnionError { fn from(_err: InvalidLength) -> OnionError { - InvalidKeyLength + OnionError::InvalidKeyLength } } impl From for OnionError { fn from(err: ser::Error) -> OnionError { - SerializationError(err) - } -} - -#[cfg(test)] -pub mod test_util { - use super::{Onion, OnionError, Payload, RawBytes}; - use crate::crypto::secp::test_util::{rand_commit, rand_proof}; - use crate::crypto::secp::{random_secret, Commitment, SecretKey}; - - 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}; - - #[derive(Clone)] - pub struct Hop { - pub server_pubkey: xPublicKey, - pub excess: SecretKey, - pub fee: FeeFields, - pub rangeproof: Option, - } - - pub fn new_hop( - server_key: &SecretKey, - hop_excess: &SecretKey, - fee: u32, - proof: Option, - ) -> Hop { - Hop { - server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())), - excess: hop_excess.clone(), - fee: FeeFields::from(fee as u32), - rangeproof: proof, - } - } - - /// Create an Onion for the Commitment, encrypting the payload for each hop - pub fn create_onion(commitment: &Commitment, hops: &Vec) -> Result { - if hops.is_empty() { - return Ok(Onion { - ephemeral_pubkey: xPublicKey::from([0u8; 32]), - commit: commitment.clone(), - enc_payloads: vec![], - }); - } - - let mut shared_secrets: Vec = Vec::new(); - let mut enc_payloads: Vec = Vec::new(); - let mut ephemeral_sk = StaticSecret::from(random_secret().0); - let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk); - for i in 0..hops.len() { - let hop = &hops[i]; - let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey); - shared_secrets.push(shared_secret); - - ephemeral_sk = StaticSecret::from(random_secret().0); - let next_ephemeral_pk = if i < (hops.len() - 1) { - xPublicKey::from(&ephemeral_sk) - } else { - xPublicKey::from([0u8; 32]) - }; - - let payload = Payload { - next_ephemeral_pk, - excess: hop.excess.clone(), - fee: hop.fee.clone(), - rangeproof: hop.rangeproof.clone(), - }; - enc_payloads.push(payload.serialize()?); - } - - for i in (0..shared_secrets.len()).rev() { - let mut cipher = super::new_stream_cipher(&shared_secrets[i])?; - for j in i..shared_secrets.len() { - cipher.apply_keystream(&mut enc_payloads[j]); - } - } - - let onion = Onion { - ephemeral_pubkey: onion_ephemeral_pk, - commit: commitment.clone(), - enc_payloads, - }; - Ok(onion) - } - - pub fn rand_onion() -> Onion { - let commit = rand_commit(); - let mut hops = Vec::new(); - let k = (thread_rng().next_u64() % 5) + 1; - for i in 0..k { - let rangeproof = if i == (k - 1) { - Some(rand_proof()) - } else { - None - }; - let hop = new_hop( - &random_secret(), - &random_secret(), - thread_rng().next_u32(), - rangeproof, - ); - hops.push(hop); - } - - create_onion(&commit, &hops).unwrap() + OnionError::SerializationError(err) } } #[cfg(test)] pub mod tests { - use super::test_util::{new_hop, Hop}; use super::*; use crate::crypto::secp::random_secret; + use crate::{new_hop, Hop}; use grin_core::core::FeeFields; diff --git a/src/util.rs b/onion/src/util.rs similarity index 100% rename from src/util.rs rename to onion/src/util.rs diff --git a/src/client.rs b/src/client.rs index d286a1b..1785e26 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,9 @@ 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_onion::onion::Onion; use grin_api::client; use grin_api::json_rpc::build_request; @@ -97,8 +97,8 @@ impl MixClient for MixClientImpl { #[cfg(test)] pub mod mock { use super::{ClientError, MixClient}; - use crate::onion::Onion; use crate::tx::TxComponents; + use grin_onion::onion::Onion; use std::collections::HashMap; @@ -136,12 +136,12 @@ 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 grin_onion::onion::Onion; use std::sync::Arc; /// Implementation of the 'MixClient' trait that calls a mix server implementation directly. diff --git a/src/main.rs b/src/main.rs index 0f4b0e0..fdf84bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,13 @@ 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_onion::crypto; +use grin_onion::crypto::dalek::DalekPublicKey; use grin_util::{StopState, ZeroingString}; use rpassword; use std::path::PathBuf; @@ -21,14 +22,11 @@ extern crate clap; mod client; mod config; -mod crypto; mod node; -mod onion; mod servers; mod store; mod tor; mod tx; -mod util; mod wallet; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; diff --git a/src/servers/mix.rs b/src/servers/mix.rs index a65430e..4510521 100644 --- a/src/servers/mix.rs +++ b/src/servers/mix.rs @@ -1,19 +1,19 @@ +use crate::client::MixClient; use crate::config::ServerConfig; -use crate::crypto::dalek::{self, DalekSignature}; -use crate::onion::{Onion, OnionError, PeeledOnion}; +use crate::tx::TxComponents; 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 grin_onion::crypto::dalek::{self, DalekSignature}; +use grin_onion::onion::{Onion, OnionError, PeeledOnion}; use itertools::Itertools; use secp256k1zkp::key::ZERO_KEY; use secp256k1zkp::Secp256k1; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use thiserror::Error; @@ -320,14 +320,13 @@ mod test_util { #[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::{DalekPublicKey, MixClient}; - use crate::onion::test_util::Hop; use ::function_name::named; + use grin_onion::crypto::secp::{self, Commitment}; + use grin_onion::test_util as onion_test_util; + use grin_onion::{create_onion, new_hop, Hop}; use secp256k1zkp::pedersen::RangeProof; use secp256k1zkp::SecretKey; use std::collections::HashSet; @@ -353,7 +352,7 @@ mod tests { impl ServerVars { fn new(fee: u32) -> Self { - let (sk, pk) = dalek::test_util::rand_keypair(); + let (sk, pk) = onion_test_util::rand_keypair(); let excess = secp::random_secret(); ServerVars { fee, @@ -364,7 +363,7 @@ mod tests { } fn build_hop(&self, proof: Option) -> Hop { - test_util::new_hop(&self.sk, &self.excess, self.fee, proof) + new_hop(&self.sk, &self.excess, self.fee, proof) } } @@ -408,7 +407,7 @@ mod tests { ); // Build rangeproof - let (output_commit, proof) = secp::test_util::proof( + let (output_commit, proof) = onion_test_util::proof( input1_value, swap_vars.fee + mix1_vars.fee + mix2_vars.fee, &input1_blind, @@ -416,7 +415,7 @@ mod tests { ); // Create Onion - let onion = test_util::create_onion( + let onion = create_onion( &input1_commit, &vec![ swap_vars.build_hop(None), diff --git a/src/servers/mix_rpc.rs b/src/servers/mix_rpc.rs index 033639b..7cb5181 100644 --- a/src/servers/mix_rpc.rs +++ b/src/servers/mix_rpc.rs @@ -1,11 +1,11 @@ +use crate::client::MixClient; 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_onion::onion::Onion; use grin_util::StopState; use jsonrpc_derive::rpc; use jsonrpc_http_server::jsonrpc_core::{self as jsonrpc, IoHandler}; diff --git a/src/servers/swap.rs b/src/servers/swap.rs index e918d08..a02095c 100644 --- a/src/servers/swap.rs +++ b/src/servers/swap.rs @@ -1,19 +1,19 @@ +use crate::client::MixClient; 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 grin_onion::onion::{Onion, OnionError}; use itertools::Itertools; use secp256k1zkp::key::ZERO_KEY; +use std::collections::HashSet; use std::result::Result; use std::sync::{Arc, Mutex}; use thiserror::Error; @@ -268,9 +268,9 @@ impl SwapServer for SwapServerImpl { pub mod mock { use super::{SwapError, SwapServer}; use crate::crypto::comsig::ComSignature; - use crate::onion::Onion; use grin_core::core::Transaction; + use grin_onion::onion::Onion; use std::collections::HashMap; pub struct MockSwapServer { @@ -339,12 +339,7 @@ pub mod test_util { #[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; @@ -353,6 +348,11 @@ mod tests { use ::function_name::named; use grin_core::core::hash::Hashed; use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting}; + use grin_onion::crypto::comsig::ComSignature; + use grin_onion::crypto::secp; + use grin_onion::onion::Onion; + use grin_onion::test_util as onion_test_util; + use grin_onion::{create_onion, new_hop, Hop}; use secp256k1zkp::key::ZERO_KEY; use std::sync::Arc; use x25519_dalek::PublicKey as xPublicKey; @@ -392,10 +392,10 @@ mod tests { 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 (output_commit, proof) = onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop.clone()])?; + let onion = create_onion(&input_commit, &vec![hop.clone()])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -464,24 +464,24 @@ mod tests { // Swapper data let swap_fee: u32 = 50_000_000; - let (swap_sk, _swap_pk) = dalek::test_util::rand_keypair(); + let (swap_sk, _swap_pk) = onion_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); + let swap_hop = new_hop(&swap_sk, &swap_hop_excess, swap_fee, None); // Mixer data let mixer_fee: u32 = 30_000_000; - let (mixer_sk, mixer_pk) = dalek::test_util::rand_keypair(); + let (mixer_sk, mixer_pk) = onion_test_util::rand_keypair(); let mixer_hop_excess = secp::random_secret(); - let (output_commit, proof) = secp::test_util::proof( + let (output_commit, proof) = onion_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)); + let mixer_hop = 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 onion = create_onion(&input_commit, &vec![swap_hop, mixer_hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; // Mock mixer @@ -540,11 +540,11 @@ mod tests { 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)); + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); let hops: Vec = vec![hop.clone(), hop.clone()]; // Multiple payloads - let onion = test_util::create_onion(&input_commit, &hops)?; + let onion = create_onion(&input_commit, &hops)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -575,10 +575,10 @@ mod tests { 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)); + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let wrong_blind = secp::random_secret(); let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?; @@ -612,10 +612,10 @@ mod tests { 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)); + onion_test_util::proof(wrong_value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -645,9 +645,9 @@ mod tests { 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 hop = new_hop(&server_key, &hop_excess, fee, None); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -678,10 +678,10 @@ mod tests { 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)); + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new()); @@ -717,10 +717,10 @@ mod tests { 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)); + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -753,12 +753,12 @@ mod tests { 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]); + onion_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 hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); @@ -785,10 +785,10 @@ mod tests { 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)); + onion_test_util::proof(value, fee, &blind, &vec![&hop_excess]); + let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); - let onion = test_util::create_onion(&input_commit, &vec![hop])?; + let onion = create_onion(&input_commit, &vec![hop])?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let node: Arc = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit])); diff --git a/src/servers/swap_rpc.rs b/src/servers/swap_rpc.rs index 313b450..788e4c1 100644 --- a/src/servers/swap_rpc.rs +++ b/src/servers/swap_rpc.rs @@ -1,12 +1,12 @@ +use crate::client::MixClient; use crate::config::ServerConfig; -use crate::crypto::comsig::{self, ComSignature}; use crate::node::GrinNode; -use crate::onion::Onion; use crate::servers::swap::{SwapError, SwapServer, SwapServerImpl}; use crate::store::SwapStore; use crate::wallet::Wallet; -use crate::client::MixClient; +use grin_onion::crypto::comsig::{self, ComSignature}; +use grin_onion::onion::Onion; use grin_util::StopState; use jsonrpc_core::Value; use jsonrpc_derive::rpc; @@ -133,11 +133,11 @@ mod tests { use crate::config::ServerConfig; use crate::crypto::comsig::ComSignature; use crate::crypto::secp; - use crate::onion::test_util; use crate::servers::swap::mock::MockSwapServer; use crate::servers::swap::{SwapError, SwapServer}; use crate::servers::swap_rpc::{RPCSwapServer, SwapReq}; + use grin_onion::create_onion; use std::net::TcpListener; use std::sync::{Arc, Mutex}; @@ -208,7 +208,7 @@ mod tests { #[test] fn swap_success() -> Result<(), Box> { let commitment = secp::commit(1234, &secp::random_secret())?; - let onion = test_util::create_onion(&commitment, &vec![])?; + let onion = create_onion(&commitment, &vec![])?; let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?; let swap = SwapReq { onion: onion.clone(), @@ -247,7 +247,7 @@ mod tests { #[test] fn swap_utxo_missing() -> Result<(), Box> { let commitment = secp::commit(1234, &secp::random_secret())?; - let onion = test_util::create_onion(&commitment, &vec![])?; + let onion = create_onion(&commitment, &vec![])?; let comsig = ComSignature::sign(1234, &secp::random_secret(), &onion.serialize()?)?; let swap = SwapReq { onion: onion.clone(), diff --git a/src/store.rs b/src/store.rs index cf5b689..2122cfc 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,7 +1,7 @@ -use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey}; -use crate::onion::Onion; -use crate::util::{read_optional, write_optional}; use grin_core::core::hash::Hash; +use grin_onion::crypto::secp::{self, Commitment, RangeProof, SecretKey}; +use grin_onion::onion::Onion; +use grin_onion::util::{read_optional, write_optional}; use grin_core::core::Input; use grin_core::ser::{ @@ -244,13 +244,12 @@ 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::store::{SwapData, SwapStatus, SwapStore}; use crate::StoreError; use grin_core::core::{Input, OutputFeatures}; use grin_core::global::{self, ChainTypes}; + use grin_onion::crypto::secp; + use grin_onion::test_util as onion_test_util; use rand::RngCore; use std::cmp::Ordering; @@ -264,11 +263,11 @@ mod tests { fn rand_swap_with_status(status: SwapStatus) -> SwapData { SwapData { excess: secp::random_secret(), - output_commit: rand_commit(), - rangeproof: Some(rand_proof()), - input: Input::new(OutputFeatures::Plain, rand_commit()), + output_commit: onion_test_util::rand_commit(), + rangeproof: Some(onion_test_util::rand_proof()), + input: Input::new(OutputFeatures::Plain, onion_test_util::rand_commit()), fee: rand::thread_rng().next_u64(), - onion: rand_onion(), + onion: onion_test_util::rand_onion(), status, } } @@ -279,12 +278,12 @@ mod tests { SwapStatus::Unprocessed } else if s == 1 { SwapStatus::InProcess { - kernel_hash: rand_hash(), + kernel_hash: onion_test_util::rand_hash(), } } else { SwapStatus::Completed { - kernel_hash: rand_hash(), - block_hash: rand_hash(), + kernel_hash: onion_test_util::rand_hash(), + block_hash: onion_test_util::rand_hash(), } }; rand_swap_with_status(status) @@ -331,7 +330,7 @@ mod tests { assert!(store.swap_exists(&swap.input.commit)?); swap.status = SwapStatus::InProcess { - kernel_hash: rand_hash(), + kernel_hash: onion_test_util::rand_hash(), }; let result = store.save_swap(&swap, false); assert_eq!( diff --git a/src/tx.rs b/src/tx.rs index 7cd47a9..73ca7fd 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -167,6 +167,47 @@ fn add_kernel_and_collect_fees( }) } +/// Builds a transaction kernel for the Grin network. +/// +/// Transaction kernels are a critical part of the Grin transaction process. Each transaction contains a +/// kernel. It includes features chosen for this transaction, a fee chosen for this transaction, and +/// a proof that the total sum of outputs, transaction fees and block reward equals the total sum of inputs. +/// The `build_kernel` function handles this process, building the kernel and handling any potential errors. +/// +/// # Arguments +/// +/// * `excess`: A reference to a `SecretKey`. This key is used as an excess value for the transaction. +/// The excess is a kind of cryptographic proof that the total sum of outputs and fees equals the +/// total sum of inputs. +/// * `fee`: An unsigned 64-bit integer representing the transaction fee in nanogrin. This is the fee +/// that will be paid to the miner who mines the block containing this transaction. +/// +/// # Returns +/// +/// The function returns a `Result` enum with `TxKernel` as the Ok variant and `TxError` as the Err variant. +/// If the kernel is successfully built, it is returned as part of the Ok variant. If there is an error at any point +/// during the process, it is returned as part of the Err variant. +/// +/// # Errors +/// +/// This function can return several types of errors, all defined in the `TxError` enum. These include: +/// +/// * `KernelFeeError`: There was an error building the kernel's fee fields. +/// * `KernelExcessError`: There was an error computing the kernel's excess. +/// * `KernelSigMessageError`: There was an error computing the kernel's signature message. +/// * `KernelSigError`: There was an error signing the kernel. +/// * `KernelVerifyError`: The built kernel failed to verify. +/// +/// # Example +/// +/// ```rust +/// use secp256k1zkp::key::SecretKey; +/// use crate::crypto::secp; +/// +/// let secret_key = SecretKey::new(&mut secp::rand::thread_rng()); +/// let fee = 10; // 10 nanogrin +/// let kernel = build_kernel(&secret_key, fee); +/// ``` 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)?,