use v3 encrypted requests, cleaner error handling, rust formatter

This commit is contained in:
scilio 2022-07-01 05:33:34 -04:00
parent bff59b08ac
commit 2baf37a5a3
13 changed files with 1471 additions and 1217 deletions

11
Cargo.lock generated
View file

@ -2497,6 +2497,7 @@ dependencies = [
"pbkdf2 0.8.0", "pbkdf2 0.8.0",
"rand 0.8.4", "rand 0.8.4",
"ring", "ring",
"rpassword",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
@ -3334,6 +3335,16 @@ dependencies = [
"opaque-debug 0.3.0", "opaque-debug 0.3.0",
] ]
[[package]]
name = "rpassword"
version = "4.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "6.3.0" version = "6.3.0"

View file

@ -23,6 +23,7 @@ lazy_static = "1"
pbkdf2 = "0.8.0" pbkdf2 = "0.8.0"
rand = "0.8.4" rand = "0.8.4"
ring = "0.16" ring = "0.16"
rpassword = "4.0"
serde = { version = "1", features= ["derive"]} serde = { version = "1", features= ["derive"]}
serde_derive = "1" serde_derive = "1"
serde_json = "1" serde_json = "1"

View file

@ -8,12 +8,6 @@ args:
short: c short: c
long: config_file long: config_file
takes_value: true takes_value: true
- pass:
help: Password to encrypt/decrypt the server key in the configuration file
short: p
long: pass
takes_value: true
required: true
- grin_node_url: - grin_node_url:
help: Api address of running GRIN node on which to check inputs and post transactions help: Api address of running GRIN node on which to check inputs and post transactions
short: n short: n

2
rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
hard_tabs = true
edition = "2021"

View file

@ -11,7 +11,7 @@ use std::io::prelude::*;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
// The decrypted server config to be passed around and used by the rest of the mwixnet code /// The decrypted server config to be passed around and used by the rest of the mwixnet code
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ServerConfig { pub struct ServerConfig {
/// private key used by the server to decrypt onion packets /// private key used by the server to decrypt onion packets
@ -38,7 +38,7 @@ struct EncryptedServerKey {
impl EncryptedServerKey { impl EncryptedServerKey {
/// Generates a random salt for pbkdf2 key derivation and a random nonce for aead sealing. /// Generates a random salt for pbkdf2 key derivation and a random nonce for aead sealing.
/// Then derives an encryption key from the password and salt. Finally, it encrypts and seals /// Then derives an encryption key from the password and salt. Finally, it encrypts and seals
/// the server key with chacha20-poly1305 using the derived key and random nonce. /// the server key with chacha20-poly1305 using the derived key and random nonce.
pub fn from_secret_key( pub fn from_secret_key(
server_key: &SecretKey, server_key: &SecretKey,
password: &ZeroingString, password: &ZeroingString,
@ -46,8 +46,8 @@ impl EncryptedServerKey {
let salt: [u8; 8] = thread_rng().gen(); let salt: [u8; 8] = thread_rng().gen();
let password = password.as_bytes(); let password = password.as_bytes();
let mut key = [0; 32]; let mut key = [0; 32];
ring::pbkdf2::derive( pbkdf2::derive(
ring::pbkdf2::PBKDF2_HMAC_SHA512, pbkdf2::PBKDF2_HMAC_SHA512,
NonZeroU32::new(100).unwrap(), NonZeroU32::new(100).unwrap(),
&salt, &salt,
password, password,
@ -60,11 +60,18 @@ impl EncryptedServerKey {
let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key);
let nonce: [u8; 12] = thread_rng().gen(); let nonce: [u8; 12] = thread_rng().gen();
let aad = aead::Aad::from(&[]); let aad = aead::Aad::from(&[]);
let _ = sealing_key.seal_in_place_append_tag( let _ = sealing_key
aead::Nonce::assume_unique_for_key(nonce), .seal_in_place_append_tag(
aad, aead::Nonce::assume_unique_for_key(nonce),
&mut enc_bytes, aad,
).map_err(|_| error::ErrorKind::SaveConfigError)?; &mut enc_bytes,
)
.map_err(|e| {
error::ErrorKind::SaveConfigError(format!(
"Failure while encrypting server key: {}",
e
))
})?;
Ok(EncryptedServerKey { Ok(EncryptedServerKey {
encrypted_key: enc_bytes.to_hex(), encrypted_key: enc_bytes.to_hex(),
@ -73,17 +80,18 @@ impl EncryptedServerKey {
}) })
} }
/// Decrypt the server secret key using the provided password.
pub fn decrypt(&self, password: &str) -> Result<SecretKey> { pub fn decrypt(&self, password: &str) -> Result<SecretKey> {
let mut encrypted_seed = grin_util::from_hex(&self.encrypted_key.clone()) let mut encrypted_seed = grin_util::from_hex(&self.encrypted_key.clone())
.map_err(|_| error::ErrorKind::LoadConfigError)?; .map_err(|_| error::ErrorKind::LoadConfigError("Seed not valid hex".to_string()))?;
let salt = grin_util::from_hex(&self.salt.clone()) let salt = grin_util::from_hex(&self.salt.clone())
.map_err(|_| error::ErrorKind::LoadConfigError)?; .map_err(|_| error::ErrorKind::LoadConfigError("Salt not valid hex".to_string()))?;
let nonce = grin_util::from_hex(&self.nonce.clone()) let nonce = grin_util::from_hex(&self.nonce.clone())
.map_err(|_| error::ErrorKind::LoadConfigError)?; .map_err(|_| error::ErrorKind::LoadConfigError("Nonce not valid hex".to_string()))?;
let password = password.as_bytes(); let password = password.as_bytes();
let mut key = [0; 32]; let mut key = [0; 32];
pbkdf2::derive( pbkdf2::derive(
ring::pbkdf2::PBKDF2_HMAC_SHA512, pbkdf2::PBKDF2_HMAC_SHA512,
NonZeroU32::new(100).unwrap(), NonZeroU32::new(100).unwrap(),
&salt, &salt,
password, password,
@ -95,18 +103,25 @@ impl EncryptedServerKey {
let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap();
let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key);
let aad = aead::Aad::from(&[]); let aad = aead::Aad::from(&[]);
let _ = opening_key.open_in_place( let _ = opening_key
aead::Nonce::assume_unique_for_key(n), .open_in_place(
aad, aead::Nonce::assume_unique_for_key(n),
&mut encrypted_seed, aad,
).map_err(|_| error::ErrorKind::LoadConfigError)?; &mut encrypted_seed,
)
.map_err(|e| {
error::ErrorKind::LoadConfigError(format!("Error decrypting seed: {}", e))
})?;
for _ in 0..aead::AES_256_GCM.tag_len() { for _ in 0..aead::AES_256_GCM.tag_len() {
encrypted_seed.pop(); encrypted_seed.pop();
} }
let secp = secp256k1zkp::Secp256k1::new(); let secp = secp256k1zkp::Secp256k1::new();
Ok(SecretKey::from_slice(&secp, &encrypted_seed).unwrap()) let decrypted = SecretKey::from_slice(&secp, &encrypted_seed).map_err(|_| {
error::ErrorKind::LoadConfigError("Decrypted key not valid".to_string())
})?;
Ok(decrypted)
} }
} }
@ -123,10 +138,14 @@ struct RawConfig {
} }
/// 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.
pub fn write_config(config_path: &PathBuf, server_config: &ServerConfig, password: &ZeroingString) -> Result<()> { pub fn write_config(
config_path: &PathBuf,
server_config: &ServerConfig,
password: &ZeroingString,
) -> Result<()> {
let encrypted = EncryptedServerKey::from_secret_key(&server_config.key, &password)?; let encrypted = EncryptedServerKey::from_secret_key(&server_config.key, &password)?;
let raw_config = RawConfig{ let raw_config = RawConfig {
encrypted_key: encrypted.encrypted_key, encrypted_key: encrypted.encrypted_key,
salt: encrypted.salt, salt: encrypted.salt,
nonce: encrypted.nonce, nonce: encrypted.nonce,
@ -135,33 +154,37 @@ pub fn write_config(config_path: &PathBuf, server_config: &ServerConfig, passwo
grin_node_url: server_config.grin_node_url, grin_node_url: server_config.grin_node_url,
wallet_owner_url: server_config.wallet_owner_url, wallet_owner_url: server_config.wallet_owner_url,
}; };
let encoded: String = toml::to_string(&raw_config).map_err(|_| error::ErrorKind::SaveConfigError)?; let encoded: String = toml::to_string(&raw_config).map_err(|e| {
error::ErrorKind::SaveConfigError(format!("Error while encoding config as toml: {}", e))
})?;
let mut file = File::create(config_path)?; let mut file = File::create(config_path)?;
file.write_all(encoded.as_bytes()).map_err(|_| error::ErrorKind::SaveConfigError)?; file.write_all(encoded.as_bytes()).map_err(|e| {
error::ErrorKind::SaveConfigError(format!("Error while writing config to file: {}", e))
})?;
Ok(()) Ok(())
} }
/// Reads the server config from the config_path given and decrypts it with the provided password. /// Reads the server config from the config_path given and decrypts it with the provided password.
pub fn load_config(config_path: &PathBuf, password: &ZeroingString) -> Result<ServerConfig> { pub fn load_config(config_path: &PathBuf, password: &ZeroingString) -> Result<ServerConfig> {
let contents = std::fs::read_to_string(config_path)?; let contents = std::fs::read_to_string(config_path)?;
let raw_config: RawConfig = toml::from_str(&contents) let raw_config: RawConfig =
.map_err(|_| error::ErrorKind::LoadConfigError)?; toml::from_str(&contents).map_err(|e| error::ErrorKind::LoadConfigError(e.to_string()))?;
let encrypted_key = EncryptedServerKey{ let encrypted_key = EncryptedServerKey {
encrypted_key: raw_config.encrypted_key, encrypted_key: raw_config.encrypted_key,
salt: raw_config.salt, salt: raw_config.salt,
nonce: raw_config.nonce nonce: raw_config.nonce,
}; };
let secret_key = encrypted_key.decrypt(&password)?; let secret_key = encrypted_key.decrypt(&password)?;
Ok(ServerConfig { Ok(ServerConfig {
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,
grin_node_url: raw_config.grin_node_url, grin_node_url: raw_config.grin_node_url,
wallet_owner_url: raw_config.wallet_owner_url wallet_owner_url: raw_config.wallet_owner_url,
}) })
} }

View file

@ -1,166 +1,161 @@
use failure::{self, Context, Fail}; use failure::{self, Context, Fail};
use grin_wallet_libwallet as libwallet;
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::io; use std::io;
use grin_wallet_libwallet as libwallet;
/// MWixnet error definition /// MWixnet error definition
#[derive(Debug)] #[derive(Debug)]
pub struct Error { pub struct Error {
inner: Context<ErrorKind>, inner: Context<ErrorKind>,
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
pub type StdResult<T, E> = std::result::Result<T, E>; pub type StdResult<T, E> = std::result::Result<T, E>;
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
/// MWixnet error types /// MWixnet error types
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
pub enum ErrorKind { pub enum ErrorKind {
/// Error from secp256k1-zkp library /// Error from secp256k1-zkp library
#[fail(display = "Secp Error")] #[fail(display = "Secp Error")]
SecpError, SecpError,
/// Invalid key length for MAC initialization /// Invalid key length for MAC initialization
#[fail(display = "InvalidKeyLength")] #[fail(display = "InvalidKeyLength")]
InvalidKeyLength, InvalidKeyLength,
/// Wraps an io error produced when reading or writing /// Wraps an io error produced when reading or writing
#[fail(display = "IOError")] #[fail(display = "IO Error: {}", _0)]
IOErr( IOErr(String, io::ErrorKind),
String, /// Data wasn't in a consumable format
io::ErrorKind, #[fail(display = "CorruptedData")]
), CorruptedData,
/// Data wasn't in a consumable format /// Error from grin's api crate
#[fail(display = "CorruptedData")] #[fail(display = "GRIN API Error: {}", _0)]
CorruptedData, GrinApiError(String),
/// Error from grin's api crate /// Error from grin core
#[fail(display = "GRIN API Error")] #[fail(display = "GRIN Core Error: {}", _0)]
GrinApiError, GrinCoreError(String),
/// Error from grin core /// Error from grin-wallet's libwallet
#[fail(display = "grincore Error")] #[fail(display = "libwallet error: {}", _0)]
GrinCoreError, LibWalletError(String),
/// Error from grin-wallet's libwallet /// Error from serde-json
#[fail(display = "libwallet Error")] #[fail(display = "serde json error: {}", _0)]
LibWalletError, SerdeJsonError(String),
/// Error from serde-json /// Error from invalid signature
#[fail(display = "serde json Error")] #[fail(display = "invalid signature")]
SerdeJsonError, InvalidSigError,
/// Error from invalid signature /// Error while saving config
#[fail(display = "invalid signature Error")] #[fail(display = "save config error: {}", _0)]
InvalidSigError, SaveConfigError(String),
/// Error while saving config /// Error while loading config
#[fail(display = "save config Error")] #[fail(display = "load config error: {}", _0)]
SaveConfigError, LoadConfigError(String),
/// Error while loading config
#[fail(display = "load config Error")]
LoadConfigError,
} }
impl std::error::Error for Error { impl std::error::Error for Error {}
}
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(e: io::Error) -> Error { fn from(e: io::Error) -> Error {
ErrorKind::IOErr(format!("{}", e), e.kind()).into() ErrorKind::IOErr(format!("{}", e), e.kind()).into()
} }
} }
impl From<io::ErrorKind> for Error { impl From<io::ErrorKind> for Error {
fn from(e: io::ErrorKind) -> Error { fn from(e: io::ErrorKind) -> Error {
ErrorKind::IOErr(format!("{}", io::Error::from(e)), e).into() ErrorKind::IOErr(format!("{}", io::Error::from(e)), e).into()
} }
} }
impl From<grin_core::ser::Error> for Error { impl From<grin_core::ser::Error> for Error {
fn from(_e: grin_core::ser::Error) -> Error { fn from(e: grin_core::ser::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::GrinCoreError), inner: Context::new(ErrorKind::GrinCoreError(e.to_string())),
} }
} }
} }
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.inner, f) Display::fmt(&self.inner, f)
} }
} }
impl Error { impl Error {
pub fn new(kind: ErrorKind) -> Error { pub fn new(kind: ErrorKind) -> Error {
Error { Error {
inner: Context::new(kind), inner: Context::new(kind),
} }
} }
pub fn message(&self) -> String { pub fn message(&self) -> String {
format!("{}", self).into() format!("{}", self).into()
} }
} }
impl From<ErrorKind> for Error { impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error { fn from(kind: ErrorKind) -> Error {
Error { Error {
inner: Context::new(kind), inner: Context::new(kind),
} }
} }
} }
impl From<Context<ErrorKind>> for Error { impl From<Context<ErrorKind>> for Error {
fn from(inner: Context<ErrorKind>) -> Error { fn from(inner: Context<ErrorKind>) -> Error {
Error { inner } Error { inner }
} }
} }
impl From<secp256k1zkp::Error> for Error { impl From<secp256k1zkp::Error> for Error {
fn from(_error: secp256k1zkp::Error) -> Error { fn from(_error: secp256k1zkp::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::SecpError), inner: Context::new(ErrorKind::SecpError),
} }
} }
} }
impl From<hmac::digest::InvalidLength> for Error { impl From<hmac::digest::InvalidLength> for Error {
fn from(_error: hmac::digest::InvalidLength) -> Error { fn from(_error: hmac::digest::InvalidLength) -> Error {
Error { Error {
inner: Context::new(ErrorKind::InvalidKeyLength), inner: Context::new(ErrorKind::InvalidKeyLength),
} }
} }
} }
impl From<grin_api::Error> for Error { impl From<grin_api::Error> for Error {
fn from(_error: grin_api::Error) -> Error { fn from(e: grin_api::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::GrinApiError), inner: Context::new(ErrorKind::GrinApiError(e.to_string())),
} }
} }
} }
impl From<grin_api::json_rpc::Error> for Error { impl From<grin_api::json_rpc::Error> for Error {
fn from(_error: grin_api::json_rpc::Error) -> Error { fn from(e: grin_api::json_rpc::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::GrinApiError), inner: Context::new(ErrorKind::GrinApiError(e.to_string())),
} }
} }
} }
impl From<grin_core::core::transaction::Error> for Error { impl From<grin_core::core::transaction::Error> for Error {
fn from(_error: grin_core::core::transaction::Error) -> Error { fn from(e: grin_core::core::transaction::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::GrinCoreError), inner: Context::new(ErrorKind::GrinCoreError(e.to_string())),
} }
} }
} }
impl From<libwallet::Error> for Error { impl From<libwallet::Error> for Error {
fn from(_error: libwallet::Error) -> Error { fn from(e: libwallet::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::LibWalletError), inner: Context::new(ErrorKind::LibWalletError(e.to_string())),
} }
} }
} }
impl From<serde_json::Error> for Error { impl From<serde_json::Error> for Error {
fn from(_error: serde_json::Error) -> Error { fn from(e: serde_json::Error) -> Error {
Error { Error {
inner: Context::new(ErrorKind::SerdeJsonError), inner: Context::new(ErrorKind::SerdeJsonError(e.to_string())),
} }
} }
} }

View file

@ -5,10 +5,12 @@ use wallet::HttpWallet;
use clap::App; use clap::App;
use grin_util::{StopState, ZeroingString}; use grin_util::{StopState, ZeroingString};
use rpassword;
use std::env; use std::env;
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::runtime::Runtime;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
@ -28,91 +30,125 @@ mod wallet;
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
fn main() { fn main() {
if let Err(e) = real_main() { real_main().unwrap();
io::stderr().write_all(format!("mwixnet server exited with error:\n{}\n", e).as_bytes()).unwrap();
std::process::exit(1);
}
std::process::exit(0); std::process::exit(0);
} }
fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> { fn real_main() -> Result<(), Box<dyn std::error::Error>> {
let yml = load_yaml!("../mwixnet.yml"); let yml = load_yaml!("../mwixnet.yml");
let args = App::from_yaml(yml).get_matches(); let args = App::from_yaml(yml).get_matches();
let config_path = match args.value_of("config_file") {
Some(path) => PathBuf::from(path),
None => {
let mut current_dir = env::current_dir()?;
current_dir.push("mwixnet-config.toml");
current_dir
}
};
let password = args.value_of("pass").ok_or(Error::new(ErrorKind::LoadConfigError))?;
let password = ZeroingString::from(password);
let round_time = args.value_of("round_time").map(|t| t.parse::<u32>().unwrap() ); let config_path = match args.value_of("config_file") {
let bind_addr = args.value_of("bind_addr"); Some(path) => PathBuf::from(path),
let grin_node_url = args.value_of("grin_node_url"); None => {
let wallet_owner_url = args.value_of("wallet_owner_url"); let mut current_dir = env::current_dir()?;
current_dir.push("mwixnet-config.toml");
// Write a new config file if init-config command is supplied current_dir
if let ("init-config", Some(_)) = args.subcommand() { }
if config_path.exists() { };
panic!("Config file already exists at {}", config_path.to_string_lossy());
}
let server_config = ServerConfig { let round_time = args
key: secp::random_secret(), .value_of("round_time")
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL), .map(|t| t.parse::<u32>().unwrap());
addr: bind_addr.unwrap_or("0.0.0.0:3000").parse()?, let bind_addr = args.value_of("bind_addr");
grin_node_url: grin_node_url.unwrap_or("127.0.0.1:3413").parse()?, let grin_node_url = args.value_of("grin_node_url");
wallet_owner_url: wallet_owner_url.unwrap_or("127.0.0.1:3420").parse()?, let wallet_owner_url = args.value_of("wallet_owner_url");
};
config::write_config(&config_path, &server_config, &password)?; // Write a new config file if init-config command is supplied
println!("Config file written to {:?}. Please back this file up in a safe place.", config_path); if let ("init-config", Some(_)) = args.subcommand() {
return Ok(()); if config_path.exists() {
} panic!(
"Config file already exists at {}",
config_path.to_string_lossy()
);
}
let mut server_config = config::load_config(&config_path, &password)?; let server_config = ServerConfig {
key: secp::random_secret(),
interval_s: round_time.unwrap_or(DEFAULT_INTERVAL),
addr: bind_addr.unwrap_or("0.0.0.0:3000").parse()?,
grin_node_url: grin_node_url.unwrap_or("127.0.0.1:3413").parse()?,
wallet_owner_url: wallet_owner_url.unwrap_or("127.0.0.1:3420").parse()?,
};
// Override bind_addr, if supplied let password = prompt_password_confirm();
if let Some(bind_addr) = bind_addr { config::write_config(&config_path, &server_config, &password)?;
server_config.addr = bind_addr.parse()?; println!(
} "Config file written to {:?}. Please back this file up in a safe place.",
config_path
);
return Ok(());
}
// Override grin_node_url, if supplied let password = prompt_password();
if let Some(grin_node_url) = grin_node_url { let mut server_config = config::load_config(&config_path, &password)?;
server_config.grin_node_url = grin_node_url.parse()?;
}
// Override wallet_owner_url, if supplied // Override bind_addr, if supplied
if let Some(wallet_owner_url) = wallet_owner_url { if let Some(bind_addr) = bind_addr {
server_config.wallet_owner_url = wallet_owner_url.parse()?; server_config.addr = bind_addr.parse()?;
} }
// Open wallet // Override grin_node_url, if supplied
let wallet_pass = args.value_of("wallet_pass").ok_or(error::Error::new(error::ErrorKind::LoadConfigError))?; if let Some(grin_node_url) = grin_node_url {
let wallet_pass = grin_util::ZeroingString::from(wallet_pass); server_config.grin_node_url = grin_node_url.parse()?;
let wallet = HttpWallet::open_wallet(&server_config.wallet_owner_url, &wallet_pass)?; }
// Create GrinNode // Override wallet_owner_url, if supplied
let node = HttpGrinNode::new(&server_config.grin_node_url, &None); if let Some(wallet_owner_url) = wallet_owner_url {
server_config.wallet_owner_url = wallet_owner_url.parse()?;
let stop_state = Arc::new(StopState::new()); }
let stop_state_clone = stop_state.clone(); // Open wallet
std::thread::spawn(move || { let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
let shutdown_signal = async move { let wallet = HttpWallet::open_wallet(&server_config.wallet_owner_url, &wallet_pass)?;
// Wait for the CTRL+C signal
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
};
futures::executor::block_on(shutdown_signal);
stop_state_clone.stop();
});
server::listen(&server_config, Arc::new(wallet), Arc::new(node), &stop_state) // Create GrinNode
} let node = HttpGrinNode::new(&server_config.grin_node_url, &None);
let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone();
let rt = Runtime::new()?;
rt.spawn(async move {
let shutdown_signal = async move {
// Wait for the CTRL+C signal
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
};
futures::executor::block_on(shutdown_signal);
stop_state_clone.stop();
});
// Start the mwixnet server
server::listen(
&server_config,
Arc::new(wallet),
Arc::new(node),
&stop_state,
)
}
fn prompt_password() -> ZeroingString {
ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap())
}
fn prompt_password_confirm() -> ZeroingString {
let mut first = "first".to_string();
let mut second = "second".to_string();
while first != second {
first = rpassword::prompt_password_stdout("Server password: ").unwrap();
second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap();
}
ZeroingString::from(first)
}
fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
match *wallet_pass {
Some(wallet_pass) => ZeroingString::from(wallet_pass),
None => {
ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap())
}
}
}

