mirror of
https://github.com/mimblewimble/mwixnet.git
synced 2025-01-20 19:11:09 +03:00
use v3 encrypted requests, cleaner error handling, rust formatter
This commit is contained in:
parent
bff59b08ac
commit
2baf37a5a3
13 changed files with 1471 additions and 1217 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -2497,6 +2497,7 @@ dependencies = [
|
|||
"pbkdf2 0.8.0",
|
||||
"rand 0.8.4",
|
||||
"ring",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
@ -3334,6 +3335,16 @@ dependencies = [
|
|||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "4.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "6.3.0"
|
||||
|
|
|
@ -23,6 +23,7 @@ lazy_static = "1"
|
|||
pbkdf2 = "0.8.0"
|
||||
rand = "0.8.4"
|
||||
ring = "0.16"
|
||||
rpassword = "4.0"
|
||||
serde = { version = "1", features= ["derive"]}
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
|
|
|
@ -8,12 +8,6 @@ args:
|
|||
short: c
|
||||
long: config_file
|
||||
takes_value: true
|
||||
- pass:
|
||||
help: Password to encrypt/decrypt the server key in the configuration file
|
||||
short: p
|
||||
long: pass
|
||||
takes_value: true
|
||||
required: true
|
||||
- grin_node_url:
|
||||
help: Api address of running GRIN node on which to check inputs and post transactions
|
||||
short: n
|
||||
|
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
hard_tabs = true
|
||||
edition = "2021"
|
|
@ -11,7 +11,7 @@ use std::io::prelude::*;
|
|||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// The decrypted server config to be passed around and used by the rest of the mwixnet code
|
||||
/// The decrypted server config to be passed around and used by the rest of the mwixnet code
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ServerConfig {
|
||||
/// private key used by the server to decrypt onion packets
|
||||
|
@ -46,8 +46,8 @@ impl EncryptedServerKey {
|
|||
let salt: [u8; 8] = thread_rng().gen();
|
||||
let password = password.as_bytes();
|
||||
let mut key = [0; 32];
|
||||
ring::pbkdf2::derive(
|
||||
ring::pbkdf2::PBKDF2_HMAC_SHA512,
|
||||
pbkdf2::derive(
|
||||
pbkdf2::PBKDF2_HMAC_SHA512,
|
||||
NonZeroU32::new(100).unwrap(),
|
||||
&salt,
|
||||
password,
|
||||
|
@ -60,11 +60,18 @@ impl EncryptedServerKey {
|
|||
let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key);
|
||||
let nonce: [u8; 12] = thread_rng().gen();
|
||||
let aad = aead::Aad::from(&[]);
|
||||
let _ = sealing_key.seal_in_place_append_tag(
|
||||
let _ = sealing_key
|
||||
.seal_in_place_append_tag(
|
||||
aead::Nonce::assume_unique_for_key(nonce),
|
||||
aad,
|
||||
&mut enc_bytes,
|
||||
).map_err(|_| error::ErrorKind::SaveConfigError)?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
error::ErrorKind::SaveConfigError(format!(
|
||||
"Failure while encrypting server key: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(EncryptedServerKey {
|
||||
encrypted_key: enc_bytes.to_hex(),
|
||||
|
@ -73,17 +80,18 @@ impl EncryptedServerKey {
|
|||
})
|
||||
}
|
||||
|
||||
/// Decrypt the server secret key using the provided password.
|
||||
pub fn decrypt(&self, password: &str) -> Result<SecretKey> {
|
||||
let mut encrypted_seed = grin_util::from_hex(&self.encrypted_key.clone())
|
||||
.map_err(|_| error::ErrorKind::LoadConfigError)?;
|
||||
.map_err(|_| error::ErrorKind::LoadConfigError("Seed not valid hex".to_string()))?;
|
||||
let salt = grin_util::from_hex(&self.salt.clone())
|
||||
.map_err(|_| error::ErrorKind::LoadConfigError)?;
|
||||
.map_err(|_| error::ErrorKind::LoadConfigError("Salt not valid hex".to_string()))?;
|
||||
let nonce = grin_util::from_hex(&self.nonce.clone())
|
||||
.map_err(|_| error::ErrorKind::LoadConfigError)?;
|
||||
.map_err(|_| error::ErrorKind::LoadConfigError("Nonce not valid hex".to_string()))?;
|
||||
let password = password.as_bytes();
|
||||
let mut key = [0; 32];
|
||||
pbkdf2::derive(
|
||||
ring::pbkdf2::PBKDF2_HMAC_SHA512,
|
||||
pbkdf2::PBKDF2_HMAC_SHA512,
|
||||
NonZeroU32::new(100).unwrap(),
|
||||
&salt,
|
||||
password,
|
||||
|
@ -95,18 +103,25 @@ impl EncryptedServerKey {
|
|||
let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap();
|
||||
let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key);
|
||||
let aad = aead::Aad::from(&[]);
|
||||
let _ = opening_key.open_in_place(
|
||||
let _ = opening_key
|
||||
.open_in_place(
|
||||
aead::Nonce::assume_unique_for_key(n),
|
||||
aad,
|
||||
&mut encrypted_seed,
|
||||
).map_err(|_| error::ErrorKind::LoadConfigError)?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
error::ErrorKind::LoadConfigError(format!("Error decrypting seed: {}", e))
|
||||
})?;
|
||||
|
||||
for _ in 0..aead::AES_256_GCM.tag_len() {
|
||||
encrypted_seed.pop();
|
||||
}
|
||||
|
||||
let secp = secp256k1zkp::Secp256k1::new();
|
||||
Ok(SecretKey::from_slice(&secp, &encrypted_seed).unwrap())
|
||||
let decrypted = SecretKey::from_slice(&secp, &encrypted_seed).map_err(|_| {
|
||||
error::ErrorKind::LoadConfigError("Decrypted key not valid".to_string())
|
||||
})?;
|
||||
Ok(decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,10 +138,14 @@ struct RawConfig {
|
|||
}
|
||||
|
||||
/// Writes the server config to the config_path given, encrypting the server_key first.
|
||||
pub fn write_config(config_path: &PathBuf, server_config: &ServerConfig, password: &ZeroingString) -> Result<()> {
|
||||
pub fn write_config(
|
||||
config_path: &PathBuf,
|
||||
server_config: &ServerConfig,
|
||||
password: &ZeroingString,
|
||||
) -> Result<()> {
|
||||
let encrypted = EncryptedServerKey::from_secret_key(&server_config.key, &password)?;
|
||||
|
||||
let raw_config = RawConfig{
|
||||
let raw_config = RawConfig {
|
||||
encrypted_key: encrypted.encrypted_key,
|
||||
salt: encrypted.salt,
|
||||
nonce: encrypted.nonce,
|
||||
|
@ -135,10 +154,14 @@ pub fn write_config(config_path: &PathBuf, server_config: &ServerConfig, passwo
|
|||
grin_node_url: server_config.grin_node_url,
|
||||
wallet_owner_url: server_config.wallet_owner_url,
|
||||
};
|
||||
let encoded: String = toml::to_string(&raw_config).map_err(|_| error::ErrorKind::SaveConfigError)?;
|
||||
let encoded: String = toml::to_string(&raw_config).map_err(|e| {
|
||||
error::ErrorKind::SaveConfigError(format!("Error while encoding config as toml: {}", e))
|
||||
})?;
|
||||
|
||||
let mut file = File::create(config_path)?;
|
||||
file.write_all(encoded.as_bytes()).map_err(|_| error::ErrorKind::SaveConfigError)?;
|
||||
file.write_all(encoded.as_bytes()).map_err(|e| {
|
||||
error::ErrorKind::SaveConfigError(format!("Error while writing config to file: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -146,13 +169,13 @@ pub fn write_config(config_path: &PathBuf, server_config: &ServerConfig, passwo
|
|||
/// 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 raw_config: RawConfig =
|
||||
toml::from_str(&contents).map_err(|e| error::ErrorKind::LoadConfigError(e.to_string()))?;
|
||||
|
||||
let encrypted_key = EncryptedServerKey{
|
||||
let encrypted_key = EncryptedServerKey {
|
||||
encrypted_key: raw_config.encrypted_key,
|
||||
salt: raw_config.salt,
|
||||
nonce: raw_config.nonce
|
||||
nonce: raw_config.nonce,
|
||||
};
|
||||
let secret_key = encrypted_key.decrypt(&password)?;
|
||||
|
||||
|
@ -161,7 +184,7 @@ pub fn load_config(config_path: &PathBuf, password: &ZeroingString) -> Result<Se
|
|||
interval_s: raw_config.interval_s,
|
||||
addr: raw_config.addr,
|
||||
grin_node_url: raw_config.grin_node_url,
|
||||
wallet_owner_url: raw_config.wallet_owner_url
|
||||
wallet_owner_url: raw_config.wallet_owner_url,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
65
src/error.rs
65
src/error.rs
|
@ -1,7 +1,7 @@
|
|||
use failure::{self, Context, Fail};
|
||||
use grin_wallet_libwallet as libwallet;
|
||||
use std::fmt::{self, Display};
|
||||
use std::io;
|
||||
use grin_wallet_libwallet as libwallet;
|
||||
|
||||
/// MWixnet error definition
|
||||
#[derive(Debug)]
|
||||
|
@ -12,8 +12,8 @@ pub struct Error {
|
|||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type StdResult<T, E> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
|
||||
/// MWixnet error types
|
||||
#[derive(Clone, Debug, Eq, Fail, PartialEq)]
|
||||
pub enum ErrorKind {
|
||||
/// Error from secp256k1-zkp library
|
||||
#[fail(display = "Secp Error")]
|
||||
|
@ -22,40 +22,35 @@ pub enum ErrorKind {
|
|||
#[fail(display = "InvalidKeyLength")]
|
||||
InvalidKeyLength,
|
||||
/// Wraps an io error produced when reading or writing
|
||||
#[fail(display = "IOError")]
|
||||
IOErr(
|
||||
String,
|
||||
io::ErrorKind,
|
||||
),
|
||||
#[fail(display = "IO Error: {}", _0)]
|
||||
IOErr(String, io::ErrorKind),
|
||||
/// Data wasn't in a consumable format
|
||||
#[fail(display = "CorruptedData")]
|
||||
CorruptedData,
|
||||
/// Error from grin's api crate
|
||||
#[fail(display = "GRIN API Error")]
|
||||
GrinApiError,
|
||||
#[fail(display = "GRIN API Error: {}", _0)]
|
||||
GrinApiError(String),
|
||||
/// Error from grin core
|
||||
#[fail(display = "grincore Error")]
|
||||
GrinCoreError,
|
||||
#[fail(display = "GRIN Core Error: {}", _0)]
|
||||
GrinCoreError(String),
|
||||
/// Error from grin-wallet's libwallet
|
||||
#[fail(display = "libwallet Error")]
|
||||
LibWalletError,
|
||||
#[fail(display = "libwallet error: {}", _0)]
|
||||
LibWalletError(String),
|
||||
/// Error from serde-json
|
||||
#[fail(display = "serde json Error")]
|
||||
SerdeJsonError,
|
||||
#[fail(display = "serde json error: {}", _0)]
|
||||
SerdeJsonError(String),
|
||||
/// Error from invalid signature
|
||||
#[fail(display = "invalid signature Error")]
|
||||
#[fail(display = "invalid signature")]
|
||||
InvalidSigError,
|
||||
/// Error while saving config
|
||||
#[fail(display = "save config Error")]
|
||||
SaveConfigError,
|
||||
#[fail(display = "save config error: {}", _0)]
|
||||
SaveConfigError(String),
|
||||
/// Error while loading config
|
||||
#[fail(display = "load config Error")]
|
||||
LoadConfigError,
|
||||
#[fail(display = "load config error: {}", _0)]
|
||||
LoadConfigError(String),
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Error {
|
||||
|
@ -70,9 +65,9 @@ impl From<io::ErrorKind> for Error {
|
|||
}
|
||||
|
||||
impl From<grin_core::ser::Error> for Error {
|
||||
fn from(_e: grin_core::ser::Error) -> Error {
|
||||
fn from(e: grin_core::ser::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::GrinCoreError),
|
||||
inner: Context::new(ErrorKind::GrinCoreError(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,41 +121,41 @@ impl From<hmac::digest::InvalidLength> for Error {
|
|||
}
|
||||
|
||||
impl From<grin_api::Error> for Error {
|
||||
fn from(_error: grin_api::Error) -> Error {
|
||||
fn from(e: grin_api::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::GrinApiError),
|
||||
inner: Context::new(ErrorKind::GrinApiError(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<grin_api::json_rpc::Error> for Error {
|
||||
fn from(_error: grin_api::json_rpc::Error) -> Error {
|
||||
fn from(e: grin_api::json_rpc::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::GrinApiError),
|
||||
inner: Context::new(ErrorKind::GrinApiError(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<grin_core::core::transaction::Error> for Error {
|
||||
fn from(_error: grin_core::core::transaction::Error) -> Error {
|
||||
fn from(e: grin_core::core::transaction::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::GrinCoreError),
|
||||
inner: Context::new(ErrorKind::GrinCoreError(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libwallet::Error> for Error {
|
||||
fn from(_error: libwallet::Error) -> Error {
|
||||
fn from(e: libwallet::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::LibWalletError),
|
||||
inner: Context::new(ErrorKind::LibWalletError(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(_error: serde_json::Error) -> Error {
|
||||
fn from(e: serde_json::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::SerdeJsonError),
|
||||
inner: Context::new(ErrorKind::SerdeJsonError(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
66
src/main.rs
66
src/main.rs
|
@ -5,10 +5,12 @@ use wallet::HttpWallet;
|
|||
|
||||
use clap::App;
|
||||
use grin_util::{StopState, ZeroingString};
|
||||
use rpassword;
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
@ -28,15 +30,11 @@ mod wallet;
|
|||
const DEFAULT_INTERVAL: u32 = 12 * 60 * 60;
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = real_main() {
|
||||
io::stderr().write_all(format!("mwixnet server exited with error:\n{}\n", e).as_bytes()).unwrap();
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
real_main().unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
fn real_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let yml = load_yaml!("../mwixnet.yml");
|
||||
let args = App::from_yaml(yml).get_matches();
|
||||
|
||||
|
@ -48,10 +46,10 @@ fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
current_dir
|
||||
}
|
||||
};
|
||||
let password = args.value_of("pass").ok_or(Error::new(ErrorKind::LoadConfigError))?;
|
||||
let password = ZeroingString::from(password);
|
||||
|
||||
let round_time = args.value_of("round_time").map(|t| t.parse::<u32>().unwrap() );
|
||||
let round_time = args
|
||||
.value_of("round_time")
|
||||
.map(|t| t.parse::<u32>().unwrap());
|
||||
let bind_addr = args.value_of("bind_addr");
|
||||
let grin_node_url = args.value_of("grin_node_url");
|
||||
let wallet_owner_url = args.value_of("wallet_owner_url");
|
||||
|
@ -59,7 +57,10 @@ fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
// 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());
|
||||
panic!(
|
||||
"Config file already exists at {}",
|
||||
config_path.to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
let server_config = ServerConfig {
|
||||
|
@ -70,11 +71,16 @@ fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
wallet_owner_url: wallet_owner_url.unwrap_or("127.0.0.1:3420").parse()?,
|
||||
};
|
||||
|
||||
let password = prompt_password_confirm();
|
||||
config::write_config(&config_path, &server_config, &password)?;
|
||||
println!("Config file written to {:?}. Please back this file up in a safe place.", config_path);
|
||||
println!(
|
||||
"Config file written to {:?}. Please back this file up in a safe place.",
|
||||
config_path
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let password = prompt_password();
|
||||
let mut server_config = config::load_config(&config_path, &password)?;
|
||||
|
||||
// Override bind_addr, if supplied
|
||||
|
@ -93,8 +99,7 @@ fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
// 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_pass = prompt_wallet_password(&args.value_of("wallet_pass"));
|
||||
let wallet = HttpWallet::open_wallet(&server_config.wallet_owner_url, &wallet_pass)?;
|
||||
|
||||
// Create GrinNode
|
||||
|
@ -103,7 +108,9 @@ fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
let stop_state = Arc::new(StopState::new());
|
||||
|
||||
let stop_state_clone = stop_state.clone();
|
||||
std::thread::spawn(move || {
|
||||
|
||||
let rt = Runtime::new()?;
|
||||
rt.spawn(async move {
|
||||
let shutdown_signal = async move {
|
||||
// Wait for the CTRL+C signal
|
||||
tokio::signal::ctrl_c()
|
||||
|
@ -114,5 +121,34 @@ fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||
stop_state_clone.stop();
|
||||
});
|
||||
|
||||
server::listen(&server_config, Arc::new(wallet), Arc::new(node), &stop_state)
|
||||
// Start the mwixnet server
|
||||
server::listen(
|
||||
&server_config,
|
||||
Arc::new(wallet),
|
||||
Arc::new(node),
|
||||
&stop_state,
|
||||
)
|
||||
}
|
||||
|
||||
fn prompt_password() -> ZeroingString {
|
||||
ZeroingString::from(rpassword::prompt_password_stdout("Server password: ").unwrap())
|
||||
}
|
||||
|
||||
fn prompt_password_confirm() -> ZeroingString {
|
||||
let mut first = "first".to_string();
|
||||
let mut second = "second".to_string();
|
||||
while first != second {
|
||||
first = rpassword::prompt_password_stdout("Server password: ").unwrap();
|
||||
second = rpassword::prompt_password_stdout("Confirm server password: ").unwrap();
|
||||
}
|
||||
ZeroingString::from(first)
|
||||
}
|
||||
|
||||
fn prompt_wallet_password(wallet_pass: &Option<&str>) -> ZeroingString {
|
||||
match *wallet_pass {
|
||||
Some(wallet_pass) => ZeroingString::from(wallet_pass),
|
||||
None => {
|
||||
ZeroingString::from(rpassword::prompt_password_stdout("Wallet password: ").unwrap())
|
||||
}
|
||||
}
|
||||
}
|
36
src/node.rs
36
src/node.rs
|
@ -13,7 +13,7 @@ use std::collections::HashMap;
|
|||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub trait GrinNode : Send + Sync {
|
||||
pub trait GrinNode: Send + Sync {
|
||||
/// Retrieves the unspent output with a matching commitment
|
||||
fn get_utxo(&self, output_commit: &Commitment) -> Result<Option<OutputPrintable>>;
|
||||
|
||||
|
@ -31,7 +31,11 @@ pub fn is_unspent(node: &Arc<dyn GrinNode>, commit: &Commitment) -> Result<bool>
|
|||
}
|
||||
|
||||
/// Checks whether a commitment is spendable at the block height provided
|
||||
pub fn is_spendable(node: &Arc<dyn GrinNode>, output_commit: &Commitment, next_block_height: u64) -> Result<bool> {
|
||||
pub fn is_spendable(
|
||||
node: &Arc<dyn GrinNode>,
|
||||
output_commit: &Commitment,
|
||||
next_block_height: u64,
|
||||
) -> Result<bool> {
|
||||
let output = node.get_utxo(&output_commit)?;
|
||||
if let Some(out) = output {
|
||||
let is_coinbase = match out.output_type {
|
||||
|
@ -72,7 +76,7 @@ pub fn build_input(node: &Arc<dyn GrinNode>, output_commit: &Commitment) -> Resu
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
/// HTTP (JSONRPC) implementation of the 'GrinNode' trait
|
||||
/// HTTP (JSON-RPC) implementation of the 'GrinNode' trait
|
||||
#[derive(Clone)]
|
||||
pub struct HttpGrinNode {
|
||||
node_url: SocketAddr,
|
||||
|
@ -105,13 +109,19 @@ impl HttpGrinNode {
|
|||
|
||||
impl GrinNode for HttpGrinNode {
|
||||
fn get_utxo(&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 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 params = json!([
|
||||
Some(commits),
|
||||
start_height,
|
||||
end_height,
|
||||
include_proof,
|
||||
include_merkle_proof
|
||||
]);
|
||||
let outputs = self.send_json_request::<Vec<OutputPrintable>>("get_outputs", ¶ms)?;
|
||||
if outputs.is_empty() {
|
||||
return Ok(None);
|
||||
|
@ -125,7 +135,7 @@ impl GrinNode for HttpGrinNode {
|
|||
let tip_json = self.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());
|
||||
.map_err(|e| ErrorKind::SerdeJsonError(e.to_string()).into());
|
||||
|
||||
Ok(tip?.height)
|
||||
}
|
||||
|
@ -137,6 +147,8 @@ impl GrinNode for HttpGrinNode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Implementation of 'GrinNode' trait that mocks a grin node instance.
|
||||
/// Use only for testing purposes.
|
||||
pub struct MockGrinNode {
|
||||
utxos: HashMap<Commitment, OutputPrintable>,
|
||||
txns_posted: RwLock<Vec<Transaction>>,
|
||||
|
@ -155,7 +167,7 @@ impl MockGrinNode {
|
|||
}
|
||||
|
||||
pub fn add_default_utxo(&mut self, output_commit: &Commitment) {
|
||||
let utxo = OutputPrintable{
|
||||
let utxo = OutputPrintable {
|
||||
output_type: OutputType::Transaction,
|
||||
commit: output_commit.to_owned(),
|
||||
spent: false,
|
||||
|
@ -163,7 +175,7 @@ impl MockGrinNode {
|
|||
proof_hash: String::from(""),
|
||||
block_height: None,
|
||||
merkle_proof: None,
|
||||
mmr_index: 0
|
||||
mmr_index: 0,
|
||||
};
|
||||
|
||||
self.add_utxo(&output_commit, &utxo);
|
||||
|
|
101
src/onion.rs
101
src/onion.rs
|
@ -2,13 +2,13 @@ use crate::error::Result;
|
|||
use crate::secp::{self, Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
|
||||
use crate::types::Payload;
|
||||
|
||||
use chacha20::{ChaCha20, Key, Nonce};
|
||||
use chacha20::cipher::{NewCipher, StreamCipher};
|
||||
use chacha20::{ChaCha20, Key, Nonce};
|
||||
use grin_core::ser::{self, ProtocolVersion, Writeable, Writer};
|
||||
use grin_util::{self, ToHex};
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::{Deserialize};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
|
||||
|
@ -40,7 +40,9 @@ impl Onion {
|
|||
cipher.apply_keystream(&mut decrypted_bytes);
|
||||
let decrypted_payload = Payload::deserialize(&decrypted_bytes)?;
|
||||
|
||||
let enc_payloads : Vec<RawBytes> = self.enc_payloads.iter()
|
||||
let enc_payloads: Vec<RawBytes> = self
|
||||
.enc_payloads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| i != 0)
|
||||
.map(|(_, enc_payload)| {
|
||||
|
@ -59,16 +61,19 @@ impl Onion {
|
|||
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,
|
||||
let peeled_onion = Onion {
|
||||
ephemeral_pubkey,
|
||||
commit: commitment.clone(),
|
||||
enc_payloads: enc_payloads,
|
||||
enc_payloads,
|
||||
};
|
||||
Ok((decrypted_payload, peeled_onion))
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_blinding_factor(shared_secret: &SharedSecret, ephemeral_pubkey: &PublicKey) -> Result<SecretKey> {
|
||||
fn calc_blinding_factor(
|
||||
shared_secret: &SharedSecret,
|
||||
ephemeral_pubkey: &PublicKey,
|
||||
) -> Result<SecretKey> {
|
||||
let serialized_pubkey = ser::ser_vec(&ephemeral_pubkey, ProtocolVersion::local())?;
|
||||
|
||||
let mut hasher = Sha256::default();
|
||||
|
@ -112,7 +117,10 @@ impl serde::ser::Serialize for Onion {
|
|||
let mut state = serializer.serialize_struct("Onion", 3)?;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
state.serialize_field("pubkey", &self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex())?;
|
||||
state.serialize_field(
|
||||
"pubkey",
|
||||
&self.ephemeral_pubkey.serialize_vec(&secp, true).to_hex(),
|
||||
)?;
|
||||
state.serialize_field("commit", &self.commit.to_hex())?;
|
||||
|
||||
let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect();
|
||||
|
@ -131,7 +139,7 @@ impl<'de> serde::de::Deserialize<'de> for Onion {
|
|||
enum Field {
|
||||
Pubkey,
|
||||
Commit,
|
||||
Data
|
||||
Data,
|
||||
}
|
||||
|
||||
struct OnionVisitor;
|
||||
|
@ -155,20 +163,27 @@ impl<'de> serde::de::Deserialize<'de> for Onion {
|
|||
match key {
|
||||
Field::Pubkey => {
|
||||
let val: String = map.next_value()?;
|
||||
let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||
let vec =
|
||||
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||
let secp = Secp256k1::new();
|
||||
pubkey = Some(PublicKey::from_slice(&secp, &vec[..]).map_err(serde::de::Error::custom)?);
|
||||
pubkey = Some(
|
||||
PublicKey::from_slice(&secp, &vec[..])
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
);
|
||||
}
|
||||
Field::Commit => {
|
||||
let val: String = map.next_value()?;
|
||||
let vec = grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||
let vec =
|
||||
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
|
||||
commit = Some(Commitment::from_vec(vec));
|
||||
}
|
||||
Field::Data => {
|
||||
let val: Vec<String> = map.next_value()?;
|
||||
let mut vec: Vec<Vec<u8>> = Vec::new();
|
||||
for hex in val {
|
||||
vec.push(grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?);
|
||||
vec.push(
|
||||
grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?,
|
||||
);
|
||||
}
|
||||
data = Some(vec);
|
||||
}
|
||||
|
@ -183,11 +198,7 @@ impl<'de> serde::de::Deserialize<'de> for Onion {
|
|||
}
|
||||
}
|
||||
|
||||
const FIELDS: &[&str] = &[
|
||||
"pubkey",
|
||||
"commit",
|
||||
"data"
|
||||
];
|
||||
const FIELDS: &[&str] = &["pubkey", "commit", "data"];
|
||||
deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor)
|
||||
}
|
||||
}
|
||||
|
@ -196,10 +207,10 @@ impl<'de> serde::de::Deserialize<'de> for Onion {
|
|||
pub mod test_util {
|
||||
use super::{Onion, RawBytes};
|
||||
use crate::error::Result;
|
||||
use crate::types::{Payload};
|
||||
use crate::secp::{Commitment, PublicKey, Secp256k1, SecretKey, SharedSecret};
|
||||
use crate::types::Payload;
|
||||
|
||||
use chacha20::cipher::{StreamCipher};
|
||||
use chacha20::cipher::StreamCipher;
|
||||
|
||||
pub struct Hop {
|
||||
pub pubkey: PublicKey,
|
||||
|
@ -207,7 +218,11 @@ pub mod test_util {
|
|||
}
|
||||
|
||||
/// Create an Onion for the Commitment, encrypting the payload for each hop
|
||||
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 mut ephemeral_key = session_key.clone();
|
||||
|
||||
|
@ -231,10 +246,10 @@ pub mod test_util {
|
|||
}
|
||||
}
|
||||
|
||||
let onion = Onion{
|
||||
let onion = Onion {
|
||||
ephemeral_pubkey: PublicKey::from_secret_key(&secp, session_key)?,
|
||||
commit: commitment.clone(),
|
||||
enc_payloads: enc_payloads,
|
||||
enc_payloads,
|
||||
};
|
||||
Ok(onion)
|
||||
}
|
||||
|
@ -243,25 +258,25 @@ pub mod test_util {
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::test_util::{self, Hop};
|
||||
use crate::types::{Payload};
|
||||
use crate::secp;
|
||||
use crate::types::Payload;
|
||||
|
||||
use grin_core::core::FeeFields;
|
||||
|
||||
/// Test end-to-end Onion creation and unwrapping logic.
|
||||
#[test]
|
||||
fn onion() {
|
||||
let total_fee : u64 = 10;
|
||||
let fee_per_hop : u64 = 2;
|
||||
let in_value : u64 = 1000;
|
||||
let out_value : u64 = in_value - total_fee;
|
||||
let total_fee: u64 = 10;
|
||||
let fee_per_hop: u64 = 2;
|
||||
let in_value: u64 = 1000;
|
||||
let out_value: u64 = in_value - total_fee;
|
||||
let blind = secp::random_secret();
|
||||
let commitment = secp::commit(in_value, &blind).unwrap();
|
||||
|
||||
let session_key = secp::random_secret();
|
||||
let mut hops : Vec<Hop> = Vec::new();
|
||||
let mut hops: Vec<Hop> = Vec::new();
|
||||
|
||||
let mut keys : Vec<secp::SecretKey> = Vec::new();
|
||||
let mut keys: Vec<secp::SecretKey> = Vec::new();
|
||||
let mut final_commit = secp::commit(out_value, &blind).unwrap();
|
||||
let mut final_blind = blind.clone();
|
||||
for i in 0..5 {
|
||||
|
@ -274,29 +289,36 @@ pub mod tests {
|
|||
final_commit = secp::add_excess(&final_commit, &excess).unwrap();
|
||||
let proof = if i == 4 {
|
||||
let n1 = secp::random_secret();
|
||||
let rp = secp.bullet_proof(out_value, final_blind.clone(), n1.clone(), n1.clone(), None, None);
|
||||
let rp = secp.bullet_proof(
|
||||
out_value,
|
||||
final_blind.clone(),
|
||||
n1.clone(),
|
||||
n1.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
|
||||
Some(rp)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
hops.push(Hop{
|
||||
hops.push(Hop {
|
||||
pubkey: secp::PublicKey::from_secret_key(&secp, &keys[i]).unwrap(),
|
||||
payload: Payload{
|
||||
excess: excess,
|
||||
payload: Payload {
|
||||
excess,
|
||||
fee: FeeFields::from(fee_per_hop as u32),
|
||||
rangeproof: proof,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let mut onion_packet = test_util::create_onion(&commitment, &session_key, &hops).unwrap();
|
||||
|
||||
let mut payload = Payload{
|
||||
let mut payload = Payload {
|
||||
excess: secp::random_secret(),
|
||||
fee: FeeFields::from(fee_per_hop as u32),
|
||||
rangeproof: None
|
||||
rangeproof: None,
|
||||
};
|
||||
for i in 0..5 {
|
||||
let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
|
||||
|
@ -305,7 +327,10 @@ pub mod tests {
|
|||
}
|
||||
|
||||
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(out_value, &final_blind).unwrap(), final_commit);
|
||||
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32));
|
||||
}
|
||||
|
|
28
src/secp.rs
28
src/secp.rs
|
@ -1,9 +1,12 @@
|
|||
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
|
||||
pub use secp256k1zkp::aggsig;
|
||||
pub use secp256k1zkp::constants::{
|
||||
AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE,
|
||||
SECRET_KEY_SIZE,
|
||||
};
|
||||
pub use secp256k1zkp::ecdh::SharedSecret;
|
||||
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
|
||||
pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
|
||||
pub use secp256k1zkp::constants::{AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE, SECRET_KEY_SIZE};
|
||||
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
|
||||
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
|
||||
|
||||
use crate::error::{Error, ErrorKind, Result};
|
||||
|
||||
|
@ -77,7 +80,12 @@ impl ComSignature {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn calc_challenge(secp: &Secp256k1, commit: &Commitment, nonce_commit: &Commitment, msg: &Vec<u8>) -> Result<SecretKey> {
|
||||
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);
|
||||
|
@ -94,8 +102,8 @@ impl ComSignature {
|
|||
pub mod comsig_serde {
|
||||
use super::ComSignature;
|
||||
use grin_core::ser::{self, ProtocolVersion};
|
||||
use serde::{Deserialize, Serializer};
|
||||
use grin_util::ToHex;
|
||||
use serde::{Deserialize, Serializer};
|
||||
|
||||
/// Serializes a ComSignature as a hex string
|
||||
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
@ -115,8 +123,8 @@ pub mod comsig_serde {
|
|||
use serde::de::Error;
|
||||
let bytes = String::deserialize(deserializer)
|
||||
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
|
||||
let sig: ComSignature = grin_core::ser::deserialize_default(&mut &bytes[..])
|
||||
.map_err(Error::custom)?;
|
||||
let sig: ComSignature =
|
||||
grin_core::ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
|
||||
Ok(sig)
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +172,7 @@ pub fn commit(value: u64, blind: &SecretKey) -> Result<Commitment> {
|
|||
/// Add a blinding factor to an existing Commitment
|
||||
pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result<Commitment> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
let excess_commit : Commitment = secp.commit(0, excess.clone())?;
|
||||
let excess_commit: Commitment = secp.commit(0, excess.clone())?;
|
||||
|
||||
let commits = vec![commitment.clone(), excess_commit.clone()];
|
||||
let sum = secp.commit_sum(commits, Vec::new())?;
|
||||
|
@ -174,7 +182,7 @@ pub fn add_excess(commitment: &Commitment, excess: &SecretKey) -> Result<Commitm
|
|||
/// Subtracts a value (v*H) from an existing commitment
|
||||
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 neg_commit: Commitment = secp.commit(value, ZERO_KEY)?;
|
||||
let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?;
|
||||
Ok(sum)
|
||||
}
|
||||
|
@ -192,8 +200,8 @@ mod tests {
|
|||
use super::{ComSignature, ContextFlag, Secp256k1, SecretKey};
|
||||
use crate::error::Result;
|
||||
|
||||
use secp256k1zkp::rand::{RngCore, thread_rng};
|
||||
use rand::Rng;
|
||||
use secp256k1zkp::rand::{thread_rng, RngCore};
|
||||
|
||||
/// Test signing and verification of ComSignatures
|
||||
#[test]
|
||||
|
|
209
src/server.rs
209
src/server.rs
|
@ -1,17 +1,17 @@
|
|||
use crate::config::ServerConfig;
|
||||
use crate::node::{self, GrinNode};
|
||||
use crate::onion::Onion;
|
||||
use crate::secp::{self, Commitment, ComSignature, RangeProof, Secp256k1, SecretKey};
|
||||
use crate::secp::{self, ComSignature, Commitment, RangeProof, Secp256k1, SecretKey};
|
||||
use crate::wallet::{self, Wallet};
|
||||
|
||||
use grin_core::core::{Input, Output, OutputFeatures, TransactionBody};
|
||||
use grin_core::global::DEFAULT_ACCEPT_FEE_BASE;
|
||||
use grin_util::StopState;
|
||||
use itertools::Itertools;
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::*;
|
||||
use jsonrpc_http_server::jsonrpc_core::*;
|
||||
use jsonrpc_core::{Result, Value};
|
||||
use jsonrpc_derive::rpc;
|
||||
use jsonrpc_http_server::jsonrpc_core::*;
|
||||
use jsonrpc_http_server::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
@ -56,8 +56,18 @@ struct ServerImpl {
|
|||
}
|
||||
|
||||
impl ServerImpl {
|
||||
fn new(server_config: ServerConfig, stop_state: Arc<StopState>, wallet: Arc<dyn Wallet>, node: Arc<dyn GrinNode>) -> Self {
|
||||
ServerImpl { server_config, stop_state, wallet, node }
|
||||
fn new(
|
||||
server_config: ServerConfig,
|
||||
stop_state: Arc<StopState>,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
) -> Self {
|
||||
ServerImpl {
|
||||
server_config,
|
||||
stop_state,
|
||||
wallet,
|
||||
node,
|
||||
}
|
||||
}
|
||||
|
||||
/// The fee base to use. For now, just using the default.
|
||||
|
@ -79,44 +89,51 @@ impl ServerImpl {
|
|||
let mut locked_state = SERVER_STATE.lock().unwrap();
|
||||
let next_block_height = self.node.get_chain_height()? + 1;
|
||||
|
||||
let spendable : Vec<Submission> = locked_state
|
||||
let spendable: Vec<Submission> = locked_state
|
||||
.values()
|
||||
.into_iter()
|
||||
.unique_by(|s| s.output_commit)
|
||||
.filter(|s| node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false))
|
||||
.filter(|s| {
|
||||
node::is_spendable(&self.node, &s.input.commit, next_block_height).unwrap_or(false)
|
||||
})
|
||||
.filter(|s| !node::is_unspent(&self.node, &s.output_commit).unwrap_or(true))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if spendable.len() == 0 {
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total_fee : u64 = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, s)| s.fee)
|
||||
.sum();
|
||||
let total_fee: u64 = spendable.iter().enumerate().map(|(_, s)| s.fee).sum();
|
||||
|
||||
let inputs : Vec<Input> = spendable
|
||||
let inputs: Vec<Input> = spendable.iter().enumerate().map(|(_, s)| s.input).collect();
|
||||
|
||||
let outputs: Vec<Output> = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, s)| s.input)
|
||||
.map(|(_, s)| {
|
||||
Output::new(
|
||||
OutputFeatures::Plain,
|
||||
s.output_commit,
|
||||
s.rangeproof.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let outputs : Vec<Output> = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, s)| Output::new(OutputFeatures::Plain, s.output_commit, s.rangeproof.unwrap()))
|
||||
.collect();
|
||||
|
||||
let excesses : Vec<SecretKey> = spendable
|
||||
let excesses: Vec<SecretKey> = spendable
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, s)| s.excess.clone())
|
||||
.collect();
|
||||
|
||||
let tx = wallet::assemble_tx(&self.wallet, &inputs, &outputs, self.get_fee_base(), total_fee, &excesses)?;
|
||||
let tx = wallet::assemble_tx(
|
||||
&self.wallet,
|
||||
&inputs,
|
||||
&outputs,
|
||||
self.get_fee_base(),
|
||||
total_fee,
|
||||
&excesses,
|
||||
)?;
|
||||
|
||||
self.node.post_tx(&tx)?;
|
||||
locked_state.clear();
|
||||
|
@ -130,66 +147,97 @@ impl Server for ServerImpl {
|
|||
fn swap(&self, swap: SwapReq) -> Result<Value> {
|
||||
// milestone 3: check that enc_payloads length matches number of configured servers
|
||||
if swap.onion.enc_payloads.len() != 1 {
|
||||
return Err(jsonrpc_core::Error::invalid_params("Multi server not supported until milestone 3"));
|
||||
return Err(Error::invalid_params(
|
||||
"Multi server not supported until milestone 3",
|
||||
));
|
||||
}
|
||||
|
||||
// Verify commitment signature to ensure caller owns the output
|
||||
let serialized_onion = swap.onion.serialize()
|
||||
.map_err(|_| jsonrpc_core::Error::internal_error())?;
|
||||
let _ = swap.comsig.verify(&swap.onion.commit, &serialized_onion)
|
||||
.map_err(|_| jsonrpc_core::Error::invalid_params("ComSignature invalid"))?;
|
||||
let serialized_onion = swap.onion.serialize().map_err(|e| Error {
|
||||
message: e.to_string(),
|
||||
code: ErrorCode::InternalError,
|
||||
data: None,
|
||||
})?;
|
||||
let _ = swap
|
||||
.comsig
|
||||
.verify(&swap.onion.commit, &serialized_onion)
|
||||
.map_err(|_| Error::invalid_params("ComSignature invalid"))?;
|
||||
|
||||
// Verify that commitment is unspent
|
||||
let input = node::build_input(&self.node, &swap.onion.commit)
|
||||
.map_err(|_| jsonrpc_core::Error::internal_error())?;
|
||||
let input = input.ok_or(jsonrpc_core::Error::invalid_params("Commitment not found"))?;
|
||||
let input = node::build_input(&self.node, &swap.onion.commit).map_err(|e| Error {
|
||||
message: e.to_string(),
|
||||
code: ErrorCode::InternalError,
|
||||
data: None,
|
||||
})?;
|
||||
let input = input.ok_or(Error::invalid_params("Commitment not found"))?;
|
||||
|
||||
let peeled = swap.onion.peel_layer(&self.server_config.key)
|
||||
.map_err(|e| jsonrpc_core::Error::invalid_params(e.message()))?;
|
||||
let peeled = swap
|
||||
.onion
|
||||
.peel_layer(&self.server_config.key)
|
||||
.map_err(|e| Error::invalid_params(e.message()))?;
|
||||
|
||||
// Verify the fee meets the minimum
|
||||
let fee: u64 = peeled.0.fee.into();
|
||||
if fee < self.get_minimum_swap_fee() {
|
||||
return Err(jsonrpc_core::Error::invalid_params("Fee does not meet minimum"));
|
||||
return Err(Error::invalid_params("Fee does not meet minimum"));
|
||||
}
|
||||
|
||||
// Calculate final output commitment
|
||||
let output_commit = secp::add_excess(&swap.onion.commit, &peeled.0.excess)
|
||||
.map_err(|_| jsonrpc_core::Error::internal_error())?;
|
||||
let output_commit = secp::sub_value(&output_commit, fee)
|
||||
.map_err(|_| jsonrpc_core::Error::internal_error())?;
|
||||
let output_commit =
|
||||
secp::add_excess(&swap.onion.commit, &peeled.0.excess).map_err(|e| Error {
|
||||
message: e.to_string(),
|
||||
code: ErrorCode::InternalError,
|
||||
data: None,
|
||||
})?;
|
||||
let output_commit = secp::sub_value(&output_commit, fee).map_err(|e| Error {
|
||||
message: e.to_string(),
|
||||
code: ErrorCode::InternalError,
|
||||
data: None,
|
||||
})?;
|
||||
|
||||
// Verify the bullet proof and build the final output
|
||||
if let Some(r) = peeled.0.rangeproof {
|
||||
let secp = Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
|
||||
secp.verify_bullet_proof(output_commit, r, None)
|
||||
.map_err(|_| jsonrpc_core::Error::invalid_params("RangeProof invalid"))?;
|
||||
.map_err(|_| Error::invalid_params("RangeProof invalid"))?;
|
||||
} else {
|
||||
// milestone 3: only the last hop will have a rangeproof
|
||||
return Err(jsonrpc_core::Error::invalid_params("Rangeproof expected"));
|
||||
return Err(Error::invalid_params("Rangeproof expected"));
|
||||
}
|
||||
|
||||
let mut locked = SERVER_STATE.lock().unwrap();
|
||||
if locked.contains_key(&swap.onion.commit) {
|
||||
return Err(jsonrpc_core::Error::invalid_params("swap already called for coin"));
|
||||
return Err(Error::invalid_params("swap already called for coin"));
|
||||
}
|
||||
|
||||
locked.insert(swap.onion.commit, Submission{
|
||||
locked.insert(
|
||||
swap.onion.commit,
|
||||
Submission {
|
||||
excess: peeled.0.excess,
|
||||
output_commit: output_commit,
|
||||
output_commit,
|
||||
rangeproof: peeled.0.rangeproof,
|
||||
input: input,
|
||||
fee: fee,
|
||||
onion: peeled.1
|
||||
});
|
||||
input,
|
||||
fee,
|
||||
onion: peeled.1,
|
||||
},
|
||||
);
|
||||
Ok(Value::String("success".into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Spin up the JSON-RPC web server
|
||||
pub fn listen(server_config: &ServerConfig, wallet: Arc<dyn Wallet>, node: Arc<dyn GrinNode>, stop_state: &Arc<StopState>) -> std::result::Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let server_impl = Arc::new(ServerImpl::new(server_config.clone(), stop_state.clone(), wallet.clone(), node.clone()));
|
||||
pub fn listen(
|
||||
server_config: &ServerConfig,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
stop_state: &Arc<StopState>,
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let server_impl = Arc::new(ServerImpl::new(
|
||||
server_config.clone(),
|
||||
stop_state.clone(),
|
||||
wallet.clone(),
|
||||
node.clone(),
|
||||
));
|
||||
|
||||
let mut io = IoHandler::new();
|
||||
io.extend_with(ServerImpl::to_delegate(server_impl.as_ref().clone()));
|
||||
|
@ -205,6 +253,7 @@ pub fn listen(server_config: &ServerConfig, wallet: Arc<dyn Wallet>, node: Arc<d
|
|||
})
|
||||
.start_http(&server_config.addr)
|
||||
.expect("Unable to start RPC server");
|
||||
println!("Server listening on {}", server_config.addr);
|
||||
|
||||
let close_handle = server.close_handle();
|
||||
|
||||
|
@ -247,8 +296,8 @@ mod tests {
|
|||
use grin_util::StopState;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use hyper::{Body, Client, Request, Response};
|
||||
use tokio::runtime;
|
||||
|
@ -263,14 +312,14 @@ mod tests {
|
|||
server_key: secp::SecretKey,
|
||||
wallet: Arc<dyn Wallet>,
|
||||
node: Arc<dyn GrinNode>,
|
||||
req: String
|
||||
req: String,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let server_config = ServerConfig {
|
||||
key: server_key,
|
||||
interval_s: 1,
|
||||
addr: TcpListener::bind("127.0.0.1:0")?.local_addr()?,
|
||||
grin_node_url: "127.0.0.1:3413".parse()?,
|
||||
wallet_owner_url: "127.0.0.1:3420".parse()?
|
||||
wallet_owner_url: "127.0.0.1:3420".parse()?,
|
||||
};
|
||||
|
||||
let threaded_rt = runtime::Runtime::new()?;
|
||||
|
@ -324,7 +373,7 @@ mod tests {
|
|||
let server_key = secp::random_secret();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64= 50_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let commitment = secp::commit(value, &blind)?;
|
||||
let hop_excess = secp::random_secret();
|
||||
|
@ -332,31 +381,41 @@ mod tests {
|
|||
|
||||
let mut final_blind = blind.clone();
|
||||
final_blind.add_assign(&secp, &hop_excess).unwrap();
|
||||
let proof = secp.bullet_proof(value - fee, final_blind.clone(), nonce.clone(), nonce.clone(), None, None);
|
||||
let proof = secp.bullet_proof(
|
||||
value - fee,
|
||||
final_blind.clone(),
|
||||
nonce.clone(),
|
||||
nonce.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let hop = Hop {
|
||||
pubkey: PublicKey::from_secret_key(&secp, &server_key)?,
|
||||
payload: Payload{
|
||||
payload: Payload {
|
||||
excess: hop_excess,
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: Some(proof),
|
||||
}
|
||||
},
|
||||
};
|
||||
let hops: Vec<Hop> = vec![hop];
|
||||
let session_key = secp::random_secret();
|
||||
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
|
||||
let swap = SwapReq{
|
||||
let swap = SwapReq {
|
||||
onion: onion_packet,
|
||||
comsig: comsig,
|
||||
comsig,
|
||||
};
|
||||
|
||||
let wallet = MockWallet{};
|
||||
let wallet = MockWallet {};
|
||||
let mut mut_node = MockGrinNode::new();
|
||||
mut_node.add_default_utxo(&commitment);
|
||||
let node = Arc::new(mut_node);
|
||||
|
||||
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", serde_json::json!(swap));
|
||||
let req = format!(
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
serde_json::json!(swap)
|
||||
);
|
||||
println!("Request: {}", req);
|
||||
let response = make_request(server_key, Arc::new(wallet), node.clone(), req)?;
|
||||
let expected = "{\"jsonrpc\":\"2.0\",\"result\":\"success\",\"id\":\"1\"}\n";
|
||||
|
@ -365,7 +424,7 @@ mod tests {
|
|||
// check that the transaction was posted
|
||||
let posted_txns = node.get_posted_txns();
|
||||
assert_eq!(posted_txns.len(), 1);
|
||||
let posted_txn : Transaction = posted_txns.into_iter().next().unwrap();
|
||||
let posted_txn: Transaction = posted_txns.into_iter().next().unwrap();
|
||||
let input_commit = posted_txn.inputs_committed().into_iter().next().unwrap();
|
||||
assert_eq!(input_commit, commitment);
|
||||
|
||||
|
@ -379,31 +438,34 @@ mod tests {
|
|||
let server_key = secp::random_secret();
|
||||
|
||||
let value: u64 = 200_000_000;
|
||||
let fee: u64= 50_000_000;
|
||||
let fee: u64 = 50_000_000;
|
||||
let blind = secp::random_secret();
|
||||
let commitment = secp::commit(value, &blind)?;
|
||||
|
||||
let hop = Hop {
|
||||
pubkey: PublicKey::from_secret_key(&secp, &server_key)?,
|
||||
payload: Payload{
|
||||
payload: Payload {
|
||||
excess: secp::random_secret(),
|
||||
fee: FeeFields::from(fee as u32),
|
||||
rangeproof: None,
|
||||
}
|
||||
},
|
||||
};
|
||||
let hops: Vec<Hop> = vec![hop];
|
||||
let session_key = secp::random_secret();
|
||||
let onion_packet = test_util::create_onion(&commitment, &session_key, &hops)?;
|
||||
let comsig = ComSignature::sign(value, &blind, &onion_packet.serialize()?)?;
|
||||
let swap = SwapReq{
|
||||
let swap = SwapReq {
|
||||
onion: onion_packet,
|
||||
comsig: comsig,
|
||||
comsig,
|
||||
};
|
||||
|
||||
let wallet = MockWallet{};
|
||||
let wallet = MockWallet {};
|
||||
let node = MockGrinNode::new();
|
||||
|
||||
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", serde_json::json!(swap));
|
||||
let req = format!(
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
serde_json::json!(swap)
|
||||
);
|
||||
let response = make_request(server_key, Arc::new(wallet), Arc::new(node), req)?;
|
||||
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Commitment not found\"},\"id\":\"1\"}\n";
|
||||
assert_eq!(response, expected);
|
||||
|
@ -414,11 +476,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn swap_bad_request() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let wallet = MockWallet{};
|
||||
let wallet = MockWallet {};
|
||||
let node = MockGrinNode::new();
|
||||
|
||||
let params = "{ \"param\": \"Not a valid Swap request\" }";
|
||||
let req = format!("{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}", params);
|
||||
let req = format!(
|
||||
"{{\"jsonrpc\": \"2.0\", \"method\": \"swap\", \"params\": [{}], \"id\": \"1\"}}",
|
||||
params
|
||||
);
|
||||
let response = make_request(secp::random_secret(), Arc::new(wallet), Arc::new(node), req)?;
|
||||
let expected = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"Invalid params: missing field `onion`.\"},\"id\":\"1\"}\n";
|
||||
assert_eq!(response, expected);
|
||||
|
|
15
src/types.rs
15
src/types.rs
|
@ -5,7 +5,7 @@ use grin_core::core::FeeFields;
|
|||
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const CURRENT_VERSION : u8 = 0;
|
||||
const CURRENT_VERSION: u8 = 0;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Payload {
|
||||
|
@ -16,7 +16,7 @@ pub struct Payload {
|
|||
|
||||
impl Payload {
|
||||
pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload> {
|
||||
let payload: Payload = grin_core::ser::deserialize_default(&mut &bytes[..])?;
|
||||
let payload: Payload = ser::deserialize_default(&mut &bytes[..])?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,7 @@ impl Readable for Payload {
|
|||
}
|
||||
|
||||
let excess = secp::read_secret_key(reader)?;
|
||||
let fee = FeeFields::try_from(reader.read_u64()?)
|
||||
.map_err(|_| ser::Error::CorruptedData)?;
|
||||
let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?;
|
||||
let rangeproof = if reader.read_u8()? == 0 {
|
||||
None
|
||||
} else {
|
||||
|
@ -45,9 +44,9 @@ impl Readable for Payload {
|
|||
};
|
||||
|
||||
let payload = Payload {
|
||||
excess: excess,
|
||||
fee: fee,
|
||||
rangeproof: rangeproof
|
||||
excess,
|
||||
fee,
|
||||
rangeproof,
|
||||
};
|
||||
Ok(payload)
|
||||
}
|
||||
|
@ -63,7 +62,7 @@ impl Writeable for Payload {
|
|||
Some(proof) => {
|
||||
writer.write_u8(1)?;
|
||||
proof.write(writer)?;
|
||||
},
|
||||
}
|
||||
None => writer.write_u8(0)?,
|
||||
};
|
||||
|
||||
|
|
117
src/wallet.rs
117
src/wallet.rs
|
@ -1,26 +1,36 @@
|
|||
use crate::error::{ErrorKind, Result};
|
||||
use crate::secp::{self, SecretKey};
|
||||
use crate::secp::{self};
|
||||
|
||||
use grin_api::client;
|
||||
use grin_api::json_rpc::{build_request, Request, Response};
|
||||
use grin_core::core::{FeeFields, Input, Inputs, KernelFeatures, Output, Transaction, TransactionBody, TxKernel};
|
||||
use grin_core::core::{
|
||||
FeeFields, Input, Inputs, KernelFeatures, Output, OutputFeatures, Transaction, TransactionBody,
|
||||
TxKernel,
|
||||
};
|
||||
use grin_core::libtx::secp_ser;
|
||||
use grin_keychain::BlindingFactor;
|
||||
use grin_util::ZeroingString;
|
||||
use grin_wallet_api::Token;
|
||||
use secp256k1zkp::{ContextFlag, Secp256k1};
|
||||
use grin_util::{ToHex, ZeroingString};
|
||||
use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId, Token};
|
||||
use secp256k1zkp::{ContextFlag, PublicKey, Secp256k1, SecretKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Wallet : Send + Sync {
|
||||
pub trait Wallet: Send + Sync {
|
||||
/// Builds an output for the wallet with the provided amount.
|
||||
fn build_output(&self, amount: u64) -> Result<(BlindingFactor, Output)>;
|
||||
}
|
||||
|
||||
/// Builds and verifies a 'Transaction' using the provided components.
|
||||
pub fn assemble_tx(wallet: &Arc<dyn Wallet>, inputs: &Vec<Input>, outputs: &Vec<Output>, fee_base: u64, total_fee: u64, excesses: &Vec<SecretKey>) -> Result<Transaction> {
|
||||
pub fn assemble_tx(
|
||||
wallet: &Arc<dyn Wallet>,
|
||||
inputs: &Vec<Input>,
|
||||
outputs: &Vec<Output>,
|
||||
fee_base: u64,
|
||||
total_fee: u64,
|
||||
excesses: &Vec<SecretKey>,
|
||||
) -> Result<Transaction> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::Commit);
|
||||
let txn_inputs = Inputs::from(inputs.as_slice());
|
||||
let mut txn_outputs = outputs.clone();
|
||||
|
@ -28,7 +38,9 @@ pub fn assemble_tx(wallet: &Arc<dyn Wallet>, inputs: &Vec<Input>, outputs: &Vec<
|
|||
let mut kernel_fee = total_fee;
|
||||
|
||||
// calculate fee required if we add our own output
|
||||
let fee_required = TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1) * fee_base;
|
||||
let fee_required =
|
||||
TransactionBody::weight_by_iok(inputs.len() as u64, (outputs.len() + 1) as u64, 1)
|
||||
* fee_base;
|
||||
|
||||
// calculate fee to spend the output to ensure there's enough leftover to cover the fees for spending it
|
||||
let fee_to_spend = TransactionBody::weight_by_iok(1, 0, 0) * fee_base;
|
||||
|
@ -41,7 +53,9 @@ pub fn assemble_tx(wallet: &Arc<dyn Wallet>, inputs: &Vec<Input>, outputs: &Vec<
|
|||
let wallet_output = wallet.build_output(amount)?;
|
||||
txn_outputs.push(wallet_output.1);
|
||||
|
||||
let output_excess = wallet_output.0.secret_key(&secp)
|
||||
let output_excess = wallet_output
|
||||
.0
|
||||
.secret_key(&secp)
|
||||
.map_err(|_| ErrorKind::CorruptedData)?;
|
||||
txn_excesses.push(output_excess);
|
||||
}
|
||||
|
@ -71,26 +85,93 @@ pub fn assemble_tx(wallet: &Arc<dyn Wallet>, inputs: &Vec<Input>, outputs: &Vec<
|
|||
#[derive(Clone)]
|
||||
pub struct HttpWallet {
|
||||
wallet_owner_url: SocketAddr,
|
||||
shared_key: SecretKey,
|
||||
token: Token,
|
||||
}
|
||||
|
||||
const ENDPOINT: &str = "/v3/owner";
|
||||
|
||||
/// Wrapper for ECDH Public keys
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct ECDHPubkey {
|
||||
/// public key, flattened
|
||||
#[serde(with = "secp_ser::pubkey_serde")]
|
||||
pub ecdh_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
impl HttpWallet {
|
||||
/// Calls the 'open_wallet' using the RPC API.
|
||||
pub fn open_wallet(wallet_owner_url: &SocketAddr, wallet_pass: &ZeroingString) -> Result<HttpWallet> {
|
||||
pub fn open_wallet(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
wallet_pass: &ZeroingString,
|
||||
) -> Result<HttpWallet> {
|
||||
println!("Opening wallet at {}", wallet_owner_url);
|
||||
let shared_key = HttpWallet::init_secure_api(&wallet_owner_url)?;
|
||||
|
||||
let open_wallet_params = json!({
|
||||
"name": null,
|
||||
"password": wallet_pass.to_string()
|
||||
});
|
||||
let token: Token = HttpWallet::send_json_request(&wallet_owner_url, "open_wallet", &open_wallet_params)?;
|
||||
let token: Token = HttpWallet::send_enc_request(
|
||||
&wallet_owner_url,
|
||||
"open_wallet",
|
||||
&open_wallet_params,
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("Connected to wallet");
|
||||
|
||||
Ok(HttpWallet {
|
||||
wallet_owner_url: wallet_owner_url.clone(),
|
||||
token: token,
|
||||
shared_key: shared_key.clone(),
|
||||
token: token.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn init_secure_api(wallet_owner_url: &SocketAddr) -> Result<SecretKey> {
|
||||
let secp = Secp256k1::new();
|
||||
let ephemeral_sk = secp::random_secret();
|
||||
let ephemeral_pk = PublicKey::from_secret_key(&secp, &ephemeral_sk)?;
|
||||
let init_params = json!({
|
||||
"ecdh_pubkey": ephemeral_pk.serialize_vec(&secp, true).to_hex()
|
||||
});
|
||||
|
||||
let response_pk: ECDHPubkey =
|
||||
HttpWallet::send_json_request(&wallet_owner_url, "init_secure_api", &init_params)?;
|
||||
|
||||
let shared_key = {
|
||||
let mut shared_pubkey = response_pk.ecdh_pubkey.clone();
|
||||
shared_pubkey.mul_assign(&secp, &ephemeral_sk)?;
|
||||
|
||||
let x_coord = shared_pubkey.serialize_vec(&secp, true);
|
||||
SecretKey::from_slice(&secp, &x_coord[1..])?
|
||||
};
|
||||
|
||||
Ok(shared_key)
|
||||
}
|
||||
|
||||
fn send_enc_request<D: serde::de::DeserializeOwned>(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
shared_key: &SecretKey,
|
||||
) -> Result<D> {
|
||||
let url = format!("http://{}{}", wallet_owner_url, ENDPOINT);
|
||||
let req = json!({
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": JsonId::IntId(1),
|
||||
"jsonrpc": "2.0",
|
||||
});
|
||||
let enc_req = EncryptedRequest::from_json(&JsonId::IntId(1), &req, &shared_key)?;
|
||||
let res =
|
||||
client::post::<EncryptedRequest, EncryptedResponse>(url.as_str(), None, &enc_req)?;
|
||||
let decrypted = res.decrypt(&shared_key)?;
|
||||
let response: Response = serde_json::from_value(decrypted.clone())?;
|
||||
let parsed = serde_json::from_value(response.result.unwrap().get("Ok").unwrap().clone())?;
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn send_json_request<D: serde::de::DeserializeOwned>(
|
||||
wallet_owner_url: &SocketAddr,
|
||||
method: &str,
|
||||
|
@ -122,17 +203,19 @@ impl Wallet for HttpWallet {
|
|||
"features": "Plain",
|
||||
"amount": amount
|
||||
});
|
||||
let output: OutputWithBlind = HttpWallet::send_json_request(&self.wallet_owner_url, "build_output", &req_json)?;
|
||||
let output: OutputWithBlind = HttpWallet::send_enc_request(
|
||||
&self.wallet_owner_url,
|
||||
"build_output",
|
||||
&req_json,
|
||||
&self.shared_key,
|
||||
)?;
|
||||
Ok((output.blind, output.output))
|
||||
}
|
||||
}
|
||||
|
||||
use grin_core::core::OutputFeatures;
|
||||
|
||||
/// HTTP (JSONRPC) implementation of the 'Wallet' trait.
|
||||
#[derive(Clone)]
|
||||
pub struct MockWallet {
|
||||
}
|
||||
pub struct MockWallet {}
|
||||
|
||||
impl Wallet for MockWallet {
|
||||
/// Builds an 'Output' for the wallet using the 'build_output' RPC API.
|
||||
|
|
Loading…
Reference in a new issue