mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-02-01 08:51:09 +03:00
wip: milestone 2
This commit is contained in:
parent
a7c811fdd6
commit
68929fd493
12 changed files with 3952 additions and 197 deletions
3145
Cargo.lock
generated
3145
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -10,19 +10,31 @@ blake2 = { package = "blake2-rfc", version = "0.2"}
|
|||
byteorder = "1"
|
||||
bytes = "0.5.6"
|
||||
chacha20 = "0.8.1"
|
||||
clap = { version = "2.33", features = ["yaml"] }
|
||||
failure = "0.1.8"
|
||||
futures = "0.3"
|
||||
hmac = { version = "0.11.0", features = ["std"]}
|
||||
hmac = { version = "0.12.0", features = ["std"]}
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
jsonrpc-core = "18.0"
|
||||
jsonrpc-derive = "18.0"
|
||||
jsonrpc-http-server = "18.0"
|
||||
lazy_static = "1"
|
||||
pbkdf2 = "0.8.0"
|
||||
rand = "0.8.4"
|
||||
ring = "0.16"
|
||||
serde = { version = "1", features= ["derive"]}
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
sha2 = "0.9.8"
|
||||
sha2 = "0.10.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
toml = "0.5"
|
||||
grin_secp256k1zkp = { version = "0.7.11", features = ["bullet-proof-sizing"]}
|
||||
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
37
mwixnet.yml
Normal 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
177
src/config.rs
Normal 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());
|
||||
}
|
||||
}
|
72
src/error.rs
72
src/error.rs
|
@ -1,6 +1,7 @@
|
|||
use failure::{self, Context, Fail};
|
||||
use std::fmt::{self, Display};
|
||||
use std::io;
|
||||
use grin_wallet_libwallet as libwallet;
|
||||
|
||||
/// MWixnet error definition
|
||||
#[derive(Debug)]
|
||||
|
@ -45,6 +46,27 @@ pub enum ErrorKind {
|
|||
/// When asked to read too much data
|
||||
#[fail(display = "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 {
|
||||
|
@ -70,6 +92,12 @@ impl Display for Error {
|
|||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(kind: ErrorKind) -> Error {
|
||||
Error {
|
||||
inner: Context::new(kind),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
self.inner.get_context().clone()
|
||||
}
|
||||
|
@ -101,10 +129,50 @@ impl From<secp256k1zkp::Error> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<hmac::crypto_mac::InvalidKeyLength> for Error {
|
||||
fn from(_error: hmac::crypto_mac::InvalidKeyLength) -> Error {
|
||||
impl From<hmac::digest::InvalidLength> for Error {
|
||||
fn from(_error: hmac::digest::InvalidLength) -> Error {
|
||||
Error {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
80
src/main.rs
80
src/main.rs
|
@ -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]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod node;
|
||||
mod onion;
|
||||
mod secp;
|
||||
mod ser;
|
||||
mod server;
|
||||
mod types;
|
||||
mod wallet;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let secret_key = secp::insecure_rand_secret()?; // todo - load from encrypted key file
|
||||
let server_config = ServerConfig {
|
||||
key: secret_key,
|
||||
addr: "127.0.0.1:3000".parse().unwrap(),
|
||||
is_first: true
|
||||
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
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 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 {
|
||||
// Wait for the CTRL+C signal
|
||||
|
@ -24,6 +87,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.await
|
||||
.expect("failed to install CTRL+C signal handler");
|
||||
};
|
||||
|
||||
server::listen(&server_config, shutdown_signal)
|
||||
server::listen(&server_config, &wallet, shutdown_signal)
|
||||
}
|
169
src/node.rs
Normal file
169
src/node.rs
Normal 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", ¶ms)?;
|
||||
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", ¶ms)?;
|
||||
|
||||
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", ¶ms)?;
|
||||
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", ¶ms)?;
|
||||
|
||||
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", ¶ms)?;
|
||||
Ok(block_printable)
|
||||
}
|
||||
}
|
34
src/onion.rs
34
src/onion.rs
|
@ -5,12 +5,13 @@ use crate::ser;
|
|||
|
||||
use chacha20::{ChaCha20, Key, Nonce};
|
||||
use chacha20::cipher::{NewCipher, StreamCipher};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
/// 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> {
|
||||
let secp = Secp256k1::new();
|
||||
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();
|
||||
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,
|
||||
|
@ -109,30 +111,35 @@ mod tests {
|
|||
use super::super::types;
|
||||
use super::super::onion;
|
||||
|
||||
use grin_core::core::FeeFields;
|
||||
|
||||
/// Test end-to-end Onion creation and unwrapping logic.
|
||||
#[test]
|
||||
fn onion() {
|
||||
let value : u64 = 1000;
|
||||
let blind = secp::insecure_rand_secret().unwrap();
|
||||
let commitment = secp::commit(value, &blind).unwrap();
|
||||
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::insecure_rand_secret().unwrap();
|
||||
let session_key = secp::random_secret();
|
||||
let mut hops : Vec<types::Hop> = 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();
|
||||
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);
|
||||
final_blind.add_assign(&secp, &excess).unwrap();
|
||||
final_commit = secp::add_excess(&final_commit, &excess).unwrap();
|
||||
let proof = if i == 4 {
|
||||
let n1 = secp::insecure_rand_secret().unwrap();
|
||||
let rp = secp.bullet_proof(value, final_blind.clone(), n1.clone(), n1.clone(), None, None);
|
||||
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 {
|
||||
|
@ -143,6 +150,7 @@ mod tests {
|
|||
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
|
||||
payload: types::Payload{
|
||||
excess: excess,
|
||||
fee: FeeFields::from(fee_per_hop as u32),
|
||||
rangeproof: proof,
|
||||
}
|
||||
});
|
||||
|
@ -151,7 +159,8 @@ mod tests {
|
|||
let mut onion_packet = onion::create_onion(&commitment, &session_key, &hops).unwrap();
|
||||
|
||||
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
|
||||
};
|
||||
for i in 0..5 {
|
||||
|
@ -162,6 +171,7 @@ mod tests {
|
|||
|
||||
assert!(payload.rangeproof.is_some());
|
||||
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));
|
||||
}
|
||||
}
|
168
src/secp.rs
168
src/secp.rs
|
@ -1,56 +1,102 @@
|
|||
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
|
||||
pub use secp256k1zkp::aggsig;
|
||||
pub use secp256k1zkp::ecdh::SharedSecret;
|
||||
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};
|
||||
|
||||
use crate::ser::{Readable, Reader, Writeable, Writer};
|
||||
use crate::error::{ErrorKind, Result};
|
||||
use crate::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
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;
|
||||
|
||||
/// 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 [u8; COM_SIGNATURE_SIZE]);
|
||||
impl ComSignature {
|
||||
/// Builds a ComSignature from a byte vector. If the vector is too short, it will be
|
||||
/// completed by zeroes. If it's too long, it will be truncated.
|
||||
pub fn from_vec(v: Vec<u8>) -> ComSignature {
|
||||
let mut h = [0; COM_SIGNATURE_SIZE];
|
||||
for i in 0..cmp::min(v.len(), COM_SIGNATURE_SIZE) {
|
||||
h[i] = v[i];
|
||||
pub struct ComSignature {
|
||||
pub pub_nonce: Commitment,
|
||||
pub s: SecretKey,
|
||||
pub t: SecretKey,
|
||||
}
|
||||
|
||||
impl ComSignature {
|
||||
pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
|
||||
ComSignature {
|
||||
pub_nonce: pub_nonce.clone(),
|
||||
s: s.clone(),
|
||||
t: t.clone(),
|
||||
}
|
||||
ComSignature(h)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn sign(_value: u64, _blind: &SecretKey, _msg: &Vec<u8>) -> Result<ComSignature> {
|
||||
// milestone 2 - todo
|
||||
let mut h = [0u8; COM_SIGNATURE_SIZE];
|
||||
for i in 0..COM_SIGNATURE_SIZE {
|
||||
h[i] = i as u8;
|
||||
}
|
||||
Ok(ComSignature(h))
|
||||
pub fn sign(amount: u64, blind: &SecretKey, msg: &Vec<u8>) -> Result<ComSignature> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
|
||||
let mut amt_bytes = [0; 32];
|
||||
BigEndian::write_u64(&mut amt_bytes[24..31], amount);
|
||||
let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
|
||||
|
||||
let k_1 = SecretKey::new(&secp, &mut thread_rng());
|
||||
let k_2 = SecretKey::new(&secp, &mut thread_rng());
|
||||
|
||||
let commitment = secp.commit(amount, blind.clone())?;
|
||||
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
|
||||
|
||||
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
|
||||
|
||||
// s = k_1 + (e * amount)
|
||||
let mut s = k_amt.clone();
|
||||
s.mul_assign(&secp, &e)?;
|
||||
s.add_assign(&secp, &k_1)?;
|
||||
|
||||
// t = k_2 + (e * blind)
|
||||
let mut t = blind.clone();
|
||||
t.mul_assign(&secp, &e)?;
|
||||
t.add_assign(&secp, &k_2)?;
|
||||
|
||||
Ok(ComSignature::new(&nonce_commitment, &s, &t))
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<()> {
|
||||
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));
|
||||
}
|
||||
|
||||
pub fn verify(self, _commit: &Commitment, _msg: &Vec<u8>) -> Result<()> {
|
||||
// milestone 2 - todo
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ComSignature {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
fn calc_challenge(secp: &Secp256k1, commit: &Commitment, nonce_commit: &Commitment, msg: &Vec<u8>) -> Result<SecretKey> {
|
||||
let mut challenge_hasher = Blake2b::new(32);
|
||||
challenge_hasher.update(&commit.0);
|
||||
challenge_hasher.update(&nonce_commit.0);
|
||||
challenge_hasher.update(msg);
|
||||
|
||||
let mut challenge = [0; 32];
|
||||
challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
|
||||
|
||||
Ok(SecretKey::from_slice(&secp, &challenge)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes a ComSignature to and from hex
|
||||
pub mod comsig_serde {
|
||||
use super::ComSignature;
|
||||
use super::ser::{self, BinReader, Readable};
|
||||
use serde::{Deserialize, Serializer};
|
||||
use std::io::Cursor;
|
||||
use grin_util::ToHex;
|
||||
|
||||
/// Serializes a ComSignature as a hex string
|
||||
|
@ -58,7 +104,9 @@ pub mod comsig_serde {
|
|||
where
|
||||
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
|
||||
|
@ -67,9 +115,12 @@ pub mod comsig_serde {
|
|||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
String::deserialize(deserializer)
|
||||
.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 bytes = String::deserialize(deserializer)
|
||||
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Generate a random SecretKey. Not for production use
|
||||
pub fn insecure_rand_secret() -> Result<SecretKey> {
|
||||
/// Generate a random SecretKey.
|
||||
pub fn random_secret() -> SecretKey {
|
||||
let secp = Secp256k1::new();
|
||||
let mut seed = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut seed);
|
||||
let secret = SecretKey::from_slice(&secp, &seed)?;
|
||||
Ok(secret)
|
||||
SecretKey::new(&secp, &mut thread_rng())
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
impl Readable for Commitment {
|
||||
|
@ -192,3 +266,21 @@ impl Writeable for SecretKey {
|
|||
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)
|
||||
}
|
||||
}
|
155
src/server.rs
155
src/server.rs
|
@ -1,26 +1,26 @@
|
|||
use crate::config::ServerConfig;
|
||||
use crate::node::GrinNode;
|
||||
use crate::onion;
|
||||
use crate::secp::{self, Commitment, ComSignature, SecretKey};
|
||||
use crate::secp::{self, ComSignature, SecretKey};
|
||||
use crate::ser;
|
||||
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_http_server::*;
|
||||
use jsonrpc_http_server::jsonrpc_core::*;
|
||||
use jsonrpc_core::{Result, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ServerConfig {
|
||||
pub key: SecretKey,
|
||||
pub addr: SocketAddr,
|
||||
pub is_first: bool,
|
||||
}
|
||||
|
||||
pub struct Submission {
|
||||
pub excess: SecretKey,
|
||||
pub input_commit: Commitment,
|
||||
pub output: Option<Output>,
|
||||
pub input: Input,
|
||||
pub fee: u64,
|
||||
pub onion: Onion,
|
||||
}
|
||||
|
||||
|
@ -42,48 +42,130 @@ pub trait Server {
|
|||
#[rpc(name = "swap")]
|
||||
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_kernel(&self, tx: Tx) -> Result<Value>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerImpl {
|
||||
server_key: SecretKey,
|
||||
server_config: ServerConfig,
|
||||
|
||||
wallet: Wallet,
|
||||
|
||||
node: GrinNode,
|
||||
}
|
||||
|
||||
impl ServerImpl {
|
||||
pub fn new(server_key: SecretKey) -> Self {
|
||||
ServerImpl { server_key }
|
||||
pub fn new(server_config: ServerConfig, wallet: Wallet, node: GrinNode) -> Self {
|
||||
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 {
|
||||
/// Implements the 'swap' API
|
||||
fn swap(&self, swap: SwapReq) -> Result<Value> {
|
||||
// milestone 2 - check that commitment is unspent
|
||||
|
||||
// Verify commitment signature to ensure caller owns the output
|
||||
let _ = swap.comsig.verify(&swap.onion.commit, &swap.msg)
|
||||
.map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?;
|
||||
|
||||
let peeled = onion::peel_layer(&swap.onion, &self.server_key)
|
||||
// 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_config.key)
|
||||
.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{
|
||||
excess: peeled.0.excess,
|
||||
input_commit: swap.onion.commit,
|
||||
output: output,
|
||||
input: input,
|
||||
fee: fee,
|
||||
onion: peeled.1
|
||||
});
|
||||
Ok(Value::String("success".into()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// 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
|
||||
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();
|
||||
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)
|
||||
.cors(DomainsValidation::Disabled)
|
||||
|
@ -97,6 +179,13 @@ where
|
|||
.start_http(&server_config.addr)
|
||||
.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();
|
||||
std::thread::spawn(move || {
|
||||
futures::executor::block_on(shutdown_signal);
|
||||
|
@ -109,7 +198,8 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
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::time::Duration;
|
||||
use std::thread;
|
||||
|
@ -124,19 +214,22 @@ mod tests {
|
|||
|
||||
/// 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>> {
|
||||
let server_config = server::ServerConfig {
|
||||
let server_config = config::ServerConfig {
|
||||
key: server_key,
|
||||
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 (shutdown_sender, shutdown_receiver) = futures::channel::oneshot::channel();
|
||||
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
|
||||
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
|
||||
|
@ -167,18 +260,20 @@ mod tests {
|
|||
/// UTXO creation and bulletproof generation reserved for milestones 2 & 3.
|
||||
#[test]
|
||||
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 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 session_key = secp::insecure_rand_secret()?;
|
||||
let session_key = secp::random_secret();
|
||||
|
||||
let hop = types::Hop {
|
||||
pubkey: secp::PublicKey::from_secret_key(&secp, &server_key)?,
|
||||
payload: types::Payload{
|
||||
excess: secp::insecure_rand_secret()?,
|
||||
excess: secp::random_secret(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: None,
|
||||
}
|
||||
};
|
||||
|
@ -203,7 +298,7 @@ mod tests {
|
|||
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::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";
|
||||
assert_eq!(response, expected);
|
||||
Ok(())
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::error::{ErrorKind, Result};
|
||||
use crate::secp::{Commitment, PublicKey, RangeProof, SecretKey, Secp256k1};
|
||||
use crate::ser::{self, BinReader, Readable, Reader, Writeable, Writer};
|
||||
|
||||
use grin_core::core::FeeFields;
|
||||
use grin_util::{self, ToHex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::ser::SerializeStruct;
|
||||
|
@ -14,6 +16,7 @@ const CURRENT_VERSION : u8 = 0;
|
|||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Payload {
|
||||
pub excess: SecretKey,
|
||||
pub fee: FeeFields,
|
||||
pub rangeproof: Option<RangeProof>,
|
||||
}
|
||||
|
||||
|
@ -25,6 +28,7 @@ impl Readable for Payload {
|
|||
}
|
||||
|
||||
let excess = SecretKey::read(reader)?;
|
||||
let fee = FeeFields::try_from(reader.read_u64()?)?;
|
||||
let rangeproof = if reader.read_u8()? == 0 {
|
||||
None
|
||||
} else {
|
||||
|
@ -33,6 +37,7 @@ impl Readable for Payload {
|
|||
|
||||
let payload = Payload {
|
||||
excess: excess,
|
||||
fee: fee,
|
||||
rangeproof: rangeproof
|
||||
};
|
||||
Ok(payload)
|
||||
|
@ -43,6 +48,7 @@ impl Writeable for Payload {
|
|||
fn write<W: Writer>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_u8(CURRENT_VERSION)?;
|
||||
writer.write_fixed_bytes(&self.excess)?;
|
||||
writer.write_u64(self.fee.into())?;
|
||||
|
||||
match &self.rangeproof {
|
||||
Some(proof) => {
|
||||
|
@ -71,6 +77,7 @@ pub struct Hop {
|
|||
pub payload: Payload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Onion {
|
||||
pub ephemeral_pubkey: PublicKey,
|
||||
pub commit: Commitment,
|
||||
|
|
91
src/wallet.rs
Normal file
91
src/wallet.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue