wip: milestone 2

This commit is contained in:
scilio 2022-02-10 20:33:35 -05:00
parent a7c811fdd6
commit 68929fd493
12 changed files with 3952 additions and 197 deletions

3145
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,19 +10,31 @@ blake2 = { package = "blake2-rfc", version = "0.2"}
byteorder = "1" byteorder = "1"
bytes = "0.5.6" bytes = "0.5.6"
chacha20 = "0.8.1" chacha20 = "0.8.1"
clap = { version = "2.33", features = ["yaml"] }
failure = "0.1.8" failure = "0.1.8"
futures = "0.3" futures = "0.3"
hmac = { version = "0.11.0", features = ["std"]} hmac = { version = "0.12.0", features = ["std"]}
hyper = { version = "0.14", features = ["full"] } hyper = { version = "0.14", features = ["full"] }
jsonrpc-core = "18.0" jsonrpc-core = "18.0"
jsonrpc-derive = "18.0" jsonrpc-derive = "18.0"
jsonrpc-http-server = "18.0" jsonrpc-http-server = "18.0"
lazy_static = "1" lazy_static = "1"
pbkdf2 = "0.8.0"
rand = "0.8.4" rand = "0.8.4"
ring = "0.16"
serde = { version = "1", features= ["derive"]} serde = { version = "1", features= ["derive"]}
serde_derive = "1" serde_derive = "1"
serde_json = "1" serde_json = "1"
sha2 = "0.9.8" sha2 = "0.10.0"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
toml = "0.5"
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]} grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
grin_util = "5" grin_util = "5"
grin_api = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
grin_core = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
grin_chain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
grin_keychain = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
grin_servers = { git = "https://github.com/mimblewimble/grin", version = "5.2.0-alpha.1" }
grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", version = "5.1.0-alpha.1" }
grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", version = "5.1.0-alpha.1" }
grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", version = "5.1.0-alpha.1" }

37
mwixnet.yml Normal file
View file

@ -0,0 +1,37 @@
name: mwixnet
about: MWixnet CoinSwap Server
author: scilio
args:
- config_file:
help: Path to load/save the mwixnet-config.toml configuration file
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
long: grin_node_url
takes_value: true
- wallet_owner_url:
help: Api address of running wallet owner listener
short: l
long: wallet_owner_url
takes_value: true
- wallet_pass:
help: The wallet's password
long: wallet_pass
takes_value: true
- bind_addr:
help: Address to bind the rpc server to (e.g. 0.0.0.0:3000)
long: bind_addr
takes_value: true
subcommands:
- init-config:
about: Writes a new configuration file

177
src/config.rs Normal file
View file

@ -0,0 +1,177 @@
use crate::error::{self, Result};
use crate::secp::SecretKey;
use core::num::NonZeroU32;
use grin_util::{ToHex, ZeroingString};
use rand::{thread_rng, Rng};
use ring::{aead, pbkdf2};
use serde_derive::{Deserialize, Serialize};
use std::fs::File;
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
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ServerConfig {
pub key: SecretKey,
pub addr: SocketAddr,
pub grin_node_url: SocketAddr,
pub wallet_owner_url: SocketAddr,
}
/// Encrypted server key, for storing on disk and decrypting with provided password
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct EncryptedServerKey {
encrypted_key: String,
pub salt: String,
pub nonce: String,
}
impl EncryptedServerKey {
pub fn from_secret_key(
server_key: &SecretKey,
password: &ZeroingString,
) -> Result<EncryptedServerKey> {
let salt: [u8; 8] = thread_rng().gen();
let nonce: [u8; 12] = thread_rng().gen();
let password = password.as_bytes();
let mut key = [0; 32];
ring::pbkdf2::derive(
ring::pbkdf2::PBKDF2_HMAC_SHA512,
NonZeroU32::new(100).unwrap(),
&salt,
password,
&mut key,
);
let content = server_key.0.to_vec();
let mut enc_bytes = content;
let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap();
let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key);
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)?;
Ok(EncryptedServerKey {
encrypted_key: enc_bytes.to_hex(),
salt: salt.to_hex(),
nonce: nonce.to_hex(),
})
}
pub fn decrypt(&self, password: &str) -> Result<SecretKey> {
let mut encrypted_seed = grin_util::from_hex(&self.encrypted_key.clone())
.map_err(|_| error::ErrorKind::LoadConfigError)?;
let salt = grin_util::from_hex(&self.salt.clone())
.map_err(|_| error::ErrorKind::LoadConfigError)?;
let nonce = grin_util::from_hex(&self.nonce.clone())
.map_err(|_| error::ErrorKind::LoadConfigError)?;
let password = password.as_bytes();
let mut key = [0; 32];
pbkdf2::derive(
ring::pbkdf2::PBKDF2_HMAC_SHA512,
NonZeroU32::new(100).unwrap(),
&salt,
password,
&mut key,
);
let mut n = [0u8; 12];
n.copy_from_slice(&nonce[0..12]);
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)?;
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())
}
}
/// The config attributes saved to disk
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct RawConfig {
pub encrypted_key: String,
pub salt: String,
pub nonce: String,
pub addr: SocketAddr,
pub grin_node_url: SocketAddr,
pub wallet_owner_url: SocketAddr,
}
/// 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<()> {
let encrypted = EncryptedServerKey::from_secret_key(&server_config.key, &password)?;
let raw_config = RawConfig{
encrypted_key: encrypted.encrypted_key,
salt: encrypted.salt,
nonce: encrypted.nonce,
addr: server_config.addr,
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 mut file = File::create(config_path)?;
file.write_all(encoded.as_bytes()).map_err(|_| error::ErrorKind::SaveConfigError)?;
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<ServerConfig> {
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{
encrypted_key: raw_config.encrypted_key,
salt: raw_config.salt,
nonce: raw_config.nonce
};
let secret_key = encrypted_key.decrypt(&password)?;
Ok(ServerConfig {
key: secret_key,
addr: raw_config.addr,
grin_node_url: raw_config.grin_node_url,
wallet_owner_url: raw_config.wallet_owner_url
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secp;
#[test]
fn server_key_encrypt() {
let password = ZeroingString::from("password");
let server_key = secp::random_secret();
let mut enc_key = EncryptedServerKey::from_secret_key(&server_key, &password).unwrap();
let decrypted_key = enc_key.decrypt(&password).unwrap();
assert_eq!(server_key, decrypted_key);
// Wrong password
let decrypted_key = enc_key.decrypt("wrongpass");
assert!(decrypted_key.is_err());
// Wrong nonce
enc_key.nonce = "wrongnonce".to_owned();
let decrypted_key = enc_key.decrypt(&password);
assert!(decrypted_key.is_err());
}
}

View file

@ -1,6 +1,7 @@
use failure::{self, Context, Fail}; use failure::{self, Context, Fail};
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::io; use std::io;
use grin_wallet_libwallet as libwallet;
/// MWixnet error definition /// MWixnet error definition
#[derive(Debug)] #[derive(Debug)]
@ -45,6 +46,27 @@ pub enum ErrorKind {
/// When asked to read too much data /// When asked to read too much data
#[fail(display = "TooLargeReadErr")] #[fail(display = "TooLargeReadErr")]
TooLargeReadErr, TooLargeReadErr,
/// 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,
} }
impl std::error::Error for Error { impl std::error::Error for Error {
@ -70,6 +92,12 @@ impl Display for Error {
} }
impl Error { impl Error {
pub fn new(kind: ErrorKind) -> Error {
Error {
inner: Context::new(kind),
}
}
pub fn kind(&self) -> ErrorKind { pub fn kind(&self) -> ErrorKind {
self.inner.get_context().clone() self.inner.get_context().clone()
} }
@ -101,10 +129,50 @@ impl From<secp256k1zkp::Error> for Error {
} }
} }
impl From<hmac::crypto_mac::InvalidKeyLength> for Error { impl From<hmac::digest::InvalidLength> for Error {
fn from(_error: hmac::crypto_mac::InvalidKeyLength) -> Error { fn from(_error: hmac::digest::InvalidLength) -> Error {
Error { Error {
inner: Context::new(ErrorKind::InvalidKeyLength), inner: Context::new(ErrorKind::InvalidKeyLength),
} }
} }
}
impl From<grin_api::Error> for Error {
fn from(_error: grin_api::Error) -> Error {
Error {
inner: Context::new(ErrorKind::GrinApiError),
}
}
}
impl From<grin_api::json_rpc::Error> for Error {
fn from(_error: grin_api::json_rpc::Error) -> Error {
Error {
inner: Context::new(ErrorKind::GrinApiError),
}
}
}
impl From<grin_core::core::transaction::Error> for Error {
fn from(_error: grin_core::core::transaction::Error) -> Error {
Error {
inner: Context::new(ErrorKind::GrinCoreError),
}
}
}
impl From<libwallet::Error> for Error {
fn from(_error: libwallet::Error) -> Error {
Error {
inner: Context::new(ErrorKind::LibWalletError),
}
}
}
impl From<serde_json::Error> for Error {
fn from(_error: serde_json::Error) -> Error {
Error {
inner: Context::new(ErrorKind::SerdeJsonError),
}
}
} }

View file

@ -1,22 +1,85 @@
use server::ServerConfig; use config::ServerConfig;
use error::{Error, ErrorKind};
use wallet::Wallet;
use clap::App;
use grin_util::ZeroingString;
use std::env;
use std::path::PathBuf;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
#[macro_use]
extern crate clap;
mod config;
mod error; mod error;
mod node;
mod onion; mod onion;
mod secp; mod secp;
mod ser; mod ser;
mod server; mod server;
mod types; mod types;
mod wallet;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let secret_key = secp::insecure_rand_secret()?; // todo - load from encrypted key file let yml = load_yaml!("../mwixnet.yml");
let server_config = ServerConfig { let args = App::from_yaml(yml).get_matches();
key: secret_key,
addr: "127.0.0.1:3000".parse().unwrap(), let config_path = match args.value_of("config_file") {
is_first: true 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 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 server_config = ServerConfig {
key: secp::random_secret(),
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()?,
};
config::write_config(&config_path, &server_config, &password)?;
return Ok(());
}
let mut server_config = config::load_config(&config_path, &password)?;
// Override bind_addr, if supplied
if let Some(bind_addr) = bind_addr {
server_config.addr = bind_addr.parse()?;
}
// Override grin_node_url, if supplied
if let Some(grin_node_url) = grin_node_url {
server_config.grin_node_url = grin_node_url.parse()?;
}
// Override wallet_owner_url, if supplied
if let Some(wallet_owner_url) = wallet_owner_url {
server_config.wallet_owner_url = wallet_owner_url.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 = Wallet::open_wallet(&server_config.wallet_owner_url, &wallet_pass)?;
let shutdown_signal = async move { let shutdown_signal = async move {
// Wait for the CTRL+C signal // Wait for the CTRL+C signal
@ -24,6 +87,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.await .await
.expect("failed to install CTRL+C signal handler"); .expect("failed to install CTRL+C signal handler");
}; };
server::listen(&server_config, &wallet, shutdown_signal)
server::listen(&server_config, shutdown_signal)
} }

169
src/node.rs Normal file
View file

@ -0,0 +1,169 @@
use crate::config::ServerConfig;
use crate::error::{ErrorKind, Result};
use crate::secp::Commitment;
use grin_api::client;
use grin_api::json_rpc::{build_request, Request, Response};
use grin_api::{BlockPrintable, LocatedTxKernel, OutputPrintable, OutputType, Tip};
use grin_core::consensus::COINBASE_MATURITY;
use grin_core::core::{Input, OutputFeatures, Transaction};
use grin_util::ToHex;
use serde_json::json;
use std::net::SocketAddr;
const ENDPOINT: &str = "/v2/foreign";
#[derive(Clone)]
pub struct HTTPNodeClient {
node_url: SocketAddr,
node_api_secret: Option<String>,
}
impl HTTPNodeClient {
/// Create a new client that will communicate with the given grin node
pub fn new(node_url: &SocketAddr, node_api_secret: Option<String>) -> HTTPNodeClient {
HTTPNodeClient {
node_url: node_url.to_owned(),
node_api_secret: node_api_secret,
}
}
fn send_json_request<D: serde::de::DeserializeOwned>(
&self,
method: &str,
params: &serde_json::Value,
) -> Result<D> {
let url = format!("http://{}{}", self.node_url, ENDPOINT);
let req = build_request(method, params);
let res =
client::post::<Request, Response>(url.as_str(), self.node_api_secret.clone(), &req)?;
let parsed = res.clone().into_result()?;
Ok(parsed)
}
}
#[derive(Clone)]
pub struct GrinNode {
client: HTTPNodeClient,
}
impl GrinNode {
pub fn new(server_config: &ServerConfig) -> GrinNode {
GrinNode {
client: HTTPNodeClient::new(&server_config.grin_node_url, None),
}
}
// Checks whether a commitment is spendable at the block height provided
pub fn is_spendable(&self, output_commit: &Commitment, next_block_height: u64) -> Result<bool> {
let output = self.get_output(&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);
}
}
return Ok(true);
}
Ok(false)
}
/// Builds an input for an unspent output commitment
pub fn build_input(&self, output_commit: &Commitment) -> Result<Option<Input>> {
let output = self.get_output(&output_commit)?;
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));
}
Ok(None)
}
fn get_output(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>> {
let commits : Vec<String> = vec![output_commit.to_hex()];
let start_height : Option<u64> = None;
let end_height : Option<u64> = None;
let include_proof : Option<bool> = Some(false);
let include_merkle_proof : Option<bool> = Some(false);
let params = json!([Some(commits), start_height, end_height, include_proof, include_merkle_proof]);
let outputs = self.client.send_json_request::<Vec<OutputPrintable>>("get_outputs", &params)?;
if outputs.is_empty() {
return Ok(None);
}
Ok(Some(outputs[0].clone()))
}
/// Gets the height of the chain tip
pub fn get_chain_height(&self) -> Result<u64> {
let params = json!([]);
let tip_json = self.client.send_json_request::<serde_json::Value>("get_tip", &params)?;
let tip: Result<Tip> = serde_json::from_value(tip_json["Ok"].clone())
.map_err(|_| ErrorKind::SerdeJsonError.into());
Ok(tip?.height)
}
/// Posts a transaction to the grin node
pub fn post_tx(&self, tx: &Transaction) -> Result<()> {
let params = json!([tx, true]);
self.client.send_json_request::<serde_json::Value>("push_transaction", &params)?;
Ok(())
}
// milestone 3: needed to handle chain reorgs
pub fn chain_has_kernel(
&self,
kernel_excess: &Commitment,
start_height: Option<u64>,
end_height: Option<u64>,
) -> Result<bool> {
let params = json!([kernel_excess, start_height, end_height]);
let located_kernel_json = self.client.send_json_request::<serde_json::Value>("get_kernel", &params)?;
let located_kernel : Result<LocatedTxKernel> = serde_json::from_value(located_kernel_json["Ok"].clone())
.map_err(|_| ErrorKind::SerdeJsonError.into());
Ok(located_kernel.is_ok())
}
// milestone 3: needed to handle chain reorgs
pub fn block_has_kernel(&self, block_hash: &String, kernel_excess: &Commitment) -> Result<bool> {
let block = self.get_block(None, Some(block_hash.clone()), None)?;
let found = block.kernels.into_iter()
.any(|kern| kern.excess == kernel_excess.to_hex());
Ok(found)
}
// milestone 3: needed to handle chain reorgs
fn get_block(&self,
height: Option<u64>,
hash: Option<String>,
commit: Option<String>,
) -> Result<BlockPrintable> {
let params = json!([height, hash, commit]);
let block_printable = self.client.send_json_request::<BlockPrintable>("get_block", &params)?;
Ok(block_printable)
}
}

View file

@ -5,12 +5,13 @@ use crate::ser;
use chacha20::{ChaCha20, Key, Nonce}; use chacha20::{ChaCha20, Key, Nonce};
use chacha20::cipher::{NewCipher, StreamCipher}; use chacha20::cipher::{NewCipher, StreamCipher};
use hmac::{Hmac, Mac, NewMac}; use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
/// Create an Onion for the Commitment, encrypting the payload for each hop /// Create an Onion for the Commitment, encrypting the payload for each hop
#[allow(dead_code)] // used by component tests
pub fn create_onion(commitment: &Commitment, session_key: &SecretKey, hops: &Vec<Hop>) -> Result<Onion> { pub fn create_onion(commitment: &Commitment, session_key: &SecretKey, hops: &Vec<Hop>) -> Result<Onion> {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let mut ephemeral_key = session_key.clone(); let mut ephemeral_key = session_key.clone();
@ -71,6 +72,7 @@ pub fn peel_layer(onion: &Onion, secret_key: &SecretKey) -> Result<(Payload, Oni
let mut commitment = onion.commit.clone(); let mut commitment = onion.commit.clone();
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?; commitment = secp::add_excess(&commitment, &decrypted_payload.excess)?;
commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())?;
let peeled_onion = Onion{ let peeled_onion = Onion{
ephemeral_pubkey: ephemeral_pubkey, ephemeral_pubkey: ephemeral_pubkey,
@ -109,30 +111,35 @@ mod tests {
use super::super::types; use super::super::types;
use super::super::onion; use super::super::onion;
use grin_core::core::FeeFields;
/// Test end-to-end Onion creation and unwrapping logic. /// Test end-to-end Onion creation and unwrapping logic.
#[test] #[test]
fn onion() { fn onion() {
let value : u64 = 1000; let total_fee : u64 = 10;
let blind = secp::insecure_rand_secret().unwrap(); let fee_per_hop : u64 = 2;
let commitment = secp::commit(value, &blind).unwrap(); 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::insecure_rand_secret().unwrap(); let session_key = secp::random_secret();
let mut hops : Vec<types::Hop> = Vec::new(); let mut hops : Vec<types::Hop> = Vec::new();
let mut keys : Vec<secp::SecretKey> = Vec::new(); let mut keys : Vec<secp::SecretKey> = Vec::new();
let mut final_commit = commitment.clone(); let mut final_commit = secp::commit(out_value, &blind).unwrap();
let mut final_blind = blind.clone(); let mut final_blind = blind.clone();
for i in 0..5 { for i in 0..5 {
keys.push(secp::insecure_rand_secret().unwrap()); keys.push(secp::random_secret());
let excess = secp::insecure_rand_secret().unwrap(); let excess = secp::random_secret();
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
final_blind.add_assign(&secp, &excess).unwrap(); final_blind.add_assign(&secp, &excess).unwrap();
final_commit = secp::add_excess(&final_commit, &excess).unwrap(); final_commit = secp::add_excess(&final_commit, &excess).unwrap();
let proof = if i == 4 { let proof = if i == 4 {
let n1 = secp::insecure_rand_secret().unwrap(); let n1 = secp::random_secret();
let rp = secp.bullet_proof(value, final_blind.clone(), n1.clone(), n1.clone(), None, None); 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()); assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
Some(rp) Some(rp)
} else { } else {
@ -143,6 +150,7 @@ mod tests {
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(), pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
payload: types::Payload{ payload: types::Payload{
excess: excess, excess: excess,
fee: FeeFields::from(fee_per_hop as u32),
rangeproof: proof, rangeproof: proof,
} }
}); });
@ -151,7 +159,8 @@ mod tests {
let mut onion_packet = onion::create_onion(&commitment, &session_key, &hops).unwrap(); let mut onion_packet = onion::create_onion(&commitment, &session_key, &hops).unwrap();
let mut payload = types::Payload{ let mut payload = types::Payload{
excess: secp::insecure_rand_secret().unwrap(), excess: secp::random_secret(),
fee: FeeFields::from(fee_per_hop as u32),
rangeproof: None rangeproof: None
}; };
for i in 0..5 { for i in 0..5 {
@ -162,6 +171,7 @@ mod tests {
assert!(payload.rangeproof.is_some()); assert!(payload.rangeproof.is_some());
assert_eq!(payload.rangeproof.unwrap(), hops[4].payload.rangeproof.unwrap()); assert_eq!(payload.rangeproof.unwrap(), hops[4].payload.rangeproof.unwrap());
assert_eq!(secp::commit(value, &final_blind).unwrap(), final_commit); assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit);
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32));
} }
} }

View file

@ -1,56 +1,102 @@
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature}; pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
pub use secp256k1zkp::aggsig;
pub use secp256k1zkp::ecdh::SharedSecret; pub use secp256k1zkp::ecdh::SharedSecret;
pub use secp256k1zkp::pedersen::{Commitment, RangeProof}; pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
pub use secp256k1zkp::key::{PublicKey, SecretKey}; 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::constants::{AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE, SECRET_KEY_SIZE};
use crate::ser::{Readable, Reader, Writeable, Writer}; use crate::ser::{self, Readable, Reader, Writeable, Writer};
use crate::error::{ErrorKind, Result}; use crate::error::{Error, ErrorKind, Result};
use rand::RngCore; use blake2::blake2b::Blake2b;
use byteorder::{BigEndian, ByteOrder};
use secp256k1zkp::rand::thread_rng;
use std::cmp; use std::cmp;
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys /// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
pub const COM_SIGNATURE_SIZE : usize = 96; pub struct ComSignature {
pub pub_nonce: Commitment,
pub s: SecretKey,
pub t: SecretKey,
}
pub struct ComSignature(pub [u8; COM_SIGNATURE_SIZE]);
impl ComSignature { impl ComSignature {
/// Builds a ComSignature from a byte vector. If the vector is too short, it will be pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
/// completed by zeroes. If it's too long, it will be truncated. ComSignature {
pub fn from_vec(v: Vec<u8>) -> ComSignature { pub_nonce: pub_nonce.clone(),
let mut h = [0; COM_SIGNATURE_SIZE]; s: s.clone(),
for i in 0..cmp::min(v.len(), COM_SIGNATURE_SIZE) { t: t.clone(),
h[i] = v[i];
} }
ComSignature(h)
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn sign(_value: u64, _blind: &SecretKey, _msg: &Vec<u8>) -> Result<ComSignature> { pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec<u8>) -> Result<ComSignature> {
// milestone 2 - todo let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut h = [0u8; COM_SIGNATURE_SIZE];
for i in 0..COM_SIGNATURE_SIZE { let mut amt_bytes = [0; 32];
h[i] = i as u8; BigEndian::write_u64(&mut amt_bytes[24..31], amount);
} let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
Ok(ComSignature(h))
let k_1 = SecretKey::new(&secp, &mut thread_rng());
let k_2 = SecretKey::new(&secp, &mut thread_rng());
let commitment = secp.commit(amount, blind.clone())?;
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
// s = k_1 + (e * amount)
let mut s = k_amt.clone();
s.mul_assign(&secp, &e)?;
s.add_assign(&secp, &k_1)?;
// t = k_2 + (e * blind)
let mut t = blind.clone();
t.mul_assign(&secp, &e)?;
t.add_assign(&secp, &k_2)?;
Ok(ComSignature::new(&nonce_commitment, &s, &t))
} }
pub fn verify(self, _commit: &Commitment, _msg: &Vec<u8>) -> Result<()> { #[allow(non_snake_case)]
// milestone 2 - todo pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<()> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
let mut Ce = commit.to_pubkey(&secp)?;
let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
Ce.mul_assign(&secp, &e)?;
let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
let S2 = secp.commit_sum(commits, Vec::new())?;
if S1 == S2 {
return Err(Error::new(ErrorKind::InvalidSigError));
}
Ok(()) Ok(())
} }
}
impl AsRef<[u8]> for ComSignature { fn calc_challenge(secp: &Secp256k1, commit: &Commitment, nonce_commit: &Commitment, msg: &Vec<u8>) -> Result<SecretKey> {
fn as_ref(&self) -> &[u8] { let mut challenge_hasher = Blake2b::new(32);
&self.0 challenge_hasher.update(&commit.0);
challenge_hasher.update(&nonce_commit.0);
challenge_hasher.update(msg);
let mut challenge = [0; 32];
challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
Ok(SecretKey::from_slice(&secp, &challenge)?)
} }
} }
/// Serializes a ComSignature to and from hex /// Serializes a ComSignature to and from hex
pub mod comsig_serde { pub mod comsig_serde {
use super::ComSignature; use super::ComSignature;
use super::ser::{self, BinReader, Readable};
use serde::{Deserialize, Serializer}; use serde::{Deserialize, Serializer};
use std::io::Cursor;
use grin_util::ToHex; use grin_util::ToHex;
/// Serializes a ComSignature as a hex string /// Serializes a ComSignature as a hex string
@ -58,7 +104,9 @@ pub mod comsig_serde {
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&comsig.to_hex()) use serde::ser::Error;
let bytes = ser::ser_vec(&comsig).map_err(Error::custom)?;
serializer.serialize_str(&bytes.to_hex())
} }
/// Creates a ComSignature from a hex string /// Creates a ComSignature from a hex string
@ -67,9 +115,12 @@ pub mod comsig_serde {
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
use serde::de::Error; use serde::de::Error;
String::deserialize(deserializer) let bytes = String::deserialize(deserializer)
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom)) .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
.and_then(|bytes: Vec<u8>| Ok(ComSignature::from_vec(bytes.to_vec())))
let mut cursor = Cursor::new(&bytes);
let mut reader = BinReader::new(&mut cursor);
ComSignature::read(&mut reader).map_err(Error::custom)
} }
} }
@ -80,13 +131,10 @@ pub fn to_public_key(secret_key: &SecretKey) -> Result<PublicKey> {
Ok(pubkey) Ok(pubkey)
} }
/// Generate a random SecretKey. Not for production use /// Generate a random SecretKey.
pub fn insecure_rand_secret() -> Result<SecretKey> { pub fn random_secret() -> SecretKey {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let mut seed = [0u8; 32]; SecretKey::new(&secp, &mut thread_rng())
rand::thread_rng().fill_bytes(&mut seed);
let secret = SecretKey::from_slice(&secp, &seed)?;
Ok(secret)
} }
/// Build a Pedersen Commitment using the provided value and blinding factor /// Build a Pedersen Commitment using the provided value and blinding factor
@ -106,6 +154,32 @@ pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result<Commitm
Ok(sum) Ok(sum)
} }
pub fn sub_value(commitment: &Commitment, value: u64) -> Result<Commitment> {
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)
}
pub fn add_blinds(excesses: &Vec<SecretKey>) -> Result<SecretKey> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let sum = secp.blind_sum(excesses.clone(), Vec::new())?;
Ok(sum)
}
pub fn sub_blinds(minuend: &SecretKey, subtrahend: &SecretKey) -> Result<SecretKey> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let result = secp.blind_sum(vec![minuend.clone()], vec![subtrahend.clone()])?;
Ok(result)
}
pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature> {
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)
}
/// secp256k1-zkp object serialization /// secp256k1-zkp object serialization
impl Readable for Commitment { impl Readable for Commitment {
@ -191,4 +265,22 @@ impl Writeable for SecretKey {
writer.write_fixed_bytes(self.0)?; writer.write_fixed_bytes(self.0)?;
Ok(()) Ok(())
} }
}
#[allow(non_snake_case)]
impl Readable for ComSignature {
fn read<R: Reader>(reader: &mut R) -> Result<ComSignature> {
let R = Commitment::read(reader)?;
let s = SecretKey::read(reader)?;
let t = SecretKey::read(reader)?;
Ok(ComSignature::new(&R, &s, &t))
}
}
impl Writeable for ComSignature {
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
writer.write_fixed_bytes(self.pub_nonce.0)?;
writer.write_fixed_bytes(self.s.0)?;
writer.write_fixed_bytes(self.t.0)
}
} }

View file

@ -1,26 +1,26 @@
use crate::config::ServerConfig;
use crate::node::GrinNode;
use crate::onion; use crate::onion;
use crate::secp::{self, Commitment, ComSignature, SecretKey}; use crate::secp::{self, ComSignature, SecretKey};
use crate::ser; use crate::ser;
use crate::types::Onion; use crate::types::Onion;
use crate::wallet::Wallet;
use grin_core::core::{Input, Output, OutputFeatures, TransactionBody};
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
use jsonrpc_derive::rpc; use jsonrpc_derive::rpc;
use jsonrpc_http_server::*; use jsonrpc_http_server::*;
use jsonrpc_http_server::jsonrpc_core::*; use jsonrpc_http_server::jsonrpc_core::*;
use jsonrpc_core::{Result, Value}; use jsonrpc_core::{Result, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::SocketAddr; use std::sync::{Arc, Mutex};
use std::sync::Mutex;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ServerConfig {
pub key: SecretKey,
pub addr: SocketAddr,
pub is_first: bool,
}
pub struct Submission { pub struct Submission {
pub excess: SecretKey, pub excess: SecretKey,
pub input_commit: Commitment, pub output: Option<Output>,
pub input: Input,
pub fee: u64,
pub onion: Onion, pub onion: Onion,
} }
@ -42,48 +42,130 @@ pub trait Server {
#[rpc(name = "swap")] #[rpc(name = "swap")]
fn swap(&self, swap: SwapReq) -> Result<Value>; fn swap(&self, swap: SwapReq) -> Result<Value>;
// milestone 3: // milestone 3: Used by mwixnet coinswap servers to communicate with each other
// fn derive_outputs(&self, entries: Vec<Onion>) -> Result<Value>; // fn derive_outputs(&self, entries: Vec<Onion>) -> Result<Value>;
// fn derive_kernel(&self, tx: Tx) -> Result<Value>; // fn derive_kernel(&self, tx: Tx) -> Result<Value>;
} }
#[derive(Clone)]
pub struct ServerImpl { pub struct ServerImpl {
server_key: SecretKey, server_config: ServerConfig,
wallet: Wallet,
node: GrinNode,
} }
impl ServerImpl { impl ServerImpl {
pub fn new(server_key: SecretKey) -> Self { pub fn new(server_config: ServerConfig, wallet: Wallet, node: GrinNode) -> Self {
ServerImpl { server_key } ServerImpl { server_config, wallet, node }
}
/// The fee base to use. For now, just using the default.
fn get_fee_base(&self) -> u64 {
DEFAULT_ACCEPT_FEE_BASE
}
/// Minimum fee to perform a swap.
/// Requires enough fee for the mwixnet server's kernel, 1 input and its output to swap.
pub 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.
pub fn execute_round(&self) -> crate::error::Result<()> {
let locked_state = SERVER_STATE.lock().unwrap();
let next_block_height = self.node.get_chain_height()? + 1;
let spendable : Vec<Submission> = locked_state
.iter()
.filter(|s| self.node.is_spendable(&s.input.commit, next_block_height).unwrap_or(false))
.cloned()
.collect();
let total_fee : u64 = spendable
.iter()
.enumerate()
.map(|(_, s)| s.fee)
.sum();
let inputs : Vec<Input> = spendable
.iter()
.enumerate()
.map(|(_, s)| s.input)
.collect();
let outputs : Vec<Output> = spendable
.iter()
.enumerate()
.filter_map(|(_, s)| s.output)
.collect();
let excesses : Vec<SecretKey> = spendable
.iter()
.enumerate()
.map(|(_, s)| s.excess.clone())
.collect();
let excess_sum = secp::add_blinds(&excesses)?;
let tx = self.wallet.assemble_tx(&inputs, &outputs, total_fee, &excess_sum)?;
self.node.post_tx(&tx)?;
Ok(())
} }
} }
impl Server for ServerImpl { impl Server for ServerImpl {
/// Implements the 'swap' API
fn swap(&self, swap: SwapReq) -> Result<Value> { fn swap(&self, swap: SwapReq) -> Result<Value> {
// milestone 2 - check that commitment is unspent
// Verify commitment signature to ensure caller owns the output // Verify commitment signature to ensure caller owns the output
let _ = swap.comsig.verify(&swap.onion.commit, &swap.msg) let _ = swap.comsig.verify(&swap.onion.commit, &swap.msg)
.map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?; .map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?;
// Verify that commitment is unspent
let input = self.node.build_input(&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 = onion::peel_layer(&swap.onion, &self.server_key) let peeled = onion::peel_layer(&swap.onion, &self.server_config.key)
.map_err(|e| jsonrpc_core::Error::invalid_params(e.message()))?; .map_err(|e| jsonrpc_core::Error::invalid_params(e.message()))?;
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 output_commit = secp::add_excess(&swap.onion.commit, &peeled.0.excess)
.map_err(|_| jsonrpc_core::Error::internal_error())?;
let output = match peeled.0.rangeproof {
Some(r) => Some(Output::new(OutputFeatures::Plain, output_commit, r)),
None => None
};
SERVER_STATE.lock().unwrap().push(Submission{ SERVER_STATE.lock().unwrap().push(Submission{
excess: peeled.0.excess, excess: peeled.0.excess,
input_commit: swap.onion.commit, output: output,
input: input,
fee: fee,
onion: peeled.1 onion: peeled.1
}); });
Ok(Value::String("success".into())) Ok(Value::String("success".into()))
} }
} }
/// Spin up the JSON-RPC web server /// Spin up the JSON-RPC web server
pub fn listen<F>(server_config: &ServerConfig, shutdown_signal: F) -> std::result::Result<(), Box<dyn std::error::Error>> pub fn listen<F>(server_config: &ServerConfig, wallet: &Wallet, shutdown_signal: F) -> std::result::Result<(), Box<dyn std::error::Error>>
where where
F: futures::future::Future<Output = ()> + Send + 'static, F: futures::future::Future<Output = ()> + Send + 'static,
{ {
let server_impl = Arc::new(ServerImpl::new(server_config.clone(), wallet.clone(), GrinNode::new(&server_config)));
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.extend_with(ServerImpl::to_delegate(ServerImpl::new(server_config.key.clone()))); io.extend_with(ServerImpl::to_delegate(server_impl.as_ref().clone()));
let server = ServerBuilder::new(io) let server = ServerBuilder::new(io)
.cors(DomainsValidation::Disabled) .cors(DomainsValidation::Disabled)
@ -97,6 +179,13 @@ where
.start_http(&server_config.addr) .start_http(&server_config.addr)
.expect("Unable to start RPC server"); .expect("Unable to start RPC server");
std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_secs(30)); // todo: Graceful shutdown
let _ = server_impl.as_ref().execute_round();
}
});
let close_handle = server.close_handle(); let close_handle = server.close_handle();
std::thread::spawn(move || { std::thread::spawn(move || {
futures::executor::block_on(shutdown_signal); futures::executor::block_on(shutdown_signal);
@ -109,7 +198,8 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{onion, secp, server, types}; use crate::{config, onion, secp, server, types, wallet};
use grin_core::core::FeeFields;
use std::net::TcpListener; use std::net::TcpListener;
use std::time::Duration; use std::time::Duration;
use std::thread; use std::thread;
@ -124,19 +214,22 @@ mod tests {
/// Spin up a temporary web service, query the API, then cleanup and return response /// Spin up a temporary web service, query the API, then cleanup and return response
fn make_request(server_key: secp::SecretKey, req: String) -> Result<String, Box<dyn std::error::Error>> { fn make_request(server_key: secp::SecretKey, req: String) -> Result<String, Box<dyn std::error::Error>> {
let server_config = server::ServerConfig { let server_config = config::ServerConfig {
key: server_key, key: server_key,
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?, addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
is_first: true 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 threaded_rt = runtime::Runtime::new()?;
let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel(); let (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel();
let uri = format!("http://{}/v1", server_config.addr); let uri = format!("http://{}/v1", server_config.addr);
let wallet = wallet::Wallet::open_wallet(&server_config.wallet_owner_url, &grin_util::ZeroingString::from("wallet_pass"))?;
// Spawn the server task // Spawn the server task
threaded_rt.spawn(async move { threaded_rt.spawn(async move {
server::listen(&server_config, async { shutdown_receiver.await.ok(); }).unwrap() server::listen(&server_config, &wallet, async { shutdown_receiver.await.ok(); }).unwrap()
}); });
// Wait for listener // Wait for listener
@ -167,18 +260,20 @@ mod tests {
/// UTXO creation and bulletproof generation reserved for milestones 2 & 3. /// UTXO creation and bulletproof generation reserved for milestones 2 & 3.
#[test] #[test]
fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> { fn swap_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
let server_key = secp::insecure_rand_secret()?; let server_key = secp::random_secret();
let secp = secp::Secp256k1::new(); let secp = secp::Secp256k1::new();
let value: u64 = 100; let value: u64 = 100;
let blind = secp::insecure_rand_secret()?; let fee: u64= 10;
let blind = secp::random_secret();
let commitment = secp::commit(value, &blind)?; let commitment = secp::commit(value, &blind)?;
let session_key = secp::insecure_rand_secret()?; let session_key = secp::random_secret();
let hop = types::Hop { let hop = types::Hop {
pubkey: secp::PublicKey::from_secret_key(&secp, &server_key)?, pubkey: secp::PublicKey::from_secret_key(&secp, &server_key)?,
payload: types::Payload{ payload: types::Payload{
excess: secp::insecure_rand_secret()?, excess: secp::random_secret(),
fee: FeeFields::from(fee as u32),
rangeproof: None, rangeproof: None,
} }
}; };
@ -203,7 +298,7 @@ mod tests {
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> { fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
let params = "{ \"param\": \"Not a valid Swap request\" }"; let params = "{ \"param\": \"Not a valid Swap request\" }";
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", params); let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", params);
let response = make_request(secp::insecure_rand_secret()?, req)?; let response = make_request(secp::random_secret(), req)?;
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n"; let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
assert_eq!(response, expected); assert_eq!(response, expected);
Ok(()) Ok(())

View file

@ -1,6 +1,8 @@
use crate::error::{ErrorKind, Result}; use crate::error::{ErrorKind, Result};
use crate::secp::{Commitment, PublicKey, RangeProof, SecretKey, Secp256k1}; use crate::secp::{Commitment, PublicKey, RangeProof, SecretKey, Secp256k1};
use crate::ser::{self, BinReader, Readable, Reader, Writeable, Writer}; use crate::ser::{self, BinReader, Readable, Reader, Writeable, Writer};
use grin_core::core::FeeFields;
use grin_util::{self, ToHex}; use grin_util::{self, ToHex};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde::ser::SerializeStruct; use serde::ser::SerializeStruct;
@ -14,6 +16,7 @@ const CURRENT_VERSION : u8 = 0;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Payload { pub struct Payload {
pub excess: SecretKey, pub excess: SecretKey,
pub fee: FeeFields,
pub rangeproof: Option<RangeProof>, pub rangeproof: Option<RangeProof>,
} }
@ -25,6 +28,7 @@ impl Readable for Payload {
} }
let excess = SecretKey::read(reader)?; let excess = SecretKey::read(reader)?;
let fee = FeeFields::try_from(reader.read_u64()?)?;
let rangeproof = if reader.read_u8()? == 0 { let rangeproof = if reader.read_u8()? == 0 {
None None
} else { } else {
@ -33,6 +37,7 @@ impl Readable for Payload {
let payload = Payload { let payload = Payload {
excess: excess, excess: excess,
fee: fee,
rangeproof: rangeproof rangeproof: rangeproof
}; };
Ok(payload) Ok(payload)
@ -43,6 +48,7 @@ impl Writeable for Payload {
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> { fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
writer.write_u8(CURRENT_VERSION)?; writer.write_u8(CURRENT_VERSION)?;
writer.write_fixed_bytes(&self.excess)?; writer.write_fixed_bytes(&self.excess)?;
writer.write_u64(self.fee.into())?;
match &self.rangeproof { match &self.rangeproof {
Some(proof) => { Some(proof) => {
@ -71,6 +77,7 @@ pub struct Hop {
pub payload: Payload, pub payload: Payload,
} }
#[derive(Clone, Debug, PartialEq)]
pub struct Onion { pub struct Onion {
pub ephemeral_pubkey: PublicKey, pub ephemeral_pubkey: PublicKey,
pub commit: Commitment, pub commit: Commitment,

91
src/wallet.rs Normal file
View file

@ -0,0 +1,91 @@
use crate::error::Result;
use crate::secp::{self, SecretKey};
use grin_api::client;
use grin_api::json_rpc::{build_request, Request, Response};
use grin_core::core::{FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TxKernel};
use grin_keychain::BlindingFactor;
use grin_util::ZeroingString;
use grin_wallet_api::Token;
use serde_json::json;
use std::net::SocketAddr;
const ENDPOINT: &str = "/v3/owner";
#[derive(Clone)]
pub struct HTTPWalletClient {
wallet_owner_url: SocketAddr,
}
impl HTTPWalletClient {
/// Create a new client that will communicate with the given grin node
pub fn new(wallet_owner_url: &SocketAddr) -> HTTPWalletClient {
HTTPWalletClient {
wallet_owner_url: wallet_owner_url.to_owned(),
}
}
fn send_json_request<D: serde::de::DeserializeOwned>(
&self,
method: &str,
params: &serde_json::Value,
) -> Result<D> {
let url = format!("http://{}{}", self.wallet_owner_url, ENDPOINT);
let req = build_request(method, params);
let res = client::post::<Request, Response>(url.as_str(), None, &req)?;
let parsed = res.clone().into_result()?;
Ok(parsed)
}
}
#[derive(Clone)]
pub struct Wallet {
pub client: HTTPWalletClient,
pub token: Token,
}
impl Wallet {
pub fn open_wallet(wallet_owner_url: &SocketAddr, wallet_pass: &ZeroingString) -> Result<Wallet> {
let client = HTTPWalletClient::new(&wallet_owner_url);
let open_wallet_params = json!({
"name": null,
"password": wallet_pass.to_string()
});
let token: Token = client.send_json_request("open_wallet", &open_wallet_params)?;
Ok(Wallet {
client: client,
token: token,
})
}
/// Builds and verifies a <code>Transaction</code> using the provided components.
pub fn assemble_tx(&self, inputs: &Vec<Input>, outputs: &Vec<Output>, total_fee: u64, total_excess: &SecretKey) -> Result<Transaction> {
// generate random transaction offset
let offset = secp::random_secret();
// build and verify kernel
let kern_excess = secp::sub_blinds(&total_excess, &offset)?;
let kern = Wallet::build_kernel(total_fee, &kern_excess)?;
// assemble the transaction
let tx = Transaction::new(Inputs::from(inputs.as_slice()), &outputs, &[kern])
.with_offset(BlindingFactor::from_secret_key(offset));
Ok(tx)
}
/// Builds and verifies a <code>TxKernel</code> from the provided fee and excess.
fn build_kernel(total_fee: u64, kern_excess: &SecretKey) -> Result<TxKernel> {
let mut kernel = TxKernel::with_features(KernelFeatures::Plain {
fee: FeeFields::new(0, total_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()?;
Ok(kernel)
}
}