View file

@ -13,184 +13,196 @@ use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
pub trait GrinNode : Send + Sync { pub trait GrinNode: Send + Sync {
/// Retrieves the unspent output with a matching commitment /// Retrieves the unspent output with a matching commitment
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>>; fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>>;
/// Gets the height of the chain tip /// Gets the height of the chain tip
fn get_chain_height(&self) -> Result<u64>; fn get_chain_height(&self) -> Result<u64>;
/// Posts a transaction to the grin node /// Posts a transaction to the grin node
fn post_tx(&self, tx: &Transaction) -> Result<()>; fn post_tx(&self, tx: &Transaction) -> Result<()>;
} }
/// Checks if a commitment is in the UTXO set /// Checks if a commitment is in the UTXO set
pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool> { pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool> {
let utxo = node.get_utxo(&commit)?; let utxo = node.get_utxo(&commit)?;
Ok(utxo.is_some()) Ok(utxo.is_some())
} }
/// 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(node: &Arc<dyn GrinNode>, output_commit: &Commitment, next_block_height: u64) -> Result<bool> { pub fn is_spendable(
let output = node.get_utxo(&output_commit)?; node: &Arc<dyn GrinNode>,
if let Some(out) = output { output_commit: &Commitment,
let is_coinbase = match out.output_type { next_block_height: u64,
OutputType::Coinbase => true, ) -> Result<bool> {
OutputType::Transaction => false, let output = node.get_utxo(&output_commit)?;
}; if let Some(out) = output {
let is_coinbase = match out.output_type {
OutputType::Coinbase => true,
OutputType::Transaction => false,
};
if is_coinbase { if is_coinbase {
if let Some(block_height) = out.block_height { if let Some(block_height) = out.block_height {
if block_height + COINBASE_MATURITY < next_block_height { if block_height + COINBASE_MATURITY < next_block_height {
return Ok(false); return Ok(false);
} }
} else { } else {
return Ok(false); return Ok(false);
} }
} }
return Ok(true); return Ok(true);
} }
Ok(false) Ok(false)
} }
/// Builds an input for an unspent output commitment /// Builds an input for an unspent output commitment
pub fn build_input(node: &Arc<dyn GrinNode>, output_commit: &Commitment) -> Result<Option<Input>> { pub fn build_input(node: &Arc<dyn GrinNode>, output_commit: &Commitment) -> Result<Option<Input>> {
let output = node.get_utxo(&output_commit)?; let output = node.get_utxo(&output_commit)?;
if let Some(out) = output { if let Some(out) = output {
let features = match out.output_type { let features = match out.output_type {
OutputType::Coinbase => OutputFeatures::Coinbase, OutputType::Coinbase => OutputFeatures::Coinbase,
OutputType::Transaction => OutputFeatures::Plain, OutputType::Transaction => OutputFeatures::Plain,
}; };
let input = Input::new(features, out.commit); let input = Input::new(features, out.commit);
return Ok(Some(input)); return Ok(Some(input));
} }
Ok(None) Ok(None)
} }
/// HTTP (JSONRPC) implementation of the 'GrinNode' trait /// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
#[derive(Clone)] #[derive(Clone)]
pub struct HttpGrinNode { pub struct HttpGrinNode {
node_url: SocketAddr, node_url: SocketAddr,
node_api_secret: Option<String>, node_api_secret: Option<String>,
} }
const ENDPOINT: &str = "/v2/foreign"; const ENDPOINT: &str = "/v2/foreign";
impl HttpGrinNode { impl HttpGrinNode {
pub fn new(node_url: &SocketAddr, node_api_secret: &Option<String>) -> HttpGrinNode { pub fn new(node_url: &SocketAddr, node_api_secret: &Option<String>) -> HttpGrinNode {
HttpGrinNode { HttpGrinNode {
node_url: node_url.to_owned(), node_url: node_url.to_owned(),
node_api_secret: node_api_secret.to_owned(), node_api_secret: node_api_secret.to_owned(),
} }
} }
fn send_json_request<D: serde::de::DeserializeOwned>( fn send_json_request<D: serde::de::DeserializeOwned>(
&self, &self,
method: &str, method: &str,
params: &serde_json::Value, params: &serde_json::Value,
) -> Result<D> { ) -> Result<D> {
let url = format!("http://{}{}", self.node_url, ENDPOINT); let url = format!("http://{}{}", self.node_url, ENDPOINT);
let req = build_request(method, params); let req = build_request(method, params);
let res = let res =
client::post::<Request, Response>(url.as_str(), self.node_api_secret.clone(), &req)?; client::post::<Request, Response>(url.as_str(), self.node_api_secret.clone(), &req)?;
let parsed = res.clone().into_result()?; let parsed = res.clone().into_result()?;
Ok(parsed) Ok(parsed)
} }
} }
impl GrinNode for HttpGrinNode { impl GrinNode for HttpGrinNode {
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>> { fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>> {
let commits : Vec<String> = vec![output_commit.to_hex()]; let commits: Vec<String> = vec![output_commit.to_hex()];
let start_height : Option<u64> = None; let start_height: Option<u64> = None;
let end_height : Option<u64> = None; let end_height: Option<u64> = None;
let include_proof : Option<bool> = Some(false); let include_proof: Option<bool> = Some(false);
let include_merkle_proof : Option<bool> = Some(false); let include_merkle_proof: Option<bool> = Some(false);
let params = json!([Some(commits), start_height, end_height, include_proof, include_merkle_proof]); let params = json!([
let outputs = self.send_json_request::<Vec<OutputPrintable>>("get_outputs", &params)?; Some(commits),
if outputs.is_empty() { start_height,
return Ok(None); end_height,
} include_proof,
include_merkle_proof
]);
let outputs = self.send_json_request::<Vec<OutputPrintable>>("get_outputs", &params)?;
if outputs.is_empty() {
return Ok(None);
}
Ok(Some(outputs[0].clone())) Ok(Some(outputs[0].clone()))
} }
fn get_chain_height(&self) -> Result<u64> { fn get_chain_height(&self) -> Result<u64> {
let params = json!([]); let params = json!([]);
let tip_json = self.send_json_request::<serde_json::Value>("get_tip", &params)?; let tip_json = self.send_json_request::<serde_json::Value>("get_tip", &params)?;
let tip: Result<Tip> = serde_json::from_value(tip_json["Ok"].clone()) let tip: Result<Tip> = serde_json::from_value(tip_json["Ok"].clone())
.map_err(|_| ErrorKind::SerdeJsonError.into()); .map_err(|e| ErrorKind::SerdeJsonError(e.to_string()).into());
Ok(tip?.height)
}
fn post_tx(&self, tx: &Transaction) -> Result<()> { Ok(tip?.height)
let params = json!([tx, true]); }
self.send_json_request::<serde_json::Value>("push_transaction", &params)?;
Ok(()) fn post_tx(&self, tx: &Transaction) -> Result<()> {
} let params = json!([tx, true]);
self.send_json_request::<serde_json::Value>("push_transaction", &params)?;
Ok(())
}
} }
/// Implementation of 'GrinNode' trait that mocks a grin node instance.
/// Use only for testing purposes.
pub struct MockGrinNode { pub struct MockGrinNode {
utxos: HashMap<Commitment, OutputPrintable>, utxos: HashMap<Commitment, OutputPrintable>,
txns_posted: RwLock<Vec<Transaction>>, txns_posted: RwLock<Vec<Transaction>>,
} }
impl MockGrinNode { impl MockGrinNode {
pub fn new() -> MockGrinNode { pub fn new() -> MockGrinNode {
MockGrinNode { MockGrinNode {
utxos: HashMap::new(), utxos: HashMap::new(),
txns_posted: RwLock::new(Vec::new()), txns_posted: RwLock::new(Vec::new()),
} }
} }
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());
} }
pub fn add_default_utxo(&mut self, output_commit: &Commitment) { pub fn add_default_utxo(&mut self, output_commit: &Commitment) {
let utxo = OutputPrintable{ let utxo = OutputPrintable {
output_type: OutputType::Transaction, output_type: OutputType::Transaction,
commit: output_commit.to_owned(), commit: output_commit.to_owned(),
spent: false, spent: false,
proof: None, proof: None,
proof_hash: String::from(""), proof_hash: String::from(""),
block_height: None, block_height: None,
merkle_proof: None, merkle_proof: None,
mmr_index: 0 mmr_index: 0,
}; };
self.add_utxo(&output_commit, &utxo); self.add_utxo(&output_commit, &utxo);
} }
pub fn get_posted_txns(&self) -> Vec<Transaction> { pub fn get_posted_txns(&self) -> Vec<Transaction> {
let read = self.txns_posted.read().unwrap(); let read = self.txns_posted.read().unwrap();
read.clone() read.clone()
} }
} }
impl GrinNode for MockGrinNode { impl GrinNode for MockGrinNode {
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>> { fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>> {
if let Some(utxo) = self.utxos.get(&output_commit) { if let Some(utxo) = self.utxos.get(&output_commit) {
return Ok(Some(utxo.clone())); return Ok(Some(utxo.clone()));
} }
Ok(None) Ok(None)
} }
fn get_chain_height(&self) -> Result<u64> { fn get_chain_height(&self) -> Result<u64> {
Ok(100) Ok(100)
} }
fn post_tx(&self, tx: &Transaction) -> Result<()> { fn post_tx(&self, tx: &Transaction) -> Result<()> {
let mut write = self.txns_posted.write().unwrap(); let mut write = self.txns_posted.write().unwrap();
write.push(tx.clone()); write.push(tx.clone());
Ok(()) Ok(())
} }
} }

View file

@ -2,13 +2,13 @@ use crate::error::Result;
use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
use crate::types::Payload; use crate::types::Payload;
use chacha20::{ChaCha20, Key, Nonce};
use chacha20::cipher::{NewCipher, StreamCipher}; use chacha20::cipher::{NewCipher, StreamCipher};
use chacha20::{ChaCha20, Key, Nonce};
use grin_core::ser::{self, ProtocolVersion, Writeable, Writer}; use grin_core::ser::{self, ProtocolVersion, Writeable, Writer};
use grin_util::{self, ToHex}; use grin_util::{self, ToHex};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use serde::{Deserialize};
use serde::ser::SerializeStruct; use serde::ser::SerializeStruct;
use serde::Deserialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::fmt; use std::fmt;
@ -17,296 +17,321 @@ type RawBytes = Vec<u8>;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Onion { pub struct Onion {
pub ephemeral_pubkey: PublicKey, pub ephemeral_pubkey: PublicKey,
pub commit: Commitment, pub commit: Commitment,
pub enc_payloads: Vec<RawBytes>, pub enc_payloads: Vec<RawBytes>,
} }
impl Onion { impl Onion {
pub fn serialize(&self) -> Result<Vec<u8>> { pub fn serialize(&self) -> Result<Vec<u8>> {
let mut vec = vec![]; let mut vec = vec![];
ser::serialize_default(&mut vec, &self)?; ser::serialize_default(&mut vec, &self)?;
Ok(vec) Ok(vec)
} }
/// 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)> { pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion)> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let shared_secret = SharedSecret::new(&secp, &self.ephemeral_pubkey, &secret_key); let shared_secret = SharedSecret::new(&secp, &self.ephemeral_pubkey, &secret_key);
let mut cipher = new_stream_cipher(&shared_secret)?; let mut cipher = new_stream_cipher(&shared_secret)?;
let mut decrypted_bytes = self.enc_payloads[0].clone(); let mut decrypted_bytes = self.enc_payloads[0].clone();
cipher.apply_keystream(&mut decrypted_bytes); cipher.apply_keystream(&mut decrypted_bytes);
let decrypted_payload = Payload::deserialize(&decrypted_bytes)?; let decrypted_payload = Payload::deserialize(&decrypted_bytes)?;
let enc_payloads : Vec<RawBytes> = self.enc_payloads.iter() let enc_payloads: Vec<RawBytes> = self
.enumerate() .enc_payloads
.filter(|&(i, _)| i != 0) .iter()
.map(|(_, enc_payload)| { .enumerate()
let mut p = enc_payload.clone(); .filter(|&(i, _)| i != 0)
cipher.apply_keystream(&mut p); .map(|(_, enc_payload)| {
p let mut p = enc_payload.clone();
}) cipher.apply_keystream(&mut p);
.collect(); p
})
let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?; .collect();
let mut ephemeral_pubkey = self.ephemeral_pubkey.clone(); let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?;
ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?;
let mut ephemeral_pubkey = self.ephemeral_pubkey.clone();
let mut commitment = self.commit.clone(); ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?;
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?;
commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())?; let mut commitment = self.commit.clone();
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?;
let peeled_onion = Onion{ commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())?;
ephemeral_pubkey: ephemeral_pubkey,
commit: commitment.clone(), let peeled_onion = Onion {
enc_payloads: enc_payloads, ephemeral_pubkey,
}; commit: commitment.clone(),
Ok((decrypted_payload, peeled_onion)) enc_payloads,
} };
Ok((decrypted_payload, peeled_onion))
}
} }
fn calc_blinding_factor(shared_secret: &SharedSecret, ephemeral_pubkey: &PublicKey) -> Result<SecretKey> { fn calc_blinding_factor(
let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?; shared_secret: &SharedSecret,
ephemeral_pubkey: &PublicKey,
) -> Result<SecretKey> {
let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?;
let mut hasher = Sha256::default(); let mut hasher = Sha256::default();
hasher.update(&serialized_pubkey); hasher.update(&serialized_pubkey);
hasher.update(&shared_secret[0..32]); hasher.update(&shared_secret[0..32]);
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let blind = SecretKey::from_slice(&secp, &hasher.finalize())?; let blind = SecretKey::from_slice(&secp, &hasher.finalize())?;
Ok(blind) Ok(blind)
} }
fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20> { fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20> {
let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?;
mu_hmac.update(&shared_secret[0..32]); mu_hmac.update(&shared_secret[0..32]);
let mukey = mu_hmac.finalize().into_bytes(); let mukey = mu_hmac.finalize().into_bytes();
let key = Key::from_slice(&mukey[0..32]); let key = Key::from_slice(&mukey[0..32]);
let nonce = Nonce::from_slice(b"NONCE1234567"); let nonce = Nonce::from_slice(b"NONCE1234567");
Ok(ChaCha20::new(&key, &nonce)) Ok(ChaCha20::new(&key, &nonce))
} }
impl Writeable for Onion { impl Writeable for Onion {
fn write<W: Writer>(&self, writer: &mut W) -> std::result::Result<(), ser::Error> { fn write<W: Writer>(&self, writer: &mut W) -> std::result::Result<(), ser::Error> {
self.ephemeral_pubkey.write(writer)?; self.ephemeral_pubkey.write(writer)?;
writer.write_fixed_bytes(&self.commit)?; writer.write_fixed_bytes(&self.commit)?;
writer.write_u64(self.enc_payloads.len() as u64)?; writer.write_u64(self.enc_payloads.len() as u64)?;
for p in &self.enc_payloads { for p in &self.enc_payloads {
writer.write_u64(p.len() as u64)?; writer.write_u64(p.len() as u64)?;
p.write(writer)?; p.write(writer)?;
} }
Ok(()) Ok(())
} }
} }
impl serde::ser::Serialize for Onion { impl serde::ser::Serialize for Onion {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where where
S: serde::ser::Serializer, S: serde::ser::Serializer,
{ {
let mut state = serializer.serialize_struct("Onion", 3)?; let mut state = serializer.serialize_struct("Onion", 3)?;
let secp = Secp256k1::new(); let secp = Secp256k1::new();
state.serialize_field("pubkey", &self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex())?; state.serialize_field(
state.serialize_field("commit", &self.commit.to_hex())?; "pubkey",
&self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex(),
)?;
state.serialize_field("commit", &self.commit.to_hex())?;
let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect(); let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect();
state.serialize_field("data", &hex_payloads)?; state.serialize_field("data", &hex_payloads)?;
state.end() state.end()
} }
} }
impl<'de> serde::de::Deserialize<'de> for Onion { impl<'de> serde::de::Deserialize<'de> for Onion {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where where
D: serde::de::Deserializer<'de>, D: serde::de::Deserializer<'de>,
{ {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")] #[serde(field_identifier, rename_all = "snake_case")]
enum Field { enum Field {
Pubkey, Pubkey,
Commit, Commit,
Data Data,
} }
struct OnionVisitor; struct OnionVisitor;
impl<'de> serde::de::Visitor<'de> for OnionVisitor { impl<'de> serde::de::Visitor<'de> for OnionVisitor {
type Value = Onion; type Value = Onion;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an Onion") formatter.write_str("an Onion")
} }
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error> fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where where
A: serde::de::MapAccess<'de>, A: serde::de::MapAccess<'de>,
{ {
let mut pubkey = None; let mut pubkey = None;
let mut commit = None; let mut commit = None;
let mut data = None; let mut data = None;
while let Some(key) = map.next_key()? { while let Some(key) = map.next_key()? {
match key { match key {
Field::Pubkey => { Field::Pubkey => {
let val: String = map.next_value()?; let val: String = map.next_value()?;
let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; let vec =
let secp = Secp256k1::new(); grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
pubkey = Some(PublicKey::from_slice(&secp, &vec[..]).map_err(serde::de::Error::custom)?); let secp = Secp256k1::new();
} pubkey = Some(
Field::Commit => { PublicKey::from_slice(&secp, &vec[..])
let val: String = map.next_value()?; .map_err(serde::de::Error::custom)?,
let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; );
commit = Some(Commitment::from_vec(vec)); }
} Field::Commit => {
Field::Data => { let val: String = map.next_value()?;
let val: Vec<String> = map.next_value()?; let vec =
let mut vec: Vec<Vec<u8>> = Vec::new(); grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
for hex in val { commit = Some(Commitment::from_vec(vec));
vec.push(grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?); }
} Field::Data => {
data = Some(vec); let val: Vec<String> = map.next_value()?;
} let mut vec: Vec<Vec<u8>> = Vec::new();
} for hex in val {
} vec.push(
grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?,
);
}
data = Some(vec);
}
}
}
Ok(Onion { Ok(Onion {
ephemeral_pubkey: pubkey.unwrap(), ephemeral_pubkey: pubkey.unwrap(),
commit: commit.unwrap(), commit: commit.unwrap(),
enc_payloads: data.unwrap(), enc_payloads: data.unwrap(),
}) })
} }
} }
const FIELDS: &[&str] = &[ const FIELDS: &[&str] = &["pubkey", "commit", "data"];
"pubkey", deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor)
"commit", }
"data"
];
deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor)
}
} }
#[cfg(test)] #[cfg(test)]
pub mod test_util { pub mod test_util {
use super::{Onion, RawBytes}; use super::{Onion, RawBytes};
use crate::error::Result; use crate::error::Result;
use crate::types::{Payload}; use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; use crate::types::Payload;
use chacha20::cipher::{StreamCipher}; use chacha20::cipher::StreamCipher;
pub struct Hop {
pub pubkey: PublicKey,
pub payload: Payload,
}
/// Create an Onion for the Commitment, encrypting the payload for each hop pub struct Hop {
pub fn create_onion(commitment: &Commitment, session_key: &SecretKey, hops: &Vec<Hop>) -> Result<Onion> { pub pubkey: PublicKey,
let secp = Secp256k1::new(); pub payload: Payload,
let mut ephemeral_key = session_key.clone(); }
let mut shared_secrets: Vec<SharedSecret> = Vec::new(); /// Create an Onion for the Commitment, encrypting the payload for each hop
let mut enc_payloads: Vec<RawBytes> = Vec::new(); pub fn create_onion(
for hop in hops { commitment: &Commitment,
let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key); session_key: &SecretKey,
hops: &Vec<Hop>,
let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)?; ) -> Result<Onion> {
let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?; let secp = Secp256k1::new();
let mut ephemeral_key = session_key.clone();
shared_secrets.push(shared_secret);
enc_payloads.push(hop.payload.serialize()?); let mut shared_secrets: Vec<SharedSecret> = Vec::new();
ephemeral_key.mul_assign(&secp, &blinding_factor)?; let mut enc_payloads: Vec<RawBytes> = Vec::new();
} for hop in hops {
let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key);
for i in (0..shared_secrets.len()).rev() {
let mut cipher = super::new_stream_cipher(&shared_secrets[i])?; let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)?;
for j in i..shared_secrets.len() { let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?;
cipher.apply_keystream(&mut enc_payloads[j]);
} shared_secrets.push(shared_secret);
} enc_payloads.push(hop.payload.serialize()?);
ephemeral_key.mul_assign(&secp, &blinding_factor)?;
let onion = Onion{ }
ephemeral_pubkey: PublicKey::from_secret_key(&secp, session_key)?,
commit: commitment.clone(), for i in (0..shared_secrets.len()).rev() {
enc_payloads: enc_payloads, let mut cipher = super::new_stream_cipher(&shared_secrets[i])?;
}; for j in i..shared_secrets.len() {
Ok(onion) cipher.apply_keystream(&mut enc_payloads[j]);
} }
}
let onion = Onion {
ephemeral_pubkey: PublicKey::from_secret_key(&secp, session_key)?,
commit: commitment.clone(),
enc_payloads,
};
Ok(onion)
}
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::test_util::{self, Hop}; use super::test_util::{self, Hop};
use crate::types::{Payload}; use crate::secp;
use crate::secp; use crate::types::Payload;
use grin_core::core::FeeFields; use grin_core::core::FeeFields;
/// Test end-to-end Onion creation and unwrapping logic. /// Test end-to-end Onion creation and unwrapping logic.
#[test] #[test]
fn onion() { fn onion() {
let total_fee : u64 = 10; let total_fee: u64 = 10;
let fee_per_hop : u64 = 2; let fee_per_hop: u64 = 2;
let in_value : u64 = 1000; let in_value: u64 = 1000;
let out_value : u64 = in_value - total_fee; let out_value: u64 = in_value - total_fee;
let blind = secp::random_secret(); let blind = secp::random_secret();
let commitment = secp::commit(in_value, &blind).unwrap(); let commitment = secp::commit(in_value, &blind).unwrap();
let session_key = secp::random_secret(); let session_key = secp::random_secret();
let mut hops : Vec<Hop> = Vec::new(); let mut hops: Vec<Hop> = Vec::new();
let mut keys : Vec<secp::SecretKey> = Vec::new(); let mut keys: Vec<secp::SecretKey> = Vec::new();
let mut final_commit = secp::commit(out_value, &blind).unwrap(); let mut final_commit = secp::commit(out_value, &blind).unwrap();
let mut final_blind = blind.clone(); let mut final_blind = blind.clone();
for i in 0..5 { for i in 0..5 {
keys.push(secp::random_secret()); keys.push(secp::random_secret());
let excess = secp::random_secret(); let excess = secp::random_secret();
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
final_blind.add_assign(&secp, &excess).unwrap(); final_blind.add_assign(&secp, &excess).unwrap();
final_commit = secp::add_excess(&final_commit, &excess).unwrap(); final_commit = secp::add_excess(&final_commit, &excess).unwrap();
let proof = if i == 4 { let proof = if i == 4 {
let n1 = secp::random_secret(); let n1 = secp::random_secret();
let rp = secp.bullet_proof(out_value, final_blind.clone(), n1.clone(), n1.clone(), None, None); let rp = secp.bullet_proof(
assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok()); out_value,
Some(rp) final_blind.clone(),
} else { n1.clone(),
None n1.clone(),
}; None,
None,
hops.push(Hop{ );
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(), assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
payload: Payload{ Some(rp)
excess: excess, } else {
fee: FeeFields::from(fee_per_hop as u32), None
rangeproof: proof, };
}
}); hops.push(Hop {
} pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
payload: Payload {
let mut onion_packet = test_util::create_onion(&commitment, &session_key, &hops).unwrap(); excess,
fee: FeeFields::from(fee_per_hop as u32),
let mut payload = Payload{ rangeproof: proof,
excess: secp::random_secret(), },
fee: FeeFields::from(fee_per_hop as u32), });
rangeproof: None }
};
for i in 0..5 { let mut onion_packet = test_util::create_onion(&commitment, &session_key, &hops).unwrap();
let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
payload = peeled.0; let mut payload = Payload {
onion_packet = peeled.1; excess: secp::random_secret(),
} fee: FeeFields::from(fee_per_hop as u32),
rangeproof: None,
assert!(payload.rangeproof.is_some()); };
assert_eq!(payload.rangeproof.unwrap(), hops[4].payload.rangeproof.unwrap()); for i in 0..5 {
assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit); let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32)); payload = peeled.0;
} onion_packet = peeled.1;
} }
assert!(payload.rangeproof.is_some());
assert_eq!(
payload.rangeproof.unwrap(),
hops[4].payload.rangeproof.unwrap()
);
assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit);
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32));
}
}

View file

@ -1,9 +1,12 @@
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
pub use secp256k1zkp::aggsig; 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::ecdh::SharedSecret;
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY}; pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
pub use secp256k1zkp::constants::{AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE, SECRET_KEY_SIZE}; pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
use crate::error::{Error, ErrorKind, Result}; use crate::error::{Error, ErrorKind, Result};
@ -14,206 +17,211 @@ use secp256k1zkp::rand::thread_rng;
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys /// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
pub struct ComSignature { pub struct ComSignature {
pub_nonce: Commitment, pub_nonce: Commitment,
s: SecretKey, s: SecretKey,
t: SecretKey, t: SecretKey,
} }
impl ComSignature { impl ComSignature {
pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
ComSignature { ComSignature {
pub_nonce: pub_nonce.to_owned(), pub_nonce: pub_nonce.to_owned(),
s: s.to_owned(), s: s.to_owned(),
t: t.to_owned(), t: t.to_owned(),
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec<u8>) -> Result<ComSignature> { pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec<u8>) -> Result<ComSignature> {
let secp = Secp256k1::with_caps(ContextFlag::Commit); let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut amt_bytes = [0; 32]; let mut amt_bytes = [0; 32];
BigEndian::write_u64(&mut amt_bytes[24..32], amount); BigEndian::write_u64(&mut amt_bytes[24..32], amount);
let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; 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 k_1 = SecretKey::new(&secp, &mut thread_rng());
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?; let k_2 = SecretKey::new(&secp, &mut thread_rng());
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?; let commitment = secp.commit(amount, blind.clone())?;
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
// s = k_1 + (e * amount) let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
let mut s = k_amt.clone();
s.mul_assign(&secp, &e)?;
s.add_assign(&secp, &k_1)?;
// t = k_2 + (e * blind) // s = k_1 + (e * amount)
let mut t = blind.clone(); let mut s = k_amt.clone();
t.mul_assign(&secp, &e)?; s.mul_assign(&secp, &e)?;
t.add_assign(&secp, &k_2)?; s.add_assign(&secp, &k_1)?;
Ok(ComSignature::new(&nonce_commitment, &s, &t)) // t = k_2 + (e * blind)
} let mut t = blind.clone();
t.mul_assign(&secp, &e)?;
t.add_assign(&secp, &k_2)?;
#[allow(non_snake_case)] Ok(ComSignature::new(&nonce_commitment, &s, &t))
pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<()> { }
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?; #[allow(non_snake_case)]
pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<()> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut Ce = commit.to_pubkey(&secp)?; let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
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 mut Ce = commit.to_pubkey(&secp)?;
let S2 = secp.commit_sum(commits, Vec::new())?; let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
Ce.mul_assign(&secp, &e)?;
if S1 != S2 { let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
return Err(Error::new(ErrorKind::InvalidSigError)); let S2 = secp.commit_sum(commits, Vec::new())?;
}
Ok(()) if S1 != S2 {
} return Err(Error::new(ErrorKind::InvalidSigError));
}
fn calc_challenge(secp: &Secp256k1, commit: &Commitment, nonce_commit: &Commitment, msg: &Vec<u8>) -> Result<SecretKey> { Ok(())
let mut challenge_hasher = Blake2b::new(32); }
challenge_hasher.update(&commit.0);
challenge_hasher.update(&nonce_commit.0); fn calc_challenge(
challenge_hasher.update(msg); secp: &Secp256k1,
commit: &Commitment,
nonce_commit: &Commitment,
msg: &Vec<u8>,
) -> Result<SecretKey> {
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]; let mut challenge = [0; 32];
challenge.copy_from_slice(challenge_hasher.finalize().as_bytes()); challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
Ok(SecretKey::from_slice(&secp, &challenge)?) Ok(SecretKey::from_slice(&secp, &challenge)?)
} }
} }
/// Serializes a ComSignature to and from hex /// Serializes a ComSignature to and from hex
pub mod comsig_serde { pub mod comsig_serde {
use super::ComSignature; use super::ComSignature;
use grin_core::ser::{self, ProtocolVersion}; use grin_core::ser::{self, ProtocolVersion};
use serde::{Deserialize, Serializer}; use grin_util::ToHex;
use grin_util::ToHex; use serde::{Deserialize, Serializer};
/// Serializes a ComSignature as a hex string /// Serializes a ComSignature as a hex string
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error> pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
use serde::ser::Error; use serde::ser::Error;
let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
serializer.serialize_str(&bytes.to_hex()) serializer.serialize_str(&bytes.to_hex())
} }
/// Creates a ComSignature from a hex string /// Creates a ComSignature from a hex string
pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<ComSignature, D::Error> pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<ComSignature, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
use serde::de::Error; use serde::de::Error;
let bytes = String::deserialize(deserializer) let bytes = String::deserialize(deserializer)
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
let sig: ComSignature = grin_core::ser::deserialize_default(&mut &bytes[..]) let sig: ComSignature =
.map_err(Error::custom)?; grin_core::ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
Ok(sig) Ok(sig)
} }
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
impl Readable for ComSignature { impl Readable for ComSignature {
fn read<R: Reader>(reader: &mut R) -> std::result::Result<Self, ser::Error> { fn read<R: Reader>(reader: &mut R) -> std::result::Result<Self, ser::Error> {
let R = Commitment::read(reader)?; let R = Commitment::read(reader)?;
let s = read_secret_key(reader)?; let s = read_secret_key(reader)?;
let t = read_secret_key(reader)?; let t = read_secret_key(reader)?;
Ok(ComSignature::new(&R, &s, &t)) Ok(ComSignature::new(&R, &s, &t))
} }
} }
impl Writeable for ComSignature { impl Writeable for ComSignature {
fn write<W: Writer>(&self, writer: &mut W) -> std::result::Result<(), ser::Error> { fn write<W: Writer>(&self, writer: &mut W) -> std::result::Result<(), ser::Error> {
writer.write_fixed_bytes(self.pub_nonce.0)?; writer.write_fixed_bytes(self.pub_nonce.0)?;
writer.write_fixed_bytes(self.s.0)?; writer.write_fixed_bytes(self.s.0)?;
writer.write_fixed_bytes(self.t.0)?; writer.write_fixed_bytes(self.t.0)?;
Ok(()) Ok(())
} }
} }
/// Generate a random SecretKey. /// Generate a random SecretKey.
pub fn random_secret() -> SecretKey { pub fn random_secret() -> SecretKey {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
SecretKey::new(&secp, &mut thread_rng()) SecretKey::new(&secp, &mut thread_rng())
} }
/// Deserialize a SecretKey from a Reader /// Deserialize a SecretKey from a Reader
pub fn read_secret_key<R: Reader>(reader: &mut R) -> std::result::Result<SecretKey, ser::Error> { pub fn read_secret_key<R: Reader>(reader: &mut R) -> std::result::Result<SecretKey, ser::Error> {
let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?;
let secp = Secp256k1::with_caps(ContextFlag::None); let secp = Secp256k1::with_caps(ContextFlag::None);
let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?;
Ok(pk) Ok(pk)
} }
/// Build a Pedersen Commitment using the provided value and blinding factor /// Build a Pedersen Commitment using the provided value and blinding factor
pub fn commit(value: u64, blind: &SecretKey) -> Result<Commitment> { pub fn commit(value: u64, blind: &SecretKey) -> Result<Commitment> {
let secp = Secp256k1::with_caps(ContextFlag::Commit); let secp = Secp256k1::with_caps(ContextFlag::Commit);
let commit = secp.commit(value, blind.clone())?; let commit = secp.commit(value, blind.clone())?;
Ok(commit) Ok(commit)
} }
/// Add a blinding factor to an existing Commitment /// Add a blinding factor to an existing Commitment
pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result<Commitment> { pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result<Commitment> {
let secp = Secp256k1::with_caps(ContextFlag::Commit); let secp = Secp256k1::with_caps(ContextFlag::Commit);
let excess_commit : Commitment = secp.commit(0, excess.clone())?; let excess_commit: Commitment = secp.commit(0, excess.clone())?;
let commits = vec![commitment.clone(), excess_commit.clone()]; let commits = vec![commitment.clone(), excess_commit.clone()];
let sum = secp.commit_sum(commits, Vec::new())?; let sum = secp.commit_sum(commits, Vec::new())?;
Ok(sum) Ok(sum)
} }
/// Subtracts a value (v*H) from an existing commitment /// Subtracts a value (v*H) from an existing commitment
pub fn sub_value(commitment: &Commitment, value: u64) -> Result<Commitment> { pub fn sub_value(commitment: &Commitment, value: u64) -> Result<Commitment> {
let secp = Secp256k1::with_caps(ContextFlag::Commit); let secp = Secp256k1::with_caps(ContextFlag::Commit);
let neg_commit : Commitment = secp.commit(value, ZERO_KEY)?; let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?;
let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?;
Ok(sum) Ok(sum)
} }
/// Signs the message with the provided SecretKey /// Signs the message with the provided SecretKey
pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature> { pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature> {
let secp = Secp256k1::with_caps(ContextFlag::Full); let secp = Secp256k1::with_caps(ContextFlag::Full);
let pubkey = PublicKey::from_secret_key(&secp, &sk)?; let pubkey = PublicKey::from_secret_key(&secp, &sk)?;
let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?; let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?;
Ok(sig) Ok(sig)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ComSignature, ContextFlag, Secp256k1, SecretKey}; use super::{ComSignature, ContextFlag, Secp256k1, SecretKey};
use crate::error::Result; use crate::error::Result;
use secp256k1zkp::rand::{RngCore, thread_rng}; use rand::Rng;
use rand::Rng; use secp256k1zkp::rand::{thread_rng, RngCore};
/// Test signing and verification of ComSignatures
#[test]
fn verify_comsig() -> Result<()> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let amount = thread_rng().next_u64(); /// Test signing and verification of ComSignatures
let blind = SecretKey::new(&secp, &mut thread_rng()); #[test]
fn verify_comsig() -> Result<()> {
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 msg: [u8; 16] = rand::thread_rng().gen();
let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?; let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
let commit = secp.commit(amount, blind.clone())?; let commit = secp.commit(amount, blind.clone())?;
assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
let wrong_msg: [u8; 16] = rand::thread_rng().gen(); let wrong_msg: [u8; 16] = rand::thread_rng().gen();
assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
Ok(()) Ok(())
} }
} }

View file

@ -1,427 +1,492 @@
use crate::config::ServerConfig; use crate::config::ServerConfig;
use crate::node::{self, GrinNode}; use crate::node::{self, GrinNode};
use crate::onion::Onion; use crate::onion::Onion;
use crate::secp::{self, Commitment, ComSignature, RangeProof, Secp256k1, SecretKey}; use crate::secp::{self, ComSignature, Commitment, RangeProof, Secp256k1, SecretKey};
use crate::wallet::{self, Wallet}; use crate::wallet::{self, Wallet};
use grin_core::core::{Input, Output, OutputFeatures, TransactionBody}; use grin_core::core::{Input, Output, OutputFeatures, TransactionBody};
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
use grin_util::StopState; use grin_util::StopState;
use itertools::Itertools; use itertools::Itertools;
use jsonrpc_derive::rpc;
use jsonrpc_http_server::*;
use jsonrpc_http_server::jsonrpc_core::*;
use jsonrpc_core::{Result, Value}; use jsonrpc_core::{Result, Value};
use jsonrpc_derive::rpc;
use jsonrpc_http_server::jsonrpc_core::*;
use jsonrpc_http_server::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct Submission { struct Submission {
excess: SecretKey, excess: SecretKey,
output_commit: Commitment, output_commit: Commitment,
rangeproof: Option<RangeProof>, rangeproof: Option<RangeProof>,
input: Input, input: Input,
fee: u64, fee: u64,
onion: Onion, onion: Onion,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct SwapReq { pub struct SwapReq {
onion: Onion, onion: Onion,
#[serde(with = "secp::comsig_serde")] #[serde(with = "secp::comsig_serde")]
comsig: ComSignature, comsig: ComSignature,
} }
lazy_static! { lazy_static! {
static ref SERVER_STATE: Mutex<HashMap<Commitment, Submission>> = Mutex::new(HashMap::new()); static ref SERVER_STATE: Mutex<HashMap<Commitment, Submission>> = Mutex::new(HashMap::new());
} }
#[rpc(server)] #[rpc(server)]
pub trait Server { pub trait Server {
#[rpc(name = "swap")] #[rpc(name = "swap")]
fn swap(&self, swap: SwapReq) -> Result<Value>; fn swap(&self, swap: SwapReq) -> Result<Value>;
// milestone 3: Used by mwixnet coinswap servers to communicate with each other // milestone 3: Used by mwixnet coinswap servers to communicate with each other
// fn derive_outputs(&self, entries: Vec<Onion>) -> Result<Value>; // fn derive_outputs(&self, entries: Vec<Onion>) -> Result<Value>;
// fn derive_kernel(&self, tx: Tx) -> Result<Value>; // fn derive_kernel(&self, tx: Tx) -> Result<Value>;
} }
#[derive(Clone)] #[derive(Clone)]
struct ServerImpl { struct ServerImpl {
server_config: ServerConfig, server_config: ServerConfig,
stop_state: Arc<StopState>, stop_state: Arc<StopState>,
wallet: Arc<dyn Wallet>, wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>, node: Arc<dyn GrinNode>,
} }
impl ServerImpl { impl ServerImpl {
fn new(server_config: ServerConfig, stop_state: Arc<StopState>, wallet: Arc<dyn Wallet>, node: Arc<dyn GrinNode>) -> Self { fn new(
ServerImpl { server_config, stop_state, wallet, node } server_config: ServerConfig,
} stop_state: Arc<StopState>,
wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
) -> Self {
ServerImpl {
server_config,
stop_state,
wallet,
node,
}
}
/// The fee base to use. For now, just using the default. /// The fee base to use. For now, just using the default.
fn get_fee_base(&self) -> u64 { fn get_fee_base(&self) -> u64 {
DEFAULT_ACCEPT_FEE_BASE DEFAULT_ACCEPT_FEE_BASE
} }
/// 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 mwixnet 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()
} }
/// 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. /// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes.
fn execute_round(&self) -> crate::error::Result<()> { fn execute_round(&self) -> crate::error::Result<()> {
let mut locked_state = SERVER_STATE.lock().unwrap(); let mut locked_state = SERVER_STATE.lock().unwrap();
let next_block_height = self.node.get_chain_height()? + 1; let next_block_height = self.node.get_chain_height()? + 1;
let spendable : Vec<Submission> = locked_state let spendable: Vec<Submission> = locked_state
.values() .values()
.into_iter() .into_iter()
.unique_by(|s| s.output_commit) .unique_by(|s| s.output_commit)
.filter(|s| node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)) .filter(|s| {
.filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true)) node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)
.cloned() })
.collect(); .filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true))
.cloned()
.collect();
if spendable.len() == 0 { if spendable.len() == 0 {
return Ok(()) return Ok(());
} }
let total_fee : u64 = spendable let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum();
.iter()
.enumerate() let inputs: Vec<Input> = spendable.iter().enumerate().map(|(_, s)| s.input).collect();
.map(|(_, s)| s.fee)
.sum(); let outputs: Vec<Output> = spendable
.iter()
let inputs : Vec<Input> = spendable .enumerate()
.iter() .map(|(_, s)| {
.enumerate() Output::new(
.map(|(_, s)| s.input) OutputFeatures::Plain,
.collect(); s.output_commit,
s.rangeproof.unwrap(),
let outputs : Vec<Output> = spendable )
.iter() })
.enumerate() .collect();
.map(|(_, s)| Output::new(OutputFeatures::Plain, s.output_commit, s.rangeproof.unwrap()))
.collect(); let excesses: Vec<SecretKey> = spendable
.iter()
let excesses : Vec<SecretKey> = spendable .enumerate()
.iter() .map(|(_, s)| s.excess.clone())
.enumerate() .collect();
.map(|(_, s)| s.excess.clone())
.collect(); let tx = wallet::assemble_tx(
&self.wallet,
let tx = wallet::assemble_tx(&self.wallet, &inputs, &outputs, self.get_fee_base(), total_fee, &excesses)?; &inputs,
&outputs,
self.node.post_tx(&tx)?; self.get_fee_base(),
locked_state.clear(); total_fee,
&excesses,
Ok(()) )?;
}
self.node.post_tx(&tx)?;
locked_state.clear();
Ok(())
}
} }
impl Server for ServerImpl { impl Server for ServerImpl {
/// Implements the 'swap' API /// Implements the 'swap' API
fn swap(&self, swap: SwapReq) -> Result<Value> { fn swap(&self, swap: SwapReq) -> Result<Value> {
// milestone 3: check that enc_payloads length matches number of configured servers // milestone 3: check that enc_payloads length matches number of configured servers
if swap.onion.enc_payloads.len() != 1 { if swap.onion.enc_payloads.len() != 1 {
return Err(jsonrpc_core::Error::invalid_params("Multi server not supported until milestone 3")); return Err(Error::invalid_params(
} "Multi server not supported until milestone 3",
));
}
// Verify commitment signature to ensure caller owns the output // Verify commitment signature to ensure caller owns the output
let serialized_onion = swap.onion.serialize() let serialized_onion = swap.onion.serialize().map_err(|e| Error {
.map_err(|_| jsonrpc_core::Error::internal_error())?; message: e.to_string(),
let _ = swap.comsig.verify(&swap.onion.commit, &serialized_onion) code: ErrorCode::InternalError,
.map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?; data: None,
})?;
let _ = swap
.comsig
.verify(&swap.onion.commit, &serialized_onion)
.map_err(|_| Error::invalid_params("ComSignature invalid"))?;
// Verify that commitment is unspent // Verify that commitment is unspent
let input = node::build_input(&self.node, &swap.onion.commit) let input = node::build_input(&self.node, &swap.onion.commit).map_err(|e| Error {
.map_err(|_| jsonrpc_core::Error::internal_error())?; message: e.to_string(),
let input = input.ok_or(jsonrpc_core::Error::invalid_params("Commitment not found"))?; code: ErrorCode::InternalError,
data: None,
let peeled = swap.onion.peel_layer(&self.server_config.key) })?;
.map_err(|e| jsonrpc_core::Error::invalid_params(e.message()))?; let input = input.ok_or(Error::invalid_params("Commitment not found"))?;
// Verify the fee meets the minimum let peeled = swap
let fee: u64 = peeled.0.fee.into(); .onion
if fee < self.get_minimum_swap_fee() { .peel_layer(&self.server_config.key)
return Err(jsonrpc_core::Error::invalid_params("Fee does not meet minimum")); .map_err(|e| Error::invalid_params(e.message()))?;
}
// Calculate final output commitment // Verify the fee meets the minimum
let output_commit = secp::add_excess(&swap.onion.commit, &peeled.0.excess) let fee: u64 = peeled.0.fee.into();
.map_err(|_| jsonrpc_core::Error::internal_error())?; if fee < self.get_minimum_swap_fee() {
let output_commit = secp::sub_value(&output_commit, fee) return Err(Error::invalid_params("Fee does not meet minimum"));
.map_err(|_| jsonrpc_core::Error::internal_error())?; }
// Verify the bullet proof and build the final output // Calculate final output commitment
if let Some(r) = peeled.0.rangeproof { let output_commit =
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); secp::add_excess(&swap.onion.commit, &peeled.0.excess).map_err(|e| Error {
secp.verify_bullet_proof(output_commit, r, None) message: e.to_string(),
.map_err(|_| jsonrpc_core::Error::invalid_params("RangeProof invalid"))?; code: ErrorCode::InternalError,
} else { data: None,
// milestone 3: only the last hop will have a rangeproof })?;
return Err(jsonrpc_core::Error::invalid_params("Rangeproof expected")); let output_commit = secp::sub_value(&output_commit, fee).map_err(|e| Error {
} message: e.to_string(),
code: ErrorCode::InternalError,
data: None,
})?;
let mut locked = SERVER_STATE.lock().unwrap(); // Verify the bullet proof and build the final output
if locked.contains_key(&swap.onion.commit) { if let Some(r) = peeled.0.rangeproof {
return Err(jsonrpc_core::Error::invalid_params("swap already called for coin")); let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
} secp.verify_bullet_proof(output_commit, r, None)
.map_err(|_| Error::invalid_params("RangeProof invalid"))?;
} else {
// milestone 3: only the last hop will have a rangeproof
return Err(Error::invalid_params("Rangeproof expected"));
}
locked.insert(swap.onion.commit, Submission{ let mut locked = SERVER_STATE.lock().unwrap();
excess: peeled.0.excess, if locked.contains_key(&swap.onion.commit) {
output_commit: output_commit, return Err(Error::invalid_params("swap already called for coin"));
rangeproof: peeled.0.rangeproof, }
input: input,
fee: fee, locked.insert(
onion: peeled.1 swap.onion.commit,
}); Submission {
Ok(Value::String("success".into())) excess: peeled.0.excess,
} output_commit,
rangeproof: peeled.0.rangeproof,
input,
fee,
onion: peeled.1,
},
);
Ok(Value::String("success".into()))
}
} }
/// Spin up the JSON-RPC web server /// Spin up the JSON-RPC web server
pub fn listen(server_config: &ServerConfig, wallet: Arc<dyn Wallet>, node: Arc<dyn GrinNode>, stop_state: &Arc<StopState>) -> std::result::Result<(), Box<dyn std::error::Error>> pub fn listen(
{ server_config: &ServerConfig,
let server_impl = Arc::new(ServerImpl::new(server_config.clone(), stop_state.clone(), wallet.clone(), node.clone())); wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>,
stop_state: &Arc<StopState>,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let server_impl = Arc::new(ServerImpl::new(
server_config.clone(),
stop_state.clone(),
wallet.clone(),
node.clone(),
));
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.extend_with(ServerImpl::to_delegate(server_impl.as_ref().clone())); io.extend_with(ServerImpl::to_delegate(server_impl.as_ref().clone()));
let server = ServerBuilder::new(io) let server = ServerBuilder::new(io)
.cors(DomainsValidation::Disabled) .cors(DomainsValidation::Disabled)
.request_middleware(|request: hyper::Request<hyper::Body>| { .request_middleware(|request: hyper::Request<hyper::Body>| {
if request.uri() == "/v1" { if request.uri() == "/v1" {
request.into() request.into()
} else { } else {
jsonrpc_http_server::Response::bad_request("Only v1 supported").into() jsonrpc_http_server::Response::bad_request("Only v1 supported").into()
} }
}) })
.start_http(&server_config.addr) .start_http(&server_config.addr)
.expect("Unable to start RPC server"); .expect("Unable to start RPC server");
println!("Server listening on {}", server_config.addr);
let close_handle = server.close_handle(); let close_handle = server.close_handle();
let config = server_config.clone(); let config = server_config.clone();
let round_handle = std::thread::spawn(move || { let round_handle = std::thread::spawn(move || {
let mut secs = 0; let mut secs = 0;
loop { loop {
if server_impl.as_ref().stop_state.is_stopped() { if server_impl.as_ref().stop_state.is_stopped() {
close_handle.close(); close_handle.close();
break; break;
} }
std::thread::sleep(std::time::Duration::from_secs(1)); std::thread::sleep(std::time::Duration::from_secs(1));
secs = (secs + 1) % config.interval_s; secs = (secs + 1) % config.interval_s;
if secs == 0 {
let _ = server_impl.as_ref().execute_round();
secs = 0;
}
}
});
server.wait(); if secs == 0 {
round_handle.join().unwrap(); let _ = server_impl.as_ref().execute_round();
secs = 0;
}
}
});
Ok(()) server.wait();
round_handle.join().unwrap();
Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::config::ServerConfig; use crate::config::ServerConfig;
use crate::node::{GrinNode, MockGrinNode}; use crate::node::{GrinNode, MockGrinNode};
use crate::onion::test_util::{self, Hop}; use crate::onion::test_util::{self, Hop};
use crate::secp::{self, ComSignature, PublicKey, Secp256k1}; use crate::secp::{self, ComSignature, PublicKey, Secp256k1};
use crate::server::{self, SwapReq}; use crate::server::{self, SwapReq};
use crate::types::Payload; use crate::types::Payload;
use crate::wallet::{MockWallet, Wallet}; use crate::wallet::{MockWallet, Wallet};
use grin_core::core::{Committed, FeeFields, Transaction}; use grin_core::core::{Committed, FeeFields, Transaction};
use grin_util::StopState; use grin_util::StopState;
use std::net::TcpListener; use std::net::TcpListener;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::thread;
use std::thread; use std::time::Duration;
use hyper::{Body, Client, Request, Response}; use hyper::{Body, Client, Request, Response};
use tokio::runtime; use tokio::runtime;
async fn body_to_string(req: Response<Body>) -> String { async fn body_to_string(req: Response<Body>) -> String {
let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap(); let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap();
String::from_utf8(body_bytes.to_vec()).unwrap() String::from_utf8(body_bytes.to_vec()).unwrap()
} }
/// 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_key: secp::SecretKey, server_key: secp::SecretKey,
wallet: Arc<dyn Wallet>, wallet: Arc<dyn Wallet>,
node: Arc<dyn GrinNode>, node: Arc<dyn GrinNode>,
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: server_key, key: server_key,
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()?,
grin_node_url: "127.0.0.1:3413".parse()?, grin_node_url: "127.0.0.1:3413".parse()?,
wallet_owner_url: "127.0.0.1:3420".parse()? wallet_owner_url: "127.0.0.1:3420".parse()?,
}; };
let threaded_rt = runtime::Runtime::new()?; let threaded_rt = runtime::Runtime::new()?;
let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel(); let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel();
let uri = format!("http://{}/v1", server_config.addr); let uri = format!("http://{}/v1", server_config.addr);
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();
// Spawn the server task // Spawn the server task
threaded_rt.spawn(async move { threaded_rt.spawn(async move {
server::listen(&server_config, wallet, node, &stop_state).unwrap() server::listen(&server_config, wallet, node, &stop_state).unwrap()
}); });
threaded_rt.spawn(async move {
futures::executor::block_on(shutdown_receiver).unwrap();
stop_state_clone.stop();
});
// Wait for listener threaded_rt.spawn(async move {
thread::sleep(Duration::from_millis(500)); futures::executor::block_on(shutdown_receiver).unwrap();
stop_state_clone.stop();
});
let do_request = async move { // Wait for listener
let request = Request::post(uri) thread::sleep(Duration::from_millis(500));
.header("Content-Type", "application/json")
.body(Body::from(req))
.unwrap();
Client::new().request(request).await let do_request = async move {
}; let request = Request::post(uri)
.header("Content-Type", "application/json")
.body(Body::from(req))
.unwrap();
let response = threaded_rt.block_on(do_request)?; Client::new().request(request).await
let response_str: String = threaded_rt.block_on(body_to_string(response)); };
// Wait for at least one round to execute
thread::sleep(Duration::from_millis(1500));
shutdown_sender.send(()).ok(); let response = threaded_rt.block_on(do_request)?;
let response_str: String = threaded_rt.block_on(body_to_string(response));
// Wait for shutdown // Wait for at least one round to execute
thread::sleep(Duration::from_millis(500)); thread::sleep(Duration::from_millis(1500));
threaded_rt.shutdown_background();
Ok(response_str) shutdown_sender.send(()).ok();
}
/// Single hop to demonstrate request validation and onion unwrapping. // Wait for shutdown
#[test] thread::sleep(Duration::from_millis(500));
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> { threaded_rt.shutdown_background();
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
let server_key = secp::random_secret();
let value: u64 = 200_000_000;
let fee: u64= 50_000_000;
let blind = secp::random_secret();
let commitment = secp::commit(value, &blind)?;
let hop_excess = secp::random_secret();
let nonce = secp::random_secret();
let mut final_blind = blind.clone();
final_blind.add_assign(&secp, &hop_excess).unwrap();
let proof = secp.bullet_proof(value - fee, final_blind.clone(), nonce.clone(), nonce.clone(), None, None);
let hop = Hop { Ok(response_str)
pubkey: PublicKey::from_secret_key(&secp, &server_key)?, }
payload: Payload{
excess: hop_excess,
fee: FeeFields::from(fee as u32),
rangeproof: Some(proof),
}
};
let hops: Vec<Hop> = vec![hop];
let session_key = secp::random_secret();
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
let swap = SwapReq{
onion: onion_packet,
comsig: comsig,
};
let wallet = MockWallet{}; /// Single hop to demonstrate request validation and onion unwrapping.
let mut mut_node = MockGrinNode::new(); #[test]
mut_node.add_default_utxo(&commitment); fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
let node = Arc::new(mut_node); let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
let server_key = secp::random_secret();
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", serde_json::json!(swap)); let value: u64 = 200_000_000;
println!("Request: {}", req); let fee: u64 = 50_000_000;
let response = make_request(server_key, Arc::new(wallet), node.clone(), req)?; let blind = secp::random_secret();
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n"; let commitment = secp::commit(value, &blind)?;
assert_eq!(response, expected); let hop_excess = secp::random_secret();
let nonce = secp::random_secret();
// check that the transaction was posted let mut final_blind = blind.clone();
let posted_txns = node.get_posted_txns(); final_blind.add_assign(&secp, &hop_excess).unwrap();
assert_eq!(posted_txns.len(), 1); let proof = secp.bullet_proof(
let posted_txn : Transaction = posted_txns.into_iter().next().unwrap(); value - fee,
let input_commit = posted_txn.inputs_committed().into_iter().next().unwrap(); final_blind.clone(),
assert_eq!(input_commit, commitment); nonce.clone(),
nonce.clone(),
None,
None,
);
Ok(()) let hop = Hop {
} pubkey: PublicKey::from_secret_key(&secp, &server_key)?,
payload: Payload {
/// Returns "Commitment not found" when there's no matching output in the UTXO set. excess: hop_excess,
#[test] fee: FeeFields::from(fee as u32),
fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> { rangeproof: Some(proof),
let secp = Secp256k1::new(); },
let server_key = secp::random_secret(); };
let hops: Vec<Hop> = vec![hop];
let value: u64 = 200_000_000; let session_key = secp::random_secret();
let fee: u64= 50_000_000; let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
let blind = secp::random_secret(); let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
let commitment = secp::commit(value, &blind)?; let swap = SwapReq {
onion: onion_packet,
comsig,
};
let hop = Hop { let wallet = MockWallet {};
pubkey: PublicKey::from_secret_key(&secp, &server_key)?, let mut mut_node = MockGrinNode::new();
payload: Payload{ mut_node.add_default_utxo(&commitment);
excess: secp::random_secret(), let node = Arc::new(mut_node);
fee: FeeFields::from(fee as u32),
rangeproof: None,
}
};
let hops: Vec<Hop> = vec![hop];
let session_key = secp::random_secret();
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
let swap = SwapReq{
onion: onion_packet,
comsig: comsig,
};
let wallet = MockWallet{}; let req = format!(
let node = MockGrinNode::new(); "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
serde_json::json!(swap)
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", serde_json::json!(swap)); );
let response = make_request(server_key, Arc::new(wallet), Arc::new(node), req)?; println!("Request: {}", req);
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Commitment not found\"},\"id\":\"1\"}\n"; let response = make_request(server_key, Arc::new(wallet), node.clone(), req)?;
assert_eq!(response, expected); let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
Ok(()) assert_eq!(response, expected);
}
// TODO: Test bulletproof verification and test minimum fee // 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();
let input_commit = posted_txn.inputs_committed().into_iter().next().unwrap();
assert_eq!(input_commit, commitment);
#[test] Ok(())
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> { }
let wallet = MockWallet{};
let node = MockGrinNode::new();
let params = "{ \"param\": \"Not a valid Swap request\" }"; /// Returns "Commitment not found" when there's no matching output in the UTXO set.
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", params); #[test]
let response = make_request(secp::random_secret(), Arc::new(wallet), Arc::new(node), req)?; fn swap_utxo_missing() -> Result<(), Box<dyn std::error::Error>> {
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n"; let secp = Secp256k1::new();
assert_eq!(response, expected); let server_key = secp::random_secret();
Ok(())
} let value: u64 = 200_000_000;
} let fee: u64 = 50_000_000;
let blind = secp::random_secret();
let commitment = secp::commit(value, &blind)?;
let hop = Hop {
pubkey: PublicKey::from_secret_key(&secp, &server_key)?,
payload: Payload {
excess: secp::random_secret(),
fee: FeeFields::from(fee as u32),
rangeproof: None,
},
};
let hops: Vec<Hop> = vec![hop];
let session_key = secp::random_secret();
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
let swap = SwapReq {
onion: onion_packet,
comsig,
};
let wallet = MockWallet {};
let node = MockGrinNode::new();
let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
serde_json::json!(swap)
);
let response = make_request(server_key, Arc::new(wallet), Arc::new(node), req)?;
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Commitment not found\"},\"id\":\"1\"}\n";
assert_eq!(response, expected);
Ok(())
}
// TODO: Test bulletproof verification and test minimum fee
#[test]
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
let wallet = MockWallet {};
let node = MockGrinNode::new();
let params = "{ \"param\": \"Not a valid Swap request\" }";
let req = format!(
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
params
);
let response = make_request(secp::random_secret(), Arc::new(wallet), Arc::new(node), req)?;
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
assert_eq!(response, expected);
Ok(())
}
}

