inter-server communication

This commit is contained in:
scilio 2023-01-07 16:09:04 -05:00
parent 9ec200f548
commit 76532c899e
23 changed files with 2756 additions and 1169 deletions

96
Cargo.lock generated
View file

@ -955,6 +955,21 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 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]] [[package]]
name = "futures" name = "futures"
version = "0.1.31" version = "0.1.31"
@ -1737,6 +1752,31 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 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]] [[package]]
name = "heck" name = "heck"
version = "0.3.3" version = "0.3.3"
@ -1897,6 +1937,24 @@ dependencies = [
"want", "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]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.20.0" version = "0.20.0"
@ -1956,6 +2014,19 @@ dependencies = [
"tokio-tls", "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]] [[package]]
name = "i18n-config" name = "i18n-config"
version = "0.4.2" version = "0.4.2"
@ -2478,6 +2549,7 @@ dependencies = [
"curve25519-dalek 2.1.3", "curve25519-dalek 2.1.3",
"dirs", "dirs",
"ed25519-dalek", "ed25519-dalek",
"function_name",
"futures 0.3.17", "futures 0.3.17",
"grin_api 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)", "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_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
@ -2493,6 +2565,7 @@ dependencies = [
"grin_wallet_util", "grin_wallet_util",
"hmac 0.12.0", "hmac 0.12.0",
"hyper 0.14.14", "hyper 0.14.14",
"hyper-proxy",
"itertools", "itertools",
"jsonrpc-core 18.0.0", "jsonrpc-core 18.0.0",
"jsonrpc-derive", "jsonrpc-derive",
@ -3280,7 +3353,7 @@ dependencies = [
"http-body 0.3.1", "http-body 0.3.1",
"hyper 0.13.10", "hyper 0.13.10",
"hyper-rustls 0.21.0", "hyper-rustls 0.21.0",
"hyper-tls", "hyper-tls 0.4.3",
"ipnet", "ipnet",
"js-sys", "js-sys",
"lazy_static", "lazy_static",
@ -3634,6 +3707,17 @@ dependencies = [
"yaml-rust 0.4.5", "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]] [[package]]
name = "sha2" name = "sha2"
version = "0.8.2" version = "0.8.2"
@ -4019,6 +4103,16 @@ dependencies = [
"syn 1.0.84", "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]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.13.1" version = "0.13.1"

View file

@ -14,9 +14,11 @@ clap = { version = "2.33", features = ["yaml"] }
curve25519-dalek = "2.1" curve25519-dalek = "2.1"
dirs = "2.0" dirs = "2.0"
ed25519-dalek = "1.0.1" ed25519-dalek = "1.0.1"
function_name = "0.3.0"
futures = "0.3" futures = "0.3"
hmac = { version = "0.12.0", features = ["std"]} hmac = { version = "0.12.0", features = ["std"]}
hyper = { version = "0.14", features = ["full"] } hyper = { version = "0.14", features = ["full"] }
hyper-proxy = "0.9.1"
itertools = { version = "0.10.3"} itertools = { version = "0.10.3"}
jsonrpc-core = "18.0" jsonrpc-core = "18.0"
jsonrpc-derive = "18.0" jsonrpc-derive = "18.0"

View file

@ -1,7 +1,11 @@
# MWixnet # MWixnet
This is an implementation of @tromp's [CoinSwap Proposal](https://forum.grin.mw/t/mimblewimble-coinswap-proposal/8322) with some slight modifications. 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 (node<sub>i</sub> with i=1...n) are agreed upon in advance. They each have a known public key. A set of n CoinSwap servers (N<sub>i</sub> with i=1...n) are agreed upon in advance. They each have a known public key.
We refer to the first server (N<sub>1</sub>) as the "Swap Server." This is the server that wallets can submit their coinswaps too.
We refer to the remaining servers (N<sub>2</sub>...N<sub>n</sub>) as "Mixers."
### Setup ### Setup
#### init-config #### 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. 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 ### SWAP API
The first CoinSwap server (n<sub>1</sub>) provides the `swap` API, publicly available for use by GRIN wallets. The Swap Server (N<sub>1</sub>) provides the `swap` API, which is publicly available for use by GRIN wallets.
**jsonrpc:** `2.0` **jsonrpc:** `2.0`
**method:** `swap` **method:** `swap`

View file

@ -38,6 +38,18 @@ args:
help: Address to bind the rpc server to (e.g. 127.0.0.1:3000) help: Address to bind the rpc server to (e.g. 127.0.0.1:3000)
long: bind_addr long: bind_addr
takes_value: true 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: subcommands:
- init-config: - init-config:
about: Writes a new configuration file about: Writes a new configuration file

171
src/client.rs Normal file
View file

@ -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<Onion>) -> Result<(Vec<usize>, 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<D: serde::de::DeserializeOwned>(
&self,
addr: &OnionV3Address,
method: &str,
params: &serde_json::Value,
) -> Result<D, ClientError> {
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<Onion>) -> Result<(Vec<usize>, 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<usize>, TxComponents)>(&self.addr, "mix", &params)
}
}
#[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<Onion>, (Vec<usize>, TxComponents)>,
}
impl MockMixClient {
pub fn new() -> MockMixClient {
MockMixClient {
results: HashMap::new(),
}
}
pub fn set_response(&mut self, onions: &Vec<Onion>, r: (Vec<usize>, TxComponents)) {
self.results.insert(onions.clone(), r);
}
}
impl MixClient for MockMixClient {
fn mix_outputs(
&self,
onions: &Vec<Onion>,
) -> Result<(Vec<usize>, 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<dyn MixServer>,
}
impl MixClient for DirectMixClient {
fn mix_outputs(
&self,
onions: &Vec<Onion>,
) -> Result<(Vec<usize>, 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())
}
}
}

View file

@ -1,8 +1,10 @@
use crate::secp::SecretKey; use crate::crypto::dalek::DalekPublicKey;
use crate::crypto::secp::SecretKey;
use core::num::NonZeroU32; use core::num::NonZeroU32;
use grin_core::global::ChainTypes; use grin_core::global::ChainTypes;
use grin_util::{file, ToHex, ZeroingString}; use grin_util::{file, ToHex, ZeroingString};
use grin_wallet_util::OnionV3Address;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use ring::{aead, pbkdf2}; use ring::{aead, pbkdf2};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
@ -26,6 +28,8 @@ pub struct ServerConfig {
pub interval_s: u32, pub interval_s: u32,
/// socket address the server listener should bind to /// socket address the server listener should bind to
pub addr: SocketAddr, pub addr: SocketAddr,
/// socket address the tor sender should bind to
pub socks_proxy_addr: SocketAddr,
/// foreign api address of the grin node /// foreign api address of the grin node
pub grin_node_url: SocketAddr, pub grin_node_url: SocketAddr,
/// path to file containing api secret for the grin node /// path to file containing api secret for the grin node
@ -34,9 +38,23 @@ pub struct ServerConfig {
pub wallet_owner_url: SocketAddr, pub wallet_owner_url: SocketAddr,
/// path to file containing secret for the grin wallet's owner api /// path to file containing secret for the grin wallet's owner api
pub wallet_owner_secret_path: Option<String>, pub wallet_owner_secret_path: Option<String>,
/// 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<DalekPublicKey>,
/// public key of the next mix server
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
pub next_server: Option<DalekPublicKey>,
} }
impl ServerConfig { 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<String> { pub fn node_api_secret(&self) -> Option<String> {
file::get_first_line(self.grin_node_secret_path.clone()) file::get_first_line(self.grin_node_secret_path.clone())
} }
@ -163,10 +181,15 @@ struct RawConfig {
nonce: String, nonce: String,
interval_s: u32, interval_s: u32,
addr: SocketAddr, addr: SocketAddr,
socks_proxy_addr: SocketAddr,
grin_node_url: SocketAddr, grin_node_url: SocketAddr,
grin_node_secret_path: Option<String>, grin_node_secret_path: Option<String>,
wallet_owner_url: SocketAddr, wallet_owner_url: SocketAddr,
wallet_owner_secret_path: Option<String>, wallet_owner_secret_path: Option<String>,
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
prev_server: Option<DalekPublicKey>,
#[serde(with = "crate::crypto::dalek::option_dalek_pubkey_serde", default)]
next_server: Option<DalekPublicKey>,
} }
/// Writes the server config to the config_path given, encrypting the server_key first. /// 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, nonce: encrypted.nonce,
interval_s: server_config.interval_s, interval_s: server_config.interval_s,
addr: server_config.addr, addr: server_config.addr,
socks_proxy_addr: server_config.socks_proxy_addr,
grin_node_url: server_config.grin_node_url, grin_node_url: server_config.grin_node_url,
grin_node_secret_path: server_config.grin_node_secret_path.clone(), grin_node_secret_path: server_config.grin_node_secret_path.clone(),
wallet_owner_url: server_config.wallet_owner_url, wallet_owner_url: server_config.wallet_owner_url,
wallet_owner_secret_path: server_config.wallet_owner_secret_path.clone(), 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 = let encoded: String =
toml::to_string(&raw_config).map_err(|e| ConfigError::EncodingError(e))?; toml::to_string(&raw_config).map_err(|e| ConfigError::EncodingError(e))?;
@ -203,10 +229,8 @@ pub fn load_config(
config_path: &PathBuf, config_path: &PathBuf,
password: &ZeroingString, password: &ZeroingString,
) -> Result<ServerConfig, ConfigError> { ) -> Result<ServerConfig, ConfigError> {
let contents = let contents = std::fs::read_to_string(config_path).map_err(ConfigError::ReadConfigError)?;
std::fs::read_to_string(config_path).map_err(|e| ConfigError::ReadConfigError(e))?; let raw_config: RawConfig = toml::from_str(&contents).map_err(ConfigError::DecodingError)?;
let raw_config: RawConfig =
toml::from_str(&contents).map_err(|e| ConfigError::DecodingError(e))?;
let encrypted_key = EncryptedServerKey { let encrypted_key = EncryptedServerKey {
encrypted_key: raw_config.encrypted_key, encrypted_key: raw_config.encrypted_key,
@ -219,10 +243,13 @@ pub fn load_config(
key: secret_key, key: secret_key,
interval_s: raw_config.interval_s, interval_s: raw_config.interval_s,
addr: raw_config.addr, addr: raw_config.addr,
socks_proxy_addr: raw_config.socks_proxy_addr,
grin_node_url: raw_config.grin_node_url, grin_node_url: raw_config.grin_node_url,
grin_node_secret_path: raw_config.grin_node_secret_path, grin_node_secret_path: raw_config.grin_node_secret_path,
wallet_owner_url: raw_config.wallet_owner_url, wallet_owner_url: raw_config.wallet_owner_url,
wallet_owner_secret_path: raw_config.wallet_owner_secret_path, 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() "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<DalekPublicKey>,
next_server: &Option<DalekPublicKey>,
) -> Result<ServerConfig, Box<dyn std::error::Error>> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::secp; use crate::crypto::secp;
#[test] #[test]
fn server_key_encrypt() { fn server_key_encrypt() {

View file

@ -1,12 +1,4 @@
pub use secp256k1zkp::aggsig; use crate::crypto::secp::{self, Commitment, ContextFlag, Secp256k1, SecretKey};
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 blake2::blake2b::Blake2b;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
@ -151,8 +143,8 @@ pub mod comsig_serde {
impl Readable for ComSignature { impl Readable for ComSignature {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> { fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let R = Commitment::read(reader)?; let R = Commitment::read(reader)?;
let s = read_secret_key(reader)?; let s = secp::read_secret_key(reader)?;
let t = read_secret_key(reader)?; let t = secp::read_secret_key(reader)?;
Ok(ComSignature::new(&R, &s, &t)) Ok(ComSignature::new(&R, &s, &t))
} }
} }
@ -166,84 +158,6 @@ impl Writeable for ComSignature {
} }
} }
/// 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<R: Reader>(reader: &mut R) -> Result<SecretKey, ser::Error> {
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<Commitment, secp256k1zkp::Error> {
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<Commitment, secp256k1zkp::Error> {
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<Commitment, secp256k1zkp::Error> {
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<Signature, secp256k1zkp::Error> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey};

285
src/crypto/dalek.rs Normal file
View file

@ -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<Self, DalekError> {
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<PublicKey> for DalekPublicKey {
fn as_ref(&self) -> &PublicKey {
&self.0
}
}
/// Serializes an Option<DalekPublicKey> 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<S>(pk: &Option<DalekPublicKey>, serializer: S) -> Result<S::Ok, S::Error>
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<Option<DalekPublicKey>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::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<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?)
.map_err(|_| ser::Error::CorruptedData)?;
Ok(DalekPublicKey(pk))
}
}
impl Writeable for DalekPublicKey {
fn write<W: Writer>(&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<Self, DalekError> {
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<Signature> 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<S>(sig: &DalekSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&sig.0.to_hex())
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<DalekSignature, D::Error>
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<DalekSignature, DalekError> {
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<DalekPublicKey>,
}
#[test]
fn pubkey_test() -> Result<(), Box<dyn std::error::Error>> {
// 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::<TestPubKeySerde>("{}").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<dyn std::error::Error>> {
// 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(())
}
}

3
src/crypto/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod comsig;
pub mod dalek;
pub mod secp;

117
src/crypto/secp.rs Normal file
View file

@ -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<R: Reader>(reader: &mut R) -> Result<SecretKey, ser::Error> {
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<Commitment, secp256k1zkp::Error> {
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<Commitment, secp256k1zkp::Error> {
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<Commitment, secp256k1zkp::Error> {
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<Signature, secp256k1zkp::Error> {
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)
}
}

View file

@ -1,18 +1,16 @@
use config::ServerConfig; use config::ServerConfig;
use node::HttpGrinNode; use node::HttpGrinNode;
use std::collections::HashMap;
use store::SwapStore; use store::SwapStore;
use wallet::HttpWallet; use wallet::HttpWallet;
use crate::client::{MixClient, MixClientImpl};
use crate::crypto::dalek::DalekPublicKey;
use crate::node::GrinNode; use crate::node::GrinNode;
use crate::store::StoreError; use crate::store::StoreError;
use clap::App; use clap::App;
use grin_core::global; use grin_core::global;
use grin_core::global::ChainTypes; use grin_core::global::ChainTypes;
use grin_util::{StopState, ZeroingString}; 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 rpassword;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@ -21,20 +19,22 @@ use tokio::runtime::Runtime;
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
mod client;
mod config; mod config;
mod crypto;
mod node; mod node;
mod onion; mod onion;
mod rpc; mod servers;
mod secp;
mod server;
mod store; mod store;
mod tor;
mod tx;
mod types; mod types;
mod wallet; mod wallet;
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
real_main().unwrap(); real_main()?;
std::process::exit(0); std::process::exit(0);
} }
@ -61,10 +61,17 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
.value_of("round_time") .value_of("round_time")
.map(|t| t.parse::<u32>().unwrap()); .map(|t| t.parse::<u32>().unwrap());
let bind_addr = args.value_of("bind_addr"); 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_url = args.value_of("grin_node_url");
let grin_node_secret_path = args.value_of("grin_node_secret_path"); 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_url = args.value_of("wallet_owner_url");
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path"); 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 // Write a new config file if init-config command is supplied
if let ("init-config", Some(_)) = args.subcommand() { if let ("init-config", Some(_)) = args.subcommand() {
@ -76,9 +83,10 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
} }
let server_config = ServerConfig { let server_config = ServerConfig {
key: secp::random_secret(), key: crypto::secp::random_secret(),
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL), interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?, 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 { grin_node_url: match grin_node_url {
Some(u) => u.parse()?, Some(u) => u.parse()?,
None => config::grin_node_url(&chain_type), None => config::grin_node_url(&chain_type),
@ -99,6 +107,8 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
.to_str() .to_str()
.map(|p| p.to_owned()), .map(|p| p.to_owned()),
}, },
prev_server,
next_server,
}; };
let password = prompt_password_confirm(); let password = prompt_password_confirm();
@ -113,11 +123,6 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
let password = prompt_password(); let password = prompt_password();
let mut server_config = config::load_config(&config_path, &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 // Override grin_node_url, if supplied
if let Some(grin_node_url) = grin_node_url { if let Some(grin_node_url) = grin_node_url {
server_config.grin_node_url = grin_node_url.parse()?; server_config.grin_node_url = grin_node_url.parse()?;
@ -138,6 +143,26 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned()); 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 // Create GrinNode
let node = HttpGrinNode::new( let node = HttpGrinNode::new(
&server_config.grin_node_url, &server_config.grin_node_url,
@ -165,17 +190,7 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
} }
}; };
// Open SwapStore let mut tor_process = tor::init_tor_listener(&server_config)?;
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 stop_state = Arc::new(StopState::new()); let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone(); let stop_state_clone = stop_state.clone();
@ -187,14 +202,52 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
stop_state_clone.stop(); stop_state_clone.stop();
}); });
// Start the mwixnet JSON-RPC HTTP server let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
rpc::listen( let client: Arc<dyn MixClient> =
server_config, Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
Arc::new(wallet), client
Arc::new(node), });
store,
stop_state, 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)] #[cfg(unix)]
@ -245,39 +298,3 @@ fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
} }
} }
} }
fn init_tor_listener(
server_config: &ServerConfig,
) -> Result<tor_process::TorProcess, Box<dyn std::error::Error>> {
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)
}

