mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-01-20 19:11:09 +03:00
inter-server communication
This commit is contained in:
parent
9ec200f548
commit
76532c899e
23 changed files with 2756 additions and 1169 deletions
96
Cargo.lock
generated
96
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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`
|
||||||
|
|
12
mwixnet.yml
12
mwixnet.yml
|
@ -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
171
src/client.rs
Normal 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", ¶ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod mock {
|
||||||
|
use super::{ClientError, MixClient};
|
||||||
|
use crate::onion::Onion;
|
||||||
|
use crate::tx::TxComponents;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct MockMixClient {
|
||||||
|
results: HashMap<Vec<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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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
285
src/crypto/dalek.rs
Normal 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
3
src/crypto/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod comsig;
|
||||||
|
pub mod dalek;
|
||||||
|
pub mod secp;
|
117
src/crypto/secp.rs
Normal file
117
src/crypto/secp.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
145
src/main.rs
145
src/main.rs
|
@ -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> =
|
||||||
|
Arc::new(MixClientImpl::new(server_config.clone(), pk.clone()));
|
||||||
|
client
|
||||||
|
});
|
||||||
|
|
||||||
|
if server_config.prev_server.is_some() {
|
||||||
|
// Start the JSON-RPC HTTP 'mix' server
|
||||||
|
println!(
|
||||||
|
"Starting MIX server with public key {:?}",
|
||||||
|
server_config.server_pubkey().to_hex()
|
||||||
|
);
|
||||||
|
|
||||||
|
servers::mix_rpc::listen(
|
||||||
server_config,
|
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(wallet),
|
||||||
Arc::new(node),
|
Arc::new(node),
|
||||||
store,
|
store,
|
||||||
stop_state,
|
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)
|
|
||||||
}
|
|
||||||
|
|
21
src/node.rs
21
src/node.rs
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
46
src/onion.rs
46
src/onion.rs
|
@ -1,4 +1,4 @@
|
||||||
use crate::secp::{self, Commitment, SecretKey};
|
use crate::crypto::secp::{self, Commitment, SecretKey};
|
||||||
use crate::types::Payload;
|
use crate::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
409
src/servers/mix.rs
Normal 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
116
src/servers/mix_rpc.rs
Normal 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
4
src/servers/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod mix;
|
||||||
|
pub mod mix_rpc;
|
||||||
|
pub mod swap;
|
||||||
|
pub mod swap_rpc;
|
|
@ -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,20 +183,42 @@ 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 outputs: Vec<Output> = spendable
|
let filtered = spendable
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(_, s)| {
|
.filter(|(i, _)| kept_indices.contains(i))
|
||||||
|
.map(|(_, j)| j.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let failed = spendable
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| !kept_indices.contains(i))
|
||||||
|
.map(|(_, j)| j.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(filtered, failed, mixed.offset, mixed.outputs, mixed.kernels)
|
||||||
|
} else {
|
||||||
|
// Build plain outputs for each swap entry
|
||||||
|
let outputs: Vec<Output> = spendable
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
Output::new(
|
Output::new(
|
||||||
OutputFeatures::Plain,
|
OutputFeatures::Plain,
|
||||||
s.output_commit,
|
s.output_commit,
|
||||||
|
@ -196,50 +227,59 @@ impl Server for ServerImpl {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let excesses: Vec<SecretKey> = spendable
|
(spendable, Vec::new(), ZERO_KEY, outputs, Vec::new())
|
||||||
.iter()
|
};
|
||||||
.enumerate()
|
|
||||||
.map(|(_, s)| s.excess.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let tx = wallet::assemble_tx(
|
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 {
|
|
@ -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\"}}",
|
12
src/store.rs
12
src/store.rs
|
@ -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
83
src/tor.rs
Normal 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
182
src/tx.rs
Normal 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)
|
||||||
|
}
|
|
@ -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};
|
||||||
|
|
111
src/wallet.rs
111
src/wallet.rs
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue