inter-server communication

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

96
Cargo.lock generated
View file

@ -955,6 +955,21 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "function_name"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1ab577a896d09940b5fe12ec5ae71f9d8211fff62c919c03a3750a9901e98a7"
dependencies = [
"function_name-proc-macro",
]
[[package]]
name = "function_name-proc-macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673464e1e314dd67a0fd9544abc99e8eb28d0c7e3b69b033bcff9b2d00b87333"
[[package]]
name = "futures"
version = "0.1.31"
@ -1737,6 +1752,31 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "headers"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [
"base64 0.13.0",
"bitflags 1.3.2",
"bytes 1.1.0",
"headers-core",
"http",
"httpdate 1.0.1",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.3.3"
@ -1897,6 +1937,24 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-proxy"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc"
dependencies = [
"bytes 1.1.0",
"futures 0.3.17",
"headers",
"http",
"hyper 0.14.14",
"hyper-tls 0.5.0",
"native-tls",
"tokio 1.12.0",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-rustls"
version = "0.20.0"
@ -1956,6 +2014,19 @@ dependencies = [
"tokio-tls",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.1.0",
"hyper 0.14.14",
"native-tls",
"tokio 1.12.0",
"tokio-native-tls",
]
[[package]]
name = "i18n-config"
version = "0.4.2"
@ -2478,6 +2549,7 @@ dependencies = [
"curve25519-dalek 2.1.3",
"dirs",
"ed25519-dalek",
"function_name",
"futures 0.3.17",
"grin_api 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
"grin_chain 5.2.0-alpha.1 (git+https://github.com/mimblewimble/grin)",
@ -2493,6 +2565,7 @@ dependencies = [
"grin_wallet_util",
"hmac 0.12.0",
"hyper 0.14.14",
"hyper-proxy",
"itertools",
"jsonrpc-core 18.0.0",
"jsonrpc-derive",
@ -3280,7 +3353,7 @@ dependencies = [
"http-body 0.3.1",
"hyper 0.13.10",
"hyper-rustls 0.21.0",
"hyper-tls",
"hyper-tls 0.4.3",
"ipnet",
"js-sys",
"lazy_static",
@ -3634,6 +3707,17 @@ dependencies = [
"yaml-rust 0.4.5",
]
[[package]]
name = "sha1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cc229fb94bcb689ffc39bd4ded842f6ff76885efede7c6d1ffb62582878bea"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.10.1",
]
[[package]]
name = "sha2"
version = "0.8.2"
@ -4019,6 +4103,16 @@ dependencies = [
"syn 1.0.84",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio 1.12.0",
]
[[package]]
name = "tokio-rustls"
version = "0.13.1"

View file

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

View file

@ -1,7 +1,11 @@
# MWixnet
This is an implementation of @tromp's [CoinSwap Proposal](https://forum.grin.mw/t/mimblewimble-coinswap-proposal/8322) with some slight modifications.
A set of n CoinSwap servers (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
#### init-config
@ -19,7 +23,7 @@ A grin-wallet account must be created for receiving extra mwixnet fees. The wall
With your wallet and fully synced node both online and listening at the addresses configured, the mwixnet server can be started by running `mwixnet` and providing the server key password and wallet password when prompted.
### SWAP API
The first CoinSwap server (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`
**method:** `swap`

View file

@ -38,6 +38,18 @@ args:
help: Address to bind the rpc server to (e.g. 127.0.0.1:3000)
long: bind_addr
takes_value: true
- socks_addr:
help: Address to bind the SOCKS5 tor proxy to (e.g. 127.0.0.1:3001)
long: socks_addr
takes_value: true
- prev_server:
help: Hex public key of the previous swap/mix server
long: prev_server
takes_value: true
- next_server:
help: Hex public key of the next mix server
long: next_server
takes_value: true
subcommands:
- init-config:
about: Writes a new configuration file

171
src/client.rs Normal file
View file

@ -0,0 +1,171 @@
use crate::config::ServerConfig;
use crate::crypto::dalek;
use crate::onion::Onion;
use crate::servers::mix_rpc::MixReq;
use crate::tx::TxComponents;
use crate::{tor, DalekPublicKey};
use grin_api::client;
use grin_api::json_rpc::build_request;
use grin_core::ser;
use grin_core::ser::ProtocolVersion;
use grin_wallet_util::OnionV3Address;
use hyper::client::HttpConnector;
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use serde_json;
use thiserror::Error;
/// Error types for interacting with nodes
#[derive(Error, Debug)]
pub enum ClientError {
#[error("Tor Error: {0:?}")]
Tor(tor::TorError),
#[error("API Error: {0:?}")]
API(grin_api::Error),
#[error("Dalek Error: {0:?}")]
Dalek(dalek::DalekError),
#[error("Error parsing response: {0:?}")]
ResponseParse(serde_json::Error),
#[error("Custom client error: {0:?}")]
Custom(String),
}
/// A client for consuming a mix API
pub trait MixClient: Send + Sync {
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<(Vec<usize>, TxComponents), ClientError>;
}
pub struct MixClientImpl {
config: ServerConfig,
addr: OnionV3Address,
}
impl MixClientImpl {
pub fn new(config: ServerConfig, next_pubkey: DalekPublicKey) -> Self {
let addr = OnionV3Address::from_bytes(next_pubkey.as_ref().to_bytes());
MixClientImpl { config, addr }
}
fn send_json_request<D: serde::de::DeserializeOwned>(
&self,
addr: &OnionV3Address,
method: &str,
params: &serde_json::Value,
) -> Result<D, ClientError> {
let _tor = tor::init_tor_sender(&self.config).map_err(ClientError::Tor)?;
let proxy = {
let proxy_uri = format!("http://{:?}", self.config.socks_proxy_addr)
.parse()
.unwrap();
let proxy = Proxy::new(Intercept::All, proxy_uri);
//proxy.set_authorization(Authorization::basic("John Doe", "Agent1234"));
let connector = HttpConnector::new();
let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap();
proxy_connector
};
let url = format!("{}/v1", addr.to_http_str());
let mut req = client::create_post_request(&url, None, &build_request(method, params))
.map_err(ClientError::API)?;
let uri = url.parse().unwrap();
if let Some(headers) = proxy.http_headers(&uri) {
req.headers_mut().extend(headers.clone().into_iter());
}
let res = client::send_request(req).map_err(ClientError::API)?;
serde_json::from_str(&res).map_err(ClientError::ResponseParse)
}
}
impl MixClient for MixClientImpl {
fn mix_outputs(&self, onions: &Vec<Onion>) -> Result<(Vec<usize>, TxComponents), ClientError> {
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
let sig =
dalek::sign(&self.config.key, serialized.as_slice()).map_err(ClientError::Dalek)?;
let mix = MixReq::new(onions.clone(), sig);
let params = serde_json::json!(mix);
self.send_json_request::<(Vec<usize>, TxComponents)>(&self.addr, "mix", &params)
}
}
#[cfg(test)]
pub mod mock {
use super::{ClientError, MixClient};
use crate::onion::Onion;
use crate::tx::TxComponents;
use std::collections::HashMap;
pub struct MockMixClient {
results: HashMap<Vec<Onion>, (Vec<usize>, TxComponents)>,
}
impl MockMixClient {
pub fn new() -> MockMixClient {
MockMixClient {
results: HashMap::new(),
}
}
pub fn set_response(&mut self, onions: &Vec<Onion>, r: (Vec<usize>, TxComponents)) {
self.results.insert(onions.clone(), r);
}
}
impl MixClient for MockMixClient {
fn mix_outputs(
&self,
onions: &Vec<Onion>,
) -> Result<(Vec<usize>, TxComponents), ClientError> {
self.results
.get(onions)
.map(|r| Ok(r.clone()))
.unwrap_or(Err(ClientError::Custom("No response set for input".into())))
}
}
}
#[cfg(test)]
pub mod test_util {
use super::{ClientError, MixClient};
use crate::crypto::dalek;
use crate::crypto::secp::SecretKey;
use crate::onion::Onion;
use crate::servers::mix::MixServer;
use crate::tx::TxComponents;
use crate::DalekPublicKey;
use grin_core::ser;
use grin_core::ser::ProtocolVersion;
use std::sync::Arc;
/// Implementation of the 'MixClient' trait that calls a mix server implementation directly.
/// No JSON-RPC serialization or socket communication occurs.
#[derive(Clone)]
pub struct DirectMixClient {
pub key: SecretKey,
pub mix_server: Arc<dyn MixServer>,
}
impl MixClient for DirectMixClient {
fn mix_outputs(
&self,
onions: &Vec<Onion>,
) -> Result<(Vec<usize>, TxComponents), ClientError> {
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
let sig = dalek::sign(&self.key, serialized.as_slice()).map_err(ClientError::Dalek)?;
sig.verify(
&DalekPublicKey::from_secret(&self.key),
serialized.as_slice(),
)
.unwrap();
Ok(self.mix_server.mix_outputs(&onions, &sig).unwrap())
}
}
}

View file

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

View file

@ -1,275 +1,189 @@
pub use secp256k1zkp::aggsig;
pub use secp256k1zkp::constants::{
AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE,
SECRET_KEY_SIZE,
};
pub use secp256k1zkp::ecdh::SharedSecret;
pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
use blake2::blake2b::Blake2b;
use byteorder::{BigEndian, ByteOrder};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use secp256k1zkp::rand::thread_rng;
use thiserror::Error;
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
#[derive(Clone)]
pub struct ComSignature {
pub_nonce: Commitment,
s: SecretKey,
t: SecretKey,
}
/// Error types for Commitment Signatures
#[derive(Error, Debug)]
pub enum ComSigError {
#[error("Commitment signature is invalid")]
InvalidSig,
#[error("Secp256k1zkp error: {0:?}")]
Secp256k1zkp(secp256k1zkp::Error),
}
impl From<secp256k1zkp::Error> for ComSigError {
fn from(err: secp256k1zkp::Error) -> ComSigError {
ComSigError::Secp256k1zkp(err)
}
}
impl ComSignature {
pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
ComSignature {
pub_nonce: pub_nonce.to_owned(),
s: s.to_owned(),
t: t.to_owned(),
}
}
#[allow(dead_code)]
pub fn sign(
amount: u64,
blind: &SecretKey,
msg: &Vec<u8>,
) -> Result<ComSignature, ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut amt_bytes = [0; 32];
BigEndian::write_u64(&mut amt_bytes[24..32], amount);
let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
let k_1 = SecretKey::new(&secp, &mut thread_rng());
let k_2 = SecretKey::new(&secp, &mut thread_rng());
let commitment = secp.commit(amount, blind.clone())?;
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
// s = k_1 + (e * amount)
let mut s = k_amt.clone();
s.mul_assign(&secp, &e)?;
s.add_assign(&secp, &k_1)?;
// t = k_2 + (e * blind)
let mut t = blind.clone();
t.mul_assign(&secp, &e)?;
t.add_assign(&secp, &k_2)?;
Ok(ComSignature::new(&nonce_commitment, &s, &t))
}
#[allow(non_snake_case)]
pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<(), ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
let mut Ce = commit.to_pubkey(&secp)?;
let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
Ce.mul_assign(&secp, &e)?;
let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
let S2 = secp.commit_sum(commits, Vec::new())?;
if S1 != S2 {
return Err(ComSigError::InvalidSig);
}
Ok(())
}
fn calc_challenge(
secp: &Secp256k1,
commit: &Commitment,
nonce_commit: &Commitment,
msg: &Vec<u8>,
) -> Result<SecretKey, ComSigError> {
let mut challenge_hasher = Blake2b::new(32);
challenge_hasher.update(&commit.0);
challenge_hasher.update(&nonce_commit.0);
challenge_hasher.update(msg);
let mut challenge = [0; 32];
challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
Ok(SecretKey::from_slice(&secp, &challenge)?)
}
}
/// Serializes a ComSignature to and from hex
pub mod comsig_serde {
use super::ComSignature;
use grin_core::ser::{self, ProtocolVersion};
use grin_util::ToHex;
use serde::{Deserialize, Serializer};
/// Serializes a ComSignature as a hex string
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error;
let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
serializer.serialize_str(&bytes.to_hex())
}
/// Creates a ComSignature from a hex string
pub fn deserialize<'de, D>(deserializer: D) -> Result<ComSignature, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let bytes = String::deserialize(deserializer)
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
Ok(sig)
}
}
#[allow(non_snake_case)]
impl Readable for ComSignature {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let R = Commitment::read(reader)?;
let s = read_secret_key(reader)?;
let t = read_secret_key(reader)?;
Ok(ComSignature::new(&R, &s, &t))
}
}
impl Writeable for ComSignature {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.pub_nonce.0)?;
writer.write_fixed_bytes(self.s.0)?;
writer.write_fixed_bytes(self.t.0)?;
Ok(())
}
}
/// Generate a random SecretKey.
pub fn random_secret() -> SecretKey {
let secp = Secp256k1::new();
SecretKey::new(&secp, &mut thread_rng())
}
/// Deserialize a SecretKey from a Reader
pub fn read_secret_key<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)]
mod tests {
use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey};
use rand::Rng;
use secp256k1zkp::rand::{thread_rng, RngCore};
/// Test signing and verification of ComSignatures
#[test]
fn verify_comsig() -> Result<(), ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let amount = thread_rng().next_u64();
let blind = SecretKey::new(&secp, &mut thread_rng());
let msg: [u8; 16] = rand::thread_rng().gen();
let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
let commit = secp.commit(amount, blind.clone())?;
assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
Ok(())
}
}
use crate::crypto::secp::{self, Commitment, ContextFlag, Secp256k1, SecretKey};
use blake2::blake2b::Blake2b;
use byteorder::{BigEndian, ByteOrder};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use secp256k1zkp::rand::thread_rng;
use thiserror::Error;
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
#[derive(Clone)]
pub struct ComSignature {
pub_nonce: Commitment,
s: SecretKey,
t: SecretKey,
}
/// Error types for Commitment Signatures
#[derive(Error, Debug)]
pub enum ComSigError {
#[error("Commitment signature is invalid")]
InvalidSig,
#[error("Secp256k1zkp error: {0:?}")]
Secp256k1zkp(secp256k1zkp::Error),
}
impl From<secp256k1zkp::Error> for ComSigError {
fn from(err: secp256k1zkp::Error) -> ComSigError {
ComSigError::Secp256k1zkp(err)
}
}
impl ComSignature {
pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
ComSignature {
pub_nonce: pub_nonce.to_owned(),
s: s.to_owned(),
t: t.to_owned(),
}
}
#[allow(dead_code)]
pub fn sign(
amount: u64,
blind: &SecretKey,
msg: &Vec<u8>,
) -> Result<ComSignature, ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut amt_bytes = [0; 32];
BigEndian::write_u64(&mut amt_bytes[24..32], amount);
let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
let k_1 = SecretKey::new(&secp, &mut thread_rng());
let k_2 = SecretKey::new(&secp, &mut thread_rng());
let commitment = secp.commit(amount, blind.clone())?;
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
// s = k_1 + (e * amount)
let mut s = k_amt.clone();
s.mul_assign(&secp, &e)?;
s.add_assign(&secp, &k_1)?;
// t = k_2 + (e * blind)
let mut t = blind.clone();
t.mul_assign(&secp, &e)?;
t.add_assign(&secp, &k_2)?;
Ok(ComSignature::new(&nonce_commitment, &s, &t))
}
#[allow(non_snake_case)]
pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<(), ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
let mut Ce = commit.to_pubkey(&secp)?;
let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
Ce.mul_assign(&secp, &e)?;
let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
let S2 = secp.commit_sum(commits, Vec::new())?;
if S1 != S2 {
return Err(ComSigError::InvalidSig);
}
Ok(())
}
fn calc_challenge(
secp: &Secp256k1,
commit: &Commitment,
nonce_commit: &Commitment,
msg: &Vec<u8>,
) -> Result<SecretKey, ComSigError> {
let mut challenge_hasher = Blake2b::new(32);
challenge_hasher.update(&commit.0);
challenge_hasher.update(&nonce_commit.0);
challenge_hasher.update(msg);
let mut challenge = [0; 32];
challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
Ok(SecretKey::from_slice(&secp, &challenge)?)
}
}
/// Serializes a ComSignature to and from hex
pub mod comsig_serde {
use super::ComSignature;
use grin_core::ser::{self, ProtocolVersion};
use grin_util::ToHex;
use serde::{Deserialize, Serializer};
/// Serializes a ComSignature as a hex string
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error;
let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
serializer.serialize_str(&bytes.to_hex())
}
/// Creates a ComSignature from a hex string
pub fn deserialize<'de, D>(deserializer: D) -> Result<ComSignature, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let bytes = String::deserialize(deserializer)
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
Ok(sig)
}
}
#[allow(non_snake_case)]
impl Readable for ComSignature {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let R = Commitment::read(reader)?;
let s = secp::read_secret_key(reader)?;
let t = secp::read_secret_key(reader)?;
Ok(ComSignature::new(&R, &s, &t))
}
}
impl Writeable for ComSignature {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.pub_nonce.0)?;
writer.write_fixed_bytes(self.s.0)?;
writer.write_fixed_bytes(self.t.0)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey};
use rand::Rng;
use secp256k1zkp::rand::{thread_rng, RngCore};
/// Test signing and verification of ComSignatures
#[test]
fn verify_comsig() -> Result<(), ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let amount = thread_rng().next_u64();
let blind = SecretKey::new(&secp, &mut thread_rng());
let msg: [u8; 16] = rand::thread_rng().gen();
let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
let commit = secp.commit(amount, blind.clone())?;
assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
Ok(())
}
}

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

@ -0,0 +1,285 @@
use crate::crypto::secp::SecretKey;
use ed25519_dalek::{Keypair, PublicKey, Signature, Signer, Verifier};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use grin_util::ToHex;
use thiserror::Error;
/// Error types for Dalek structures and logic
#[derive(Clone, Error, Debug, PartialEq)]
pub enum DalekError {
#[error("Hex error {0:?}")]
HexError(String),
#[error("Failed to parse secret key")]
KeyParseError,
#[error("Failed to verify signature")]
SigVerifyFailed,
}
/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization
#[derive(Clone, Debug, PartialEq)]
pub struct DalekPublicKey(PublicKey);
impl DalekPublicKey {
/// Convert DalekPublicKey to hex string
pub fn to_hex(&self) -> String {
self.0.to_hex()
}
/// Convert hex string to DalekPublicKey.
pub fn from_hex(hex: &str) -> Result<Self, DalekError> {
let bytes = grin_util::from_hex(hex)
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
let pk = PublicKey::from_bytes(bytes.as_ref())
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
Ok(DalekPublicKey(pk))
}
/// Compute DalekPublicKey from a SecretKey
pub fn from_secret(key: &SecretKey) -> Self {
let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap();
let pk: PublicKey = (&secret).into();
DalekPublicKey(pk)
}
}
impl AsRef<PublicKey> for DalekPublicKey {
fn as_ref(&self) -> &PublicKey {
&self.0
}
}
/// Serializes an Option<DalekPublicKey> to and from hex
pub mod option_dalek_pubkey_serde {
use super::DalekPublicKey;
use grin_util::ToHex;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
///
pub fn serialize<S>(pk: &Option<DalekPublicKey>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match pk {
Some(pk) => serializer.serialize_str(&pk.0.to_hex()),
None => serializer.serialize_none(),
}
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DalekPublicKey>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).and_then(|res| match res {
Some(string) => DalekPublicKey::from_hex(&string)
.map_err(|e| Error::custom(e.to_string()))
.and_then(|pk: DalekPublicKey| Ok(Some(pk))),
None => Ok(None),
})
}
}
impl Readable for DalekPublicKey {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?)
.map_err(|_| ser::Error::CorruptedData)?;
Ok(DalekPublicKey(pk))
}
}
impl Writeable for DalekPublicKey {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.0.to_bytes())?;
Ok(())
}
}
/// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization
#[derive(Clone, Debug, PartialEq)]
pub struct DalekSignature(Signature);
impl DalekSignature {
/// Convert hex string to DalekSignature.
pub fn from_hex(hex: &str) -> Result<Self, DalekError> {
let bytes = grin_util::from_hex(hex)
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
let sig = Signature::from_bytes(bytes.as_ref())
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
Ok(DalekSignature(sig))
}
/// Verifies DalekSignature
pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> {
pk.as_ref()
.verify(&msg, &self.0)
.map_err(|_| DalekError::SigVerifyFailed)
}
}
impl AsRef<Signature> for DalekSignature {
fn as_ref(&self) -> &Signature {
&self.0
}
}
/// Serializes a DalekSignature to and from hex
pub mod dalek_sig_serde {
use super::DalekSignature;
use grin_util::ToHex;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
///
pub fn serialize<S>(sig: &DalekSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&sig.0.to_hex())
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<DalekSignature, D::Error>
where
D: Deserializer<'de>,
{
let str = String::deserialize(deserializer)?;
let sig = DalekSignature::from_hex(&str).map_err(|e| Error::custom(e.to_string()))?;
Ok(sig)
}
}
pub fn sign(sk: &SecretKey, message: &[u8]) -> Result<DalekSignature, DalekError> {
let secret =
ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?;
let public: PublicKey = (&secret).into();
let keypair = Keypair { secret, public };
let sig = keypair.sign(&message);
Ok(DalekSignature(sig))
}
#[cfg(test)]
pub mod test_util {
use super::*;
use crate::crypto::secp::random_secret;
pub fn rand_keypair() -> (SecretKey, DalekPublicKey) {
let sk = random_secret();
let pk = DalekPublicKey::from_secret(&sk);
(sk, pk)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::dalek::test_util;
use crate::crypto::dalek::test_util::rand_keypair;
use grin_core::ser::{self, ProtocolVersion};
use grin_util::ToHex;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
struct TestPubKeySerde {
#[serde(with = "option_dalek_pubkey_serde", default)]
pk: Option<DalekPublicKey>,
}
#[test]
fn pubkey_test() -> Result<(), Box<dyn std::error::Error>> {
// Test from_hex
let rand_pk = test_util::rand_keypair().1;
let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap();
assert_eq!(rand_pk.0, pk_from_hex.0);
// Test ser (de-)serialization
let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap();
assert_eq!(bytes.len(), 32);
let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap();
assert_eq!(rand_pk.0, pk_from_deser.0);
// Test serde with Some(rand_pk)
let some = TestPubKeySerde {
pk: Some(rand_pk.clone()),
};
let val = serde_json::to_value(some.clone()).unwrap();
if let Value::Object(o) = &val {
if let Value::String(s) = o.get("pk").unwrap() {
assert_eq!(s, &rand_pk.0.to_hex());
} else {
panic!("Invalid type");
}
} else {
panic!("Invalid type")
}
assert_eq!(some, serde_json::from_value(val).unwrap());
// Test serde with empty pk field
let none = TestPubKeySerde { pk: None };
let val = serde_json::to_value(none.clone()).unwrap();
if let Value::Object(o) = &val {
if let Value::Null = o.get("pk").unwrap() {
// ok
} else {
panic!("Invalid type");
}
} else {
panic!("Invalid type")
}
assert_eq!(none, serde_json::from_value(val).unwrap());
// Test serde with no pk field
let none2 = serde_json::from_str::<TestPubKeySerde>("{}").unwrap();
assert_eq!(none, none2);
Ok(())
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
struct TestSigSerde {
#[serde(with = "dalek_sig_serde")]
sig: DalekSignature,
}
#[test]
fn sig_test() -> Result<(), Box<dyn std::error::Error>> {
// Sign a message
let (sk, pk) = rand_keypair();
let msg: [u8; 16] = rand::thread_rng().gen();
let sig = sign(&sk, &msg).unwrap();
// Verify signature
assert!(sig.verify(&pk, &msg).is_ok());
// Wrong message
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
assert!(sig.verify(&pk, &wrong_msg).is_err());
// Wrong pubkey
let wrong_pk = rand_keypair().1;
assert!(sig.verify(&wrong_pk, &msg).is_err());
// Test from_hex
let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap();
assert_eq!(sig.0, sig_from_hex.0);
// Test serde (de-)serialization
let serde_test = TestSigSerde { sig: sig.clone() };
let val = serde_json::to_value(serde_test.clone()).unwrap();
if let Value::Object(o) = &val {
if let Value::String(s) = o.get("sig").unwrap() {
assert_eq!(s, &sig.0.to_hex());
} else {
panic!("Invalid type");
}
} else {
panic!("Invalid type")
}
assert_eq!(serde_test, serde_json::from_value(val).unwrap());
Ok(())
}
}

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

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

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

@ -0,0 +1,117 @@
pub use secp256k1zkp::aggsig;
pub use secp256k1zkp::constants::{
AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE,
SECRET_KEY_SIZE,
};
pub use secp256k1zkp::ecdh::SharedSecret;
pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
use grin_core::ser::{self, Reader};
use secp256k1zkp::rand::thread_rng;
/// Generate a random SecretKey.
pub fn random_secret() -> SecretKey {
let secp = Secp256k1::new();
SecretKey::new(&secp, &mut thread_rng())
}
/// Deserialize a SecretKey from a Reader
pub fn read_secret_key<R: Reader>(reader: &mut R) -> Result<SecretKey, ser::Error> {
let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?;
let secp = Secp256k1::with_caps(ContextFlag::None);
let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?;
Ok(pk)
}
/// Build a Pedersen Commitment using the provided value and blinding factor
pub fn commit(value: u64, blind: &SecretKey) -> Result<Commitment, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let commit = secp.commit(value, blind.clone())?;
Ok(commit)
}
/// Add a blinding factor to an existing Commitment
pub fn add_excess(
commitment: &Commitment,
excess: &SecretKey,
) -> Result<Commitment, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let excess_commit: Commitment = secp.commit(0, excess.clone())?;
let commits = vec![commitment.clone(), excess_commit.clone()];
let sum = secp.commit_sum(commits, Vec::new())?;
Ok(sum)
}
/// Subtracts a value (v*H) from an existing commitment
pub fn sub_value(commitment: &Commitment, value: u64) -> Result<Commitment, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?;
let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?;
Ok(sum)
}
/// Signs the message with the provided SecretKey
pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Full);
let pubkey = PublicKey::from_secret_key(&secp, &sk)?;
let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?;
Ok(sig)
}
#[cfg(test)]
pub mod test_util {
use crate::crypto::secp::{self, Commitment, RangeProof, Secp256k1, SecretKey};
use grin_core::core::hash::Hash;
use grin_util::ToHex;
use rand::RngCore;
pub fn rand_commit() -> Commitment {
secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap()
}
pub fn rand_hash() -> Hash {
Hash::from_hex(secp::random_secret().to_hex().as_str()).unwrap()
}
pub fn rand_proof() -> RangeProof {
let secp = Secp256k1::new();
secp.bullet_proof(
rand::thread_rng().next_u64(),
secp::random_secret(),
secp::random_secret(),
secp::random_secret(),
None,
None,
)
}
pub fn proof(
value: u64,
fee: u64,
input_blind: &SecretKey,
hop_excesses: &Vec<&SecretKey>,
) -> (Commitment, RangeProof) {
let secp = Secp256k1::new();
let mut blind = input_blind.clone();
for hop_excess in hop_excesses {
blind.add_assign(&secp, &hop_excess).unwrap();
}
let out_value = value - fee;
let rp = secp.bullet_proof(
out_value,
blind.clone(),
secp::random_secret(),
secp::random_secret(),
None,
None,
);
(secp::commit(out_value, &blind).unwrap(), rp)
}
}

View file

@ -1,18 +1,16 @@
use config::ServerConfig;
use node::HttpGrinNode;
use std::collections::HashMap;
use store::SwapStore;
use wallet::HttpWallet;
use crate::client::{MixClient, MixClientImpl};
use crate::crypto::dalek::DalekPublicKey;
use crate::node::GrinNode;
use crate::store::StoreError;
use clap::App;
use grin_core::global;
use grin_core::global::ChainTypes;
use grin_util::{StopState, ZeroingString};
use grin_wallet_impls::tor::config as tor_config;
use grin_wallet_impls::tor::process as tor_process;
use grin_wallet_util::OnionV3Address;
use rpassword;
use std::path::PathBuf;
use std::sync::Arc;
@ -21,20 +19,22 @@ use tokio::runtime::Runtime;
#[macro_use]
extern crate clap;
mod client;
mod config;
mod crypto;
mod node;
mod onion;
mod rpc;
mod secp;
mod server;
mod servers;
mod store;
mod tor;
mod tx;
mod types;
mod wallet;
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
fn main() {
real_main().unwrap();
fn main() -> Result<(), Box<dyn std::error::Error>> {
real_main()?;
std::process::exit(0);
}
@ -61,10 +61,17 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
.value_of("round_time")
.map(|t| t.parse::<u32>().unwrap());
let bind_addr = args.value_of("bind_addr");
let socks_addr = args.value_of("socks_addr");
let grin_node_url = args.value_of("grin_node_url");
let grin_node_secret_path = args.value_of("grin_node_secret_path");
let wallet_owner_url = args.value_of("wallet_owner_url");
let wallet_owner_secret_path = args.value_of("wallet_owner_secret_path");
let prev_server = args
.value_of("prev_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
let next_server = args
.value_of("next_server")
.map(|p| DalekPublicKey::from_hex(&p).unwrap());
// Write a new config file if init-config command is supplied
if let ("init-config", Some(_)) = args.subcommand() {
@ -76,9 +83,10 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
}
let server_config = ServerConfig {
key: secp::random_secret(),
key: crypto::secp::random_secret(),
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
addr: bind_addr.unwrap_or("127.0.0.1:3000").parse()?,
socks_proxy_addr: socks_addr.unwrap_or("127.0.0.1:3001").parse()?,
grin_node_url: match grin_node_url {
Some(u) => u.parse()?,
None => config::grin_node_url(&chain_type),
@ -99,6 +107,8 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
.to_str()
.map(|p| p.to_owned()),
},
prev_server,
next_server,
};
let password = prompt_password_confirm();
@ -113,11 +123,6 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
let password = prompt_password();
let mut server_config = config::load_config(&config_path, &password)?;
// Override bind_addr, if supplied
if let Some(bind_addr) = bind_addr {
server_config.addr = bind_addr.parse()?;
}
// Override grin_node_url, if supplied
if let Some(grin_node_url) = grin_node_url {
server_config.grin_node_url = grin_node_url.parse()?;
@ -138,6 +143,26 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
server_config.wallet_owner_secret_path = Some(wallet_owner_secret_path.to_owned());
}
// Override bind_addr, if supplied
if let Some(bind_addr) = bind_addr {
server_config.addr = bind_addr.parse()?;
}
// Override socks_addr, if supplied
if let Some(socks_addr) = socks_addr {
server_config.socks_proxy_addr = socks_addr.parse()?;
}
// Override prev_server, if supplied
if let Some(prev_server) = prev_server {
server_config.prev_server = Some(prev_server);
}
// Override next_server, if supplied
if let Some(next_server) = next_server {
server_config.next_server = Some(next_server);
}
// Create GrinNode
let node = HttpGrinNode::new(
&server_config.grin_node_url,
@ -165,17 +190,7 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
}
};
// Open SwapStore
let store = SwapStore::new(
config::get_grin_path(&chain_type) // todo: load from config
.join("db")
.to_str()
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
"db_root path error".to_string(),
)))?,
)?;
let mut tor_process = init_tor_listener(&server_config)?;
let mut tor_process = tor::init_tor_listener(&server_config)?;
let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone();
@ -187,14 +202,52 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
stop_state_clone.stop();
});
// Start the mwixnet JSON-RPC HTTP server
rpc::listen(
server_config,
Arc::new(wallet),
Arc::new(node),
store,
stop_state,
)
let next_mixer: Option<Arc<dyn MixClient>> = server_config.next_server.clone().map(|pk| {
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,
next_mixer,
Arc::new(wallet),
Arc::new(node),
stop_state,
)
} else {
println!(
"Starting SWAP server with public key {:?}",
server_config.server_pubkey().to_hex()
);
// Open SwapStore
let store = SwapStore::new(
config::get_grin_path(&chain_type)
.join("db")
.to_str()
.ok_or(StoreError::OpenError(grin_store::lmdb::Error::FileErr(
"db_root path error".to_string(),
)))?,
)?;
// Start the mwixnet JSON-RPC HTTP 'swap' server
servers::swap_rpc::listen(
server_config,
next_mixer,
Arc::new(wallet),
Arc::new(node),
store,
stop_state,
)
}
}
#[cfg(unix)]
@ -245,39 +298,3 @@ fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
}
}
}
fn init_tor_listener(
server_config: &ServerConfig,
) -> Result<tor_process::TorProcess, Box<dyn std::error::Error>> {
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
tor_dir.push("tor/listener");
let mut torrc_dir = tor_dir.clone();
torrc_dir.push("torrc");
tor_config::output_tor_listener_config(
tor_dir.to_str().unwrap(),
server_config.addr.to_string().as_str(),
&vec![server_config.key.clone()],
HashMap::new(),
HashMap::new(),
)
.unwrap();
// Start TOR process
let mut process = tor_process::TorProcess::new();
process
.torrc_path(torrc_dir.to_str().unwrap())
.working_dir(tor_dir.to_str().unwrap())
.timeout(20)
.completion_percent(100)
.launch()
.unwrap();
let onion_address = OnionV3Address::from_private(&server_config.key.0).unwrap();
println!(
"Server listening at http://{}.onion",
onion_address.to_ov3_str()
);
Ok(process)
}

View file

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

View file

@ -1,4 +1,4 @@
use crate::secp::{self, Commitment, SecretKey};
use crate::crypto::secp::{self, Commitment, SecretKey};
use crate::types::Payload;
use crate::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)
}
/// An onion with a layer decrypted
#[derive(Clone, Debug)]
pub struct PeeledOnion {
/// The payload from the peeled layer
pub payload: Payload,
/// The onion remaining after a layer was peeled
pub onion: Onion,
}
impl Onion {
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
let mut vec = vec![];
@ -65,7 +74,7 @@ impl Onion {
}
/// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload
pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion), OnionError> {
pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<PeeledOnion, OnionError> {
let shared_secret = StaticSecret::from(secret_key.0).diffie_hellman(&self.ephemeral_pubkey);
let mut cipher = new_stream_cipher(&shared_secret)?;
@ -106,7 +115,10 @@ impl Onion {
commit: commitment.clone(),
enc_payloads,
};
Ok((decrypted_payload, peeled_onion))
Ok(PeeledOnion {
payload: decrypted_payload,
onion: peeled_onion,
})
}
}
@ -292,13 +304,14 @@ impl From<ser::Error> for OnionError {
#[cfg(test)]
pub mod test_util {
use super::{Onion, OnionError, RawBytes};
use crate::secp::test_util::{rand_commit, rand_proof};
use crate::secp::{random_secret, Commitment, SecretKey};
use crate::crypto::secp::test_util::{rand_commit, rand_proof};
use crate::crypto::secp::{random_secret, Commitment, SecretKey};
use crate::types::Payload;
use chacha20::cipher::StreamCipher;
use grin_core::core::FeeFields;
use rand::{thread_rng, RngCore};
use secp256k1zkp::pedersen::RangeProof;
use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::{SharedSecret, StaticSecret};
@ -307,6 +320,23 @@ pub mod test_util {
pub pubkey: xPublicKey,
pub payload: Payload,
}
pub fn new_hop(
server_key: &SecretKey,
hop_excess: &SecretKey,
fee: u64,
proof: Option<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
Build a rangeproof for Cn=Cin+(Σx1...n)*G and include it in payload Pn
@ -391,7 +421,7 @@ pub mod test_util {
#[cfg(test)]
pub mod tests {
use super::test_util::{self, Hop};
use crate::secp;
use crate::crypto::secp;
use crate::types::Payload;
use grin_core::core::FeeFields;
@ -454,8 +484,8 @@ pub mod tests {
};
for i in 0..5 {
let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
payload = peeled.0;
onion_packet = peeled.1;
payload = peeled.payload;
onion_packet = peeled.onion;
}
assert!(payload.rangeproof.is_some());

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

@ -0,0 +1,409 @@
use crate::config::ServerConfig;
use crate::crypto::dalek::{self, DalekSignature};
use crate::onion::{Onion, OnionError, PeeledOnion};
use crate::wallet::Wallet;
use crate::{node, tx, GrinNode};
use std::collections::{HashMap, HashSet};
use crate::client::MixClient;
use crate::tx::TxComponents;
use grin_core::core::{Output, OutputFeatures, TransactionBody};
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
use grin_core::ser;
use grin_core::ser::ProtocolVersion;
use itertools::Itertools;
use secp256k1zkp::key::ZERO_KEY;
use secp256k1zkp::Secp256k1;
use std::sync::Arc;
use thiserror::Error;
/// Mixer error types
#[derive(Error, Debug)]
pub enum MixError {
#[error("Invalid number of payloads provided")]
InvalidPayloadLength,
#[error("Signature is invalid")]
InvalidSignature,
#[error("Rangeproof is invalid")]
InvalidRangeproof,
#[error("Rangeproof is required but was not supplied")]
MissingRangeproof,
#[error("Failed to peel onion layer: {0:?}")]
PeelOnionFailure(OnionError),
#[error("Fee too low (expected >= {minimum_fee:?}, actual {actual_fee:?})")]
FeeTooLow { minimum_fee: u64, actual_fee: u64 },
#[error("None of the outputs could be mixed")]
NoValidOutputs,
#[error("Dalek error: {0:?}")]
Dalek(dalek::DalekError),
#[error("Secp error: {0:?}")]
Secp(grin_util::secp::Error),
#[error("Error building transaction: {0:?}")]
TxError(tx::TxError),
#[error("Wallet error: {0:?}")]
WalletError(crate::wallet::WalletError),
#[error("Client comm error: {0:?}")]
Client(crate::client::ClientError),
}
/// An internal MWixnet server - a "Mixer"
pub trait MixServer: Send + Sync {
/// Swaps the outputs provided and returns the final swapped outputs and kernels.
fn mix_outputs(
&self,
onions: &Vec<Onion>,
sig: &DalekSignature,
) -> Result<(Vec<usize>, TxComponents), MixError>;
}
/// The standard MWixnet "Mixer" implementation
#[derive(Clone)]
pub struct MixServerImpl {
secp: Secp256k1,
server_config: ServerConfig,
mix_client: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
}
impl MixServerImpl {
/// Create a new 'Mix' server
pub fn new(
server_config: ServerConfig,
mix_client: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
) -> Self {
MixServerImpl {
secp: Secp256k1::new(),
server_config,
mix_client,
wallet,
node,
}
}
/// The fee base to use. For now, just using the default.
fn get_fee_base(&self) -> u64 {
DEFAULT_ACCEPT_FEE_BASE
}
/// Minimum fee to perform a mix.
/// Requires enough fee for the mixer's kernel.
fn get_minimum_mix_fee(&self) -> u64 {
TransactionBody::weight_by_iok(0, 0, 1) * self.get_fee_base()
}
fn peel_onion(&self, onion: &Onion) -> Result<PeeledOnion, MixError> {
// Verify that more than 1 payload exists when there's a next server,
// or that exactly 1 payload exists when this is the final server
if self.server_config.next_server.is_some() && onion.enc_payloads.len() <= 1
|| self.server_config.next_server.is_none() && onion.enc_payloads.len() != 1
{
return Err(MixError::InvalidPayloadLength);
}
// Peel the top layer
let peeled = onion
.peel_layer(&self.server_config.key)
.map_err(|e| MixError::PeelOnionFailure(e))?;
// Verify the fee meets the minimum
let fee: u64 = peeled.payload.fee.into();
if fee < self.get_minimum_mix_fee() {
return Err(MixError::FeeTooLow {
minimum_fee: self.get_minimum_mix_fee(),
actual_fee: fee,
});
}
if let Some(r) = peeled.payload.rangeproof {
// Verify the bullet proof
self.secp
.verify_bullet_proof(peeled.onion.commit, r, None)
.map_err(|_| MixError::InvalidRangeproof)?;
} else if peeled.onion.enc_payloads.is_empty() {
// A rangeproof is required in the last payload
return Err(MixError::MissingRangeproof);
}
Ok(peeled)
}
fn build_final_outputs(
&self,
peeled: &Vec<(usize, PeeledOnion)>,
) -> Result<(Vec<usize>, TxComponents), MixError> {
// Filter out commitments that already exist in the UTXO set
let filtered: Vec<&(usize, PeeledOnion)> = peeled
.iter()
.filter(|(_, p)| !node::is_unspent(&self.node, &p.onion.commit).unwrap_or(true))
.collect();
// Build plain outputs for each mix entry
let outputs: Vec<Output> = filtered
.iter()
.map(|(_, p)| {
Output::new(
OutputFeatures::Plain,
p.onion.commit,
p.payload.rangeproof.unwrap(),
)
})
.collect();
let fees_paid = filtered.iter().map(|(_, p)| p.payload.fee.fee()).sum();
let output_excesses = filtered
.iter()
.map(|(_, p)| p.payload.excess.clone())
.collect();
let components = tx::assemble_components(
&self.wallet,
&TxComponents {
offset: ZERO_KEY,
kernels: Vec::new(),
outputs,
},
&output_excesses,
self.get_fee_base(),
fees_paid,
)
.map_err(MixError::TxError)?;
let indices = filtered.iter().map(|(i, _)| *i).collect();
Ok((indices, components))
}
fn call_next_mixer(
&self,
peeled: &Vec<(usize, PeeledOnion)>,
) -> Result<(Vec<usize>, TxComponents), MixError> {
// Sort by commitment
let mut onions_with_index = peeled.clone();
onions_with_index
.sort_by(|(_, a), (_, b)| a.onion.commit.partial_cmp(&b.onion.commit).unwrap());
// Create map of prev indices to next indices
let map_indices: HashMap<usize, usize> =
HashMap::from_iter(onions_with_index.iter().enumerate().map(|(i, j)| (j.0, i)));
// Call next server
let onions = peeled.iter().map(|(_, p)| p.onion.clone()).collect();
let (mixed_indices, mixed_components) = self
.mix_client
.as_ref()
.unwrap()
.mix_outputs(&onions)
.map_err(MixError::Client)?;
// Remove filtered entries
let kept_next_indices = HashSet::<_>::from_iter(mixed_indices.clone());
let filtered_onions: Vec<&(usize, PeeledOnion)> = onions_with_index
.iter()
.filter(|(i, _)| {
map_indices.contains_key(i)
&& kept_next_indices.contains(map_indices.get(i).unwrap())
})
.collect();
// Calculate excess of entries kept
let excesses = filtered_onions
.iter()
.map(|(_, p)| p.payload.excess.clone())
.collect();
// Calculate total fee of entries kept
let fees_paid = filtered_onions
.iter()
.fold(0, |f, (_, p)| f + p.payload.fee.fee());
let indices = kept_next_indices.into_iter().sorted().collect();
let components = tx::assemble_components(
&self.wallet,
&mixed_components,
&excesses,
self.get_fee_base(),
fees_paid,
)
.map_err(MixError::TxError)?;
Ok((indices, components))
}
}
impl MixServer for MixServerImpl {
fn mix_outputs(
&self,
onions: &Vec<Onion>,
sig: &DalekSignature,
) -> Result<(Vec<usize>, TxComponents), MixError> {
// Verify Signature
let serialized = ser::ser_vec(&onions, ProtocolVersion::local()).unwrap();
sig.verify(
self.server_config.prev_server.as_ref().unwrap(),
serialized.as_slice(),
)
.map_err(|_| MixError::InvalidSignature)?;
// Peel onions and filter
let mut peeled: Vec<(usize, PeeledOnion)> = onions
.iter()
.enumerate()
.filter_map(|(i, o)| {
if let Some(p) = self.peel_onion(&o).ok() {
Some((i, p))
} else {
None
}
})
.collect();
// Remove duplicate commitments
peeled.sort_by_key(|(_, o)| o.onion.commit);
peeled.dedup_by_key(|(_, o)| o.onion.commit);
peeled.sort_by_key(|(i, _)| *i);
if peeled.is_empty() {
return Err(MixError::NoValidOutputs);
}
if self.server_config.next_server.is_some() {
self.call_next_mixer(&peeled)
} else {
self.build_final_outputs(&peeled)
}
}
}
#[cfg(test)]
mod test_util {
use crate::client::test_util::DirectMixClient;
use crate::node::mock::MockGrinNode;
use crate::wallet::mock::MockWallet;
use crate::{config, DalekPublicKey, MixClient};
use crate::servers::mix::MixServerImpl;
use secp256k1zkp::SecretKey;
use std::sync::Arc;
pub fn new_mixer(
server_key: &SecretKey,
prev_server: (&SecretKey, &DalekPublicKey),
next_server: &Option<(DalekPublicKey, Arc<dyn MixClient>)>,
node: &Arc<MockGrinNode>,
) -> (Arc<DirectMixClient>, Arc<MockWallet>) {
let config = config::test_util::local_config(
&server_key,
&Some(prev_server.1.clone()),
&next_server.as_ref().map(|(k, _)| k.clone()),
)
.unwrap();
let wallet = Arc::new(MockWallet::new());
let mix_server = Arc::new(MixServerImpl::new(
config,
next_server.as_ref().map(|(_, c)| c.clone()),
wallet.clone(),
node.clone(),
));
let client = Arc::new(DirectMixClient {
key: prev_server.0.clone(),
mix_server: mix_server.clone(),
});
(client, wallet)
}
}
#[cfg(test)]
mod tests {
use crate::crypto::dalek;
use crate::crypto::secp::{self, Commitment};
use crate::node::mock::MockGrinNode;
use crate::onion::test_util;
use crate::MixClient;
use ::function_name::named;
use std::collections::HashSet;
use std::sync::Arc;
macro_rules! init_test {
() => {{
grin_core::global::set_local_chain_type(
grin_core::global::ChainTypes::AutomatedTesting,
);
let db_root = concat!("./target/tmp/.", function_name!());
let _ = std::fs::remove_dir_all(db_root);
()
}};
}
#[test]
#[named]
fn mix_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
init_test!();
let value: u64 = 200_000_000;
let fee: u64 = 50_000_000;
let blind = secp::random_secret();
let input_commit = secp::commit(value, &blind)?;
let node = Arc::new(MockGrinNode::new_with_utxos(&vec![&input_commit]));
let (swap_sk, swap_pk) = dalek::test_util::rand_keypair();
let (mix1_sk, mix1_pk) = dalek::test_util::rand_keypair();
let (mix2_sk, mix2_pk) = dalek::test_util::rand_keypair();
let swap_hop_excess = secp::random_secret();
let swap_hop = test_util::new_hop(&swap_sk, &swap_hop_excess, fee, None);
let mix1_hop_excess = secp::random_secret();
let mix1_hop = test_util::new_hop(&mix1_sk, &mix1_hop_excess, fee, None);
let mix2_hop_excess = secp::random_secret();
let (out_commit, proof) = secp::test_util::proof(
value,
fee * 3,
&blind,
&vec![&swap_hop_excess, &mix1_hop_excess, &mix2_hop_excess],
);
let mix2_hop = test_util::new_hop(&mix2_sk, &mix2_hop_excess, fee, Some(proof));
let onion = test_util::create_onion(&input_commit, &vec![swap_hop, mix1_hop, mix2_hop])?;
let (mixer2_client, mixer2_wallet) =
super::test_util::new_mixer(&mix2_sk, (&mix1_sk, &mix1_pk), &None, &node);
let (mixer1_client, mixer1_wallet) = super::test_util::new_mixer(
&mix1_sk,
(&swap_sk, &swap_pk),
&Some((mix2_pk.clone(), mixer2_client.clone())),
&node,
);
// Emulate the swap server peeling the onion and then calling mix1
let mix1_onion = onion.peel_layer(&swap_sk)?;
let (mixed_indices, mixed_components) =
mixer1_client.mix_outputs(&vec![mix1_onion.onion.clone()])?;
// Verify 3 outputs are returned: mixed output, mixer1's output, and mixer2's output
assert_eq!(mixed_indices, vec![0 as usize]);
assert_eq!(mixed_components.outputs.len(), 3);
let output_commits: HashSet<Commitment> = mixed_components
.outputs
.iter()
.map(|o| o.identifier.commit.clone())
.collect();
assert!(output_commits.contains(&out_commit));
assert_eq!(mixer1_wallet.built_outputs().len(), 1);
assert!(output_commits.contains(mixer1_wallet.built_outputs().get(0).unwrap()));
assert_eq!(mixer2_wallet.built_outputs().len(), 1);
assert!(output_commits.contains(mixer2_wallet.built_outputs().get(0).unwrap()));
Ok(())
}
}

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

@ -0,0 +1,116 @@
use crate::config::ServerConfig;
use crate::crypto::dalek::{self, DalekSignature};
use crate::node::GrinNode;
use crate::onion::Onion;
use crate::servers::mix::{MixError, MixServer, MixServerImpl};
use crate::wallet::Wallet;
use crate::client::MixClient;
use grin_util::StopState;
use jsonrpc_derive::rpc;
use jsonrpc_http_server::jsonrpc_core::{self as jsonrpc, IoHandler};
use jsonrpc_http_server::{DomainsValidation, ServerBuilder};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::thread::{sleep, spawn};
use std::time::Duration;
#[derive(Serialize, Deserialize)]
pub struct MixReq {
onions: Vec<Onion>,
#[serde(with = "dalek::dalek_sig_serde")]
sig: DalekSignature,
}
impl MixReq {
pub fn new(onions: Vec<Onion>, sig: DalekSignature) -> Self {
MixReq { onions, sig }
}
}
#[rpc(server)]
pub trait MixAPI {
#[rpc(name = "mix")]
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value>;
}
#[derive(Clone)]
struct RPCMixServer {
server_config: ServerConfig,
server: Arc<Mutex<dyn MixServer>>,
}
impl RPCMixServer {
/// Spin up an instance of the JSON-RPC HTTP server.
fn start_http(&self) -> jsonrpc_http_server::Server {
let mut io = IoHandler::new();
io.extend_with(RPCMixServer::to_delegate(self.clone()));
ServerBuilder::new(io)
.cors(DomainsValidation::Disabled)
.request_middleware(|request: hyper::Request<hyper::Body>| {
if request.uri() == "/v1" {
request.into()
} else {
jsonrpc_http_server::Response::bad_request("Only v1 supported").into()
}
})
.start_http(&self.server_config.addr)
.expect("Unable to start RPC server")
}
}
impl From<MixError> for jsonrpc::Error {
fn from(e: MixError) -> Self {
jsonrpc::Error::invalid_params(e.to_string())
}
}
impl MixAPI for RPCMixServer {
fn mix(&self, mix: MixReq) -> jsonrpc::Result<jsonrpc::Value> {
self.server
.lock()
.unwrap()
.mix_outputs(&mix.onions, &mix.sig)?;
Ok(jsonrpc::Value::String("success".into()))
}
}
/// Spin up the JSON-RPC web server
pub fn listen(
server_config: ServerConfig,
next_server: Option<Arc<dyn MixClient>>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
stop_state: Arc<StopState>,
) -> Result<(), Box<dyn std::error::Error>> {
let server = MixServerImpl::new(
server_config.clone(),
next_server,
wallet.clone(),
node.clone(),
);
let server = Arc::new(Mutex::new(server));
let rpc_server = RPCMixServer {
server_config: server_config.clone(),
server: server.clone(),
};
let http_server = rpc_server.start_http();
let close_handle = http_server.close_handle();
let round_handle = spawn(move || loop {
if stop_state.is_stopped() {
close_handle.close();
break;
}
sleep(Duration::from_millis(100));
});
http_server.wait();
round_handle.join().unwrap();
Ok(())
}

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

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

83
src/tor.rs Normal file
View file

@ -0,0 +1,83 @@
use crate::config::{self, ServerConfig};
use grin_core::global;
use grin_wallet_impls::tor::config as tor_config;
use grin_wallet_impls::tor::process::TorProcess;
use std::collections::HashMap;
use thiserror::Error;
/// Tor error types
#[derive(Error, Debug)]
pub enum TorError {
#[error("Error generating config: {0:?}")]
ConfigError(String),
#[error("Error starting process: {0:?}")]
ProcessError(grin_wallet_impls::tor::process::Error),
}
pub fn init_tor_listener(server_config: &ServerConfig) -> Result<TorProcess, TorError> {
println!("Initializing tor listener");
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
tor_dir.push("tor/listener");
let mut torrc_dir = tor_dir.clone();
torrc_dir.push("torrc");
tor_config::output_tor_listener_config(
tor_dir.to_str().unwrap(),
server_config.addr.to_string().as_str(),
&vec![server_config.key.clone()],
HashMap::new(),
HashMap::new(),
)
.map_err(|e| TorError::ConfigError(e.to_string()))?;
// Start TOR process
let mut process = TorProcess::new();
process
.torrc_path(torrc_dir.to_str().unwrap())
.working_dir(tor_dir.to_str().unwrap())
.timeout(20)
.completion_percent(100)
.launch()
.map_err(TorError::ProcessError)?;
println!(
"Server listening at http://{}.onion",
server_config.onion_address().to_ov3_str()
);
Ok(process)
}
pub fn init_tor_sender(server_config: &ServerConfig) -> Result<TorProcess, TorError> {
println!(
"Starting TOR Process for send at {:?}",
server_config.socks_proxy_addr
);
let mut tor_dir = config::get_grin_path(&global::get_chain_type());
tor_dir.push("tor/sender");
let mut torrc_dir = tor_dir.clone();
torrc_dir.push("torrc");
tor_config::output_tor_sender_config(
tor_dir.to_str().unwrap(),
&server_config.socks_proxy_addr.to_string(),
HashMap::new(),
HashMap::new(),
)
.map_err(|e| TorError::ConfigError(e.to_string()))?;
// Start TOR process
let mut tor_process = TorProcess::new();
tor_process
.torrc_path(torrc_dir.to_str().unwrap())
.working_dir(tor_dir.to_str().unwrap())
.timeout(20)
.completion_percent(100)
.launch()
.map_err(TorError::ProcessError)?;
Ok(tor_process)
}

182
src/tx.rs Normal file
View file

@ -0,0 +1,182 @@
use crate::crypto::secp;
use crate::wallet::Wallet;
use grin_core::core::{
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
};
use grin_keychain::BlindingFactor;
use secp256k1zkp::{ContextFlag, Secp256k1, SecretKey};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use thiserror::Error;
/// Error types for interacting with wallets
#[derive(Error, Debug)]
pub enum TxError {
#[error("Error computing transactions's offset: {0:?}")]
OffsetError(secp256k1zkp::Error),
#[error("Error building kernel's fee fields: {0:?}")]
KernelFeeError(grin_core::core::transaction::Error),
#[error("Error computing kernel's excess: {0:?}")]
KernelExcessError(secp256k1zkp::Error),
#[error("Error computing kernel's signature message: {0:?}")]
KernelSigMessageError(grin_core::core::transaction::Error),
#[error("Error signing kernel: {0:?}")]
KernelSigError(secp256k1zkp::Error),
#[error("Built kernel failed to verify: {0:?}")]
KernelVerifyError(grin_core::core::transaction::Error),
#[error("Output blinding factor is invalid: {0:?}")]
OutputBlindError(secp256k1zkp::Error),
#[error("Wallet error: {0:?}")]
WalletError(crate::wallet::WalletError),
}
/// A collection of transaction components
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TxComponents {
/// Transaction offset
pub offset: SecretKey,
/// Transaction kernels
pub kernels: Vec<TxKernel>,
/// Transaction outputs
pub outputs: Vec<Output>,
}
/// Builds and verifies the finalized swap 'Transaction' using the provided components.
pub fn assemble_tx(
wallet: &Arc<dyn Wallet>,
inputs: &Vec<Input>,
outputs: &Vec<Output>,
kernels: &Vec<TxKernel>,
fee_base: u64,
fees_paid: u64,
prev_offset: &SecretKey,
output_excesses: &Vec<SecretKey>,
) -> Result<Transaction, TxError> {
// calculate minimum fee required for the kernel
let min_kernel_fee =
TransactionBody::weight_by_iok(inputs.len() as u64, outputs.len() as u64, 1) * fee_base;
let components = add_kernel_and_collect_fees(
&wallet,
&outputs,
&kernels,
fee_base,
min_kernel_fee,
fees_paid,
&prev_offset,
&output_excesses,
)?;
// assemble the transaction
let tx = Transaction::new(
Inputs::from(inputs.as_slice()),
&components.outputs,
&components.kernels,
)
.with_offset(BlindingFactor::from_secret_key(components.offset));
Ok(tx)
}
/// Adds a kernel and output to a collection of transaction components to consume fees and offset excesses.
pub fn assemble_components(
wallet: &Arc<dyn Wallet>,
components: &TxComponents,
output_excesses: &Vec<SecretKey>,
fee_base: u64,
fees_paid: u64,
) -> Result<TxComponents, TxError> {
// calculate minimum fee required for the kernel
let min_kernel_fee = TransactionBody::weight_by_iok(0, 0, 1) * fee_base;
add_kernel_and_collect_fees(
&wallet,
&components.outputs,
&components.kernels,
fee_base,
min_kernel_fee,
fees_paid,
&components.offset,
&output_excesses,
)
}
fn add_kernel_and_collect_fees(
wallet: &Arc<dyn Wallet>,
outputs: &Vec<Output>,
kernels: &Vec<TxKernel>,
fee_base: u64,
min_kernel_fee: u64,
fees_paid: u64,
prev_offset: &SecretKey,
output_excesses: &Vec<SecretKey>,
) -> Result<TxComponents, TxError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut txn_outputs = outputs.clone();
let mut txn_excesses = output_excesses.clone();
let mut txn_kernels = kernels.clone();
let mut kernel_fee = fees_paid;
// calculate fee required if we add our own output
let fee_to_collect = TransactionBody::weight_by_iok(0, 1, 0) * fee_base;
// calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it
let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base;
// collect any leftover fees
if fees_paid > min_kernel_fee + fee_to_collect + fee_to_spend {
let amount = fees_paid - (min_kernel_fee + fee_to_collect);
kernel_fee -= amount;
let wallet_output = wallet.build_output(amount).map_err(TxError::WalletError)?;
txn_outputs.push(wallet_output.1);
let output_excess = SecretKey::from_slice(&secp, &wallet_output.0.as_ref())
.map_err(TxError::OutputBlindError)?;
txn_excesses.push(output_excess);
}
// generate random transaction offset
let our_offset = secp::random_secret();
let txn_offset = secp
.blind_sum(vec![prev_offset.clone(), our_offset.clone()], Vec::new())
.map_err(TxError::OffsetError)?;
// calculate kernel excess
let kern_excess = secp
.blind_sum(txn_excesses, vec![our_offset.clone()])
.map_err(TxError::KernelExcessError)?;
// build and verify kernel
let kernel = build_kernel(&kern_excess, kernel_fee)?;
txn_kernels.push(kernel);
// Sort outputs & kernels by commitment
txn_kernels.sort_by(|a, b| a.excess.partial_cmp(&b.excess).unwrap());
txn_outputs.sort_by(|a, b| {
a.identifier
.commit
.partial_cmp(&b.identifier.commit)
.unwrap()
});
Ok(TxComponents {
offset: txn_offset,
kernels: txn_kernels,
outputs: txn_outputs,
})
}
pub fn build_kernel(excess: &SecretKey, fee: u64) -> Result<TxKernel, TxError> {
let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
fee: FeeFields::new(0, fee).map_err(TxError::KernelFeeError)?,
});
let msg = kernel
.msg_to_sign()
.map_err(TxError::KernelSigMessageError)?;
kernel.excess = secp::commit(0, &excess).map_err(TxError::KernelExcessError)?;
kernel.excess_sig = secp::sign(&excess, &msg).map_err(TxError::KernelSigError)?;
kernel.verify().map_err(TxError::KernelVerifyError)?;
Ok(kernel)
}

View file

@ -1,4 +1,4 @@
use crate::secp::{self, RangeProof, SecretKey};
use crate::crypto::secp::{self, RangeProof, SecretKey};
use grin_core::core::FeeFields;
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};

View file

@ -1,19 +1,16 @@
use crate::secp;
use crate::crypto::secp;
use grin_api::client;
use grin_api::json_rpc::{build_request, Request, Response};
use grin_core::core::{
FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel,
};
use grin_core::core::Output;
use grin_core::libtx::secp_ser;
use grin_keychain::BlindingFactor;
use grin_util::{ToHex, ZeroingString};
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token};
use secp256k1zkp::{ContextFlag, PublicKey, Secp256k1, SecretKey};
use secp256k1zkp::{PublicKey, Secp256k1, SecretKey};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::net::SocketAddr;
use std::sync::Arc;
use thiserror::Error;
pub trait Wallet: Send + Sync {
@ -24,18 +21,6 @@ pub trait Wallet: Send + Sync {
/// Error types for interacting with wallets
#[derive(Error, Debug)]
pub enum WalletError {
#[error("Error building kernel's fee fields: {0:?}")]
KernelFeeError(grin_core::core::transaction::Error),
#[error("Error computing kernel's excess: {0:?}")]
KernelExcessError(secp256k1zkp::Error),
#[error("Error computing kernel's signature message: {0:?}")]
KernelSigMessageError(grin_core::core::transaction::Error),
#[error("Error signing kernel: {0:?}")]
KernelSigError(secp256k1zkp::Error),
#[error("Built kernel failed to verify: {0:?}")]
KernelVerifyError(grin_core::core::transaction::Error),
#[error("Output blinding factor is invalid: {0:?}")]
OutputBlindError(secp256k1zkp::Error),
#[error("Error encrypting request: {0:?}")]
EncryptRequestError(grin_wallet_libwallet::Error),
#[error("Error decrypting response: {0:?}")]
@ -48,67 +33,6 @@ pub enum WalletError {
ResponseParseError(grin_api::json_rpc::Error),
}
/// Builds and verifies a 'Transaction' using the provided components.
pub fn assemble_tx(
wallet: &Arc<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.
#[derive(Clone)]
pub struct HttpWallet {
@ -272,15 +196,34 @@ impl Wallet for HttpWallet {
#[cfg(test)]
pub mod mock {
use super::{Wallet, WalletError};
use crate::secp;
use crate::crypto::secp;
use std::borrow::BorrowMut;
use grin_core::core::{Output, OutputFeatures};
use grin_keychain::BlindingFactor;
use secp256k1zkp::pedersen::Commitment;
use secp256k1zkp::Secp256k1;
use std::sync::{Arc, Mutex};
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
/// Mock implementation of the 'Wallet' trait for unit-tests.
#[derive(Clone)]
pub struct MockWallet {}
pub struct MockWallet {
built_outputs: Arc<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 {
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
@ -297,6 +240,10 @@ pub mod mock {
None,
);
let output = Output::new(OutputFeatures::Plain, commit.clone(), proof);
let mut locked = self.built_outputs.lock().unwrap();
locked.borrow_mut().push(output.commitment().clone());
Ok((BlindingFactor::from_secret_key(blind), output))
}
}