View file

@ -1,4 +1,4 @@
use crate::secp::Commitment; use crate::crypto::secp::Commitment;
use grin_api::client; use grin_api::client;
use grin_api::json_rpc::{build_request, Request, Response}; use grin_api::json_rpc::{build_request, Request, Response};
@ -43,10 +43,10 @@ pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool,
/// Checks whether a commitment is spendable at the block height provided /// Checks whether a commitment is spendable at the block height provided
pub fn is_spendable( pub fn is_spendable(
node: &Arc<dyn GrinNode>, node: &Arc<dyn GrinNode>,
output_commit: &Commitment, commit: &Commitment,
next_block_height: u64, next_block_height: u64,
) -> Result<bool, NodeError> { ) -> Result<bool, NodeError> {
let output = node.get_utxo(&output_commit)?; let output = node.get_utxo(&commit)?;
if let Some(out) = output { if let Some(out) = output {
let is_coinbase = match out.output_type { let is_coinbase = match out.output_type {
OutputType::Coinbase => true, OutputType::Coinbase => true,
@ -166,7 +166,7 @@ impl GrinNode for HttpGrinNode {
#[cfg(test)] #[cfg(test)]
pub mod mock { pub mod mock {
use super::{GrinNode, NodeError}; use super::{GrinNode, NodeError};
use crate::secp::Commitment; use crate::crypto::secp::Commitment;
use grin_api::{OutputPrintable, OutputType}; use grin_api::{OutputPrintable, OutputType};
use grin_core::core::Transaction; use grin_core::core::Transaction;
@ -181,13 +181,24 @@ pub mod mock {
} }
impl MockGrinNode { impl MockGrinNode {
pub fn new() -> MockGrinNode { pub fn new() -> Self {
MockGrinNode { MockGrinNode {
utxos: HashMap::new(), utxos: HashMap::new(),
txns_posted: RwLock::new(Vec::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) { pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) {
self.utxos.insert(output_commit.clone(), utxo.clone()); self.utxos.insert(output_commit.clone(), utxo.clone());
} }

View file

@ -1,4 +1,4 @@
use crate::secp::{self, Commitment, SecretKey}; use crate::crypto::secp::{self, Commitment, SecretKey};
use crate::types::Payload; use crate::types::Payload;
use crate::onion::OnionError::{InvalidKeyLength, SerializationError}; use crate::onion::OnionError::{InvalidKeyLength, SerializationError};
@ -57,6 +57,15 @@ fn vec_to_32_byte_arr(v: Vec<u8>) -> Result<[u8; 32], OnionError> {
v.try_into().map_err(|_| InvalidKeyLength) 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 { impl Onion {
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> { pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
let mut vec = vec![]; 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 /// 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<PeeledOnion, OnionError> {
let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey); let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey);
let mut cipher = new_stream_cipher(&shared_secret)?; let mut cipher = new_stream_cipher(&shared_secret)?;
@ -106,7 +115,10 @@ impl Onion {
commit: commitment.clone(), commit: commitment.clone(),
enc_payloads, enc_payloads,
}; };
Ok((decrypted_payload, peeled_onion)) Ok(PeeledOnion {
payload: decrypted_payload,
onion: peeled_onion,
})
} }
} }
@ -292,13 +304,14 @@ impl From<ser::Error> for OnionError {
#[cfg(test)] #[cfg(test)]
pub mod test_util { pub mod test_util {
use super::{Onion, OnionError, RawBytes}; use super::{Onion, OnionError, RawBytes};
use crate::secp::test_util::{rand_commit, rand_proof}; use crate::crypto::secp::test_util::{rand_commit, rand_proof};
use crate::secp::{random_secret, Commitment, SecretKey}; use crate::crypto::secp::{random_secret, Commitment, SecretKey};
use crate::types::Payload; use crate::types::Payload;
use chacha20::cipher::StreamCipher; use chacha20::cipher::StreamCipher;
use grin_core::core::FeeFields; use grin_core::core::FeeFields;
use rand::{thread_rng, RngCore}; use rand::{thread_rng, RngCore};
use secp256k1zkp::pedersen::RangeProof;
use x25519_dalek::PublicKey as xPublicKey; use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::{SharedSecret, StaticSecret}; use x25519_dalek::{SharedSecret, StaticSecret};
@ -307,6 +320,23 @@ pub mod test_util {
pub pubkey: xPublicKey, pub pubkey: xPublicKey,
pub payload: Payload, pub payload: Payload,
} }
pub fn new_hop(
server_key: &SecretKey,
hop_excess: &SecretKey,
fee: u64,
proof: Option<RangeProof>,
) -> Hop {
Hop {
pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
payload: Payload {
excess: hop_excess.clone(),
fee: FeeFields::from(fee as u32),
rangeproof: proof,
},
}
}
/* /*
Choose random xi for each node ni and create a Payload (Pi) for each containing xi 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 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)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::test_util::{self, Hop}; use super::test_util::{self, Hop};
use crate::secp; use crate::crypto::secp;
use crate::types::Payload; use crate::types::Payload;
use grin_core::core::FeeFields; use grin_core::core::FeeFields;
@ -454,8 +484,8 @@ pub mod tests {
}; };
for i in 0..5 { for i in 0..5 {
let peeled = onion_packet.peel_layer(&keys[i]).unwrap(); let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
payload = peeled.0; payload = peeled.payload;
onion_packet = peeled.1; onion_packet = peeled.onion;
} }
assert!(payload.rangeproof.is_some()); assert!(payload.rangeproof.is_some());

409
src/servers/mix.rs Normal file
View file

@ -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<Onion>,
sig: &DalekSignature,
) -> Result<(Vec<usize>, TxComponents), MixError>;
}
/// The standard MWixnet "Mixer" implementation
#[derive(Clone)]
pub struct MixServerImpl {
secp: Secp256k1,
server_config: ServerConfig,
mix_client: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
}
impl MixServerImpl {
/// Create a new 'Mix' server
pub fn new(
server_config: ServerConfig,
mix_client: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
) -> 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<PeeledOnion, MixError> {
// 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<usize>, 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<Output> = 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<usize>, 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<usize, usize> =
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<Onion>,
sig: &DalekSignature,
) -> Result<(Vec<usize>, 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<dyn MixClient>)>,
node: &Arc<MockGrinNode>,
) -> (Arc<DirectMixClient>, Arc<MockWallet>) {
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<dyn std::error::Error>> {
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<Commitment> = 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(())
}
}

116
src/servers/mix_rpc.rs Normal file
View file

@ -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<Onion>,
#[serde(with = "dalek::dalek_sig_serde")]
sig: DalekSignature,
}
impl MixReq {
pub fn new(onions: Vec<Onion>, sig: DalekSignature) -> Self {
MixReq { onions, sig }
}
}
#[rpc(server)]
pub trait MixAPI {
#[rpc(name = "mix")]
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value>;
}
#[derive(Clone)]
struct RPCMixServer {
server_config: ServerConfig,
server: Arc<Mutex<dyn MixServer>>,
}
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<hyper::Body>| {
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<MixError> 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<jsonrpc::Value> {
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<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
stop_state: Arc<StopState>,
) -> Result<(), Box<dyn std::error::Error>> {
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(())
}

4
src/servers/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod mix;
pub mod mix_rpc;
pub mod swap;
pub mod swap_rpc;

View file

@ -1,14 +1,19 @@
use crate::config::ServerConfig; use crate::config::ServerConfig;
use crate::crypto::comsig::ComSignature;
use crate::crypto::secp::{Commitment, Secp256k1, SecretKey};
use crate::node::{self, GrinNode}; use crate::node::{self, GrinNode};
use crate::onion::{Onion, OnionError}; use crate::onion::{Onion, OnionError};
use crate::secp::{ComSignature, Commitment, Secp256k1, SecretKey};
use crate::store::{StoreError, SwapData, SwapStatus, SwapStore}; use crate::store::{StoreError, SwapData, SwapStatus, SwapStore};
use crate::wallet::{self, Wallet}; 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::hash::Hashed;
use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody}; use grin_core::core::{Input, Output, OutputFeatures, Transaction, TransactionBody};
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
use itertools::Itertools; use itertools::Itertools;
use secp256k1zkp::key::ZERO_KEY;
use std::result::Result; use std::result::Result;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use thiserror::Error; use thiserror::Error;
@ -16,8 +21,8 @@ use thiserror::Error;
/// Swap error types /// Swap error types
#[derive(Clone, Error, Debug, PartialEq)] #[derive(Clone, Error, Debug, PartialEq)]
pub enum SwapError { pub enum SwapError {
#[error("Invalid number of payloads provided (expected {expected:?}, found {found:?})")] #[error("Invalid number of payloads provided")]
InvalidPayloadLength { expected: usize, found: usize }, InvalidPayloadLength,
#[error("Commitment Signature is invalid")] #[error("Commitment Signature is invalid")]
InvalidComSignature, InvalidComSignature,
#[error("Rangeproof is invalid")] #[error("Rangeproof is invalid")]
@ -34,41 +39,44 @@ pub enum SwapError {
FeeTooLow { minimum_fee: u64, actual_fee: u64 }, FeeTooLow { minimum_fee: u64, actual_fee: u64 },
#[error("Error saving swap to data store: {0}")] #[error("Error saving swap to data store: {0}")]
StoreError(StoreError), StoreError(StoreError),
#[error("Client communication error: {0:?}")]
ClientError(String),
#[error("{0}")] #[error("{0}")]
UnknownError(String), UnknownError(String),
} }
/// A MWixnet server /// A public MWixnet server - the "Swap Server"
pub trait Server: Send + Sync { pub trait SwapServer: Send + Sync {
/// Submit a new output to be swapped. /// Submit a new output to be swapped.
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>; fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError>;
/// Iterate through all saved submissions, filter out any inputs that are no longer spendable, /// 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. /// and assemble the coinswap transaction, posting the transaction to the configured node.
///
/// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes.
fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>>; fn execute_round(&self) -> Result<Option<Transaction>, Box<dyn std::error::Error>>;
} }
/// The standard MWixnet server implementation /// The standard MWixnet server implementation
#[derive(Clone)] #[derive(Clone)]
pub struct ServerImpl { pub struct SwapServerImpl {
server_config: ServerConfig, server_config: ServerConfig,
next_server: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>, wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>, node: Arc<dyn GrinNode>,
store: Arc<Mutex<SwapStore>>, store: Arc<Mutex<SwapStore>>,
} }
impl ServerImpl { impl SwapServerImpl {
/// Create a new MWixnet server /// Create a new MWixnet server
pub fn new( pub fn new(
server_config: ServerConfig, server_config: ServerConfig,
next_server: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>, wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>, node: Arc<dyn GrinNode>,
store: SwapStore, store: SwapStore,
) -> Self { ) -> Self {
ServerImpl { SwapServerImpl {
server_config, server_config,
next_server,
wallet, wallet,
node, node,
store: Arc::new(Mutex::new(store)), store: Arc::new(Mutex::new(store)),
@ -81,20 +89,20 @@ impl ServerImpl {
} }
/// Minimum fee to perform a swap. /// Minimum fee to perform a swap.
/// Requires enough fee for the mwixnet server's kernel, 1 input and its output to swap. /// Requires enough fee for the swap server's kernel, 1 input and its output to swap.
fn get_minimum_swap_fee(&self) -> u64 { fn get_minimum_swap_fee(&self) -> u64 {
TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base() TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base()
} }
} }
impl Server for ServerImpl { impl SwapServer for SwapServerImpl {
fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> { fn swap(&self, onion: &Onion, comsig: &ComSignature) -> Result<(), SwapError> {
// milestone 3: check that enc_payloads length matches number of configured servers // Verify that more than 1 payload exists when there's a next server,
if onion.enc_payloads.len() != 1 { // or that exactly 1 payload exists when this is the final server
return Err(SwapError::InvalidPayloadLength { if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1
expected: 1, || self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1
found: onion.enc_payloads.len(), {
}); return Err(SwapError::InvalidPayloadLength);
} }
// Verify commitment signature to ensure caller owns the output // Verify commitment signature to ensure caller owns the output
@ -112,12 +120,13 @@ impl Server for ServerImpl {
commit: onion.commit.clone(), commit: onion.commit.clone(),
})?; })?;
// Peel off top layer of encryption
let peeled = onion let peeled = onion
.peel_layer(&self.server_config.key) .peel_layer(&self.server_config.key)
.map_err(|e| SwapError::PeelOnionFailure(e))?; .map_err(|e| SwapError::PeelOnionFailure(e))?;
// Verify the fee meets the minimum // Verify the fee meets the minimum
let fee: u64 = peeled.0.fee.into(); let fee: u64 = peeled.payload.fee.into();
if fee < self.get_minimum_swap_fee() { if fee < self.get_minimum_swap_fee() {
return Err(SwapError::FeeTooLow { return Err(SwapError::FeeTooLow {
minimum_fee: self.get_minimum_swap_fee(), minimum_fee: self.get_minimum_swap_fee(),
@ -125,13 +134,13 @@ impl Server for ServerImpl {
}); });
} }
// Verify the bullet proof and build the final output // Verify the rangeproof
if let Some(r) = peeled.0.rangeproof { if let Some(r) = peeled.payload.rangeproof {
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
secp.verify_bullet_proof(peeled.1.commit, r, None) secp.verify_bullet_proof(peeled.onion.commit, r, None)
.map_err(|_| SwapError::InvalidRangeproof)?; .map_err(|_| SwapError::InvalidRangeproof)?;
} else { } else if peeled.onion.enc_payloads.is_empty() {
// milestone 3: only the last hop will have a rangeproof // A rangeproof is required in the last payload
return Err(SwapError::MissingRangeproof); return Err(SwapError::MissingRangeproof);
} }
@ -140,12 +149,12 @@ impl Server for ServerImpl {
locked locked
.save_swap( .save_swap(
&SwapData { &SwapData {
excess: peeled.0.excess, excess: peeled.payload.excess,
output_commit: peeled.1.commit, output_commit: peeled.onion.commit,
rangeproof: peeled.0.rangeproof, rangeproof: peeled.payload.rangeproof,
input, input,
fee, fee,
onion: peeled.1, onion: peeled.onion,
status: SwapStatus::Unprocessed, status: SwapStatus::Unprocessed,
}, },
false, false,
@ -174,72 +183,103 @@ impl Server for ServerImpl {
node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false) 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)) .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(); .collect();
if spendable.len() == 0 { if spendable.len() == 0 {
return Ok(None); return Ok(None);
} }
let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum(); 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()))?;
let inputs: Vec<Input> = spendable.iter().enumerate().map(|(_, s)| s.input).collect(); // 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 outputs: Vec<Output> = spendable let failed = spendable
.iter() .iter()
.enumerate() .enumerate()
.map(|(_, s)| { .filter(|(i, _)| !kept_indices.contains(i))
Output::new( .map(|(_, j)| j.clone())
OutputFeatures::Plain, .collect();
s.output_commit,
s.rangeproof.unwrap(),
)
})
.collect();
let excesses: Vec<SecretKey> = spendable (filtered, failed, mixed.offset, mixed.outputs, mixed.kernels)
.iter() } else {
.enumerate() // Build plain outputs for each swap entry
.map(|(_, s)| s.excess.clone()) let outputs: Vec<Output> = spendable
.collect(); .iter()
.map(|s| {
Output::new(
OutputFeatures::Plain,
s.output_commit,
s.rangeproof.unwrap(),
)
})
.collect();
let tx = wallet::assemble_tx( (spendable, Vec::new(), ZERO_KEY, outputs, Vec::new())
};
let fees_paid: u64 = filtered.iter().map(|s| s.fee).sum();
let inputs: Vec<Input> = filtered.iter().map(|s| s.input).collect();
let output_excesses: Vec<SecretKey> = filtered.iter().map(|s| s.excess.clone()).collect();
let tx = tx::assemble_tx(
&self.wallet, &self.wallet,
&inputs, &inputs,
&outputs, &outputs,
&kernels,
self.get_fee_base(), self.get_fee_base(),
total_fee, fees_paid,
&excesses, &offset,
&output_excesses,
)?; )?;
self.node.post_tx(&tx)?; self.node.post_tx(&tx)?;
// Update status to in process // Update status to in process
let kernel_hash = tx.kernels().first().unwrap().hash(); let kernel_hash = tx.kernels().first().unwrap().hash();
for mut swap in spendable { for mut swap in filtered {
swap.status = SwapStatus::InProcess { kernel_hash }; swap.status = SwapStatus::InProcess { kernel_hash };
locked_store.save_swap(&swap, true)?; 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)) Ok(Some(tx))
} }
} }
#[cfg(test)] #[cfg(test)]
pub mod mock { pub mod mock {
use super::{Server, SwapError}; use super::{SwapError, SwapServer};
use crate::crypto::comsig::ComSignature;
use crate::onion::Onion; use crate::onion::Onion;
use crate::secp::ComSignature;
use grin_core::core::Transaction; use grin_core::core::Transaction;
use std::collections::HashMap; use std::collections::HashMap;
pub struct MockServer { pub struct MockSwapServer {
errors: HashMap<Onion, SwapError>, errors: HashMap<Onion, SwapError>,
} }
impl MockServer { impl MockSwapServer {
pub fn new() -> MockServer { pub fn new() -> MockSwapServer {
MockServer { MockSwapServer {
errors: HashMap::new(), errors: HashMap::new(),
} }
} }
@ -249,7 +289,7 @@ pub mod mock {
} }
} }
impl Server for MockServer { impl SwapServer for MockSwapServer {
fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> { fn swap(&self, onion: &Onion, _comsig: &ComSignature) -> Result<(), SwapError> {
if let Some(e) = self.errors.get(&onion) { if let Some(e) = self.errors.get(&onion) {
return Err(e.clone()); return Err(e.clone());
@ -264,25 +304,57 @@ pub mod mock {
} }
} }
#[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<dyn MixClient>)>,
node: Arc<dyn GrinNode>,
) -> (Arc<SwapServerImpl>, Arc<MockWallet>) {
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)] #[cfg(test)]
mod tests { mod tests {
use crate::config::ServerConfig; use crate::crypto::comsig::ComSignature;
use crate::crypto::dalek;
use crate::crypto::secp;
use crate::node::mock::MockGrinNode; use crate::node::mock::MockGrinNode;
use crate::onion::test_util::{self, Hop}; use crate::onion::test_util::{self, Hop};
use crate::onion::Onion; use crate::onion::Onion;
use crate::secp::{self, ComSignature, Commitment, RangeProof, Secp256k1, SecretKey}; use crate::servers::swap::{SwapError, SwapServer};
use crate::server::{Server, ServerImpl, SwapError}; use crate::store::{SwapData, SwapStatus};
use crate::store::{SwapData, SwapStatus, SwapStore}; use crate::tx::TxComponents;
use crate::types::Payload; use crate::{client, tx, MixClient};
use crate::wallet::mock::MockWallet;
use ::function_name::named;
use grin_core::core::hash::Hashed; use grin_core::core::hash::Hashed;
use grin_core::core::{Committed, FeeFields, Input, OutputFeatures, Transaction, Weighting}; use grin_core::core::{Committed, Input, Output, OutputFeatures, Transaction, Weighting};
use grin_core::global::{self, ChainTypes}; use secp256k1zkp::key::ZERO_KEY;
use std::net::TcpListener;
use std::sync::Arc; use std::sync::Arc;
use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::StaticSecret;
macro_rules! assert_error_type { macro_rules! assert_error_type {
($result:expr, $error_type:pat) => { ($result:expr, $error_type:pat) => {
@ -295,75 +367,23 @@ mod tests {
}; };
} }
fn new_server( macro_rules! init_test {
test_name: &str, () => {{
server_key: &SecretKey, grin_core::global::set_local_chain_type(
utxos: &Vec<&Commitment>, grin_core::global::ChainTypes::AutomatedTesting,
) -> (ServerImpl, Arc<MockGrinNode>) { );
global::set_local_chain_type(ChainTypes::AutomatedTesting); let test_dir = concat!("./target/tmp/.", function_name!());
let db_root = format!("./target/tmp/.{}", test_name); let _ = std::fs::remove_dir_all(test_dir);
let _ = std::fs::remove_dir_all(db_root.as_str()); test_dir
}};
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 { /// Standalone swap server to demonstrate request validation and onion unwrapping.
let secp = Secp256k1::new();
let nonce = secp::random_secret();
let mut blind = input_blind.clone();
blind.add_assign(&secp, &hop_excess).unwrap();
secp.bullet_proof(
value - fee,
blind.clone(),
nonce.clone(),
nonce.clone(),
None,
None,
)
}
fn new_hop(
server_key: &SecretKey,
hop_excess: &SecretKey,
fee: u64,
proof: Option<RangeProof>,
) -> Hop {
Hop {
pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
payload: Payload {
excess: hop_excess.clone(),
fee: FeeFields::from(fee as u32),
rangeproof: proof,
},
}
}
/// Single hop to demonstrate request validation and onion unwrapping.
#[test] #[test]
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> { #[named]
fn swap_standalone() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -371,19 +391,17 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (output_commit, proof) = secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]);
let hop = new_hop(&server_key, &hop_excess, fee, Some(proof)); 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])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, node) = new_server("swap_lifecycle", &server_key, &vec![&input_commit]); let node: Arc<MockGrinNode> = 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)?; server.swap(&onion, &comsig)?;
// Make sure entry is added to server. // 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 { let expected = SwapData {
excess: hop_excess.clone(), excess: hop_excess.clone(),
output_commit: output_commit.clone(), output_commit: output_commit.clone(),
@ -431,9 +449,88 @@ mod tests {
Ok(()) Ok(())
} }
/// Multi-server test to verify proper MixClient communication.
#[test]
#[named]
fn swap_multiserver() -> Result<(), Box<dyn std::error::Error>> {
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<MockGrinNode> = 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<dyn MixClient> = 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. /// Returns InvalidPayloadLength when too many payloads are provided.
#[test] #[test]
#[named]
fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> { fn swap_too_many_payloads() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -441,23 +538,18 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (_output_commit, proof) =
let hop = new_hop(&server_key, &hop_excess, fee, Some(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<Hop> = vec![hop.clone(), hop.clone()]; // Multiple payloads let hops: Vec<Hop> = vec![hop.clone(), hop.clone()]; // Multiple payloads
let onion = test_util::create_onion(&input_commit, &hops)?; let onion = test_util::create_onion(&input_commit, &hops)?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
new_server("swap_too_many_payloads", &server_key, &vec![&input_commit]); let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
let result = server.swap(&onion, &comsig); let result = server.swap(&onion, &comsig);
assert_eq!( assert_eq!(Err(SwapError::InvalidPayloadLength), result);
Err(SwapError::InvalidPayloadLength {
expected: 1,
found: 2
}),
result
);
// Make sure no entry is added to the store // Make sure no entry is added to the store
assert_eq!( assert_eq!(
@ -470,7 +562,10 @@ mod tests {
/// Returns InvalidComSignature when ComSignature fails to verify. /// Returns InvalidComSignature when ComSignature fails to verify.
#[test] #[test]
#[named]
fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> { fn swap_invalid_com_signature() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -478,19 +573,17 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (_output_commit, proof) =
let hop = new_hop(&server_key, &hop_excess, fee, Some(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])?;
let wrong_blind = secp::random_secret(); let wrong_blind = secp::random_secret();
let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &wrong_blind, &onion.serialize()?)?;
let (server, _node) = new_server( let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
"swap_invalid_com_signature", let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
&server_key,
&vec![&input_commit],
);
let result = server.swap(&onion, &comsig); let result = server.swap(&onion, &comsig);
assert_eq!(Err(SwapError::InvalidComSignature), result); assert_eq!(Err(SwapError::InvalidComSignature), result);
@ -505,7 +598,10 @@ mod tests {
/// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment. /// Returns InvalidRangeProof when the rangeproof fails to verify for the commitment.
#[test] #[test]
#[named]
fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> { fn swap_invalid_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -514,14 +610,15 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let wrong_value = value + 10_000_000; let wrong_value = value + 10_000_000;
let proof = proof(wrong_value, fee, &blind, &hop_excess); let (_output_commit, proof) =
let hop = new_hop(&server_key, &hop_excess, fee, Some(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 onion = test_util::create_onion(&input_commit, &vec![hop])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
new_server("swap_invalid_rangeproof", &server_key, &vec![&input_commit]); let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
let result = server.swap(&onion, &comsig); let result = server.swap(&onion, &comsig);
assert_eq!(Err(SwapError::InvalidRangeproof), result); assert_eq!(Err(SwapError::InvalidRangeproof), result);
@ -536,7 +633,10 @@ mod tests {
/// Returns MissingRangeproof when no rangeproof is provided. /// Returns MissingRangeproof when no rangeproof is provided.
#[test] #[test]
#[named]
fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> { fn swap_missing_rangeproof() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -544,13 +644,13 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let hop = new_hop(&server_key, &hop_excess, fee, None); let hop = test_util::new_hop(&server_key, &hop_excess, fee, None);
let onion = test_util::create_onion(&input_commit, &vec![hop])?; let onion = test_util::create_onion(&input_commit, &vec![hop])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
new_server("swap_missing_rangeproof", &server_key, &vec![&input_commit]); let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
let result = server.swap(&onion, &comsig); let result = server.swap(&onion, &comsig);
assert_eq!(Err(SwapError::MissingRangeproof), result); assert_eq!(Err(SwapError::MissingRangeproof), result);
@ -565,7 +665,10 @@ mod tests {
/// Returns CoinNotFound when there's no matching output in the UTXO set. /// Returns CoinNotFound when there's no matching output in the UTXO set.
#[test] #[test]
#[named]
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> { fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -573,13 +676,15 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (_output_commit, proof) =
let hop = new_hop(&server_key, &hop_excess, fee, Some(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])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = new_server("swap_utxo_missing", &server_key, &vec![]); let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new());
let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
let result = server.swap(&onion, &comsig); let result = server.swap(&onion, &comsig);
assert_eq!( assert_eq!(
Err(SwapError::CoinNotFound { Err(SwapError::CoinNotFound {
@ -599,7 +704,10 @@ mod tests {
/// Returns AlreadySwapped when trying to swap the same commitment multiple times. /// Returns AlreadySwapped when trying to swap the same commitment multiple times.
#[test] #[test]
#[named]
fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> { fn swap_already_swapped() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -607,13 +715,15 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (_output_commit, proof) =
let hop = new_hop(&server_key, &hop_excess, fee, Some(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])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = new_server("swap_already_swapped", &server_key, &vec![&input_commit]); let node: Arc<MockGrinNode> = 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)?; server.swap(&onion, &comsig)?;
// Call swap a second time // Call swap a second time
@ -630,7 +740,10 @@ mod tests {
/// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload. /// Returns PeelOnionFailure when a failure occurs trying to decrypt the onion payload.
#[test] #[test]
#[named]
fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> { fn swap_peel_onion_failure() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 50_000_000; let fee: u64 = 50_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -638,16 +751,17 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (_output_commit, proof) =
secp::test_util::proof(value, fee, &blind, &vec![&hop_excess]);
let wrong_server_key = secp::random_secret(); let wrong_server_key = secp::random_secret();
let hop = new_hop(&wrong_server_key, &hop_excess, fee, Some(proof)); 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 onion = test_util::create_onion(&input_commit, &vec![hop])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = let node: Arc<MockGrinNode> = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
new_server("swap_peel_onion_failure", &server_key, &vec![&input_commit]); let (server, _) = super::test_util::new_swapper(&test_dir, &server_key, None, node.clone());
let result = server.swap(&onion, &comsig); let result = server.swap(&onion, &comsig);
assert!(result.is_err()); assert!(result.is_err());
@ -658,7 +772,10 @@ mod tests {
/// Returns FeeTooLow when the minimum fee is not met. /// Returns FeeTooLow when the minimum fee is not met.
#[test] #[test]
#[named]
fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> { fn swap_fee_too_low() -> Result<(), Box<dyn std::error::Error>> {
let test_dir = init_test!();
let value: u64 = 200_000_000; let value: u64 = 200_000_000;
let fee: u64 = 1_000_000; let fee: u64 = 1_000_000;
let blind = secp::random_secret(); let blind = secp::random_secret();
@ -666,13 +783,15 @@ mod tests {
let server_key = secp::random_secret(); let server_key = secp::random_secret();
let hop_excess = secp::random_secret(); let hop_excess = secp::random_secret();
let proof = proof(value, fee, &blind, &hop_excess); let (_output_commit, proof) =
let hop = new_hop(&server_key, &hop_excess, fee, Some(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])?;
let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?; let comsig = ComSignature::sign(value, &blind, &onion.serialize()?)?;
let (server, _node) = new_server("swap_fee_too_low", &server_key, &vec![&input_commit]); let node: Arc<MockGrinNode> = 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); let result = server.swap(&onion, &comsig);
assert_eq!( assert_eq!(
Err(SwapError::FeeTooLow { Err(SwapError::FeeTooLow {

View file

@ -1,11 +1,12 @@
use crate::config::ServerConfig; use crate::config::ServerConfig;
use crate::crypto::comsig::{self, ComSignature};
use crate::node::GrinNode; use crate::node::GrinNode;
use crate::onion::Onion; use crate::onion::Onion;
use crate::secp::{self, ComSignature}; use crate::servers::swap::{SwapError, SwapServer, SwapServerImpl};
use crate::server::{Server, ServerImpl, SwapError};
use crate::store::SwapStore; use crate::store::SwapStore;
use crate::wallet::Wallet; use crate::wallet::Wallet;
use crate::client::MixClient;
use grin_util::StopState; use grin_util::StopState;
use jsonrpc_core::Value; use jsonrpc_core::Value;
use jsonrpc_derive::rpc; use jsonrpc_derive::rpc;
@ -19,31 +20,27 @@ use std::time::Duration;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct SwapReq { pub struct SwapReq {
onion: Onion, onion: Onion,
#[serde(with = "secp::comsig_serde")] #[serde(with = "comsig::comsig_serde")]
comsig: ComSignature, comsig: ComSignature,
} }
#[rpc(server)] #[rpc(server)]
pub trait API { pub trait SwapAPI {
#[rpc(name = "swap")] #[rpc(name = "swap")]
fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value>; fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value>;
// milestone 3: Used by mwixnet coinswap servers to communicate with each other
// fn derive_outputs(&self, entries: Vec<Onion>) -> jsonrpc_core::Result<Value>;
// fn derive_kernel(&self, tx: Tx) -> jsonrpc_core::Result<Value>;
} }
#[derive(Clone)] #[derive(Clone)]
struct RPCServer { struct RPCSwapServer {
server_config: ServerConfig, server_config: ServerConfig,
server: Arc<Mutex<dyn Server>>, server: Arc<Mutex<dyn SwapServer>>,
} }
impl RPCServer { impl RPCSwapServer {
/// Spin up an instance of the JSON-RPC HTTP server. /// Spin up an instance of the JSON-RPC HTTP server.
fn start_http(&self) -> jsonrpc_http_server::Server { fn start_http(&self) -> jsonrpc_http_server::Server {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.extend_with(RPCServer::to_delegate(self.clone())); io.extend_with(RPCSwapServer::to_delegate(self.clone()));
ServerBuilder::new(io) ServerBuilder::new(io)
.cors(DomainsValidation::Disabled) .cors(DomainsValidation::Disabled)
@ -72,8 +69,7 @@ impl From<SwapError> for Error {
} }
} }
impl API for RPCServer { impl SwapAPI for RPCSwapServer {
/// Implements the 'swap' API
fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value> { fn swap(&self, swap: SwapReq) -> jsonrpc_core::Result<Value> {
self.server self.server
.lock() .lock()
@ -86,15 +82,22 @@ impl API for RPCServer {
/// Spin up the JSON-RPC web server /// Spin up the JSON-RPC web server
pub fn listen( pub fn listen(
server_config: ServerConfig, server_config: ServerConfig,
next_server: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>, wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>, node: Arc<dyn GrinNode>,
store: SwapStore, store: SwapStore,
stop_state: Arc<StopState>, stop_state: Arc<StopState>,
) -> std::result::Result<(), Box<dyn std::error::Error>> { ) -> std::result::Result<(), Box<dyn std::error::Error>> {
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 server = Arc::new(Mutex::new(server));
let rpc_server = RPCServer { let rpc_server = RPCSwapServer {
server_config: server_config.clone(), server_config: server_config.clone(),
server: server.clone(), server: server.clone(),
}; };
@ -128,11 +131,12 @@ pub fn listen(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::config::ServerConfig; use crate::config::ServerConfig;
use crate::crypto::comsig::ComSignature;
use crate::crypto::secp;
use crate::onion::test_util; use crate::onion::test_util;
use crate::rpc::{RPCServer, SwapReq}; use crate::servers::swap::mock::MockSwapServer;
use crate::secp::{self, ComSignature}; use crate::servers::swap::{SwapError, SwapServer};
use crate::server::mock::MockServer; use crate::servers::swap_rpc::{RPCSwapServer, SwapReq};
use crate::server::{Server, SwapError};
use std::net::TcpListener; use std::net::TcpListener;
use std::sync::{Arc, Mutex}; 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 /// Spin up a temporary web service, query the API, then cleanup and return response
fn make_request( fn make_request(
server: Arc<Mutex<dyn Server>>, server: Arc<Mutex<dyn SwapServer>>,
req: String, req: String,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let server_config = ServerConfig { let server_config = ServerConfig {
key: secp::random_secret(), key: secp::random_secret(),
interval_s: 1, interval_s: 1,
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, 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_url: "127.0.0.1:3413".parse()?,
grin_node_secret_path: None, grin_node_secret_path: None,
wallet_owner_url: "127.0.0.1:3420".parse()?, wallet_owner_url: "127.0.0.1:3420".parse()?,
wallet_owner_secret_path: None, 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_config: server_config.clone(),
server: server.clone(), server: server.clone(),
}; };
@ -208,7 +215,7 @@ mod tests {
comsig, comsig,
}; };
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(MockServer::new())); let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(MockSwapServer::new()));
let req = format!( let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
@ -224,7 +231,7 @@ mod tests {
#[test] #[test]
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> { fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(MockServer::new())); let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(MockSwapServer::new()));
let params = "{ \"param\": \"Not a valid Swap request\" }"; let params = "{ \"param\": \"Not a valid Swap request\" }";
let req = format!( let req = format!(
@ -248,14 +255,14 @@ mod tests {
comsig, comsig,
}; };
let mut server = MockServer::new(); let mut server = MockSwapServer::new();
server.set_response( server.set_response(
&onion, &onion,
SwapError::CoinNotFound { SwapError::CoinNotFound {
commit: commitment.clone(), commit: commitment.clone(),
}, },
); );
let server: Arc<Mutex<dyn Server>> = Arc::new(Mutex::new(server)); let server: Arc<Mutex<dyn SwapServer>> = Arc::new(Mutex::new(server));
let req = format!( let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
@ -263,9 +270,9 @@ mod tests {
); );
let response = make_request(server, req)?; let response = make_request(server, req)?;
let expected = format!( let expected = format!(
"{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n", "{{\"jsonrpc\":\"2.0\",\"error\":{{\"code\":-32602,\"message\":\"Output {:?} does not exist, or is already spent.\"}},\"id\":\"1\"}}\n",
commitment commitment
); );
assert_eq!(response, expected); assert_eq!(response, expected);
Ok(()) Ok(())
} }

View file

@ -1,5 +1,5 @@
use crate::crypto::secp::{self, Commitment, RangeProof, SecretKey};
use crate::onion::Onion; use crate::onion::Onion;
use crate::secp::{self, Commitment, RangeProof, SecretKey};
use crate::types::{read_optional, write_optional}; use crate::types::{read_optional, write_optional};
use grin_core::core::hash::Hash; use grin_core::core::hash::Hash;
@ -23,6 +23,7 @@ pub enum SwapStatus {
Unprocessed, Unprocessed,
InProcess { kernel_hash: Hash }, InProcess { kernel_hash: Hash },
Completed { kernel_hash: Hash, block_hash: Hash }, Completed { kernel_hash: Hash, block_hash: Hash },
Failed,
} }
impl Writeable for SwapStatus { impl Writeable for SwapStatus {
@ -43,6 +44,9 @@ impl Writeable for SwapStatus {
kernel_hash.write(writer)?; kernel_hash.write(writer)?;
block_hash.write(writer)?; block_hash.write(writer)?;
} }
SwapStatus::Failed => {
writer.write_u8(3)?;
}
}; };
Ok(()) Ok(())
@ -65,6 +69,7 @@ impl Readable for SwapStatus {
block_hash, block_hash,
} }
} }
3 => SwapStatus::Failed,
_ => { _ => {
return Err(ser::Error::CorruptedData); return Err(ser::Error::CorruptedData);
} }
@ -239,10 +244,11 @@ impl SwapStore {
#[cfg(test)] #[cfg(test)]
mod tests { 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::onion::test_util::rand_onion;
use crate::secp::test_util::{rand_commit, rand_hash, rand_proof};
use crate::store::{SwapData, SwapStatus, SwapStore}; use crate::store::{SwapData, SwapStatus, SwapStore};
use crate::{secp, StoreError}; use crate::StoreError;
use grin_core::core::{Input, OutputFeatures}; use grin_core::core::{Input, OutputFeatures};
use grin_core::global::{self, ChainTypes}; use grin_core::global::{self, ChainTypes};
use rand::RngCore; use rand::RngCore;

83
src/tor.rs Normal file
View file

@ -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<TorProcess, TorError> {
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<TorProcess, TorError> {
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)
}

182
src/tx.rs Normal file
View file

@ -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<TxKernel>,
/// Transaction outputs
pub outputs: Vec<Output>,
}
/// Builds and verifies the finalized swap 'Transaction' using the provided components.
pub fn assemble_tx(
wallet: &Arc<dyn Wallet>,
inputs: &Vec<Input>,
outputs: &Vec<Output>,
kernels: &Vec<TxKernel>,
fee_base: u64,
fees_paid: u64,
prev_offset: &SecretKey,
output_excesses: &Vec<SecretKey>,
) -> Result<Transaction, TxError> {
// 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<dyn Wallet>,
components: &TxComponents,
output_excesses: &Vec<SecretKey>,
fee_base: u64,
fees_paid: u64,
) -> Result<TxComponents, TxError> {
// 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<dyn Wallet>,
outputs: &Vec<Output>,
kernels: &Vec<TxKernel>,
fee_base: u64,
min_kernel_fee: u64,
fees_paid: u64,
prev_offset: &SecretKey,
output_excesses: &Vec<SecretKey>,
) -> Result<TxComponents, TxError> {
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<TxKernel, TxError> {
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)
}

View file

@ -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::core::FeeFields;
use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; use grin_core::ser::{self, Readable, Reader, Writeable, Writer};

View file

@ -1,19 +1,16 @@
use crate::secp; use crate::crypto::secp;
use grin_api::client; use grin_api::client;
use grin_api::json_rpc::{build_request, Request, Response}; use grin_api::json_rpc::{build_request, Request, Response};
use grin_core::core::{ use grin_core::core::Output;
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
};
use grin_core::libtx::secp_ser; use grin_core::libtx::secp_ser;
use grin_keychain::BlindingFactor; use grin_keychain::BlindingFactor;
use grin_util::{ToHex, ZeroingString}; use grin_util::{ToHex, ZeroingString};
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token}; 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::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
pub trait Wallet: Send + Sync { pub trait Wallet: Send + Sync {
@ -24,18 +21,6 @@ pub trait Wallet: Send + Sync {
/// Error types for interacting with wallets /// Error types for interacting with wallets
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum WalletError { 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:?}")] #[error("Error encrypting request: {0:?}")]
EncryptRequestError(grin_wallet_libwallet::Error), EncryptRequestError(grin_wallet_libwallet::Error),
#[error("Error decrypting response: {0:?}")] #[error("Error decrypting response: {0:?}")]
@ -48,67 +33,6 @@ pub enum WalletError {
ResponseParseError(grin_api::json_rpc::Error), ResponseParseError(grin_api::json_rpc::Error),
} }
/// Builds and verifies a 'Transaction' using the provided components.
pub fn assemble_tx(
wallet: &Arc<dyn Wallet>,
inputs: &Vec<Input>,
outputs: &Vec<Output>,
fee_base: u64,
total_fee: u64,
excesses: &Vec<SecretKey>,
) -> Result<Transaction, WalletError> {
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. /// HTTP (JSONRPC) implementation of the 'Wallet' trait.
#[derive(Clone)] #[derive(Clone)]
pub struct HttpWallet { pub struct HttpWallet {
@ -272,15 +196,34 @@ impl Wallet for HttpWallet {
#[cfg(test)] #[cfg(test)]
pub mod mock { pub mod mock {
use super::{Wallet, WalletError}; use super::{Wallet, WalletError};
use crate::secp; use crate::crypto::secp;
use std::borrow::BorrowMut;
use grin_core::core::{Output, OutputFeatures}; use grin_core::core::{Output, OutputFeatures};
use grin_keychain::BlindingFactor; use grin_keychain::BlindingFactor;
use secp256k1zkp::pedersen::Commitment;
use secp256k1zkp::Secp256k1; 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)] #[derive(Clone)]
pub struct MockWallet {} pub struct MockWallet {
built_outputs: Arc<Mutex<Vec<Commitment>>>,
}
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<Commitment> {
self.built_outputs.lock().unwrap().clone()
}
}
impl Wallet for MockWallet { impl Wallet for MockWallet {
/// Builds an 'Output' for the wallet using the 'build_output' RPC API. /// Builds an 'Output' for the wallet using the 'build_output' RPC API.
@ -297,6 +240,10 @@ pub mod mock {
None, None,
); );
let output = Output::new(OutputFeatures::Plain, commit.clone(), proof); 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)) Ok((BlindingFactor::from_secret_key(blind), output))
} }
} }