From 2baf37a5a33778ba81ccf79a732bb81df425637b Mon Sep 17 00:00:00 2001 From: scilio Date: Fri, 1 Jul 2022 05:33:34 -0400 Subject: [PATCH] use v3 encrypted requests, cleaner error handling, rust formatter --- Cargo.lock | 11 + Cargo.toml | 1 + mwixnet.yml | 6 - rustfmt.toml | 2 + src/config.rs | 87 +++--- src/error.rs | 211 +++++++------- src/main.rs | 180 +++++++----- src/node.rs | 268 +++++++++--------- src/onion.rs | 535 +++++++++++++++++++----------------- src/secp.rs | 276 ++++++++++--------- src/server.rs | 739 +++++++++++++++++++++++++++----------------------- src/types.rs | 95 ++++--- src/wallet.rs | 277 ++++++++++++------- 13 files changed, 1471 insertions(+), 1217 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.lock b/Cargo.lock index 457a17d..e80ca8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2497,6 +2497,7 @@ dependencies = [ "pbkdf2 0.8.0", "rand 0.8.4", "ring", + "rpassword", "serde", "serde_derive", "serde_json", @@ -3334,6 +3335,16 @@ dependencies = [ "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]] name = "rust-embed" version = "6.3.0" diff --git a/Cargo.toml b/Cargo.toml index 79bcd6f..3e12750 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ lazy_static = "1" pbkdf2 = "0.8.0" rand = "0.8.4" ring = "0.16" +rpassword = "4.0" serde = { version = "1", features= ["derive"]} serde_derive = "1" serde_json = "1" diff --git a/mwixnet.yml b/mwixnet.yml index c89c380..591dce2 100644 --- a/mwixnet.yml +++ b/mwixnet.yml @@ -8,12 +8,6 @@ args: short: c long: config_file 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: help: Api address of running GRIN node on which to check inputs and post transactions short: n diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c691d12 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +hard_tabs = true +edition = "2021" diff --git a/src/config.rs b/src/config.rs index 4f931a2..c068cd4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ use std::io::prelude::*; use std::net::SocketAddr; 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)] pub struct ServerConfig { /// private key used by the server to decrypt onion packets @@ -38,7 +38,7 @@ struct EncryptedServerKey { impl EncryptedServerKey { /// 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 - /// 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( server_key: &SecretKey, password: &ZeroingString, @@ -46,8 +46,8 @@ impl EncryptedServerKey { let salt: [u8; 8] = thread_rng().gen(); let password = password.as_bytes(); let mut key = [0; 32]; - ring::pbkdf2::derive( - ring::pbkdf2::PBKDF2_HMAC_SHA512, + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA512, NonZeroU32::new(100).unwrap(), &salt, password, @@ -60,11 +60,18 @@ impl EncryptedServerKey { let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); let nonce: [u8; 12] = thread_rng().gen(); let aad = aead::Aad::from(&[]); - let _ = sealing_key.seal_in_place_append_tag( - aead::Nonce::assume_unique_for_key(nonce), - aad, - &mut enc_bytes, - ).map_err(|_| error::ErrorKind::SaveConfigError)?; + let _ = sealing_key + .seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(nonce), + aad, + &mut enc_bytes, + ) + .map_err(|e| { + error::ErrorKind::SaveConfigError(format!( + "Failure while encrypting server key: {}", + e + )) + })?; Ok(EncryptedServerKey { 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 { 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()) - .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()) - .map_err(|_| error::ErrorKind::LoadConfigError)?; + .map_err(|_| error::ErrorKind::LoadConfigError("Nonce not valid hex".to_string()))?; let password = password.as_bytes(); let mut key = [0; 32]; pbkdf2::derive( - ring::pbkdf2::PBKDF2_HMAC_SHA512, + pbkdf2::PBKDF2_HMAC_SHA512, NonZeroU32::new(100).unwrap(), &salt, password, @@ -95,18 +103,25 @@ impl EncryptedServerKey { let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); let aad = aead::Aad::from(&[]); - let _ = opening_key.open_in_place( - aead::Nonce::assume_unique_for_key(n), - aad, - &mut encrypted_seed, - ).map_err(|_| error::ErrorKind::LoadConfigError)?; + let _ = opening_key + .open_in_place( + aead::Nonce::assume_unique_for_key(n), + aad, + &mut encrypted_seed, + ) + .map_err(|e| { + error::ErrorKind::LoadConfigError(format!("Error decrypting seed: {}", e)) + })?; for _ in 0..aead::AES_256_GCM.tag_len() { encrypted_seed.pop(); } 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. -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 raw_config = RawConfig{ + let raw_config = RawConfig { encrypted_key: encrypted.encrypted_key, salt: encrypted.salt, 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, 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)?; - 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(()) } /// 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 { - let contents = std::fs::read_to_string(config_path)?; - let raw_config: RawConfig = toml::from_str(&contents) - .map_err(|_| error::ErrorKind::LoadConfigError)?; - - let encrypted_key = EncryptedServerKey{ + let contents = std::fs::read_to_string(config_path)?; + let raw_config: RawConfig = + toml::from_str(&contents).map_err(|e| error::ErrorKind::LoadConfigError(e.to_string()))?; + + let encrypted_key = EncryptedServerKey { encrypted_key: raw_config.encrypted_key, salt: raw_config.salt, - nonce: raw_config.nonce + nonce: raw_config.nonce, }; let secret_key = encrypted_key.decrypt(&password)?; - + Ok(ServerConfig { key: secret_key, interval_s: raw_config.interval_s, addr: raw_config.addr, grin_node_url: raw_config.grin_node_url, - wallet_owner_url: raw_config.wallet_owner_url + wallet_owner_url: raw_config.wallet_owner_url, }) } diff --git a/src/error.rs b/src/error.rs index f552f42..fe54f7a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,166 +1,161 @@ use failure::{self, Context, Fail}; +use grin_wallet_libwallet as libwallet; use std::fmt::{self, Display}; use std::io; -use grin_wallet_libwallet as libwallet; /// MWixnet error definition #[derive(Debug)] pub struct Error { - inner: Context, + inner: Context, } pub type Result = std::result::Result; pub type StdResult = std::result::Result; -#[derive(Clone, Debug, Eq, Fail, PartialEq)] /// MWixnet error types +#[derive(Clone, Debug, Eq, Fail, PartialEq)] pub enum ErrorKind { - /// Error from secp256k1-zkp library - #[fail(display = "Secp Error")] - SecpError, - /// Invalid key length for MAC initialization - #[fail(display = "InvalidKeyLength")] - InvalidKeyLength, - /// Wraps an io error produced when reading or writing - #[fail(display = "IOError")] - IOErr( - String, - io::ErrorKind, - ), - /// Data wasn't in a consumable format - #[fail(display = "CorruptedData")] - CorruptedData, - /// Error from grin's api crate - #[fail(display = "GRIN API Error")] - GrinApiError, - /// Error from grin core - #[fail(display = "grincore Error")] - GrinCoreError, - /// Error from grin-wallet's libwallet - #[fail(display = "libwallet Error")] - LibWalletError, - /// Error from serde-json - #[fail(display = "serde json Error")] - SerdeJsonError, - /// Error from invalid signature - #[fail(display = "invalid signature Error")] - InvalidSigError, - /// Error while saving config - #[fail(display = "save config Error")] - SaveConfigError, - /// Error while loading config - #[fail(display = "load config Error")] - LoadConfigError, + /// Error from secp256k1-zkp library + #[fail(display = "Secp Error")] + SecpError, + /// Invalid key length for MAC initialization + #[fail(display = "InvalidKeyLength")] + InvalidKeyLength, + /// Wraps an io error produced when reading or writing + #[fail(display = "IO Error: {}", _0)] + IOErr(String, io::ErrorKind), + /// Data wasn't in a consumable format + #[fail(display = "CorruptedData")] + CorruptedData, + /// Error from grin's api crate + #[fail(display = "GRIN API Error: {}", _0)] + GrinApiError(String), + /// Error from grin core + #[fail(display = "GRIN Core Error: {}", _0)] + GrinCoreError(String), + /// Error from grin-wallet's libwallet + #[fail(display = "libwallet error: {}", _0)] + LibWalletError(String), + /// Error from serde-json + #[fail(display = "serde json error: {}", _0)] + SerdeJsonError(String), + /// Error from invalid signature + #[fail(display = "invalid signature")] + InvalidSigError, + /// Error while saving config + #[fail(display = "save config error: {}", _0)] + SaveConfigError(String), + /// Error while loading config + #[fail(display = "load config error: {}", _0)] + LoadConfigError(String), } -impl std::error::Error for Error { - -} +impl std::error::Error for Error {} impl From for Error { - fn from(e: io::Error) -> Error { - ErrorKind::IOErr(format!("{}", e), e.kind()).into() - } + fn from(e: io::Error) -> Error { + ErrorKind::IOErr(format!("{}", e), e.kind()).into() + } } impl From for Error { - fn from(e: io::ErrorKind) -> Error { - ErrorKind::IOErr(format!("{}", io::Error::from(e)), e).into() - } + fn from(e: io::ErrorKind) -> Error { + ErrorKind::IOErr(format!("{}", io::Error::from(e)), e).into() + } } impl From for Error { - fn from(_e: grin_core::ser::Error) -> Error { - Error { - inner: Context::new(ErrorKind::GrinCoreError), - } - } + fn from(e: grin_core::ser::Error) -> Error { + Error { + inner: Context::new(ErrorKind::GrinCoreError(e.to_string())), + } + } } impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(&self.inner, f) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.inner, f) + } } impl Error { - pub fn new(kind: ErrorKind) -> Error { - Error { - inner: Context::new(kind), - } - } + pub fn new(kind: ErrorKind) -> Error { + Error { + inner: Context::new(kind), + } + } - pub fn message(&self) -> String { - format!("{}", self).into() - } + pub fn message(&self) -> String { + format!("{}", self).into() + } } impl From for Error { - fn from(kind: ErrorKind) -> Error { - Error { - inner: Context::new(kind), - } - } + fn from(kind: ErrorKind) -> Error { + Error { + inner: Context::new(kind), + } + } } impl From> for Error { - fn from(inner: Context) -> Error { - Error { inner } - } + fn from(inner: Context) -> Error { + Error { inner } + } } impl From for Error { - fn from(_error: secp256k1zkp::Error) -> Error { - Error { - inner: Context::new(ErrorKind::SecpError), - } - } + fn from(_error: secp256k1zkp::Error) -> Error { + Error { + inner: Context::new(ErrorKind::SecpError), + } + } } impl From for Error { - fn from(_error: hmac::digest::InvalidLength) -> Error { - Error { - inner: Context::new(ErrorKind::InvalidKeyLength), - } - } + fn from(_error: hmac::digest::InvalidLength) -> Error { + Error { + inner: Context::new(ErrorKind::InvalidKeyLength), + } + } } impl From for Error { - fn from(_error: grin_api::Error) -> Error { - Error { - inner: Context::new(ErrorKind::GrinApiError), - } - } + fn from(e: grin_api::Error) -> Error { + Error { + inner: Context::new(ErrorKind::GrinApiError(e.to_string())), + } + } } impl From for Error { - fn from(_error: grin_api::json_rpc::Error) -> Error { - Error { - inner: Context::new(ErrorKind::GrinApiError), - } - } + fn from(e: grin_api::json_rpc::Error) -> Error { + Error { + inner: Context::new(ErrorKind::GrinApiError(e.to_string())), + } + } } impl From for Error { - fn from(_error: grin_core::core::transaction::Error) -> Error { - Error { - inner: Context::new(ErrorKind::GrinCoreError), - } - } + fn from(e: grin_core::core::transaction::Error) -> Error { + Error { + inner: Context::new(ErrorKind::GrinCoreError(e.to_string())), + } + } } impl From for Error { - fn from(_error: libwallet::Error) -> Error { - Error { - inner: Context::new(ErrorKind::LibWalletError), - } - } + fn from(e: libwallet::Error) -> Error { + Error { + inner: Context::new(ErrorKind::LibWalletError(e.to_string())), + } + } } impl From for Error { - fn from(_error: serde_json::Error) -> Error { - Error { - inner: Context::new(ErrorKind::SerdeJsonError), - } - } -} \ No newline at end of file + fn from(e: serde_json::Error) -> Error { + Error { + inner: Context::new(ErrorKind::SerdeJsonError(e.to_string())), + } + } +} diff --git a/src/main.rs b/src/main.rs index 7211b91..b1f83cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,12 @@ use wallet::HttpWallet; use clap::App; use grin_util::{StopState, ZeroingString}; +use rpassword; use std::env; use std::io::{self, Write}; use std::path::PathBuf; use std::sync::Arc; +use tokio::runtime::Runtime; #[macro_use] extern crate lazy_static; @@ -28,91 +30,125 @@ mod wallet; const DEFAULT_INTERVAL: u32 = 12 * 60 * 60; fn main() { - if let Err(e) = real_main() { - io::stderr().write_all(format!("mwixnet server exited with error:\n{}\n", e).as_bytes()).unwrap(); - std::process::exit(1); - } - + real_main().unwrap(); std::process::exit(0); } -fn real_main() -> std::result::Result<(), Box> { +fn real_main() -> Result<(), Box> { let yml = load_yaml!("../mwixnet.yml"); 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::().unwrap() ); - let bind_addr = args.value_of("bind_addr"); - let grin_node_url = args.value_of("grin_node_url"); - let wallet_owner_url = args.value_of("wallet_owner_url"); - - // Write a new config file if init-config command is supplied - if let ("init-config", Some(_)) = args.subcommand() { - if config_path.exists() { - panic!("Config file already exists at {}", config_path.to_string_lossy()); - } + 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 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()?, - }; + let round_time = args + .value_of("round_time") + .map(|t| t.parse::().unwrap()); + let bind_addr = args.value_of("bind_addr"); + let grin_node_url = args.value_of("grin_node_url"); + let wallet_owner_url = args.value_of("wallet_owner_url"); - config::write_config(&config_path, &server_config, &password)?; - println!("Config file written to {:?}. Please back this file up in a safe place.", config_path); - return Ok(()); - } + // Write a new config file if init-config command is supplied + if let ("init-config", Some(_)) = args.subcommand() { + 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 - if let Some(bind_addr) = bind_addr { - server_config.addr = bind_addr.parse()?; - } + let password = prompt_password_confirm(); + config::write_config(&config_path, &server_config, &password)?; + println!( + "Config file written to {:?}. Please back this file up in a safe place.", + config_path + ); + return Ok(()); + } - // Override grin_node_url, if supplied - if let Some(grin_node_url) = grin_node_url { - server_config.grin_node_url = grin_node_url.parse()?; - } + let password = prompt_password(); + let mut server_config = config::load_config(&config_path, &password)?; - // Override wallet_owner_url, if supplied - if let Some(wallet_owner_url) = wallet_owner_url { - server_config.wallet_owner_url = wallet_owner_url.parse()?; - } + // Override bind_addr, if supplied + if let Some(bind_addr) = bind_addr { + server_config.addr = bind_addr.parse()?; + } - // Open wallet - let wallet_pass = args.value_of("wallet_pass").ok_or(error::Error::new(error::ErrorKind::LoadConfigError))?; - let wallet_pass = grin_util::ZeroingString::from(wallet_pass); - let wallet = HttpWallet::open_wallet(&server_config.wallet_owner_url, &wallet_pass)?; + // Override grin_node_url, if supplied + if let Some(grin_node_url) = grin_node_url { + server_config.grin_node_url = grin_node_url.parse()?; + } - // Create GrinNode - let node = HttpGrinNode::new(&server_config.grin_node_url, &None); - - let stop_state = Arc::new(StopState::new()); + // Override wallet_owner_url, if supplied + if let Some(wallet_owner_url) = wallet_owner_url { + server_config.wallet_owner_url = wallet_owner_url.parse()?; + } - let stop_state_clone = stop_state.clone(); - std::thread::spawn(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(); - }); + // Open wallet + let wallet_pass = prompt_wallet_password(&args.value_of("wallet_pass")); + let wallet = HttpWallet::open_wallet(&server_config.wallet_owner_url, &wallet_pass)?; - server::listen(&server_config, Arc::new(wallet), Arc::new(node), &stop_state) -} \ No newline at end of file + // 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()) + } + } +} diff --git a/src/node.rs b/src/node.rs index d00ede1..7837c52 100644 --- a/src/node.rs +++ b/src/node.rs @@ -13,184 +13,196 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, RwLock}; -pub trait GrinNode : Send + Sync { - /// Retrieves the unspent output with a matching commitment - fn get_utxo(&self, output_commit: &Commitment) -> Result>; +pub trait GrinNode: Send + Sync { + /// Retrieves the unspent output with a matching commitment + fn get_utxo(&self, output_commit: &Commitment) -> Result>; - /// Gets the height of the chain tip - fn get_chain_height(&self) -> Result; + /// Gets the height of the chain tip + fn get_chain_height(&self) -> Result; - /// Posts a transaction to the grin node - fn post_tx(&self, tx: &Transaction) -> Result<()>; + /// Posts a transaction to the grin node + fn post_tx(&self, tx: &Transaction) -> Result<()>; } /// Checks if a commitment is in the UTXO set pub fn is_unspent(node: &Arc, commit: &Commitment) -> Result { - let utxo = node.get_utxo(&commit)?; - Ok(utxo.is_some()) + let utxo = node.get_utxo(&commit)?; + Ok(utxo.is_some()) } /// Checks whether a commitment is spendable at the block height provided -pub fn is_spendable(node: &Arc, output_commit: &Commitment, next_block_height: u64) -> Result { - 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, - }; +pub fn is_spendable( + node: &Arc, + output_commit: &Commitment, + next_block_height: u64, +) -> Result { + 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 let Some(block_height) = out.block_height { - if block_height + COINBASE_MATURITY < next_block_height { - return Ok(false); - } - } else { - return Ok(false); - } - } + if is_coinbase { + if let Some(block_height) = out.block_height { + if block_height + COINBASE_MATURITY < next_block_height { + return Ok(false); + } + } else { + return Ok(false); + } + } - return Ok(true); - } + return Ok(true); + } - Ok(false) + Ok(false) } /// Builds an input for an unspent output commitment pub fn build_input(node: &Arc, output_commit: &Commitment) -> Result> { - let output = node.get_utxo(&output_commit)?; + let output = node.get_utxo(&output_commit)?; - if let Some(out) = output { - let features = match out.output_type { - OutputType::Coinbase => OutputFeatures::Coinbase, - OutputType::Transaction => OutputFeatures::Plain, - }; + if let Some(out) = output { + let features = match out.output_type { + OutputType::Coinbase => OutputFeatures::Coinbase, + OutputType::Transaction => OutputFeatures::Plain, + }; - let input = Input::new(features, out.commit); - return Ok(Some(input)); - } + let input = Input::new(features, out.commit); + 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)] pub struct HttpGrinNode { - node_url: SocketAddr, - node_api_secret: Option, + node_url: SocketAddr, + node_api_secret: Option, } const ENDPOINT: &str = "/v2/foreign"; impl HttpGrinNode { - pub fn new(node_url: &SocketAddr, node_api_secret: &Option) -> HttpGrinNode { - HttpGrinNode { - node_url: node_url.to_owned(), - node_api_secret: node_api_secret.to_owned(), - } - } + pub fn new(node_url: &SocketAddr, node_api_secret: &Option) -> HttpGrinNode { + HttpGrinNode { + node_url: node_url.to_owned(), + node_api_secret: node_api_secret.to_owned(), + } + } - fn send_json_request( - &self, - method: &str, - params: &serde_json::Value, - ) -> Result { - let url = format!("http://{}{}", self.node_url, ENDPOINT); - let req = build_request(method, params); - let res = - client::post::(url.as_str(), self.node_api_secret.clone(), &req)?; - let parsed = res.clone().into_result()?; - Ok(parsed) - } + fn send_json_request( + &self, + method: &str, + params: &serde_json::Value, + ) -> Result { + let url = format!("http://{}{}", self.node_url, ENDPOINT); + let req = build_request(method, params); + let res = + client::post::(url.as_str(), self.node_api_secret.clone(), &req)?; + let parsed = res.clone().into_result()?; + Ok(parsed) + } } impl GrinNode for HttpGrinNode { - fn get_utxo(&self, output_commit: &Commitment) -> Result> { - let commits : Vec = vec![output_commit.to_hex()]; - let start_height : Option = None; - let end_height : Option = None; - let include_proof : Option = Some(false); - let include_merkle_proof : Option = Some(false); + fn get_utxo(&self, output_commit: &Commitment) -> Result> { + let commits: Vec = vec![output_commit.to_hex()]; + let start_height: Option = None; + let end_height: Option = None; + let include_proof: Option = Some(false); + let include_merkle_proof: Option = Some(false); - let params = json!([Some(commits), start_height, end_height, include_proof, include_merkle_proof]); - let outputs = self.send_json_request::>("get_outputs", ¶ms)?; - if outputs.is_empty() { - return Ok(None); - } + let params = json!([ + Some(commits), + start_height, + end_height, + include_proof, + include_merkle_proof + ]); + let outputs = self.send_json_request::>("get_outputs", ¶ms)?; + if outputs.is_empty() { + return Ok(None); + } - Ok(Some(outputs[0].clone())) - } + Ok(Some(outputs[0].clone())) + } - fn get_chain_height(&self) -> Result { - let params = json!([]); - let tip_json = self.send_json_request::("get_tip", ¶ms)?; + fn get_chain_height(&self) -> Result { + let params = json!([]); + let tip_json = self.send_json_request::("get_tip", ¶ms)?; - let tip: Result = serde_json::from_value(tip_json["Ok"].clone()) - .map_err(|_| ErrorKind::SerdeJsonError.into()); - - Ok(tip?.height) - } + let tip: Result = serde_json::from_value(tip_json["Ok"].clone()) + .map_err(|e| ErrorKind::SerdeJsonError(e.to_string()).into()); - fn post_tx(&self, tx: &Transaction) -> Result<()> { - let params = json!([tx, true]); - self.send_json_request::("push_transaction", ¶ms)?; - Ok(()) - } + Ok(tip?.height) + } + + fn post_tx(&self, tx: &Transaction) -> Result<()> { + let params = json!([tx, true]); + self.send_json_request::("push_transaction", ¶ms)?; + Ok(()) + } } +/// Implementation of 'GrinNode' trait that mocks a grin node instance. +/// Use only for testing purposes. pub struct MockGrinNode { - utxos: HashMap, - txns_posted: RwLock>, + utxos: HashMap, + txns_posted: RwLock>, } impl MockGrinNode { - pub fn new() -> MockGrinNode { - MockGrinNode { - utxos: HashMap::new(), - txns_posted: RwLock::new(Vec::new()), - } - } + pub fn new() -> MockGrinNode { + MockGrinNode { + utxos: HashMap::new(), + txns_posted: RwLock::new(Vec::new()), + } + } - pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) { - self.utxos.insert(output_commit.clone(), utxo.clone()); - } + pub fn add_utxo(&mut self, output_commit: &Commitment, utxo: &OutputPrintable) { + self.utxos.insert(output_commit.clone(), utxo.clone()); + } - pub fn add_default_utxo(&mut self, output_commit: &Commitment) { - let utxo = OutputPrintable{ - output_type: OutputType::Transaction, - commit: output_commit.to_owned(), - spent: false, - proof: None, - proof_hash: String::from(""), - block_height: None, - merkle_proof: None, - mmr_index: 0 - }; + pub fn add_default_utxo(&mut self, output_commit: &Commitment) { + let utxo = OutputPrintable { + output_type: OutputType::Transaction, + commit: output_commit.to_owned(), + spent: false, + proof: None, + proof_hash: String::from(""), + block_height: None, + merkle_proof: None, + mmr_index: 0, + }; - self.add_utxo(&output_commit, &utxo); - } + self.add_utxo(&output_commit, &utxo); + } - pub fn get_posted_txns(&self) -> Vec { - let read = self.txns_posted.read().unwrap(); - read.clone() - } + pub fn get_posted_txns(&self) -> Vec { + let read = self.txns_posted.read().unwrap(); + read.clone() + } } impl GrinNode for MockGrinNode { - fn get_utxo(&self, output_commit: &Commitment) -> Result> { - if let Some(utxo) = self.utxos.get(&output_commit) { - return Ok(Some(utxo.clone())); - } + fn get_utxo(&self, output_commit: &Commitment) -> Result> { + if let Some(utxo) = self.utxos.get(&output_commit) { + return Ok(Some(utxo.clone())); + } - Ok(None) - } + Ok(None) + } - fn get_chain_height(&self) -> Result { - Ok(100) - } + fn get_chain_height(&self) -> Result { + Ok(100) + } - fn post_tx(&self, tx: &Transaction) -> Result<()> { - let mut write = self.txns_posted.write().unwrap(); - write.push(tx.clone()); - Ok(()) - } -} \ No newline at end of file + fn post_tx(&self, tx: &Transaction) -> Result<()> { + let mut write = self.txns_posted.write().unwrap(); + write.push(tx.clone()); + Ok(()) + } +} diff --git a/src/onion.rs b/src/onion.rs index 11541aa..fe409d8 100644 --- a/src/onion.rs +++ b/src/onion.rs @@ -2,13 +2,13 @@ use crate::error::Result; use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; use crate::types::Payload; -use chacha20::{ChaCha20, Key, Nonce}; use chacha20::cipher::{NewCipher, StreamCipher}; +use chacha20::{ChaCha20, Key, Nonce}; use grin_core::ser::{self, ProtocolVersion, Writeable, Writer}; use grin_util::{self, ToHex}; use hmac::{Hmac, Mac}; -use serde::{Deserialize}; use serde::ser::SerializeStruct; +use serde::Deserialize; use sha2::{Digest, Sha256}; use std::fmt; @@ -17,296 +17,321 @@ type RawBytes = Vec; #[derive(Clone, Debug, PartialEq)] pub struct Onion { - pub ephemeral_pubkey: PublicKey, - pub commit: Commitment, - pub enc_payloads: Vec, + pub ephemeral_pubkey: PublicKey, + pub commit: Commitment, + pub enc_payloads: Vec, } impl Onion { - pub fn serialize(&self) -> Result> { - let mut vec = vec![]; - ser::serialize_default(&mut vec, &self)?; - Ok(vec) - } + pub fn serialize(&self) -> Result> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } - /// 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)> { - let secp = Secp256k1::new(); - - let shared_secret = SharedSecret::new(&secp, &self.ephemeral_pubkey, &secret_key); - let mut cipher = new_stream_cipher(&shared_secret)?; - - let mut decrypted_bytes = self.enc_payloads[0].clone(); - cipher.apply_keystream(&mut decrypted_bytes); - let decrypted_payload = Payload::deserialize(&decrypted_bytes)?; - - let enc_payloads : Vec = self.enc_payloads.iter() - .enumerate() - .filter(|&(i, _)| i != 0) - .map(|(_, enc_payload)| { - let mut p = enc_payload.clone(); - cipher.apply_keystream(&mut p); - p - }) - .collect(); - - let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?; - - let mut ephemeral_pubkey = self.ephemeral_pubkey.clone(); - ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?; - - let mut commitment = self.commit.clone(); - commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?; - commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())?; - - let peeled_onion = Onion{ - ephemeral_pubkey: ephemeral_pubkey, - commit: commitment.clone(), - enc_payloads: enc_payloads, - }; - Ok((decrypted_payload, peeled_onion)) - } + /// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload + pub fn peel_layer(&self, secret_key: &SecretKey) -> Result<(Payload, Onion)> { + let secp = Secp256k1::new(); + + let shared_secret = SharedSecret::new(&secp, &self.ephemeral_pubkey, &secret_key); + let mut cipher = new_stream_cipher(&shared_secret)?; + + let mut decrypted_bytes = self.enc_payloads[0].clone(); + cipher.apply_keystream(&mut decrypted_bytes); + let decrypted_payload = Payload::deserialize(&decrypted_bytes)?; + + let enc_payloads: Vec = self + .enc_payloads + .iter() + .enumerate() + .filter(|&(i, _)| i != 0) + .map(|(_, enc_payload)| { + let mut p = enc_payload.clone(); + cipher.apply_keystream(&mut p); + p + }) + .collect(); + + let blinding_factor = calc_blinding_factor(&shared_secret, &self.ephemeral_pubkey)?; + + let mut ephemeral_pubkey = self.ephemeral_pubkey.clone(); + ephemeral_pubkey.mul_assign(&secp, &blinding_factor)?; + + let mut commitment = self.commit.clone(); + commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?; + commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())?; + + let peeled_onion = Onion { + ephemeral_pubkey, + commit: commitment.clone(), + enc_payloads, + }; + Ok((decrypted_payload, peeled_onion)) + } } -fn calc_blinding_factor(shared_secret: &SharedSecret, ephemeral_pubkey: &PublicKey) -> Result { - let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?; +fn calc_blinding_factor( + shared_secret: &SharedSecret, + ephemeral_pubkey: &PublicKey, +) -> Result { + let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?; - let mut hasher = Sha256::default(); - hasher.update(&serialized_pubkey); - hasher.update(&shared_secret[0..32]); - - let secp = Secp256k1::new(); - let blind = SecretKey::from_slice(&secp, &hasher.finalize())?; - Ok(blind) + let mut hasher = Sha256::default(); + hasher.update(&serialized_pubkey); + hasher.update(&shared_secret[0..32]); + + let secp = Secp256k1::new(); + let blind = SecretKey::from_slice(&secp, &hasher.finalize())?; + Ok(blind) } fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { - let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; - mu_hmac.update(&shared_secret[0..32]); - let mukey = mu_hmac.finalize().into_bytes(); + let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; + mu_hmac.update(&shared_secret[0..32]); + let mukey = mu_hmac.finalize().into_bytes(); - let key = Key::from_slice(&mukey[0..32]); - let nonce = Nonce::from_slice(b"NONCE1234567"); - - Ok(ChaCha20::new(&key, &nonce)) + let key = Key::from_slice(&mukey[0..32]); + let nonce = Nonce::from_slice(b"NONCE1234567"); + + Ok(ChaCha20::new(&key, &nonce)) } impl Writeable for Onion { - fn write(&self, writer: &mut W) -> std::result::Result<(), ser::Error> { - self.ephemeral_pubkey.write(writer)?; - writer.write_fixed_bytes(&self.commit)?; - writer.write_u64(self.enc_payloads.len() as u64)?; - for p in &self.enc_payloads { - writer.write_u64(p.len() as u64)?; - p.write(writer)?; - } - Ok(()) - } + fn write(&self, writer: &mut W) -> std::result::Result<(), ser::Error> { + self.ephemeral_pubkey.write(writer)?; + writer.write_fixed_bytes(&self.commit)?; + writer.write_u64(self.enc_payloads.len() as u64)?; + for p in &self.enc_payloads { + writer.write_u64(p.len() as u64)?; + p.write(writer)?; + } + Ok(()) + } } impl serde::ser::Serialize for Onion { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::ser::Serializer, - { - let mut state = serializer.serialize_struct("Onion", 3)?; + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::ser::Serializer, + { + let mut state = serializer.serialize_struct("Onion", 3)?; - let secp = Secp256k1::new(); - state.serialize_field("pubkey", &self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex())?; - state.serialize_field("commit", &self.commit.to_hex())?; + let secp = Secp256k1::new(); + state.serialize_field( + "pubkey", + &self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex(), + )?; + state.serialize_field("commit", &self.commit.to_hex())?; - let hex_payloads: Vec = self.enc_payloads.iter().map(|v| v.to_hex()).collect(); - state.serialize_field("data", &hex_payloads)?; - state.end() - } + let hex_payloads: Vec = self.enc_payloads.iter().map(|v| v.to_hex()).collect(); + state.serialize_field("data", &hex_payloads)?; + state.end() + } } impl<'de> serde::de::Deserialize<'de> for Onion { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::de::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "snake_case")] - enum Field { - Pubkey, - Commit, - Data - } + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::de::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Pubkey, + Commit, + Data, + } - struct OnionVisitor; + struct OnionVisitor; - impl<'de> serde::de::Visitor<'de> for OnionVisitor { - type Value = Onion; + impl<'de> serde::de::Visitor<'de> for OnionVisitor { + type Value = Onion; - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("an Onion") - } + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an Onion") + } - fn visit_map(self, mut map: A) -> std::result::Result - where - A: serde::de::MapAccess<'de>, - { - let mut pubkey = None; - let mut commit = None; - let mut data = None; + fn visit_map(self, mut map: A) -> std::result::Result + where + A: serde::de::MapAccess<'de>, + { + let mut pubkey = None; + let mut commit = None; + let mut data = None; - while let Some(key) = map.next_key()? { - match key { - Field::Pubkey => { - let val: String = map.next_value()?; - let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; - let secp = Secp256k1::new(); - pubkey = Some(PublicKey::from_slice(&secp, &vec[..]).map_err(serde::de::Error::custom)?); - } - Field::Commit => { - let val: String = map.next_value()?; - let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; - commit = Some(Commitment::from_vec(vec)); - } - Field::Data => { - let val: Vec = map.next_value()?; - let mut vec: Vec> = Vec::new(); - for hex in val { - vec.push(grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?); - } - data = Some(vec); - } - } - } + while let Some(key) = map.next_key()? { + match key { + Field::Pubkey => { + let val: String = map.next_value()?; + let vec = + grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; + let secp = Secp256k1::new(); + pubkey = Some( + PublicKey::from_slice(&secp, &vec[..]) + .map_err(serde::de::Error::custom)?, + ); + } + Field::Commit => { + let val: String = map.next_value()?; + let vec = + grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; + commit = Some(Commitment::from_vec(vec)); + } + Field::Data => { + let val: Vec = map.next_value()?; + let mut vec: Vec> = Vec::new(); + for hex in val { + vec.push( + grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?, + ); + } + data = Some(vec); + } + } + } - Ok(Onion { - ephemeral_pubkey: pubkey.unwrap(), - commit: commit.unwrap(), - enc_payloads: data.unwrap(), - }) - } - } + Ok(Onion { + ephemeral_pubkey: pubkey.unwrap(), + commit: commit.unwrap(), + enc_payloads: data.unwrap(), + }) + } + } - const FIELDS: &[&str] = &[ - "pubkey", - "commit", - "data" - ]; - deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor) - } + const FIELDS: &[&str] = &["pubkey", "commit", "data"]; + deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor) + } } #[cfg(test)] pub mod test_util { - use super::{Onion, RawBytes}; - use crate::error::Result; - use crate::types::{Payload}; - use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; + use super::{Onion, RawBytes}; + use crate::error::Result; + use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret}; + use crate::types::Payload; - use chacha20::cipher::{StreamCipher}; - - pub struct Hop { - pub pubkey: PublicKey, - pub payload: Payload, - } + use chacha20::cipher::StreamCipher; - /// Create an Onion for the Commitment, encrypting the payload for each hop - pub fn create_onion(commitment: &Commitment, session_key: &SecretKey, hops: &Vec) -> Result { - let secp = Secp256k1::new(); - let mut ephemeral_key = session_key.clone(); - - let mut shared_secrets: Vec = Vec::new(); - let mut enc_payloads: Vec = Vec::new(); - for hop in hops { - let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key); - - let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)?; - let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?; - - shared_secrets.push(shared_secret); - enc_payloads.push(hop.payload.serialize()?); - ephemeral_key.mul_assign(&secp, &blinding_factor)?; - } - - for i in (0..shared_secrets.len()).rev() { - let mut cipher = super::new_stream_cipher(&shared_secrets[i])?; - for j in i..shared_secrets.len() { - cipher.apply_keystream(&mut enc_payloads[j]); - } - } - - let onion = Onion{ - ephemeral_pubkey: PublicKey::from_secret_key(&secp, session_key)?, - commit: commitment.clone(), - enc_payloads: enc_payloads, - }; - Ok(onion) - } + pub struct Hop { + pub pubkey: PublicKey, + pub payload: Payload, + } + + /// Create an Onion for the Commitment, encrypting the payload for each hop + pub fn create_onion( + commitment: &Commitment, + session_key: &SecretKey, + hops: &Vec, + ) -> Result { + let secp = Secp256k1::new(); + let mut ephemeral_key = session_key.clone(); + + let mut shared_secrets: Vec = Vec::new(); + let mut enc_payloads: Vec = Vec::new(); + for hop in hops { + let shared_secret = SharedSecret::new(&secp, &hop.pubkey, &ephemeral_key); + + let ephemeral_pubkey = PublicKey::from_secret_key(&secp, &ephemeral_key)?; + let blinding_factor = super::calc_blinding_factor(&shared_secret, &ephemeral_pubkey)?; + + shared_secrets.push(shared_secret); + enc_payloads.push(hop.payload.serialize()?); + ephemeral_key.mul_assign(&secp, &blinding_factor)?; + } + + for i in (0..shared_secrets.len()).rev() { + let mut cipher = super::new_stream_cipher(&shared_secrets[i])?; + for j in i..shared_secrets.len() { + cipher.apply_keystream(&mut enc_payloads[j]); + } + } + + let onion = Onion { + ephemeral_pubkey: PublicKey::from_secret_key(&secp, session_key)?, + commit: commitment.clone(), + enc_payloads, + }; + Ok(onion) + } } #[cfg(test)] pub mod tests { - use super::test_util::{self, Hop}; - use crate::types::{Payload}; - use crate::secp; + use super::test_util::{self, Hop}; + use crate::secp; + use crate::types::Payload; - use grin_core::core::FeeFields; - - /// Test end-to-end Onion creation and unwrapping logic. - #[test] - fn onion() { - let total_fee : u64 = 10; - let fee_per_hop : u64 = 2; - let in_value : u64 = 1000; - let out_value : u64 = in_value - total_fee; - let blind = secp::random_secret(); - let commitment = secp::commit(in_value, &blind).unwrap(); - - let session_key = secp::random_secret(); - let mut hops : Vec = Vec::new(); - - let mut keys : Vec = Vec::new(); - let mut final_commit = secp::commit(out_value, &blind).unwrap(); - let mut final_blind = blind.clone(); - for i in 0..5 { - keys.push(secp::random_secret()); - - let excess = secp::random_secret(); - - let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); - final_blind.add_assign(&secp, &excess).unwrap(); - final_commit = secp::add_excess(&final_commit, &excess).unwrap(); - let proof = if i == 4 { - let n1 = secp::random_secret(); - let rp = secp.bullet_proof(out_value, final_blind.clone(), n1.clone(), n1.clone(), None, None); - assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok()); - Some(rp) - } else { - None - }; - - hops.push(Hop{ - pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(), - payload: Payload{ - excess: excess, - fee: FeeFields::from(fee_per_hop as u32), - rangeproof: proof, - } - }); - } - - let mut onion_packet = test_util::create_onion(&commitment, &session_key, &hops).unwrap(); - - let mut payload = Payload{ - excess: secp::random_secret(), - fee: FeeFields::from(fee_per_hop as u32), - rangeproof: None - }; - for i in 0..5 { - let peeled = onion_packet.peel_layer(&keys[i]).unwrap(); - 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)); - } -} \ No newline at end of file + use grin_core::core::FeeFields; + + /// Test end-to-end Onion creation and unwrapping logic. + #[test] + fn onion() { + let total_fee: u64 = 10; + let fee_per_hop: u64 = 2; + let in_value: u64 = 1000; + let out_value: u64 = in_value - total_fee; + let blind = secp::random_secret(); + let commitment = secp::commit(in_value, &blind).unwrap(); + + let session_key = secp::random_secret(); + let mut hops: Vec = Vec::new(); + + let mut keys: Vec = Vec::new(); + let mut final_commit = secp::commit(out_value, &blind).unwrap(); + let mut final_blind = blind.clone(); + for i in 0..5 { + keys.push(secp::random_secret()); + + let excess = secp::random_secret(); + + let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); + final_blind.add_assign(&secp, &excess).unwrap(); + final_commit = secp::add_excess(&final_commit, &excess).unwrap(); + let proof = if i == 4 { + let n1 = secp::random_secret(); + let rp = secp.bullet_proof( + out_value, + final_blind.clone(), + n1.clone(), + n1.clone(), + None, + None, + ); + assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok()); + Some(rp) + } else { + None + }; + + hops.push(Hop { + pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(), + payload: Payload { + excess, + fee: FeeFields::from(fee_per_hop as u32), + rangeproof: proof, + }, + }); + } + + let mut onion_packet = test_util::create_onion(&commitment, &session_key, &hops).unwrap(); + + let mut payload = Payload { + excess: secp::random_secret(), + fee: FeeFields::from(fee_per_hop as u32), + rangeproof: None, + }; + for i in 0..5 { + let peeled = onion_packet.peel_layer(&keys[i]).unwrap(); + 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)); + } +} diff --git a/src/secp.rs b/src/secp.rs index 4e45827..5aef196 100644 --- a/src/secp.rs +++ b/src/secp.rs @@ -1,9 +1,12 @@ -pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature}; 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::pedersen::{Commitment, RangeProof}; 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}; @@ -14,206 +17,211 @@ use secp256k1zkp::rand::thread_rng; /// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys pub struct ComSignature { - pub_nonce: Commitment, - s: SecretKey, - t: SecretKey, + pub_nonce: Commitment, + s: SecretKey, + t: SecretKey, } impl ComSignature { - pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { - ComSignature { - pub_nonce: pub_nonce.to_owned(), - s: s.to_owned(), - t: t.to_owned(), - } - } + pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { + ComSignature { + pub_nonce: pub_nonce.to_owned(), + s: s.to_owned(), + t: t.to_owned(), + } + } - #[allow(dead_code)] - pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); + #[allow(dead_code)] + pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); let mut amt_bytes = [0; 32]; BigEndian::write_u64(&mut amt_bytes[24..32], amount); - let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; - - let k_1 = SecretKey::new(&secp, &mut thread_rng()); - let k_2 = SecretKey::new(&secp, &mut thread_rng()); + let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; - let commitment = secp.commit(amount, blind.clone())?; - let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?; + let k_1 = SecretKey::new(&secp, &mut thread_rng()); + 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 mut s = k_amt.clone(); - s.mul_assign(&secp, &e)?; - s.add_assign(&secp, &k_1)?; + let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?; - // t = k_2 + (e * blind) - let mut t = blind.clone(); - t.mul_assign(&secp, &e)?; - t.add_assign(&secp, &k_2)?; + // s = k_1 + (e * amount) + let mut s = k_amt.clone(); + s.mul_assign(&secp, &e)?; + 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)] - pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<()> { - let secp = Secp256k1::with_caps(ContextFlag::Commit); + Ok(ComSignature::new(&nonce_commitment, &s, &t)) + } - let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?; + #[allow(non_snake_case)] + pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<()> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); - let mut Ce = commit.to_pubkey(&secp)?; - let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?; - Ce.mul_assign(&secp, &e)?; + let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?; - let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()]; - let S2 = secp.commit_sum(commits, Vec::new())?; + let mut Ce = commit.to_pubkey(&secp)?; + let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?; + Ce.mul_assign(&secp, &e)?; - if S1 != S2 { - return Err(Error::new(ErrorKind::InvalidSigError)); - } + let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()]; + 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) -> Result { - let mut challenge_hasher = Blake2b::new(32); - challenge_hasher.update(&commit.0); - challenge_hasher.update(&nonce_commit.0); - challenge_hasher.update(msg); + Ok(()) + } + + fn calc_challenge( + secp: &Secp256k1, + commit: &Commitment, + nonce_commit: &Commitment, + msg: &Vec, + ) -> Result { + let mut challenge_hasher = Blake2b::new(32); + challenge_hasher.update(&commit.0); + challenge_hasher.update(&nonce_commit.0); + challenge_hasher.update(msg); let mut challenge = [0; 32]; challenge.copy_from_slice(challenge_hasher.finalize().as_bytes()); - Ok(SecretKey::from_slice(&secp, &challenge)?) - } + Ok(SecretKey::from_slice(&secp, &challenge)?) + } } /// Serializes a ComSignature to and from hex pub mod comsig_serde { - use super::ComSignature; - use grin_core::ser::{self, ProtocolVersion}; - use serde::{Deserialize, Serializer}; - use grin_util::ToHex; + use super::ComSignature; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use serde::{Deserialize, Serializer}; - /// Serializes a ComSignature as a hex string - pub fn serialize(comsig: &ComSignature, serializer: S) -> Result - where - S: Serializer, - { - use serde::ser::Error; - let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; - serializer.serialize_str(&bytes.to_hex()) - } + /// Serializes a ComSignature as a hex string + pub fn serialize(comsig: &ComSignature, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::Error; + let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; + serializer.serialize_str(&bytes.to_hex()) + } - /// Creates a ComSignature from a hex string - pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - let bytes = String::deserialize(deserializer) - .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; - let sig: ComSignature = grin_core::ser::deserialize_default(&mut &bytes[..]) - .map_err(Error::custom)?; - Ok(sig) - } + /// Creates a ComSignature from a hex string + pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let bytes = String::deserialize(deserializer) + .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; + let sig: ComSignature = + grin_core::ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?; + Ok(sig) + } } #[allow(non_snake_case)] impl Readable for ComSignature { - fn read(reader: &mut R) -> std::result::Result { - let R = Commitment::read(reader)?; - let s = read_secret_key(reader)?; - let t = read_secret_key(reader)?; - Ok(ComSignature::new(&R, &s, &t)) - } + fn read(reader: &mut R) -> std::result::Result { + let R = Commitment::read(reader)?; + let s = read_secret_key(reader)?; + let t = read_secret_key(reader)?; + Ok(ComSignature::new(&R, &s, &t)) + } } impl Writeable for ComSignature { - fn write(&self, writer: &mut W) -> std::result::Result<(), ser::Error> { - writer.write_fixed_bytes(self.pub_nonce.0)?; - writer.write_fixed_bytes(self.s.0)?; - writer.write_fixed_bytes(self.t.0)?; - Ok(()) - } + fn write(&self, writer: &mut W) -> std::result::Result<(), ser::Error> { + writer.write_fixed_bytes(self.pub_nonce.0)?; + writer.write_fixed_bytes(self.s.0)?; + writer.write_fixed_bytes(self.t.0)?; + Ok(()) + } } /// Generate a random SecretKey. pub fn random_secret() -> SecretKey { - let secp = Secp256k1::new(); - SecretKey::new(&secp, &mut thread_rng()) + let secp = Secp256k1::new(); + SecretKey::new(&secp, &mut thread_rng()) } /// Deserialize a SecretKey from a Reader pub fn read_secret_key(reader: &mut R) -> std::result::Result { - let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; - let secp = Secp256k1::with_caps(ContextFlag::None); - let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; - Ok(pk) + let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; + let secp = Secp256k1::with_caps(ContextFlag::None); + let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; + Ok(pk) } /// Build a Pedersen Commitment using the provided value and blinding factor pub fn commit(value: u64, blind: &SecretKey) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let commit = secp.commit(value, blind.clone())?; - Ok(commit) + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let commit = secp.commit(value, blind.clone())?; + Ok(commit) } /// Add a blinding factor to an existing Commitment pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let excess_commit : Commitment = secp.commit(0, excess.clone())?; + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let excess_commit: Commitment = secp.commit(0, excess.clone())?; - let commits = vec![commitment.clone(), excess_commit.clone()]; - let sum = secp.commit_sum(commits, Vec::new())?; - Ok(sum) + let commits = vec![commitment.clone(), excess_commit.clone()]; + let sum = secp.commit_sum(commits, Vec::new())?; + Ok(sum) } /// Subtracts a value (v*H) from an existing commitment pub fn sub_value(commitment: &Commitment, value: u64) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let neg_commit : Commitment = secp.commit(value, ZERO_KEY)?; - let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; - Ok(sum) + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?; + let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; + Ok(sum) } /// Signs the message with the provided SecretKey pub fn sign(sk: &SecretKey, msg: &Message) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Full); - let pubkey = PublicKey::from_secret_key(&secp, &sk)?; - let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?; - Ok(sig) + let secp = Secp256k1::with_caps(ContextFlag::Full); + let pubkey = PublicKey::from_secret_key(&secp, &sk)?; + let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?; + Ok(sig) } #[cfg(test)] mod tests { - use super::{ComSignature, ContextFlag, Secp256k1, SecretKey}; - use crate::error::Result; + use super::{ComSignature, ContextFlag, Secp256k1, SecretKey}; + use crate::error::Result; - use secp256k1zkp::rand::{RngCore, thread_rng}; - use rand::Rng; - - /// Test signing and verification of ComSignatures - #[test] - fn verify_comsig() -> Result<()> { - let secp = Secp256k1::with_caps(ContextFlag::Commit); + use rand::Rng; + use secp256k1zkp::rand::{thread_rng, RngCore}; - let amount = thread_rng().next_u64(); - let blind = SecretKey::new(&secp, &mut thread_rng()); + /// Test signing and verification of ComSignatures + #[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 comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?; - - let commit = secp.commit(amount, blind.clone())?; - assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); + let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?; + + let commit = secp.commit(amount, blind.clone())?; + assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); let wrong_msg: [u8; 16] = rand::thread_rng().gen(); - assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); + assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); - let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; - assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); + let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; + assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); - Ok(()) - } -} \ No newline at end of file + Ok(()) + } +} diff --git a/src/server.rs b/src/server.rs index 7794f53..8e14925 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,427 +1,492 @@ use crate::config::ServerConfig; use crate::node::{self, GrinNode}; 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 grin_core::core::{Input, Output, OutputFeatures, TransactionBody}; use grin_core::global::DEFAULT_ACCEPT_FEE_BASE; use grin_util::StopState; use itertools::Itertools; -use jsonrpc_derive::rpc; -use jsonrpc_http_server::*; -use jsonrpc_http_server::jsonrpc_core::*; use jsonrpc_core::{Result, Value}; +use jsonrpc_derive::rpc; +use jsonrpc_http_server::jsonrpc_core::*; +use jsonrpc_http_server::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; #[derive(Clone, Debug, PartialEq)] struct Submission { - excess: SecretKey, - output_commit: Commitment, - rangeproof: Option, - input: Input, - fee: u64, - onion: Onion, + excess: SecretKey, + output_commit: Commitment, + rangeproof: Option, + input: Input, + fee: u64, + onion: Onion, } #[derive(Serialize, Deserialize)] pub struct SwapReq { - onion: Onion, - #[serde(with = "secp::comsig_serde")] - comsig: ComSignature, + onion: Onion, + #[serde(with = "secp::comsig_serde")] + comsig: ComSignature, } lazy_static! { - static ref SERVER_STATE: Mutex> = Mutex::new(HashMap::new()); + static ref SERVER_STATE: Mutex> = Mutex::new(HashMap::new()); } #[rpc(server)] pub trait Server { - #[rpc(name = "swap")] - fn swap(&self, swap: SwapReq) -> Result; + #[rpc(name = "swap")] + fn swap(&self, swap: SwapReq) -> Result; - // milestone 3: Used by mwixnet coinswap servers to communicate with each other - // fn derive_outputs(&self, entries: Vec) -> Result; - // fn derive_kernel(&self, tx: Tx) -> Result; + // milestone 3: Used by mwixnet coinswap servers to communicate with each other + // fn derive_outputs(&self, entries: Vec) -> Result; + // fn derive_kernel(&self, tx: Tx) -> Result; } #[derive(Clone)] struct ServerImpl { - server_config: ServerConfig, - stop_state: Arc, - wallet: Arc, - node: Arc, + server_config: ServerConfig, + stop_state: Arc, + wallet: Arc, + node: Arc, } impl ServerImpl { - fn new(server_config: ServerConfig, stop_state: Arc, wallet: Arc, node: Arc) -> Self { - ServerImpl { server_config, stop_state, wallet, node } - } + fn new( + server_config: ServerConfig, + stop_state: Arc, + wallet: Arc, + node: Arc, + ) -> Self { + ServerImpl { + server_config, + stop_state, + wallet, + node, + } + } - /// The fee base to use. For now, just using the default. - fn get_fee_base(&self) -> u64 { - DEFAULT_ACCEPT_FEE_BASE - } + /// The fee base to use. For now, just using the default. + fn get_fee_base(&self) -> u64 { + DEFAULT_ACCEPT_FEE_BASE + } - /// Minimum fee to perform a swap. - /// Requires enough fee for the mwixnet server's kernel, 1 input and its output to swap. - fn get_minimum_swap_fee(&self) -> u64 { - TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base() - } + /// Minimum fee to perform a swap. + /// Requires enough fee for the mwixnet server's kernel, 1 input and its output to swap. + fn get_minimum_swap_fee(&self) -> u64 { + TransactionBody::weight_by_iok(1, 1, 1) * self.get_fee_base() + } - /// Iterate through all saved submissions, filter out any inputs that are no longer spendable, - /// and assemble the coinswap transaction, posting the transaction to the configured node. - /// - /// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes. - fn execute_round(&self) -> crate::error::Result<()> { - let mut locked_state = SERVER_STATE.lock().unwrap(); - let next_block_height = self.node.get_chain_height()? + 1; + /// Iterate through all saved submissions, filter out any inputs that are no longer spendable, + /// and assemble the coinswap transaction, posting the transaction to the configured node. + /// + /// Currently only a single mix node is used. Milestone 3 will include support for multiple mix nodes. + fn execute_round(&self) -> crate::error::Result<()> { + let mut locked_state = SERVER_STATE.lock().unwrap(); + let next_block_height = self.node.get_chain_height()? + 1; - let spendable : Vec = locked_state - .values() - .into_iter() - .unique_by(|s| s.output_commit) - .filter(|s| node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)) - .filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true)) - .cloned() - .collect(); + let spendable: Vec = locked_state + .values() + .into_iter() + .unique_by(|s| s.output_commit) + .filter(|s| { + node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false) + }) + .filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true)) + .cloned() + .collect(); - if spendable.len() == 0 { - return Ok(()) - } + if spendable.len() == 0 { + return Ok(()); + } - let total_fee : u64 = spendable - .iter() - .enumerate() - .map(|(_, s)| s.fee) - .sum(); - - let inputs : Vec = spendable - .iter() - .enumerate() - .map(|(_, s)| s.input) - .collect(); - - let outputs : Vec = spendable - .iter() - .enumerate() - .map(|(_, s)| Output::new(OutputFeatures::Plain, s.output_commit, s.rangeproof.unwrap())) - .collect(); - - let excesses : Vec = spendable - .iter() - .enumerate() - .map(|(_, s)| s.excess.clone()) - .collect(); - - let tx = wallet::assemble_tx(&self.wallet, &inputs, &outputs, self.get_fee_base(), total_fee, &excesses)?; - - self.node.post_tx(&tx)?; - locked_state.clear(); - - Ok(()) - } + let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum(); + + let inputs: Vec = spendable.iter().enumerate().map(|(_, s)| s.input).collect(); + + let outputs: Vec = spendable + .iter() + .enumerate() + .map(|(_, s)| { + Output::new( + OutputFeatures::Plain, + s.output_commit, + s.rangeproof.unwrap(), + ) + }) + .collect(); + + let excesses: Vec = spendable + .iter() + .enumerate() + .map(|(_, s)| s.excess.clone()) + .collect(); + + let tx = wallet::assemble_tx( + &self.wallet, + &inputs, + &outputs, + self.get_fee_base(), + total_fee, + &excesses, + )?; + + self.node.post_tx(&tx)?; + locked_state.clear(); + + Ok(()) + } } impl Server for ServerImpl { - /// Implements the 'swap' API - fn swap(&self, swap: SwapReq) -> Result { - // milestone 3: check that enc_payloads length matches number of configured servers - if swap.onion.enc_payloads.len() != 1 { - return Err(jsonrpc_core::Error::invalid_params("Multi server not supported until milestone 3")); - } + /// Implements the 'swap' API + fn swap(&self, swap: SwapReq) -> Result { + // milestone 3: check that enc_payloads length matches number of configured servers + if swap.onion.enc_payloads.len() != 1 { + return Err(Error::invalid_params( + "Multi server not supported until milestone 3", + )); + } - // Verify commitment signature to ensure caller owns the output - let serialized_onion = swap.onion.serialize() - .map_err(|_| jsonrpc_core::Error::internal_error())?; - let _ = swap.comsig.verify(&swap.onion.commit, &serialized_onion) - .map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?; + // Verify commitment signature to ensure caller owns the output + let serialized_onion = swap.onion.serialize().map_err(|e| Error { + message: e.to_string(), + code: ErrorCode::InternalError, + data: None, + })?; + let _ = swap + .comsig + .verify(&swap.onion.commit, &serialized_onion) + .map_err(|_| Error::invalid_params("ComSignature invalid"))?; - // Verify that commitment is unspent - let input = node::build_input(&self.node, &swap.onion.commit) - .map_err(|_| jsonrpc_core::Error::internal_error())?; - let input = input.ok_or(jsonrpc_core::Error::invalid_params("Commitment not found"))?; - - let peeled = swap.onion.peel_layer(&self.server_config.key) - .map_err(|e| jsonrpc_core::Error::invalid_params(e.message()))?; + // Verify that commitment is unspent + let input = node::build_input(&self.node, &swap.onion.commit).map_err(|e| Error { + message: e.to_string(), + code: ErrorCode::InternalError, + data: None, + })?; + let input = input.ok_or(Error::invalid_params("Commitment not found"))?; - // Verify the fee meets the minimum - let fee: u64 = peeled.0.fee.into(); - if fee < self.get_minimum_swap_fee() { - return Err(jsonrpc_core::Error::invalid_params("Fee does not meet minimum")); - } + let peeled = swap + .onion + .peel_layer(&self.server_config.key) + .map_err(|e| Error::invalid_params(e.message()))?; - // Calculate final output commitment - let output_commit = secp::add_excess(&swap.onion.commit, &peeled.0.excess) - .map_err(|_| jsonrpc_core::Error::internal_error())?; - let output_commit = secp::sub_value(&output_commit, fee) - .map_err(|_| jsonrpc_core::Error::internal_error())?; + // Verify the fee meets the minimum + let fee: u64 = peeled.0.fee.into(); + if fee < self.get_minimum_swap_fee() { + return Err(Error::invalid_params("Fee does not meet minimum")); + } - // Verify the bullet proof and build the final output - if let Some(r) = peeled.0.rangeproof { - let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); - secp.verify_bullet_proof(output_commit, r, None) - .map_err(|_| jsonrpc_core::Error::invalid_params("RangeProof invalid"))?; - } else { - // milestone 3: only the last hop will have a rangeproof - return Err(jsonrpc_core::Error::invalid_params("Rangeproof expected")); - } + // Calculate final output commitment + let output_commit = + secp::add_excess(&swap.onion.commit, &peeled.0.excess).map_err(|e| Error { + message: e.to_string(), + code: ErrorCode::InternalError, + data: None, + })?; + 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(); - if locked.contains_key(&swap.onion.commit) { - return Err(jsonrpc_core::Error::invalid_params("swap already called for coin")); - } + // Verify the bullet proof and build the final output + if let Some(r) = peeled.0.rangeproof { + let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); + secp.verify_bullet_proof(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{ - excess: peeled.0.excess, - output_commit: output_commit, - rangeproof: peeled.0.rangeproof, - input: input, - fee: fee, - onion: peeled.1 - }); - Ok(Value::String("success".into())) - } + let mut locked = SERVER_STATE.lock().unwrap(); + if locked.contains_key(&swap.onion.commit) { + return Err(Error::invalid_params("swap already called for coin")); + } + + locked.insert( + swap.onion.commit, + Submission { + 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 -pub fn listen(server_config: &ServerConfig, wallet: Arc, node: Arc, stop_state: &Arc) -> std::result::Result<(), Box> -{ - let server_impl = Arc::new(ServerImpl::new(server_config.clone(), stop_state.clone(), wallet.clone(), node.clone())); +pub fn listen( + server_config: &ServerConfig, + wallet: Arc, + node: Arc, + stop_state: &Arc, +) -> std::result::Result<(), Box> { + let server_impl = Arc::new(ServerImpl::new( + server_config.clone(), + stop_state.clone(), + wallet.clone(), + node.clone(), + )); - let mut io = IoHandler::new(); - io.extend_with(ServerImpl::to_delegate(server_impl.as_ref().clone())); + let mut io = IoHandler::new(); + io.extend_with(ServerImpl::to_delegate(server_impl.as_ref().clone())); - let server = ServerBuilder::new(io) - .cors(DomainsValidation::Disabled) - .request_middleware(|request: hyper::Request| { - if request.uri() == "/v1" { - request.into() - } else { - jsonrpc_http_server::Response::bad_request("Only v1 supported").into() - } - }) - .start_http(&server_config.addr) - .expect("Unable to start RPC server"); + let server = ServerBuilder::new(io) + .cors(DomainsValidation::Disabled) + .request_middleware(|request: hyper::Request| { + if request.uri() == "/v1" { + request.into() + } else { + jsonrpc_http_server::Response::bad_request("Only v1 supported").into() + } + }) + .start_http(&server_config.addr) + .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 round_handle = std::thread::spawn(move || { - let mut secs = 0; - loop { - if server_impl.as_ref().stop_state.is_stopped() { - close_handle.close(); - break; - } + let config = server_config.clone(); + let round_handle = std::thread::spawn(move || { + let mut secs = 0; + loop { + if server_impl.as_ref().stop_state.is_stopped() { + close_handle.close(); + break; + } - std::thread::sleep(std::time::Duration::from_secs(1)); - secs = (secs + 1) % config.interval_s; - - if secs == 0 { - let _ = server_impl.as_ref().execute_round(); - secs = 0; - } - } - }); + std::thread::sleep(std::time::Duration::from_secs(1)); + secs = (secs + 1) % config.interval_s; - server.wait(); - round_handle.join().unwrap(); + if secs == 0 { + let _ = server_impl.as_ref().execute_round(); + secs = 0; + } + } + }); - Ok(()) + server.wait(); + round_handle.join().unwrap(); + + Ok(()) } #[cfg(test)] mod tests { - use crate::config::ServerConfig; - use crate::node::{GrinNode, MockGrinNode}; - use crate::onion::test_util::{self, Hop}; - use crate::secp::{self, ComSignature, PublicKey, Secp256k1}; - use crate::server::{self, SwapReq}; - use crate::types::Payload; - use crate::wallet::{MockWallet, Wallet}; + use crate::config::ServerConfig; + use crate::node::{GrinNode, MockGrinNode}; + use crate::onion::test_util::{self, Hop}; + use crate::secp::{self, ComSignature, PublicKey, Secp256k1}; + use crate::server::{self, SwapReq}; + use crate::types::Payload; + use crate::wallet::{MockWallet, Wallet}; - use grin_core::core::{Committed, FeeFields, Transaction}; - use grin_util::StopState; - use std::net::TcpListener; - use std::sync::Arc; - use std::time::Duration; - use std::thread; + use grin_core::core::{Committed, FeeFields, Transaction}; + use grin_util::StopState; + use std::net::TcpListener; + use std::sync::Arc; + use std::thread; + use std::time::Duration; - use hyper::{Body, Client, Request, Response}; - use tokio::runtime; + use hyper::{Body, Client, Request, Response}; + use tokio::runtime; - async fn body_to_string(req: Response) -> String { - let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap(); - String::from_utf8(body_bytes.to_vec()).unwrap() - } + async fn body_to_string(req: Response) -> String { + let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap(); + String::from_utf8(body_bytes.to_vec()).unwrap() + } - /// Spin up a temporary web service, query the API, then cleanup and return response - fn make_request( - server_key: secp::SecretKey, - wallet: Arc, - node: Arc, - req: String - ) -> Result> { - let server_config = ServerConfig { - key: server_key, - interval_s: 1, - addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, - grin_node_url: "127.0.0.1:3413".parse()?, - wallet_owner_url: "127.0.0.1:3420".parse()? - }; + /// Spin up a temporary web service, query the API, then cleanup and return response + fn make_request( + server_key: secp::SecretKey, + wallet: Arc, + node: Arc, + req: String, + ) -> Result> { + let server_config = ServerConfig { + key: server_key, + interval_s: 1, + addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, + grin_node_url: "127.0.0.1:3413".parse()?, + wallet_owner_url: "127.0.0.1:3420".parse()?, + }; - let threaded_rt = runtime::Runtime::new()?; - let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel(); - let uri = format!("http://{}/v1", server_config.addr); + let threaded_rt = runtime::Runtime::new()?; + let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel(); + let uri = format!("http://{}/v1", server_config.addr); - let stop_state = Arc::new(StopState::new()); - let stop_state_clone = stop_state.clone(); + let stop_state = Arc::new(StopState::new()); + let stop_state_clone = stop_state.clone(); - // Spawn the server task - threaded_rt.spawn(async move { - 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(); - }); + // Spawn the server task + threaded_rt.spawn(async move { + server::listen(&server_config, wallet, node, &stop_state).unwrap() + }); - // Wait for listener - thread::sleep(Duration::from_millis(500)); + threaded_rt.spawn(async move { + futures::executor::block_on(shutdown_receiver).unwrap(); + stop_state_clone.stop(); + }); - let do_request = async move { - let request = Request::post(uri) - .header("Content-Type", "application/json") - .body(Body::from(req)) - .unwrap(); + // Wait for listener + thread::sleep(Duration::from_millis(500)); - 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)?; - 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)); + Client::new().request(request).await + }; - 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 - thread::sleep(Duration::from_millis(500)); - threaded_rt.shutdown_background(); + // Wait for at least one round to execute + thread::sleep(Duration::from_millis(1500)); - Ok(response_str) - } + shutdown_sender.send(()).ok(); - /// Single hop to demonstrate request validation and onion unwrapping. - #[test] - fn swap_lifecycle() -> Result<(), Box> { - 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); + // Wait for shutdown + thread::sleep(Duration::from_millis(500)); + threaded_rt.shutdown_background(); - let hop = Hop { - 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 = 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, - }; + Ok(response_str) + } - let wallet = MockWallet{}; - let mut mut_node = MockGrinNode::new(); - mut_node.add_default_utxo(&commitment); - let node = Arc::new(mut_node); + /// Single hop to demonstrate request validation and onion unwrapping. + #[test] + fn swap_lifecycle() -> Result<(), Box> { + 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)); - println!("Request: {}", req); - let response = make_request(server_key, Arc::new(wallet), node.clone(), req)?; - let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n"; - assert_eq!(response, expected); + 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(); - // 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); + 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, + ); - Ok(()) - } - - /// Returns "Commitment not found" when there's no matching output in the UTXO set. - #[test] - fn swap_utxo_missing() -> Result<(), Box> { - let secp = Secp256k1::new(); - 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 = Hop { + 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 = 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 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 = 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 mut mut_node = MockGrinNode::new(); + mut_node.add_default_utxo(&commitment); + let node = Arc::new(mut_node); - 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(()) - } + let req = format!( + "{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", + serde_json::json!(swap) + ); + println!("Request: {}", req); + let response = make_request(server_key, Arc::new(wallet), node.clone(), req)?; + let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n"; + 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] - fn swap_bad_request() -> Result<(), Box> { - let wallet = MockWallet{}; - let node = MockGrinNode::new(); + Ok(()) + } - 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(()) - } -} \ No newline at end of file + /// Returns "Commitment not found" when there's no matching output in the UTXO set. + #[test] + fn swap_utxo_missing() -> Result<(), Box> { + let secp = Secp256k1::new(); + 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 = 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 = 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> { + 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(()) + } +} diff --git a/src/types.rs b/src/types.rs index abb7782..9a586f1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,68 +5,67 @@ use grin_core::core::FeeFields; use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; use serde::{Deserialize, Serialize}; -const CURRENT_VERSION : u8 = 0; +const CURRENT_VERSION: u8 = 0; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Payload { - pub excess: SecretKey, - pub fee: FeeFields, - pub rangeproof: Option, + pub excess: SecretKey, + pub fee: FeeFields, + pub rangeproof: Option, } impl Payload { - pub fn deserialize(bytes: &Vec) -> Result { - let payload: Payload = grin_core::ser::deserialize_default(&mut &bytes[..])?; - Ok(payload) - } + pub fn deserialize(bytes: &Vec) -> Result { + let payload: Payload = ser::deserialize_default(&mut &bytes[..])?; + Ok(payload) + } - #[cfg(test)] - pub fn serialize(&self) -> Result> { - let mut vec = vec![]; - ser::serialize_default(&mut vec, &self)?; - Ok(vec) - } + #[cfg(test)] + pub fn serialize(&self) -> Result> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } } impl Readable for Payload { - fn read(reader: &mut R) -> StdResult { - let version = reader.read_u8()?; - if version != CURRENT_VERSION { - return Err(ser::Error::UnsupportedProtocolVersion); - } + fn read(reader: &mut R) -> StdResult { + let version = reader.read_u8()?; + if version != CURRENT_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } - let excess = secp::read_secret_key(reader)?; - let fee = FeeFields::try_from(reader.read_u64()?) - .map_err(|_| ser::Error::CorruptedData)?; - let rangeproof = if reader.read_u8()? == 0 { - None - } else { - Some(RangeProof::read(reader)?) - }; + let excess = secp::read_secret_key(reader)?; + let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?; + let rangeproof = if reader.read_u8()? == 0 { + None + } else { + Some(RangeProof::read(reader)?) + }; - let payload = Payload { - excess: excess, - fee: fee, - rangeproof: rangeproof - }; - Ok(payload) - } + let payload = Payload { + excess, + fee, + rangeproof, + }; + Ok(payload) + } } impl Writeable for Payload { - fn write(&self, writer: &mut W) -> StdResult<(), ser::Error> { - writer.write_u8(CURRENT_VERSION)?; - writer.write_fixed_bytes(&self.excess)?; - writer.write_u64(self.fee.into())?; + fn write(&self, writer: &mut W) -> StdResult<(), ser::Error> { + writer.write_u8(CURRENT_VERSION)?; + writer.write_fixed_bytes(&self.excess)?; + writer.write_u64(self.fee.into())?; - match &self.rangeproof { - Some(proof) => { - writer.write_u8(1)?; - proof.write(writer)?; - }, - None => writer.write_u8(0)?, - }; + match &self.rangeproof { + Some(proof) => { + writer.write_u8(1)?; + proof.write(writer)?; + } + None => writer.write_u8(0)?, + }; - Ok(()) - } -} \ No newline at end of file + Ok(()) + } +} diff --git a/src/wallet.rs b/src/wallet.rs index e48429f..bcdb7db 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,107 +1,188 @@ use crate::error::{ErrorKind, Result}; -use crate::secp::{self, SecretKey}; +use crate::secp::{self}; use grin_api::client; use grin_api::json_rpc::{build_request, Request, Response}; -use grin_core::core::{FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel}; +use grin_core::core::{ + FeeFields, Input, Inputs, KernelFeatures, Output, OutputFeatures, Transaction, TransactionBody, + TxKernel, +}; use grin_core::libtx::secp_ser; use grin_keychain::BlindingFactor; -use grin_util::ZeroingString; -use grin_wallet_api::Token; -use secp256k1zkp::{ContextFlag, Secp256k1}; +use grin_util::{ToHex, ZeroingString}; +use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token}; +use secp256k1zkp::{ContextFlag, PublicKey, Secp256k1, SecretKey}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::net::SocketAddr; use std::sync::Arc; -pub trait Wallet : Send + Sync { - /// Builds an output for the wallet with the provided amount. - fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)>; +pub trait Wallet: Send + Sync { + /// Builds an output for the wallet with the provided amount. + fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)>; } /// Builds and verifies a 'Transaction' using the provided components. -pub fn assemble_tx(wallet: &Arc, inputs: &Vec, outputs: &Vec, fee_base: u64, total_fee: u64, excesses: &Vec) -> Result { - let secp = Secp256k1::with_caps(ContextFlag::Commit); - let txn_inputs = Inputs::from(inputs.as_slice()); - let mut txn_outputs = outputs.clone(); - let mut txn_excesses = excesses.clone(); - let mut kernel_fee = total_fee; +pub fn assemble_tx( + wallet: &Arc, + inputs: &Vec, + outputs: &Vec, + fee_base: u64, + total_fee: u64, + excesses: &Vec, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let txn_inputs = Inputs::from(inputs.as_slice()); + let mut txn_outputs = outputs.clone(); + let mut txn_excesses = excesses.clone(); + let mut kernel_fee = total_fee; - // calculate fee required if we add our own output - let fee_required = TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1) * fee_base; + // calculate fee required if we add our own output + let fee_required = + TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1) + * fee_base; - // calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it - let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base; + // calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it + let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base; - // collect any leftover fees - if total_fee > fee_required + fee_to_spend { - let amount = total_fee - fee_required; - kernel_fee -= amount; + // collect any leftover fees + if total_fee > fee_required + fee_to_spend { + let amount = total_fee - fee_required; + kernel_fee -= amount; - let wallet_output = wallet.build_output(amount)?; - txn_outputs.push(wallet_output.1); + let wallet_output = wallet.build_output(amount)?; + txn_outputs.push(wallet_output.1); - let output_excess = wallet_output.0.secret_key(&secp) - .map_err(|_| ErrorKind::CorruptedData)?; - txn_excesses.push(output_excess); - } + let output_excess = wallet_output + .0 + .secret_key(&secp) + .map_err(|_| ErrorKind::CorruptedData)?; + txn_excesses.push(output_excess); + } - // generate random transaction offset - let offset = secp::random_secret(); + // generate random transaction offset + let offset = secp::random_secret(); - // calculate kernel excess - let kern_excess = secp.blind_sum(txn_excesses, vec![offset.clone()])?; + // calculate kernel excess + let kern_excess = secp.blind_sum(txn_excesses, vec![offset.clone()])?; - // build and verify kernel - let mut kernel = TxKernel::with_features(KernelFeatures::Plain { - fee: FeeFields::new(0, kernel_fee).unwrap(), - }); - let msg = kernel.msg_to_sign()?; - kernel.excess = secp::commit(0, &kern_excess)?; - kernel.excess_sig = secp::sign(&kern_excess, &msg)?; - kernel.verify()?; + // build and verify kernel + let mut kernel = TxKernel::with_features(KernelFeatures::Plain { + fee: FeeFields::new(0, kernel_fee).unwrap(), + }); + let msg = kernel.msg_to_sign()?; + kernel.excess = secp::commit(0, &kern_excess)?; + kernel.excess_sig = secp::sign(&kern_excess, &msg)?; + kernel.verify()?; - // assemble the transaction - let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel]) - .with_offset(BlindingFactor::from_secret_key(offset)); - Ok(tx) + // assemble the transaction + let tx = Transaction::new(txn_inputs, &txn_outputs, &[kernel]) + .with_offset(BlindingFactor::from_secret_key(offset)); + Ok(tx) } /// HTTP (JSONRPC) implementation of the 'Wallet' trait. #[derive(Clone)] pub struct HttpWallet { - wallet_owner_url: SocketAddr, - token: Token, + wallet_owner_url: SocketAddr, + shared_key: SecretKey, + token: Token, } 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 { - /// Calls the 'open_wallet' using the RPC API. - pub fn open_wallet(wallet_owner_url: &SocketAddr, wallet_pass: &ZeroingString) -> Result { - let open_wallet_params = json!({ - "name": null, - "password": wallet_pass.to_string() - }); - let token: Token = HttpWallet::send_json_request(&wallet_owner_url, "open_wallet", &open_wallet_params)?; + /// Calls the 'open_wallet' using the RPC API. + pub fn open_wallet( + wallet_owner_url: &SocketAddr, + wallet_pass: &ZeroingString, + ) -> Result { + println!("Opening wallet at {}", wallet_owner_url); + let shared_key = HttpWallet::init_secure_api(&wallet_owner_url)?; - Ok(HttpWallet { - wallet_owner_url: wallet_owner_url.clone(), - token: token, - }) - } + let open_wallet_params = json!({ + "name": null, + "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( - wallet_owner_url: &SocketAddr, - method: &str, - params: &serde_json::Value, - ) -> Result { - let url = format!("http://{}{}", wallet_owner_url, ENDPOINT); - let req = build_request(method, params); - let res = client::post::(url.as_str(), None, &req)?; - let parsed = res.clone().into_result()?; - Ok(parsed) - } + Ok(HttpWallet { + wallet_owner_url: wallet_owner_url.clone(), + shared_key: shared_key.clone(), + token: token.clone(), + }) + } + + fn init_secure_api(wallet_owner_url: &SocketAddr) -> Result { + let secp = Secp256k1::new(); + 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( + wallet_owner_url: &SocketAddr, + method: &str, + params: &serde_json::Value, + shared_key: &SecretKey, + ) -> Result { + 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::(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( + wallet_owner_url: &SocketAddr, + method: &str, + params: &serde_json::Value, + ) -> Result { + let url = format!("http://{}{}", wallet_owner_url, ENDPOINT); + let req = build_request(method, params); + let res = client::post::(url.as_str(), None, &req)?; + let parsed = res.clone().into_result()?; + Ok(parsed) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -115,40 +196,42 @@ pub struct OutputWithBlind { } impl Wallet for HttpWallet { - /// Builds an 'Output' for the wallet using the 'build_output' RPC API. - fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> { - let req_json = json!({ + /// Builds an 'Output' for the wallet using the 'build_output' RPC API. + fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> { + let req_json = json!({ "token": self.token.keychain_mask.clone().unwrap().0, "features": "Plain", "amount": amount - }); - let output: OutputWithBlind = HttpWallet::send_json_request(&self.wallet_owner_url, "build_output", &req_json)?; - Ok((output.blind, output.output)) - } + }); + let output: OutputWithBlind = HttpWallet::send_enc_request( + &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. #[derive(Clone)] -pub struct MockWallet { -} +pub struct MockWallet {} impl Wallet for MockWallet { - /// Builds an 'Output' for the wallet using the 'build_output' RPC API. - fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> { - let secp = Secp256k1::new(); - let blind = secp::random_secret(); - let commit = secp::commit(amount, &blind)?; - let proof = secp.bullet_proof( - amount, - blind.clone(), - secp::random_secret(), - secp::random_secret(), - None, - None, - ); - let output = Output::new(OutputFeatures::Plain, commit.clone(), proof); - Ok((BlindingFactor::from_secret_key(blind), output)) - } -} \ No newline at end of file + /// Builds an 'Output' for the wallet using the 'build_output' RPC API. + fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)> { + let secp = Secp256k1::new(); + let blind = secp::random_secret(); + let commit = secp::commit(amount, &blind)?; + let proof = secp.bullet_proof( + amount, + blind.clone(), + secp::random_secret(), + secp::random_secret(), + None, + None, + ); + let output = Output::new(OutputFeatures::Plain, commit.clone(), proof); + Ok((BlindingFactor::from_secret_key(blind), output)) + } +}