View file

@ -5,68 +5,67 @@ use grin_core::core::FeeFields;
use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const CURRENT_VERSION : u8 = 0; const CURRENT_VERSION: u8 = 0;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Payload { pub struct Payload {
pub excess: SecretKey, pub excess: SecretKey,
pub fee: FeeFields, pub fee: FeeFields,
pub rangeproof: Option<RangeProof>, pub rangeproof: Option<RangeProof>,
} }
impl Payload { impl Payload {
pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload> { pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload> {
let payload: Payload = grin_core::ser::deserialize_default(&mut &bytes[..])?; let payload: Payload = ser::deserialize_default(&mut &bytes[..])?;
Ok(payload) Ok(payload)
} }
#[cfg(test)] #[cfg(test)]
pub fn serialize(&self) -> Result<Vec<u8>> { pub fn serialize(&self) -> Result<Vec<u8>> {
let mut vec = vec![]; let mut vec = vec![];
ser::serialize_default(&mut vec, &self)?; ser::serialize_default(&mut vec, &self)?;
Ok(vec) Ok(vec)
} }
} }
impl Readable for Payload { impl Readable for Payload {
fn read<R: Reader>(reader: &mut R) -> StdResult<Payload, ser::Error> { fn read<R: Reader>(reader: &mut R) -> StdResult<Payload, ser::Error> {
let version = reader.read_u8()?; let version = reader.read_u8()?;
if version != CURRENT_VERSION { if version != CURRENT_VERSION {
return Err(ser::Error::UnsupportedProtocolVersion); return Err(ser::Error::UnsupportedProtocolVersion);
} }
let excess = secp::read_secret_key(reader)?; let excess = secp::read_secret_key(reader)?;
let fee = FeeFields::try_from(reader.read_u64()?) let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?;
.map_err(|_| ser::Error::CorruptedData)?; let rangeproof = if reader.read_u8()? == 0 {
let rangeproof = if reader.read_u8()? == 0 { None
None } else {
} else { Some(RangeProof::read(reader)?)
Some(RangeProof::read(reader)?) };
};
let payload = Payload { let payload = Payload {
excess: excess, excess,
fee: fee, fee,
rangeproof: rangeproof rangeproof,
}; };
Ok(payload) Ok(payload)
} }
} }
impl Writeable for Payload { impl Writeable for Payload {
fn write<W: Writer>(&self, writer: &mut W) -> StdResult<(), ser::Error> { fn write<W: Writer>(&self, writer: &mut W) -> StdResult<(), ser::Error> {
writer.write_u8(CURRENT_VERSION)?; writer.write_u8(CURRENT_VERSION)?;
writer.write_fixed_bytes(&self.excess)?; writer.write_fixed_bytes(&self.excess)?;
writer.write_u64(self.fee.into())?; writer.write_u64(self.fee.into())?;
match &self.rangeproof { match &self.rangeproof {
Some(proof) => { Some(proof) => {
writer.write_u8(1)?; writer.write_u8(1)?;
proof.write(writer)?; proof.write(writer)?;
}, }
None => writer.write_u8(0)?, None => writer.write_u8(0)?,
}; };
Ok(()) Ok(())
} }
} }

View file

@ -1,107 +1,188 @@
use crate::error::{ErrorKind, Result}; use crate::error::{ErrorKind, Result};
use crate::secp::{self, SecretKey}; use crate::secp::{self};
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::{FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel}; use grin_core::core::{
FeeFields, Input, Inputs, KernelFeatures, Output, OutputFeatures, 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::ZeroingString; use grin_util::{ToHex, ZeroingString};
use grin_wallet_api::Token; use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token};
use secp256k1zkp::{ContextFlag, Secp256k1}; use secp256k1zkp::{ContextFlag, 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 std::sync::Arc;
pub trait Wallet : Send + Sync { pub trait Wallet: Send + Sync {
/// Builds an output for the wallet with the provided amount. /// Builds an output for the wallet with the provided amount.
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)>; fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)>;
} }
/// Builds and verifies a 'Transaction' using the provided components. /// 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> { pub fn assemble_tx(
let secp = Secp256k1::with_caps(ContextFlag::Commit); wallet: &Arc<dyn Wallet>,
let txn_inputs = Inputs::from(inputs.as_slice()); inputs: &Vec<Input>,
let mut txn_outputs = outputs.clone(); outputs: &Vec<Output>,
let mut txn_excesses = excesses.clone(); fee_base: u64,
let mut kernel_fee = total_fee; total_fee: u64,
excesses: &Vec<SecretKey>,
) -> Result<Transaction> {
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 // 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; 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 // 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; let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base;
// collect any leftover fees // collect any leftover fees
if total_fee > fee_required + fee_to_spend { if total_fee > fee_required + fee_to_spend {
let amount = total_fee - fee_required; let amount = total_fee - fee_required;
kernel_fee -= amount; kernel_fee -= amount;
let wallet_output = wallet.build_output(amount)?; let wallet_output = wallet.build_output(amount)?;
txn_outputs.push(wallet_output.1); txn_outputs.push(wallet_output.1);
let output_excess = wallet_output.0.secret_key(&secp) let output_excess = wallet_output
.map_err(|_| ErrorKind::CorruptedData)?; .0
txn_excesses.push(output_excess); .secret_key(&secp)
} .map_err(|_| ErrorKind::CorruptedData)?;
txn_excesses.push(output_excess);
}
// generate random transaction offset // generate random transaction offset
let offset = secp::random_secret(); let offset = secp::random_secret();
// calculate kernel excess // calculate kernel excess
let kern_excess = secp.blind_sum(txn_excesses, vec![offset.clone()])?; let kern_excess = secp.blind_sum(txn_excesses, vec![offset.clone()])?;
// build and verify kernel // build and verify kernel
let mut kernel = TxKernel::with_features(KernelFeatures::Plain { let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
fee: FeeFields::new(0, kernel_fee).unwrap(), fee: FeeFields::new(0, kernel_fee).unwrap(),
}); });
let msg = kernel.msg_to_sign()?; let msg = kernel.msg_to_sign()?;
kernel.excess = secp::commit(0, &kern_excess)?; kernel.excess = secp::commit(0, &kern_excess)?;
kernel.excess_sig = secp::sign(&kern_excess, &msg)?; kernel.excess_sig = secp::sign(&kern_excess, &msg)?;
kernel.verify()?; kernel.verify()?;
// assemble the transaction // assemble the transaction
let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel]) let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel])
.with_offset(BlindingFactor::from_secret_key(offset)); .with_offset(BlindingFactor::from_secret_key(offset));
Ok(tx) 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 {
wallet_owner_url: SocketAddr, wallet_owner_url: SocketAddr,
token: Token, shared_key: SecretKey,
token: Token,
} }
const ENDPOINT: &str = "/v3/owner"; const ENDPOINT: &str = "/v3/owner";
/// Wrapper for ECDH Public keys
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(transparent)]
pub struct ECDHPubkey {
/// public key, flattened
#[serde(with = "secp_ser::pubkey_serde")]
pub ecdh_pubkey: PublicKey,
}
impl HttpWallet { impl HttpWallet {
/// Calls the 'open_wallet' using the RPC API. /// Calls the 'open_wallet' using the RPC API.
pub fn open_wallet(wallet_owner_url: &SocketAddr, wallet_pass: &ZeroingString) -> Result<HttpWallet> { pub fn open_wallet(
let open_wallet_params = json!({ wallet_owner_url: &SocketAddr,
"name": null, wallet_pass: &ZeroingString,
"password": wallet_pass.to_string() ) -> Result<HttpWallet> {
}); println!("Opening wallet at {}", wallet_owner_url);
let token: Token = HttpWallet::send_json_request(&wallet_owner_url, "open_wallet", &open_wallet_params)?; let shared_key = HttpWallet::init_secure_api(&wallet_owner_url)?;
Ok(HttpWallet { let open_wallet_params = json!({
wallet_owner_url: wallet_owner_url.clone(), "name": null,
token: token, "password": wallet_pass.to_string()
}) });
} let token: Token = HttpWallet::send_enc_request(
&wallet_owner_url,
"open_wallet",
&open_wallet_params,
&shared_key,
)?;
println!("Connected to wallet");
fn send_json_request<D: serde::de::DeserializeOwned>( Ok(HttpWallet {
wallet_owner_url: &SocketAddr, wallet_owner_url: wallet_owner_url.clone(),
method: &str, shared_key: shared_key.clone(),
params: &serde_json::Value, token: token.clone(),
) -> Result<D> { })
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT); }
let req = build_request(method, params);
let res = client::post::<Request, Response>(url.as_str(), None, &req)?; fn init_secure_api(wallet_owner_url: &SocketAddr) -> Result<SecretKey> {
let parsed = res.clone().into_result()?; let secp = Secp256k1::new();
Ok(parsed) let ephemeral_sk = secp::random_secret();
} let ephemeral_pk = PublicKey::from_secret_key(&secp, &ephemeral_sk)?;
let init_params = json!({
"ecdh_pubkey": ephemeral_pk.serialize_vec(&secp, true).to_hex()
});
let response_pk: ECDHPubkey =
HttpWallet::send_json_request(&wallet_owner_url, "init_secure_api", &init_params)?;
let shared_key = {
let mut shared_pubkey = response_pk.ecdh_pubkey.clone();
shared_pubkey.mul_assign(&secp, &ephemeral_sk)?;
let x_coord = shared_pubkey.serialize_vec(&secp, true);
SecretKey::from_slice(&secp, &x_coord[1..])?
};
Ok(shared_key)
}
fn send_enc_request<D: serde::de::DeserializeOwned>(
wallet_owner_url: &SocketAddr,
method: &str,
params: &serde_json::Value,
shared_key: &SecretKey,
) -> Result<D> {
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
let req = json!({
"method": method,
"params": params,
"id": JsonId::IntId(1),
"jsonrpc": "2.0",
});
let enc_req = EncryptedRequest::from_json(&JsonId::IntId(1), &req, &shared_key)?;
let res =
client::post::<EncryptedRequest, EncryptedResponse>(url.as_str(), None, &enc_req)?;
let decrypted = res.decrypt(&shared_key)?;
let response: Response = serde_json::from_value(decrypted.clone())?;
let parsed = serde_json::from_value(response.result.unwrap().get("Ok").unwrap().clone())?;
Ok(parsed)
}
fn send_json_request<D: serde::de::DeserializeOwned>(
wallet_owner_url: &SocketAddr,
method: &str,
params: &serde_json::Value,
) -> Result<D> {
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
let req = build_request(method, params);
let res = client::post::<Request, Response>(url.as_str(), None, &req)?;
let parsed = res.clone().into_result()?;
Ok(parsed)
}
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -115,40 +196,42 @@ pub struct OutputWithBlind {
} }
impl Wallet for HttpWallet { impl Wallet for HttpWallet {
/// Builds an 'Output' for the wallet using the 'build_output' RPC API. /// Builds an 'Output' for the wallet using the 'build_output' RPC API.
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> { fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> {
let req_json = json!({ let req_json = json!({
"token": self.token.keychain_mask.clone().unwrap().0, "token": self.token.keychain_mask.clone().unwrap().0,
"features": "Plain", "features": "Plain",
"amount": amount "amount": amount
}); });
let output: OutputWithBlind = HttpWallet::send_json_request(&self.wallet_owner_url, "build_output", &req_json)?; let output: OutputWithBlind = HttpWallet::send_enc_request(
Ok((output.blind, output.output)) &self.wallet_owner_url,
} "build_output",
&req_json,
&self.shared_key,
)?;
Ok((output.blind, output.output))
}
} }
use grin_core::core::OutputFeatures;
/// HTTP (JSONRPC) implementation of the 'Wallet' trait. /// HTTP (JSONRPC) implementation of the 'Wallet' trait.
#[derive(Clone)] #[derive(Clone)]
pub struct MockWallet { pub struct MockWallet {}
}
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.
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> { fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let blind = secp::random_secret(); let blind = secp::random_secret();
let commit = secp::commit(amount, &blind)?; let commit = secp::commit(amount, &blind)?;
let proof = secp.bullet_proof( let proof = secp.bullet_proof(
amount, amount,
blind.clone(), blind.clone(),
secp::random_secret(), secp::random_secret(),
secp::random_secret(), secp::random_secret(),
None, None,
None, None,
); );
let output = Output::new(OutputFeatures::Plain, commit.clone(), proof); let output = Output::new(OutputFeatures::Plain, commit.clone(), proof);
Ok((BlindingFactor::from_secret_key(blind), output)) Ok((BlindingFactor::from_secret_key(blind), output))
} }
} }